diff --git a/.gitignore b/.gitignore index 24476c5..8186abf 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ migrate_working_dir/ *.iws .idea/ +*.freezed.dart +*.g.dart + # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..aa9a0b8 --- /dev/null +++ b/Makefile @@ -0,0 +1,5 @@ +code_gen: + flutter pub run build_runner build --delete-conflicting-outputs + +avd: + ./scripts/run_avd.sh \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index 61b6c4d..5e9b488 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,29 +1,47 @@ -# This file configures the analyzer, which statically analyzes Dart code to -# check for errors, warnings, and lints. -# -# The issues identified by the analyzer are surfaced in the UI of Dart-enabled -# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be -# invoked from the command line by running `flutter analyze`. +include: package:surf_lint_rules/analysis_options.yaml -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. -include: package:flutter_lints/flutter.yaml +analyzer: + exclude: + - "ios/**" + - "android/**" + - "build/**" + - "script/**" + - "**/*.g.dart" + - "**/*.freezed.dart" + errors: + invalid_annotation_target: ignore -linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at - # https://dart-lang.github.io/linter/lints/index.html. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. +dart_code_metrics: + metrics: + cyclomatic-complexity: 20 + maximum-nesting-level: 5 + number-of-parameters: 4 + source-lines-of-code: 50 + number-of-methods: 10 + weight-of-class: 0.33 + maintainability-index: 50 + anti-patterns: + - long-method + - long-parameter-list + metrics-exclude: + - test/** rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + - always-remove-listener + - avoid-unused-parameters + - avoid-unnecessary-setstate + - double-literal-format + - newline-before-return + - no-boolean-literal-compare + - prefer-conditional-expressions + - prefer-intl-name + - provide-correct-intl-args + - prefer-match-file-name: + exclude: + - lib/** + - test/** -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options +linter: + rules: + public_member_api_docs: false + always_put_required_named_parameters_first: false + always_use_package_imports: true diff --git a/android/build.gradle b/android/build.gradle index 58a8c74..713d7f6 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -26,6 +26,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/docs/RESULT.md b/docs/RESULT.md index 1299d66..39f8a56 100644 --- a/docs/RESULT.md +++ b/docs/RESULT.md @@ -1,11 +1,16 @@ # Целевая платформа -[здесь вставьте платформу/платформы под которые вы разрабатывали, это поможет нам при проверке заданий] +Android # Результаты -[здесь можете похвастаться, что успели реализовать, или сделали что-то сверх задания] +* Реализовать доменную модель +* Применил подход описания ValueObject с помощью библиотеки dartz. Это позволяет писать код, который не позволяет забыть обработать ситуацию что-то пошло не так +* Реализовал локальную базу данных +* Подготовил код для загрузки файлов билетов с возможностью приостановки загрузки с помощью библиотеки dio +* При решении части задач использовал chat-gpt # Ссылки на демонстрацию работы/скриншоты -[здесь оставьте ссылки на скринкаст/скриншоты, можно в Github-репозитории или в отдельном облаке] \ No newline at end of file +Вид экрана списка билетов, когда что-то пошло не так +![something-wrong.png](./presentation/something-wrong.png) \ No newline at end of file diff --git a/docs/presentation/something-wrong.png b/docs/presentation/something-wrong.png new file mode 100644 index 0000000..d9aca34 Binary files /dev/null and b/docs/presentation/something-wrong.png differ diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..fdcc671 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,44 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/lib/assets/strings/locale/ru.dart b/lib/assets/strings/locale/ru.dart new file mode 100644 index 0000000..2cc9d60 --- /dev/null +++ b/lib/assets/strings/locale/ru.dart @@ -0,0 +1,37 @@ +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/status/value_object.dart'; +import 'dart:math' as math; + +class AppStrings { + static const ticketStorage = _TicketStorageStrings(); +} + +class _TicketStorageStrings { + String get addButtonLabel => 'Добавить'; + String get ticketsTitle => 'Хранение билетов'; + String get enterUrlPlaceholder => 'Введите url'; + String get emptyList => 'Здесь пока ничего нет'; + String get unexpectedFailure => 'Что-то пошло не так'; + + const _TicketStorageStrings(); + + String statusTitle(TicketStatus status) { + return status.when( + initialized: () => 'Необходимо закончить редактирование', + readyToLoadFile: (_) => 'Ожидает начала загрузки', + loadingFile: (current, total) => + 'Загружается ${_formatBytes(current, 1)} из ${_formatBytes(total, 1)}', + fileLoaded: (_) => 'Файл загружен', + paused: (_) => 'Приостановлено', + ); + } +} + +String _formatBytes(int bytes, int decimals) { + if (bytes <= 0) return '0 B'; + const suffixes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; + + final i = (math.log(bytes) / math.log(1024)).floor(); + final bytesAsString = (bytes / math.pow(1024, i)).toStringAsFixed(decimals); + + return '$bytesAsString ${suffixes[i]}'; +} diff --git a/lib/assets/text/text_extension.dart b/lib/assets/text/text_extension.dart new file mode 100644 index 0000000..687b21d --- /dev/null +++ b/lib/assets/text/text_extension.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:surf_flutter_study_jam_2023/assets/text/text_style.dart'; + +/// App text style scheme. +class AppTextTheme extends ThemeExtension { + /// Text style 14_140. + final TextStyle regular14; + + /// Text style 16_124. + final TextStyle regular16; + + /// Text style 14_140_500. + final TextStyle medium14; + + /// Text style 16_124_500. + final TextStyle medium16; + + /// Text style 14_140_700. + final TextStyle bold14; + + /// Text style 16_124_700. + final TextStyle bold16; + + AppTextTheme._({ + required this.regular14, + required this.regular16, + required this.medium14, + required this.medium16, + required this.bold14, + required this.bold16, + }); + + /// Base app text theme. + AppTextTheme.base() + : regular14 = AppTextStyle.regular14.value, + regular16 = AppTextStyle.regular16.value, + medium14 = AppTextStyle.medium14.value, + medium16 = AppTextStyle.medium16.value, + bold14 = AppTextStyle.bold14.value, + bold16 = AppTextStyle.bold16.value; + + @override + ThemeExtension lerp( + ThemeExtension? other, + double t, + ) { + if (other is! AppTextTheme) { + return this; + } + + return copyWith( + regular14: TextStyle.lerp(regular14, other.regular14, t), + regular16: TextStyle.lerp(regular16, other.regular16, t), + medium14: TextStyle.lerp(medium14, other.medium14, t), + medium16: TextStyle.lerp(medium16, other.medium16, t), + bold14: TextStyle.lerp(bold14, other.bold14, t), + bold16: TextStyle.lerp(bold16, other.bold16, t), + ); + } + + @override + ThemeExtension copyWith({ + TextStyle? regular14, + TextStyle? regular16, + TextStyle? medium14, + TextStyle? medium16, + TextStyle? bold14, + TextStyle? bold16, + }) { + return AppTextTheme._( + regular14: regular14 ?? this.regular14, + regular16: regular16 ?? this.regular16, + medium14: medium14 ?? this.medium14, + medium16: medium16 ?? this.medium16, + bold14: bold14 ?? this.bold14, + bold16: bold16 ?? this.bold16, + ); + } + + /// Return text theme for app from context. + static AppTextTheme of(BuildContext context) { + return Theme.of(context).extension() ?? + _throwThemeExceptionFromFunc(context); + } +} + +Never _throwThemeExceptionFromFunc(BuildContext context) => + throw Exception('$AppTextTheme не найдена в $context'); diff --git a/lib/assets/text/text_style.dart b/lib/assets/text/text_style.dart new file mode 100644 index 0000000..f5f9de9 --- /dev/null +++ b/lib/assets/text/text_style.dart @@ -0,0 +1,19 @@ +//ignore_for_file: public_member_api_docs + +import 'package:flutter/material.dart'; + +/// App text style. +enum AppTextStyle { + regular14(TextStyle(fontSize: 14, height: 1.4)), + regular16(TextStyle(fontSize: 16, height: 1.24)), + + medium14(TextStyle(fontSize: 14, height: 1.4, fontWeight: FontWeight.w500)), + medium16(TextStyle(fontSize: 16, height: 1.24, fontWeight: FontWeight.w500)), + + bold14(TextStyle(fontSize: 14, height: 1.4, fontWeight: FontWeight.w700)), + bold16(TextStyle(fontSize: 16, height: 1.24, fontWeight: FontWeight.w700)); + + final TextStyle value; + + const AppTextStyle(this.value); +} diff --git a/lib/assets/themes/theme_data.dart b/lib/assets/themes/theme_data.dart new file mode 100644 index 0000000..bc083bf --- /dev/null +++ b/lib/assets/themes/theme_data.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:surf_flutter_study_jam_2023/assets/text/text_extension.dart'; + +/// Class of the app themes data. +abstract class AppThemeData { + /// Light theme configuration. + static final ThemeData lightTheme = ThemeData( + useMaterial3: true, + brightness: Brightness.light, + extensions: [_textTheme], + ); + + /// Dark theme configuration. + static final ThemeData darkTheme = ThemeData( + brightness: Brightness.dark, + extensions: [_textTheme], + ); + + static final _textTheme = AppTextTheme.base(); +} \ No newline at end of file diff --git a/lib/data/database.dart b/lib/data/database.dart new file mode 100644 index 0000000..57cbad3 --- /dev/null +++ b/lib/data/database.dart @@ -0,0 +1,33 @@ +import 'dart:io'; + +import 'package:drift/drift.dart'; +import 'package:drift/native.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; +import 'package:surf_flutter_study_jam_2023/data/tickets_table.dart'; + +part 'database.g.dart'; + +@DriftDatabase( + tables: [ + TicketsTable, + ], +) +class AppDb extends _$AppDb { + @override + int get schemaVersion => 1; + + AppDb() : super(_openConnection()); +} + +LazyDatabase _openConnection() { + return LazyDatabase(() async { + final dbFolder = await getApplicationDocumentsDirectory(); + final file = File(p.join(dbFolder.path, 'db.sqlite')); + + return NativeDatabase.createInBackground( + file, + logStatements: true, + ); + }); +} diff --git a/lib/data/tickets_converter.dart b/lib/data/tickets_converter.dart new file mode 100644 index 0000000..cd60149 --- /dev/null +++ b/lib/data/tickets_converter.dart @@ -0,0 +1,71 @@ +import 'package:surf_flutter_study_jam_2023/data/database.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/value_objects.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/file_path/value_object.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/model.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/status/value_object.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/title/value_object.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/url/value_object.dart'; + +class TicketModelPersistenceModelConverter { + TicketModel toDomainModel(TicketsPersistenceModel pm) { + late TicketStatus status; + + switch (pm.status) { + case 'initialized': + status = const TicketStatus.initialized(); + break; + case 'readyToLoadFile': + status = TicketStatus.readyToLoadFile(TicketUrl(pm.url ?? '')); + break; + case 'loadingFile': + status = TicketStatus.loadingFile( + pm.currentSize ?? 0, + pm.totalSize ?? 0, + ); + break; + case 'fileLoaded': + status = TicketStatus.fileLoaded(TicketFilePath(pm.filePath ?? '')); + break; + } + + return TicketModel( + id: UniqueId.fromUniqueString(pm.id), + title: TicketTitle(pm.title), + status: status, + dateCreated: pm.dateCreated, + ); + } + + TicketsPersistenceModel toPersistenceModel(TicketModel m) { + return TicketsPersistenceModel( + id: m.id.getOrCrash(), + title: m.title.value.fold((l) => l.failedValue, (r) => r), + status: m.status.when( + initialized: () => 'initialized', + readyToLoadFile: (_) => 'readyToLoadFile', + loadingFile: (_, __) => 'loadingFile', + fileLoaded: (_) => 'fileLoaded', + paused: (_) => 'paused', + ), + filePath: m.status.maybeWhen( + fileLoaded: (status) => + status.value.fold((l) => l.failedValue, (r) => r), + orElse: () => null, + ), + url: m.status.maybeWhen( + readyToLoadFile: (status) => + status.value.fold((l) => l.failedValue, (r) => r), + orElse: () => null, + ), + currentSize: m.status.maybeWhen( + loadingFile: (currentSize, _) => currentSize, + orElse: () => null, + ), + totalSize: m.status.maybeWhen( + loadingFile: (_, totalSize) => totalSize, + orElse: () => null, + ), + dateCreated: m.dateCreated, + ); + } +} diff --git a/lib/data/tickets_table.dart b/lib/data/tickets_table.dart new file mode 100644 index 0000000..f218169 --- /dev/null +++ b/lib/data/tickets_table.dart @@ -0,0 +1,15 @@ +import 'package:drift/drift.dart'; + +@DataClassName('TicketsPersistenceModel') +class TicketsTable extends Table { + TextColumn get id => text()(); + TextColumn get title => text()(); + TextColumn get status => text()(); + TextColumn get url => text().nullable()(); + TextColumn get filePath => text().nullable()(); + IntColumn get currentSize => integer().nullable()(); + IntColumn get totalSize => integer().nullable()(); + DateTimeColumn get dateCreated => dateTime()(); + @override + Set get primaryKey => {id}; +} diff --git a/lib/features/core/domain/common_interfaces.dart b/lib/features/core/domain/common_interfaces.dart new file mode 100644 index 0000000..91c0212 --- /dev/null +++ b/lib/features/core/domain/common_interfaces.dart @@ -0,0 +1,3 @@ +abstract class IValidatable { + bool isValid(); +} diff --git a/lib/features/core/domain/entity.dart b/lib/features/core/domain/entity.dart new file mode 100644 index 0000000..66a1fa4 --- /dev/null +++ b/lib/features/core/domain/entity.dart @@ -0,0 +1,5 @@ +import 'package:surf_flutter_study_jam_2023/features/core/domain/value_objects.dart'; + +abstract class IEntity { + UniqueId get id; +} diff --git a/lib/features/core/domain/errors.dart b/lib/features/core/domain/errors.dart new file mode 100644 index 0000000..c4adf40 --- /dev/null +++ b/lib/features/core/domain/errors.dart @@ -0,0 +1,16 @@ +import 'package:surf_flutter_study_jam_2023/features/core/domain/failures.dart'; + +class NotAuthenticatedError extends Error {} + +class UnexpectedValueError extends Error { + final ValueFailure valueFailure; + + UnexpectedValueError(this.valueFailure); + + @override + String toString() { + const explanation = + 'Encountered a ValueFailure at an unrecoverable point. Terminating.'; + return Error.safeToString('$explanation Failure was: $valueFailure'); + } +} diff --git a/lib/features/core/domain/failures.dart b/lib/features/core/domain/failures.dart new file mode 100644 index 0000000..d16703a --- /dev/null +++ b/lib/features/core/domain/failures.dart @@ -0,0 +1,20 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'failures.freezed.dart'; + +@freezed +class ValueFailure with _$ValueFailure { + const factory ValueFailure.exceedingLength({ + required T failedValue, + required int max, + }) = ExceedingLength; + const factory ValueFailure.empty({ + required T failedValue, + }) = Empty; + const factory ValueFailure.multiline({ + required T failedValue, + }) = Multiline; + const factory ValueFailure.invalidFileUrl({ + required T failedValue, + }) = InvalidFileUrl; +} diff --git a/lib/features/core/domain/value_objects.dart b/lib/features/core/domain/value_objects.dart new file mode 100644 index 0000000..4110e66 --- /dev/null +++ b/lib/features/core/domain/value_objects.dart @@ -0,0 +1,83 @@ +import 'package:dartz/dartz.dart'; +import 'package:meta/meta.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/common_interfaces.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/errors.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/failures.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/value_validators.dart'; +import 'package:uuid/uuid.dart'; + +@immutable +abstract class ValueObject implements IValidatable { + Either, Unit> get failureOrUnit { + return value.fold( + (l) => left(l), + (r) => right(unit), + ); + } + + Either, T> get value; + + @override + int get hashCode => value.hashCode; + + const ValueObject(); + + @override + bool isValid() { + return value.isRight(); + } + + @override + bool operator ==(Object o) { + if (identical(this, o)) return true; + + return o is ValueObject && o.value == value; + } + + @override + String toString() => 'Value($value)'; + + /// Throws [UnexpectedValueError] containing the [ValueFailure]. + T getOrCrash() { + // Id = identity - same as writing (right) => right. + return value.fold((f) => throw UnexpectedValueError(f), id); + } + + T getOrElse(T defaultValue) { + return value.getOrElse(() => defaultValue); + } +} + +class UniqueId extends ValueObject { + @override + final Either, String> value; + + // We cannot let a simple String be passed in. This would allow for possible non-unique IDs. + factory UniqueId() { + return UniqueId._( + right(const Uuid().v4()), + ); + } + + const UniqueId._(this.value); + + /// Used with strings we trust are unique, such as database IDs. + factory UniqueId.fromUniqueString(String uniqueIdStr) { + return UniqueId._( + right(uniqueIdStr), + ); + } +} + +class StringSingleLine extends ValueObject { + @override + final Either, String> value; + + factory StringSingleLine(String input) { + return StringSingleLine._( + validateSingleLine(input), + ); + } + + const StringSingleLine._(this.value); +} diff --git a/lib/features/core/domain/value_validators.dart b/lib/features/core/domain/value_validators.dart new file mode 100644 index 0000000..23c66fd --- /dev/null +++ b/lib/features/core/domain/value_validators.dart @@ -0,0 +1,32 @@ +import 'package:dartz/dartz.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/failures.dart'; + +Either, String> validateMaxStringLength( + String input, + int maxLength, +) { + if (input.length <= maxLength) { + return right(input); + } + + return left(ValueFailure.exceedingLength( + failedValue: input, + max: maxLength, + )); +} + +Either, String> validateStringNotEmpty(String input) { + if (input.isEmpty) { + return left(ValueFailure.empty(failedValue: input)); + } + + return right(input); +} + +Either, String> validateSingleLine(String input) { + if (input.contains('\n')) { + return left(ValueFailure.multiline(failedValue: input)); + } + + return right(input); +} diff --git a/lib/features/ticket_storage/domain/tickets/repository.dart b/lib/features/ticket_storage/domain/tickets/repository.dart new file mode 100644 index 0000000..ea7e60f --- /dev/null +++ b/lib/features/ticket_storage/domain/tickets/repository.dart @@ -0,0 +1,104 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; +import 'package:drift/drift.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:surf_flutter_study_jam_2023/data/database.dart'; +import 'package:surf_flutter_study_jam_2023/data/tickets_converter.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/model.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket_failure.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/uploading_file/model.dart'; +import 'package:uuid/uuid.dart'; + +class TicketsRepository { + final AppDb _db; + final Dio _dio; + final TicketModelPersistenceModelConverter _converter; + + const TicketsRepository( + this._db, + this._converter, + this._dio, + ); + + Future uploadFile( + UploadingTicketFile file, + void Function(UploadingTicketFile file) update, + ) async { + + final appDirectory = await getApplicationDocumentsDirectory(); + final savePath = '${appDirectory.path}/${const Uuid().v4()}.pdf'; + + file.ticket.status.maybeWhen( + readyToLoadFile: (url) { + _dio.download( + url.getOrCrash(), + savePath, + onReceiveProgress: (receivedBytes, totalBytes) { + if (file.cancelToken.isCancelled) { + // todo uploading status pause + + update(file); + } else { + // todo uploading status progress + // print('Received: ${receivedBytes ~/ 1024} KB, Total: ${totalBytes ~/ 1024} KB'); + + update(file); + } + }, + cancelToken: file.cancelToken, + ); + }, + orElse: () { + + }, + ); + } + + Future>> getAll() async { + try { + final persistenceModels = await (_db.select(_db.ticketsTable) + ..orderBy([ + (place) => OrderingTerm( + expression: place.dateCreated, + mode: OrderingMode.desc, + ), + ])) + .get(); + + final converter = TicketModelPersistenceModelConverter(); + + return right( + persistenceModels.map(converter.toDomainModel).toBuiltList(), + ); + } on Exception { + return left(const TicketFailure.unexpected()); + } + } + + Future> save(TicketModel ticket) async { + try { + await _db.into(_db.ticketsTable).insertOnConflictUpdate( + _converter.toPersistenceModel(ticket), + ); + + return right(ticket); + } on Exception { + return left(const TicketFailure.unexpected()); + } + } + + Future> delete(TicketModel ticket) async { + try { + await (_db.delete(_db.ticketsTable) + ..where( + (t) => t.id.equals(ticket.id.getOrCrash()), + )) + .go(); + + return right(unit); + } on Exception { + return left(const TicketFailure.unexpected()); + } + } +} diff --git a/lib/features/ticket_storage/domain/tickets/ticket/file_path/value_object.dart b/lib/features/ticket_storage/domain/tickets/ticket/file_path/value_object.dart new file mode 100644 index 0000000..7ef8400 --- /dev/null +++ b/lib/features/ticket_storage/domain/tickets/ticket/file_path/value_object.dart @@ -0,0 +1,18 @@ +import 'package:dartz/dartz.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/failures.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/value_objects.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/value_validators.dart'; + +class TicketFilePath extends ValueObject { + @override + final Either, String> value; + + factory TicketFilePath(String input) { + return TicketFilePath._( + validateStringNotEmpty(input) + .flatMap(validateSingleLine), + ); + } + + const TicketFilePath._(this.value); +} diff --git a/lib/features/ticket_storage/domain/tickets/ticket/model.dart b/lib/features/ticket_storage/domain/tickets/ticket/model.dart new file mode 100644 index 0000000..b31edd7 --- /dev/null +++ b/lib/features/ticket_storage/domain/tickets/ticket/model.dart @@ -0,0 +1,47 @@ +import 'package:dartz/dartz.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/failures.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/value_objects.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/status/value_object.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/title/value_object.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/url/value_object.dart'; + +part 'model.freezed.dart'; + +@freezed +class TicketModel with _$TicketModel { + const factory TicketModel({ + required UniqueId id, + required TicketTitle title, + required TicketStatus status, + required DateTime dateCreated, + }) = _TicketModel; + + const TicketModel._(); + + factory TicketModel.empty(DateTime? dateCreated) => TicketModel( + id: UniqueId(), + title: TicketTitle(''), + status: const TicketStatus.initialized(), + dateCreated: dateCreated ?? DateTime.now(), + ); + + TicketModel editUrl(String url) { + return copyWith( + status: TicketStatus.readyToLoadFile(TicketUrl(url)), + ); + } +} + +extension TicketModelX on TicketModel { + Option> get failureOption { + return title.failureOrUnit + .andThen(status.maybeMap( + readyToLoadFile: (status) => status.url.failureOrUnit, + orElse: () { + return right(unit); + }, + )) + .fold(some, (_) => none()); + } +} diff --git a/lib/features/ticket_storage/domain/tickets/ticket/status/value_object.dart b/lib/features/ticket_storage/domain/tickets/ticket/status/value_object.dart new file mode 100644 index 0000000..5d49666 --- /dev/null +++ b/lib/features/ticket_storage/domain/tickets/ticket/status/value_object.dart @@ -0,0 +1,15 @@ +import 'package:dio/dio.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/file_path/value_object.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/url/value_object.dart'; + +part 'value_object.freezed.dart'; + +@freezed +class TicketStatus with _$TicketStatus { + const factory TicketStatus.initialized() = Initialized; + const factory TicketStatus.readyToLoadFile(TicketUrl url) = ReadyToLoadFile; + const factory TicketStatus.loadingFile(int currentSize, int totalSize) = Loading; + const factory TicketStatus.fileLoaded(TicketFilePath file) = FileLoaded; + const factory TicketStatus.paused(CancelToken token) = Pause; +} \ No newline at end of file diff --git a/lib/features/ticket_storage/domain/tickets/ticket/title/value_object.dart b/lib/features/ticket_storage/domain/tickets/ticket/title/value_object.dart new file mode 100644 index 0000000..b25086d --- /dev/null +++ b/lib/features/ticket_storage/domain/tickets/ticket/title/value_object.dart @@ -0,0 +1,17 @@ +import 'package:dartz/dartz.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/failures.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/value_objects.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/value_validators.dart'; + +class TicketTitle extends ValueObject { + @override + final Either, String> value; + + factory TicketTitle(String input) { + return TicketTitle._( + validateStringNotEmpty(input).flatMap(validateSingleLine), + ); + } + + const TicketTitle._(this.value); +} diff --git a/lib/features/ticket_storage/domain/tickets/ticket/url/value_object.dart b/lib/features/ticket_storage/domain/tickets/ticket/url/value_object.dart new file mode 100644 index 0000000..133c4eb --- /dev/null +++ b/lib/features/ticket_storage/domain/tickets/ticket/url/value_object.dart @@ -0,0 +1,34 @@ + + +import 'package:dartz/dartz.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/failures.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/value_objects.dart'; +import 'package:surf_flutter_study_jam_2023/features/core/domain/value_validators.dart'; + +class TicketUrl extends ValueObject { + @override + final Either, String> value; + + factory TicketUrl(String input) { + return TicketUrl._( + validateStringNotEmpty(input) + .flatMap(validateSingleLine) + .flatMap(validateTicketUrl), + ); + } + + const TicketUrl._(this.value); +} + +/// Ссылка начинается с http:// или https:// и заканчивается на .pdf. +Either, String> validateTicketUrl(String input) { + final r = RegExp(r'^https?://.*\.pdf$'); + + if (r.hasMatch(input)) { + return right(input); + } + + return left(ValueFailure.invalidFileUrl( + failedValue: input, + )); +} \ No newline at end of file diff --git a/lib/features/ticket_storage/domain/tickets/ticket_failure.dart b/lib/features/ticket_storage/domain/tickets/ticket_failure.dart new file mode 100644 index 0000000..d32c3df --- /dev/null +++ b/lib/features/ticket_storage/domain/tickets/ticket_failure.dart @@ -0,0 +1,8 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'ticket_failure.freezed.dart'; + +@freezed +class TicketFailure with _$TicketFailure { + const factory TicketFailure.unexpected() = Unexpected; +} diff --git a/lib/features/ticket_storage/domain/tickets/uploading_file/model.dart b/lib/features/ticket_storage/domain/tickets/uploading_file/model.dart new file mode 100644 index 0000000..e4733d2 --- /dev/null +++ b/lib/features/ticket_storage/domain/tickets/uploading_file/model.dart @@ -0,0 +1,13 @@ +import 'package:dio/dio.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/model.dart'; + +part 'model.freezed.dart'; + +@freezed +class UploadingTicketFile with _$UploadingTicketFile { + factory UploadingTicketFile({ + required CancelToken cancelToken, + required TicketModel ticket, + }) = _UploadingTicketFile; +} \ No newline at end of file diff --git a/lib/features/ticket_storage/ticket_storage_page.dart b/lib/features/ticket_storage/ticket_storage_page.dart deleted file mode 100644 index 1ab6dd5..0000000 --- a/lib/features/ticket_storage/ticket_storage_page.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:flutter/material.dart'; - -/// Экран “Хранения билетов”. -class TicketStoragePage extends StatelessWidget { - const TicketStoragePage({Key? key}) : super(key: key); - - @override - Widget build(BuildContext context) { - return const FlutterLogo(); - } -} diff --git a/lib/features/ticket_storage/ui/screen/ticket_storage_screen.dart b/lib/features/ticket_storage/ui/screen/ticket_storage_screen.dart new file mode 100644 index 0000000..e99d042 --- /dev/null +++ b/lib/features/ticket_storage/ui/screen/ticket_storage_screen.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/model.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/ui/screen/ticket_storage_screen_model.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/ui/widgets/add_new_ticker_button/widget.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/ui/widgets/tickets/widget.dart'; + +/// Экран “Хранения билетов”. +class TicketStorageScreen extends StatelessWidget { + const TicketStorageScreen({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final state = context.read(); + + return Scaffold( + body: TicketsWidget( + onInit: () { + state.loadTickets(); + }, + ), + floatingActionButton: AddNewTicketButton( + onPressed: () async { + final ticket = await showModalBottomSheet( + context: context, + builder: (BuildContext context) { + return Container( + height: 200.0, + color: Colors.white, + child: Center( + child: Text('This is a bottom sheet'), + ), + ); + }, + ); + }, + ), + ); + } +} diff --git a/lib/features/ticket_storage/ui/screen/ticket_storage_screen_model.dart b/lib/features/ticket_storage/ui/screen/ticket_storage_screen_model.dart new file mode 100644 index 0000000..7e2b9f4 --- /dev/null +++ b/lib/features/ticket_storage/ui/screen/ticket_storage_screen_model.dart @@ -0,0 +1,161 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:dartz/dartz.dart'; +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/repository.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/model.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket_failure.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/uploading_file/model.dart'; + +class TicketStorageScreenModel extends ChangeNotifier { + final TicketsRepository _repository; + + TicketsData tickets; + BuiltList uploadingTicketFiles; + + TicketStorageScreenModel({ + required TicketsRepository repository, + required this.tickets, + required this.uploadingTicketFiles, + }) : _repository = repository; + + factory TicketStorageScreenModel.init(TicketsRepository repository) { + return TicketStorageScreenModel( + repository: repository, + tickets: TicketsData.init(), + uploadingTicketFiles: BuiltList([]), + ); + } + + void loadTickets() { + tickets = TicketsData.loading(); + + _repository.getAll().then((data) { + tickets = TicketsData.loaded(data); + + notifyListeners(); + }); + + notifyListeners(); + } + + void addTicket(TicketModel ticket) { + _repository.save(ticket); + tickets = tickets.data.fold( + (l) => tickets.update([ticket].toBuiltList()), + (r) => tickets.update(r.rebuild((b) => b.insert(0, ticket))), + ); + + notifyListeners(); + } + + void saveTicket(TicketModel ticket) { + _repository.save(ticket); + + tickets = tickets.data.fold( + (l) => tickets.update([ticket].toBuiltList()), + (r) => tickets.update(r.rebuild( + (b) => b + ..removeWhere((t) => t.id == ticket.id) + ..insert(0, ticket), + )), + ); + + notifyListeners(); + } + + void deleteTicket(TicketModel ticket) { + _repository.delete(ticket); + + tickets = tickets.data.fold( + (l) => TicketsData.init(), + (r) => tickets.update( + r.rebuild((b) => b.removeWhere((t) => t.id == ticket.id)), + ), + ); + + notifyListeners(); + } + + void uploadTicketFile(TicketModel ticket) { + final token = CancelToken(); + final file = UploadingTicketFile( + ticket: ticket, + cancelToken: token, + ); + uploadingTicketFiles.rebuild((b) => b.add(file)); + + _repository.uploadFile( + file, + (nextFile) { + uploadingTicketFiles.rebuild( + (b) => b.remove(file), + ); + }, + ); + + notifyListeners(); + } + + void _updateTickets(TicketModel ticket) { + tickets = tickets.data.fold( + (l) => tickets.update([ticket].toBuiltList()), + (r) { + final index = r.indexWhere((t) => t.id == ticket.id); + + return tickets.update(r.rebuild( + (b) { + b + ..removeWhere((t) => t.id == ticket.id) + ..replaceRange(index, index + 1, [ticket]); + }, + )); + }, + ); + } +} + +class TicketsData { + final bool loading; + final bool loaded; + final Either> data; + + const TicketsData({ + required this.data, + required this.loading, + required this.loaded, + }); + + factory TicketsData.init() { + return TicketsData( + data: right(BuiltList([])), + loading: false, + loaded: false, + ); + } + + factory TicketsData.loading() { + return TicketsData( + data: right(BuiltList([])), + loading: true, + loaded: false, + ); + } + + factory TicketsData.loaded( + Either> data) { + return TicketsData( + data: data, + loading: false, + loaded: true, + ); + } + + TicketsData update(BuiltList data) { + return TicketsData( + data: right(data), + loading: false, + loaded: true, + ); + } +} diff --git a/lib/features/ticket_storage/ui/widgets/add_new_ticker_button/widget.dart b/lib/features/ticket_storage/ui/widgets/add_new_ticker_button/widget.dart new file mode 100644 index 0000000..6c244ae --- /dev/null +++ b/lib/features/ticket_storage/ui/widgets/add_new_ticker_button/widget.dart @@ -0,0 +1,25 @@ +import 'package:flutter/material.dart'; +import 'package:surf_flutter_study_jam_2023/assets/strings/locale/ru.dart'; +import 'package:surf_flutter_study_jam_2023/assets/text/text_extension.dart'; + +class AddNewTicketButton extends StatelessWidget { + final VoidCallback onPressed; + + const AddNewTicketButton({ + super.key, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + final textTheme = AppTextTheme.of(context); + + return FloatingActionButton.extended( + onPressed: onPressed, + label: Text( + AppStrings.ticketStorage.addButtonLabel, + style: textTheme.medium14, + ), + ); + } +} diff --git a/lib/features/ticket_storage/ui/widgets/ticket_editing/widget.dart b/lib/features/ticket_storage/ui/widgets/ticket_editing/widget.dart new file mode 100644 index 0000000..e69de29 diff --git a/lib/features/ticket_storage/ui/widgets/ticket_title_field/widget.dart b/lib/features/ticket_storage/ui/widgets/ticket_title_field/widget.dart new file mode 100644 index 0000000..8b32189 --- /dev/null +++ b/lib/features/ticket_storage/ui/widgets/ticket_title_field/widget.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:surf_flutter_study_jam_2023/assets/strings/locale/ru.dart'; +import 'package:surf_flutter_study_jam_2023/assets/text/text_extension.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/model.dart'; + +class TicketTitleField extends StatefulWidget { + final TicketModel ticket; + final ValueChanged onUrlChanged; + + const TicketTitleField({ + super.key, + required this.ticket, + required this.onUrlChanged, + }); + + @override + State createState() => _TicketTitleFieldState(); +} + +class _TicketTitleFieldState extends State { + late final TextEditingController _urlController; + + @override + void initState() { + super.initState(); + + final initialUrl = widget.ticket.status.maybeWhen( + initialized: () => '', + readyToLoadFile: (url) => url.value.fold( + (l) => l.failedValue, + (r) => r, + ), + orElse: () => '', + ); + _urlController = TextEditingController(text: initialUrl); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: _urlController, + decoration: InputDecoration( + labelText: AppStrings.ticketStorage.enterUrlPlaceholder, + border: const OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + widget.onUrlChanged(_urlController.text); + Navigator.pop(context); + }, + child: Text( + AppStrings.ticketStorage.addButtonLabel, + style: AppTextTheme.of(context).regular14, + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/features/ticket_storage/ui/widgets/ticket_url_edit_field/widget.dart b/lib/features/ticket_storage/ui/widgets/ticket_url_edit_field/widget.dart new file mode 100644 index 0000000..effbbeb --- /dev/null +++ b/lib/features/ticket_storage/ui/widgets/ticket_url_edit_field/widget.dart @@ -0,0 +1,69 @@ +import 'package:flutter/material.dart'; +import 'package:surf_flutter_study_jam_2023/assets/strings/locale/ru.dart'; +import 'package:surf_flutter_study_jam_2023/assets/text/text_extension.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/model.dart'; + +class TicketUrlField extends StatefulWidget { + final TicketModel ticket; + final ValueChanged onUrlChanged; + + const TicketUrlField({ + super.key, + required this.ticket, + required this.onUrlChanged, + }); + + @override + State createState() => _TicketUrlFieldState(); +} + +class _TicketUrlFieldState extends State { + late final TextEditingController _urlController; + + @override + void initState() { + super.initState(); + + final initialUrl = widget.ticket.status.maybeWhen( + initialized: () => '', + readyToLoadFile: (url) => url.value.fold( + (l) => l.failedValue, + (r) => r, + ), + orElse: () => '', + ); + _urlController = TextEditingController(text: initialUrl); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + TextField( + controller: _urlController, + decoration: InputDecoration( + labelText: AppStrings.ticketStorage.enterUrlPlaceholder, + border: const OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () { + widget.onUrlChanged(_urlController.text); + Navigator.pop(context); + }, + child: Text( + AppStrings.ticketStorage.addButtonLabel, + style: AppTextTheme.of(context).regular14, + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/features/ticket_storage/ui/widgets/tickets/list/item/widget.dart b/lib/features/ticket_storage/ui/widgets/tickets/list/item/widget.dart new file mode 100644 index 0000000..b77138d --- /dev/null +++ b/lib/features/ticket_storage/ui/widgets/tickets/list/item/widget.dart @@ -0,0 +1,55 @@ +import 'package:flutter/material.dart'; +import 'package:surf_flutter_study_jam_2023/assets/strings/locale/ru.dart'; +import 'package:surf_flutter_study_jam_2023/assets/text/text_extension.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/model.dart'; + +class TicketListItemWidget extends StatelessWidget { + final TicketModel ticket; + final ValueChanged onTap; + + const TicketListItemWidget({ + super.key, + required this.ticket, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: () { + onTap(ticket); + }, + child: ListTile( + leading: const Icon(Icons.airplane_ticket_outlined), + trailing: ticket.status.when( + initialized: () => const Icon( + Icons.edit, + ), + readyToLoadFile: (url) => const Icon( + Icons.cloud_download_outlined, + ), + loadingFile: (_, __) => const Icon( + Icons.pause_circle_outline, + ), + paused: (_) => const Icon( + Icons.pause_circle_outline, + ), + fileLoaded: (filePath) => const Icon( + Icons.cloud_done, + ), + ), + title: Text( + ticket.title.value.fold( + (l) => l.failedValue, + (r) => r, + ), + style: AppTextTheme.of(context).regular16, + ), + subtitle: Text( + AppStrings.ticketStorage.statusTitle(ticket.status), + style: AppTextTheme.of(context).regular14, + ), + ), + ); + } +} diff --git a/lib/features/ticket_storage/ui/widgets/tickets/list/widget.dart b/lib/features/ticket_storage/ui/widgets/tickets/list/widget.dart new file mode 100644 index 0000000..807b578 --- /dev/null +++ b/lib/features/ticket_storage/ui/widgets/tickets/list/widget.dart @@ -0,0 +1,42 @@ + +import 'package:built_collection/built_collection.dart'; +import 'package:flutter/material.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/model.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/ui/widgets/tickets/list/item/widget.dart'; + +class TicketsListWidget extends StatelessWidget { + final BuiltList tickets; + + const TicketsListWidget({ + required this.tickets, + Key? key, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return SliverPadding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + childCount: tickets.length * 2 - 1, + semanticIndexCallback: (_, index) { + return index.isEven ? index ~/ 2 : null; + }, + (context, index) { + final itemIndex = index ~/ 2; + if (index.isOdd) { + return const SizedBox(height: 12.0); + } + + return TicketListItemWidget( + ticket: tickets[itemIndex], + onTap: (ticket) { + + }, + ); + }, + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/features/ticket_storage/ui/widgets/tickets/widget.dart b/lib/features/ticket_storage/ui/widgets/tickets/widget.dart new file mode 100644 index 0000000..cee7360 --- /dev/null +++ b/lib/features/ticket_storage/ui/widgets/tickets/widget.dart @@ -0,0 +1,86 @@ +import 'package:built_collection/built_collection.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:surf_flutter_study_jam_2023/assets/strings/locale/ru.dart'; +import 'package:surf_flutter_study_jam_2023/assets/text/text_extension.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/ticket/model.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/ui/screen/ticket_storage_screen_model.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/ui/widgets/tickets/list/widget.dart'; + +class TicketsWidget extends StatefulWidget { + final VoidCallback onInit; + + const TicketsWidget({ + super.key, + required this.onInit, + }); + + @override + State createState() => _TicketsWidgetState(); +} + +class _TicketsWidgetState extends State { + @override + void initState() { + super.initState(); + + WidgetsBinding.instance.addPostFrameCallback((_) { + widget.onInit(); + }); + } + + @override + Widget build(BuildContext context) { + final textTheme = AppTextTheme.of(context); + + return CustomScrollView( + slivers: [ + SliverAppBar( + title: Text( + AppStrings.ticketStorage.ticketsTitle, + style: textTheme.medium16, + ), + floating: true, + pinned: true, + ), + const _TicketsContent(), + ], + ); + } +} + +class _TicketsContent extends StatelessWidget { + const _TicketsContent({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final model = + context.select((s) => s.tickets); + + if (model.loading) { + return const SliverToBoxAdapter( + child: Center(child: CircularProgressIndicator()), + ); + } + + return model.data.fold( + (failure) { + return SliverToBoxAdapter( + child: + Center(child: Text(AppStrings.ticketStorage.unexpectedFailure)), + ); + }, + (tickets) { + if (tickets.isEmpty) { + return SliverToBoxAdapter( + child: Center(child: Text(AppStrings.ticketStorage.emptyList)), + ); + } + + return TicketsListWidget( + tickets: tickets, + ); + }, + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index c1be491..7aad0b8 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,21 +1,44 @@ +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; -import 'package:surf_flutter_study_jam_2023/features/ticket_storage/ticket_storage_page.dart'; +import 'package:provider/provider.dart'; +import 'package:surf_flutter_study_jam_2023/assets/themes/theme_data.dart'; +import 'package:surf_flutter_study_jam_2023/data/database.dart'; +import 'package:surf_flutter_study_jam_2023/data/tickets_converter.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/domain/tickets/repository.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/ui/screen/ticket_storage_screen.dart'; +import 'package:surf_flutter_study_jam_2023/features/ticket_storage/ui/screen/ticket_storage_screen_model.dart'; void main() { - runApp(const MyApp()); + WidgetsFlutterBinding.ensureInitialized(); + final database = AppDb(); + final dio = Dio(); + final ticketsRepository = TicketsRepository( + database, + TicketModelPersistenceModelConverter(), + dio, + ); + + runApp( + MultiProvider( + providers: [ + ChangeNotifierProvider( + create: (_) => TicketStorageScreenModel.init(ticketsRepository), + ), + ], + child: const App(), + ), + ); } -class MyApp extends StatelessWidget { - const MyApp({Key? key}) : super(key: key); +class App extends StatelessWidget { + const App({Key? key}) : super(key: key); @override Widget build(BuildContext context) { return MaterialApp( title: 'Flutter Demo', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - home: const TicketStoragePage(), + theme: AppThemeData.lightTheme, + home: const TicketStorageScreen(), ); } } diff --git a/pubspec.lock b/pubspec.lock index b2cb69d..54425cc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,14 +1,54 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: a36ec4843dc30ea6bf652bf25e3448db6c5e8bcf4aa55f063a5d1dad216d8214 + url: "https://pub.dev" + source: hosted + version: "58.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: cc4242565347e98424ce9945c819c192ec0838cb9d1f6aa4a97cc96becbc5b27 + url: "https://pub.dev" + source: hosted + version: "5.10.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: c1d5f167683de03d5ab6c3b53fc9aeefc5d59476e7810ba7bbddff50c6f4392d + url: "https://pub.dev" + source: hosted + version: "0.11.2" + ansicolor: + dependency: transitive + description: + name: ansicolor + sha256: "607f8fa9786f392043f169898923e6c59b4518242b68b8862eb8a8b7d9c30b4a" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + args: + dependency: transitive + description: + name: args + sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + url: "https://pub.dev" + source: hosted + version: "2.4.0" async: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -17,14 +57,102 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + url: "https://pub.dev" + source: hosted + version: "2.3.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + url: "https://pub.dev" + source: hosted + version: "4.0.0" + build_resolvers: + dependency: transitive + description: + name: build_resolvers + sha256: db49b8609ef8c81cca2b310618c3017c00f03a92af44c04d310b907b2d692d95 + url: "https://pub.dev" + source: hosted + version: "2.2.0" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "7b25ba738bc74c94187cebeb9cc29d38a32e8279ce950eabd821d3b454a5f03d" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + url: "https://pub.dev" + source: hosted + version: "7.2.7" + built_collection: + dependency: "direct main" + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: "31b7c748fd4b9adf8d25d72a4c4a59ef119f12876cf414f94f8af5131d5fa2b0" + url: "https://pub.dev" + source: hosted + version: "8.4.4" characters: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" + charcode: + dependency: transitive + description: + name: charcode + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" + source: hosted + version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + url: "https://pub.dev" + source: hosted + version: "2.0.2" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + url: "https://pub.dev" + source: hosted + version: "0.4.0" clock: dependency: transitive description: @@ -33,14 +161,46 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0d43dd1288fd145de1ecc9a3948ad4a6d5a82f0a14c4fdd0892260787d975cbe" + url: "https://pub.dev" + source: hosted + version: "4.4.0" collection: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" + url: "https://pub.dev" + source: hosted + version: "1.17.1" + convert: + dependency: transitive + description: + name: convert + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + url: "https://pub.dev" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + url: "https://pub.dev" + source: hosted + version: "3.0.2" + csslib: + dependency: transitive + description: + name: csslib + sha256: b36c7f7e24c0bdf1bf9a3da461c837d1de64b9f8beb190c9011d8c72a3dfd745 url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "0.17.2" cupertino_icons: dependency: "direct main" description: @@ -49,6 +209,62 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + dart_code_metrics: + dependency: transitive + description: + name: dart_code_metrics + sha256: "7d16c5cf8648ad44de679040f7ee0fdc0ac751ad2769a4ea49fed54f58e98c98" + url: "https://pub.dev" + source: hosted + version: "5.7.1" + dart_code_metrics_presets: + dependency: transitive + description: + name: dart_code_metrics_presets + sha256: "9c9b0ecc5e23937a4b62c1fe3b3edb72d3c1b08563a1eac69fb75f1306bb57c4" + url: "https://pub.dev" + source: hosted + version: "1.6.0" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "6d691edde054969f0e0f26abb1b30834b5138b963793e56f69d3a9a4435e6352" + url: "https://pub.dev" + source: hosted + version: "2.3.0" + dartz: + dependency: "direct main" + description: + name: dartz + sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 + url: "https://pub.dev" + source: hosted + version: "0.10.1" + dio: + dependency: "direct main" + description: + name: dio + sha256: "0894a098594263fe1caaba3520e3016d8a855caeb010a882273189cca10f11e9" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + drift: + dependency: "direct main" + description: + name: drift + sha256: a843a73ccb6703b7dec59c0b3ebf9404a1e534d22289ce6399e7a440c604b9b4 + url: "https://pub.dev" + source: hosted + version: "2.6.0" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: "152bd8cd782e88beef2a5b2eff8036b90f7f9f310b6790be103f4048d6ee9208" + url: "https://pub.dev" + source: hosted + version: "2.6.0" fake_async: dependency: transitive description: @@ -57,13 +273,37 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + file: + dependency: transitive + description: + name: file + sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + url: "https://pub.dev" + source: hosted + version: "6.1.4" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" flutter_lints: - dependency: "direct dev" + dependency: transitive description: name: flutter_lints sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c @@ -75,14 +315,110 @@ packages: description: flutter source: sdk version: "0.0.0" + freezed: + dependency: "direct dev" + description: + name: freezed + sha256: e819441678f1679b719008ff2ff0ef045d66eed9f9ec81166ca0d9b02a187454 + url: "https://pub.dev" + source: hosted + version: "2.3.2" + freezed_annotation: + dependency: "direct main" + description: + name: freezed_annotation + sha256: aeac15850ef1b38ee368d4c53ba9a847e900bb2c53a4db3f6881cbb3cb684338 + url: "https://pub.dev" + source: hosted + version: "2.2.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + url: "https://pub.dev" + source: hosted + version: "3.2.0" + glob: + dependency: transitive + description: + name: glob + sha256: "4515b5b6ddb505ebdd242a5f2cc5d22d3d6a80013789debfbda7777f47ea308c" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + graphs: + dependency: transitive + description: + name: graphs + sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + url: "https://pub.dev" + source: hosted + version: "2.2.0" + html: + dependency: transitive + description: + name: html + sha256: "79d498e6d6761925a34ee5ea8fa6dfef38607781d2fa91e37523474282af55cb" + url: "https://pub.dev" + source: hosted + version: "0.15.2" + http: + dependency: transitive + description: + name: http + sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + url: "https://pub.dev" + source: hosted + version: "0.13.5" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: "97486f20f9c2f7be8f514851703d0119c3596d14ea63227af6f7a481ef2b2f8b" + url: "https://pub.dev" + source: hosted + version: "3.2.1" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + io: + dependency: transitive + description: + name: io + sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + url: "https://pub.dev" + source: hosted + version: "1.0.4" js: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" + json_annotation: + dependency: "direct main" + description: + name: json_annotation + sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + url: "https://pub.dev" + source: hosted + version: "4.8.0" + json_serializable: + dependency: "direct dev" + description: + name: json_serializable + sha256: dadc08bd61f72559f938dd08ec20dbfec6c709bba83515085ea943d2078d187a + url: "https://pub.dev" + source: hosted + version: "6.6.1" lints: dependency: transitive description: @@ -91,14 +427,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: "04094f2eb032cbb06c6f6e8d3607edcfcb0455e2bb6cbc010cb01171dcb64e6d" + url: "https://pub.dev" + source: hosted + version: "1.1.1" matcher: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -111,23 +455,207 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" + mime: + dependency: transitive + description: + name: mime + sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + url: "https://pub.dev" + source: hosted + version: "1.0.4" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "1c5b77ccc91e4823a5af61ee74e6b972db1ef98c2ff5a18d3161c982a55448bd" + url: "https://pub.dev" + source: hosted + version: "2.1.0" path: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" + url: "https://pub.dev" + source: hosted + version: "1.8.3" + path_provider: + dependency: "direct main" + description: + name: path_provider + sha256: c7edf82217d4b2952b2129a61d3ad60f1075b9299e629e149a8d2e39c2e6aad4 + url: "https://pub.dev" + source: hosted + version: "2.0.14" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "019f18c9c10ae370b08dce1f3e3b73bc9f58e7f087bb5e921f06529438ac0ae7" + url: "https://pub.dev" + source: hosted + version: "2.0.24" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "818b2dc38b0f178e0ea3f7cf3b28146faab11375985d815942a68eee11c2d0f7" + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" + url: "https://pub.dev" + source: hosted + version: "2.1.10" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "57585299a729335f1298b43245842678cb9f43a6310351b18fb577d6e33165ec" + url: "https://pub.dev" + source: hosted + version: "2.0.6" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: f53720498d5a543f9607db4b0e997c4b5438884de25b0f73098cc2671a51b130 + url: "https://pub.dev" + source: hosted + version: "2.1.5" + petitparser: + dependency: transitive + description: + name: petitparser + sha256: a9346a3fbba7546a28374bdbcd7f54ea48bb47772bf3a7ab4bfaadc40bc8b8c6 + url: "https://pub.dev" + source: hosted + version: "5.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "6a2128648c854906c53fa8e33986fc0247a1116122f9534dd20e3ab9e16a32bc" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + process: + dependency: transitive + description: + name: process + sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" + url: "https://pub.dev" + source: hosted + version: "4.2.4" + provider: + dependency: "direct main" + description: + name: provider + sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + url: "https://pub.dev" + source: hosted + version: "6.0.5" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + pub_updater: + dependency: transitive + description: + name: pub_updater + sha256: "42890302ab2672adf567dc2b20e55b4ecc29d7e19c63b6b98143ab68dd717d3a" + url: "https://pub.dev" + source: hosted + version: "0.2.4" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c + url: "https://pub.dev" + source: hosted + version: "1.2.2" + recase: + dependency: transitive + description: + name: recase + sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213 + url: "https://pub.dev" + source: hosted + version: "4.1.0" + shelf: + dependency: transitive + description: + name: shelf + sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + url: "https://pub.dev" + source: hosted + version: "1.4.0" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.0.3" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: c2bea18c95cfa0276a366270afaa2850b09b4a76db95d546f3d003dcc7011298 + url: "https://pub.dev" + source: hosted + version: "1.2.7" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "3b67aade1d52416149c633ba1bb36df44d97c6b51830c2198e934e3fca87ca1f" + url: "https://pub.dev" + source: hosted + version: "1.3.3" source_span: dependency: transitive description: @@ -136,6 +664,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "822d321a008e194d7929357e5b58d2e4a04ab670d137182f9759152aa33180ff" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: a96a7851b69b54c40425ef8da15a19701ec4c994fbcebf4fb4fbf53782859160 + url: "https://pub.dev" + source: hosted + version: "0.28.0" stack_trace: dependency: transitive description: @@ -152,6 +696,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.1" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: "14a00e794c7c11aa145a170587321aedce29769c08d7f58b1d141da75e3b1c6f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" string_scanner: dependency: transitive description: @@ -160,6 +712,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + surf_lint_rules: + dependency: "direct dev" + description: + name: surf_lint_rules + sha256: "0465c70586a137db13a1e6afb1bd10b48a95c90fe29fdcee15a96c9f70fe2b19" + url: "https://pub.dev" + source: hosted + version: "2.1.0" term_glyph: dependency: transitive description: @@ -172,10 +732,34 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" + timing: + dependency: transitive + description: + name: timing + sha256: "70a3b636575d4163c477e6de42f247a23b315ae20e86442bebe32d3cabf61c32" + url: "https://pub.dev" + source: hosted + version: "1.0.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + url: "https://pub.dev" + source: hosted + version: "3.0.7" vector_math: dependency: transitive description: @@ -184,5 +768,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" + watcher: + dependency: transitive + description: + name: watcher + sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + url: "https://pub.dev" + source: hosted + version: "2.3.0" + win32: + dependency: transitive + description: + name: win32 + sha256: a6f0236dbda0f63aa9a25ad1ff9a9d8a4eaaa5012da0dc59d21afdb1dc361ca4 + url: "https://pub.dev" + source: hosted + version: "3.1.4" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: ee1505df1426458f7f60aac270645098d318a8b4766d85fde75f76f2e21807d1 + url: "https://pub.dev" + source: hosted + version: "1.0.0" + xml: + dependency: transitive + description: + name: xml + sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + url: "https://pub.dev" + source: hosted + version: "6.2.2" + yaml: + dependency: transitive + description: + name: yaml + sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + url: "https://pub.dev" + source: hosted + version: "3.1.1" sdks: - dart: ">=2.19.6 <3.0.0" + dart: ">=3.0.0-134.0.dev <4.0.0" + flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 5abeaf8..5c763ec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,90 +1,34 @@ name: surf_flutter_study_jam_2023 description: Surf Flutter Study Jam 2023. -# The following line prevents the package from being accidentally published to -# pub.dev using `flutter pub publish`. This is preferred for private packages. -publish_to: 'none' # Remove this line if you wish to publish to pub.dev +publish_to: 'none' -# The following defines the version and build number for your application. -# A version number is three numbers separated by dots, like 1.2.43 -# followed by an optional build number separated by a +. -# Both the version and the builder number may be overridden in flutter -# build by specifying --build-name and --build-number, respectively. -# In Android, build-name is used as versionName while build-number used as versionCode. -# Read more about Android versioning at https://developer.android.com/studio/publish/versioning -# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. -# Read more about iOS versioning at -# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -# In Windows, build-name is used as the major, minor, and patch parts -# of the product and file versions while build-number is used as the build suffix. version: 1.0.0+1 environment: sdk: '>=2.19.6 <3.0.0' -# Dependencies specify other packages that your package needs in order to work. -# To automatically upgrade your package dependencies to the latest versions -# consider running `flutter pub upgrade --major-versions`. Alternatively, -# dependencies can be manually updated by changing the version numbers below to -# the latest version available on pub.dev. To see which dependencies have newer -# versions available, run `flutter pub outdated`. dependencies: + built_collection: ^5.1.1 + cupertino_icons: ^1.0.2 + dartz: ^0.10.1 + dio: ^5.1.1 + drift: ^2.6.0 flutter: sdk: flutter - - - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.2 + freezed_annotation: ^2.2.0 + json_annotation: ^4.8.0 + path_provider: ^2.0.14 + provider: ^6.0.5 + uuid: ^3.0.7 dev_dependencies: + build_runner: ^2.4.1 + drift_dev: ^2.6.0 flutter_test: sdk: flutter + freezed: ^2.3.2 + json_serializable: ^6.6.1 + surf_lint_rules: ^2.1.0 - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^2.0.0 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - - # To add assets to your application, add an assets section, like this: - # assets: - # - images/a_dot_burr.jpeg - # - images/a_dot_ham.jpeg - - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: - # fonts: - # - family: Schyler - # fonts: - # - asset: fonts/Schyler-Regular.ttf - # - asset: fonts/Schyler-Italic.ttf - # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/scripts/run_avd.sh b/scripts/run_avd.sh new file mode 100644 index 0000000..97ec5e3 --- /dev/null +++ b/scripts/run_avd.sh @@ -0,0 +1,14 @@ +#!/bin/bash +echo 'Choose Android AVD to cold-boot:' + +select avd in $(emulator -list-avds); +do + if [ -n "$avd" ] + then + echo "Cold-booting AVD '$avd'" + emulator @$avd -no-snapshot-load -writable-system + break + else + echo "Unknown option: '$REPLY'" + fi +done