RuStore Remote Config

This commit is contained in:
2024-12-22 16:39:43 +03:00
parent 86e539daf1
commit 0cda6f9a48
27 changed files with 693 additions and 380 deletions

View File

@@ -1,11 +1,7 @@
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:get/get.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reverse_nn/application/controllers/application_controller.dart';
import 'package:reverse_nn/pages/home_page.dart';
import 'package:reverse_nn/ui/screens/home_screen.dart';
class ReversNNApplication extends StatelessWidget {

View File

@@ -1,11 +1,13 @@
import 'package:get/get.dart';
import 'package:reverse_nn/application/dto/schedule.dart';
import 'package:reverse_nn/application/services/remote_config/service.dart';
import 'package:reverse_nn/application/services/schedule.dart';
import 'package:reverse_nn/ui/screens/calendar_screen.dart';
class CalendarScheduleController extends GetxController {
Rx<DateTime> date = DateTime.now().copyWith(hour: 12, minute: 00).obs;
RxBool loading = false.obs;
Rx<List<Map<String, dynamic>>?> schedule = null.obs;
Rx<List<ScheduleItem>?> schedule = null.obs;
@override void onReady() {
super.onReady();
@@ -26,14 +28,26 @@ class CalendarScheduleController extends GetxController {
}
void goToPrevDay() async {
if(date.value.isBefore(DateTime(2024, 8, 1, 23, 59))) return;
date = (date.value.copyWith().subtract(const Duration(days: 1)).copyWith(hour: 12, minute: 0)).obs;
final newDate = date.value
.copyWith(hour: 12, minute: 0, second: 0, microsecond: 0, millisecond: 0)
.subtract(const Duration(days: 1));
final dateStart = await ReverseNNRuStoreRemoteConfig().getStartScheduleDate();
if(newDate.isBefore(dateStart)) return;
date = newDate.obs;
loadSchedule(date.value);
}
void goToNextDay() async {
if(date.value.isAfter(DateTime(2025, 12, 31))) return;
date = (date.value.copyWith().add(const Duration(days: 1)).copyWith(hour: 12, minute: 0)).obs;
final newDate = date.value
.copyWith(hour: 12, minute: 0, second: 0, microsecond: 0, millisecond: 0)
.add(const Duration(days: 1));
final dateEnd = await ReverseNNRuStoreRemoteConfig().getEndScheduleDate();
if(newDate.isAfter(dateEnd)) return;
date = newDate.obs;
loadSchedule(date.value);
}
}

View File

@@ -2,10 +2,11 @@ import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:get/get.dart';
import 'package:reverse_nn/application/dto/schedule.dart';
import 'package:reverse_nn/application/services/schedule.dart';
class ScheduleController extends GetxController {
Rx<Map<String, dynamic>?> currentSchedule = null.obs;
Rx<ScheduleItem?> currentSchedule = null.obs;
@override void onReady() {
super.onReady();

View File

@@ -0,0 +1,25 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';
enum Direction {
@JsonValue('in_city') inCity,
@JsonValue('change') change,
@JsonValue('out_city') outCity;
String displayName() {
switch(this) {
case Direction.inCity: return 'В город';
case Direction.change: return 'Переключение';
case Direction.outCity: return 'Из города';
}
}
IconData icon() {
switch(this) {
case Direction.inCity: return Icons.input_outlined;
case Direction.change: return Icons.change_circle_outlined;
case Direction.outCity: return Icons.output_outlined;
}
}
}

View File

@@ -0,0 +1,15 @@
import "package:json_annotation/json_annotation.dart" show FieldRename, JsonSerializable;
part 'rule.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
class Rule {
final DateTime after;
final DateTime? before;
final Map<String, String> dataset;
Rule({required this.after, required this.before, required this.dataset});
factory Rule.fromJson(Map<String, dynamic> json) => _$RuleFromJson(json);
Map<String, dynamic> toJson() => _$RuleToJson(this);
}

View File

@@ -0,0 +1,21 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'rule.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
Rule _$RuleFromJson(Map<String, dynamic> json) => Rule(
after: DateTime.parse(json['after'] as String),
before: json['before'] == null
? null
: DateTime.parse(json['before'] as String),
dataset: Map<String, String>.from(json['dataset'] as Map),
);
Map<String, dynamic> _$RuleToJson(Rule instance) => <String, dynamic>{
'after': instance.after.toIso8601String(),
'before': instance.before?.toIso8601String(),
'dataset': instance.dataset,
};

View File

@@ -0,0 +1,41 @@
import 'package:json_annotation/json_annotation.dart';
import 'package:reverse_nn/application/dto/direction.dart';
part 'schedule.g.dart';
@JsonSerializable(fieldRename: FieldRename.snake, explicitToJson: true)
class ScheduleItem {
final int duration;
final Direction direction;
final DateTime? start;
final DateTime? end;
final bool? showStartDate;
final bool? showEndDate;
ScheduleItem({
required this.duration,
required this.direction,
this.start,
this.end,
this.showStartDate,
this.showEndDate
});
factory ScheduleItem.fromJson(Map<String, dynamic> json) => _$ScheduleItemFromJson(json);
Map<String, dynamic> toJson() => _$ScheduleItemToJson(this);
ScheduleItem fillAdditional({
required DateTime start,
required DateTime end,
required bool showStartDate,
required bool showEndDate
}) => ScheduleItem(
duration: duration,
direction: direction,
start: start,
end: end,
showStartDate: showStartDate,
showEndDate: showEndDate,
);
}

View File

@@ -0,0 +1,34 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'schedule.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ScheduleItem _$ScheduleItemFromJson(Map<String, dynamic> json) => ScheduleItem(
duration: (json['duration'] as num).toInt(),
direction: $enumDecode(_$DirectionEnumMap, json['direction']),
start: json['start'] == null
? null
: DateTime.parse(json['start'] as String),
end: json['end'] == null ? null : DateTime.parse(json['end'] as String),
showStartDate: json['show_start_date'] as bool?,
showEndDate: json['show_end_date'] as bool?,
);
Map<String, dynamic> _$ScheduleItemToJson(ScheduleItem instance) =>
<String, dynamic>{
'duration': instance.duration,
'direction': _$DirectionEnumMap[instance.direction]!,
'start': instance.start?.toIso8601String(),
'end': instance.end?.toIso8601String(),
'show_start_date': instance.showStartDate,
'show_end_date': instance.showEndDate,
};
const _$DirectionEnumMap = {
Direction.inCity: 'in_city',
Direction.change: 'change',
Direction.outCity: 'out_city',
};

View File

@@ -0,0 +1,5 @@
import 'package:flutter_rustore_remoteconfig/rustore_remote_config.dart';
class ReverseNNRemoteConfigParameters extends StaticParameters {
}

View File

@@ -0,0 +1,131 @@
import 'dart:convert';
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/services.dart';
import 'package:flutter_rustore_remoteconfig/flutter_rustore_remoteconfig.dart';
import 'package:flutter_rustore_remoteconfig/rustore_remote_config.dart';
import 'package:reverse_nn/application/dto/rule.dart';
import 'package:reverse_nn/application/dto/schedule.dart';
import 'package:reverse_nn/configuration.dart';
import 'package:reverse_nn/application/services/remote_config/parameters.dart';
class ReverseNNRuStoreRemoteConfig {
void create() {
FlutterRustoreRemoteconfig.create(
Configuration.ruStoreRemoteConfigAppID,
PluginUpdateBehavior.defaultBehavior,
15,
ReverseNNRemoteConfigParameters(),
onInitComplete: () {
if(kDebugMode) { log('RuStore Remote Config - onInitComplete'); }
},
onFirstLoadComplete: () {
if(kDebugMode) { log('RuStore Remote Config - onFirstLoadComplete'); }
},
onMemoryCacheUpdated: () {
if(kDebugMode) { log('RuStore Remote Config - onMemoryCacheUpdated'); }
}
);
}
Future<DateTime> getStartScheduleDate() async {
try {
FlutterRustoreRemoteconfig.init();
final remoteConfig = await FlutterRustoreRemoteconfig.getRemoteConfig();
final startScheduleDate = remoteConfig.getString('startScheduleDate');
return startScheduleDate != null
? DateTime.parse(startScheduleDate)
: DateTime(2024, 8, 1);
}
catch (error) {
if(kDebugMode) {
log('ReverseNNRuStoreRemoteConfig.getStartScheduleDate - error - ${error.toString()}');
}
return DateTime(2024, 8, 1);
}
}
Future<DateTime> getEndScheduleDate() async {
try {
FlutterRustoreRemoteconfig.init();
final remoteConfig = await FlutterRustoreRemoteconfig.getRemoteConfig();
final endScheduleDate = remoteConfig.getString('endScheduleDate');
return endScheduleDate != null
? DateTime.parse(endScheduleDate)
: DateTime(2025, 12, 31, 23, 59, 59, 999, 999);
}
catch (error) {
if(kDebugMode) {
log('ReverseNNRuStoreRemoteConfig.getEndScheduleDate - error - ${error.toString()}');
}
return DateTime(2025, 12, 31, 23, 59, 59, 999, 999);
}
}
Future<List<Rule>?> getRules() async {
try {
FlutterRustoreRemoteconfig.init();
final remoteConfig = await FlutterRustoreRemoteconfig.getRemoteConfig();
final rules = remoteConfig.getString('scheduleRules');
return rules != null
? (jsonDecode(rules) as List<dynamic>).map((el) => Rule.fromJson(el)).toList()
: await _getRulesFromStorage();
}
catch(error) {
if(kDebugMode) {
log('ReverseNNRuStoreRemoteConfig.getRules - error - ${error.toString()}');
}
return await _getRulesFromStorage();
}
}
Future<List<Rule>?> _getRulesFromStorage() async {
final Map<String, dynamic>? schema = await _getSchemaFromStorage();
if(schema == null || !schema.containsKey('rules')) return null;
return (schema['rules'] as List<dynamic>)
.map((el) => Rule.fromJson(el))
.toList();
}
Future<List<ScheduleItem>?> getScheduleByKey(String key) async {
try {
FlutterRustoreRemoteconfig.init();
final remoteConfig = await FlutterRustoreRemoteconfig.getRemoteConfig();
final schedule = remoteConfig.getString('schedules-$key');
return schedule != null
? (jsonDecode(schedule) as List<dynamic>)
.map((el) => ScheduleItem.fromJson(el))
.toList()
: await _getScheduleByKeyFromStorage(key);
}
catch(error) {
if(kDebugMode) {
log('ReverseNNRuStoreRemoteConfig.getScheduleByKey($key) - error - ${error.toString()}');
}
return await _getScheduleByKeyFromStorage(key);
}
}
Future<List<ScheduleItem>?> _getScheduleByKeyFromStorage(String key) async {
final Map<String, dynamic>? schema = await _getSchemaFromStorage();
if(
schema == null
|| !schema.containsKey('schedules')
|| !(schema['schedules'] as Map<String, dynamic>).containsKey(key)
) return null;
return (schema['schedules'][key] as List<dynamic>)
.map((el) => ScheduleItem.fromJson(el))
.toList();
}
Future<Map<String, dynamic>?> _getSchemaFromStorage() async {
return jsonDecode(await rootBundle.loadString('assets/schedule.json'));
}
}

View File

@@ -5,43 +5,25 @@ import 'package:flutter/material.dart';
import 'package:intl/intl.dart' show DateFormat;
import 'package:flutter/services.dart';
import 'package:reverse_nn/application/dto/rule.dart';
import 'package:reverse_nn/application/dto/schedule.dart';
import 'package:reverse_nn/application/services/remote_config/service.dart';
class ScheduleService {
static const Duration dayOffset = Duration(hours: 4);
Future<Map<String, dynamic>> getSchema() async {
final source = await rootBundle
.loadString('assets/schedule.json');
return jsonDecode(source);
}
Future<Rule?> getRuleByDate(DateTime date) async {
final List<Rule>? rules = await ReverseNNRuStoreRemoteConfig().getRules();
if(rules == null) return null;
Future<Map<String, dynamic>?> getRule(DateTime day) async {
final Map<String, dynamic> schema = await getSchema();
if(!schema.containsKey('rules')) return null;
for(final Map<String, dynamic> rule in (schema['rules'] as List<dynamic>)) {
final DateTime after = DateTime.parse(rule['after']);
final DateTime? before = rule['before'] != null
? DateTime.parse(rule['before'])
: null;
if(day.isAfter(after) && (before == null || day.isBefore(before)))
for(final Rule rule in rules) {
if(date.isAfter(rule.after) && (rule.before == null || date.isBefore(rule.before!)))
{ return rule; }
}
return null;
}
Future<Map<String, String>?> getDataset(DateTime day) async {
final Map<String, dynamic>? rule = await getRule(day);
if(rule == null || !rule.containsKey('dataset')) return null;
return (rule['dataset'] as Map<String, dynamic>).map((key, value) {
return MapEntry(key, value.toString());
});
}
String datetimeWeekdayToDatasetKey(int weekday) {
switch(weekday){
case DateTime.monday: return 'monday';
@@ -56,17 +38,7 @@ class ScheduleService {
return 'monday';
}
Future<List<Map<String, dynamic>>?> getScheduleByKey(String key) async {
final Map<String, dynamic> schema = await getSchema();
if(!schema.containsKey('schedules')) return null;
if(!(schema['schedules'] as Map<String, dynamic>).containsKey(key)) return null;
return (schema['schedules'][key] as List<dynamic>).map((element) {
return element as Map<String, dynamic>;
}).toList();
}
Future<List<Map<String, dynamic>>?> getScheduleByDate(DateTime datetime, {bool force = false}) async {
Future<List<ScheduleItem>?> getScheduleByDate(DateTime datetime, {bool force = false}) async {
final dateStart = datetime.copyWith(hour: 0, minute: 0, second: 0, microsecond: 0, millisecond: 0);
final dateEnd = datetime.copyWith(hour: 23, minute: 59, second: 59, microsecond: 999, millisecond: 999);
final datetimeStartWithOffset = dateStart.copyWith().add(dayOffset);
@@ -75,7 +47,7 @@ class ScheduleService {
? datetime
: datetime.copyWith().subtract(const Duration(days: 1));
final Map<String, String>? dataset = await getDataset(usedScheduleDatetime);
final Map<String, String>? dataset = (await getRuleByDate(usedScheduleDatetime))?.dataset;
if(dataset == null) return null;
final String dayKey = DateFormat('y-M-d').format(usedScheduleDatetime);
@@ -87,74 +59,55 @@ class ScheduleService {
if(scheduleKey == null) return null;
List<Map<String, dynamic>>? schedule = await getScheduleByKey(scheduleKey);
List<ScheduleItem>? schedule = await ReverseNNRuStoreRemoteConfig().getScheduleByKey(scheduleKey);
if(schedule == null) return null;
int durationOffset = 0;
for (var i = 0; i < schedule.length; i++) {
final scheduleItem = schedule[i];
final int duration = (scheduleItem['duration'] as int);
final ScheduleItem scheduleItem = schedule[i];
DateTime start = datetimeStartWithOffset.copyWith().add(Duration(minutes: durationOffset));
DateTime end = datetimeStartWithOffset.copyWith().add(Duration(minutes: durationOffset + duration));
durationOffset += duration;
DateTime end = datetimeStartWithOffset.copyWith().add(Duration(minutes: durationOffset + scheduleItem.duration));
durationOffset += scheduleItem.duration;
if(!force) {
if(i == 0) {
final schedulePrevDay = await getScheduleByDate(datetime.copyWith().subtract(const Duration(days: 1)), force: true);
final lastScheduleElement = schedulePrevDay?.last;
if(lastScheduleElement != null && (lastScheduleElement['direction'] as String) == (scheduleItem['direction'] as String)) {
start = start.subtract(Duration(minutes: lastScheduleElement['duration'] as int));
if(lastScheduleElement != null && lastScheduleElement.direction == scheduleItem.direction) {
start = start.subtract(Duration(minutes: lastScheduleElement.duration));
}
}
if(i == (schedule.length -1)) {
final scheduleNextDay = await getScheduleByDate(datetime.copyWith().add(const Duration(days: 1)), force: true);
final firstScheduleElement = scheduleNextDay?.first;
if(firstScheduleElement != null && (firstScheduleElement['direction'] as String) == (scheduleItem['direction'] as String)) {
end = end.add(Duration(minutes: firstScheduleElement['duration'] as int));
if(firstScheduleElement != null && firstScheduleElement.direction == scheduleItem.direction) {
end = end.add(Duration(minutes: firstScheduleElement.duration));
}
}
}
schedule[i]['start'] = start;
schedule[i]['show_start_date'] = !(start.isAfter(dateStart) && start.isBefore(dateEnd));
schedule[i]['end'] = end;
schedule[i]['show_end_date'] = !(end.isAfter(dateStart) && end.isBefore(dateEnd));
schedule[i] = scheduleItem.fillAdditional(
start: start,
end: end,
showStartDate: !(start.isAfter(dateStart) && start.isBefore(dateEnd)),
showEndDate: !(end.isAfter(dateStart) && end.isBefore(dateEnd))
);
}
return schedule;
}
Future<Map<String, dynamic>?> getCurrentStatus() async {
Future<ScheduleItem?> getCurrentStatus() async {
final DateTime now = DateTime.now();
final List<Map<String, dynamic>>? schedule = await getScheduleByDate(now);
final List<ScheduleItem>? schedule = await getScheduleByDate(now);
if(schedule == null) return null;
for(var i = 0; i < schedule.length; i++) {
final start = schedule[i]['start'];
final end = schedule[i]['end'];
if(now.isAfter(start) && now.isBefore(end)) {
return schedule[i];
}
for(final ScheduleItem item in schedule) {
if(item.start == null || item.end == null) continue;
if(now.isAfter(item.start!) && now.isBefore(item.end!)) return item;
}
return null;
}
static String formatDirection(String direction) {
switch(direction) {
case 'in_city': return 'В город';
case 'out_city': return 'Из города';
default: return 'Переключение';
}
}
static IconData getIconByDirection(String? direction) {
switch(direction) {
case 'in_city': return Icons.input_outlined;
case 'out_city': return Icons.output_outlined;
default: return Icons.change_circle_outlined;
}
}
}

3
lib/configuration.dart Normal file
View File

@@ -0,0 +1,3 @@
class Configuration {
static const String ruStoreRemoteConfigAppID = '12cbda1f-4601-4f6d-924d-64a210005e96';
}

View File

@@ -1,11 +0,0 @@
enum Direction {
inCity(displayName: 'В город'),
change(displayName: 'Переключение'),
outCity(displayName: 'Из города');
const Direction({
required this.displayName
});
final String displayName;
}

View File

@@ -1,67 +0,0 @@
import 'package:reverse_nn/enums/direction.dart';
import 'package:sprintf/sprintf.dart';
enum Schedule {
daily(elements: [
ScheduleElement(startHours: 4, startMinutes: 0, duration: 415, direction: Direction.inCity),
ScheduleElement(startHours: 10, startMinutes: 55, duration: 5, direction: Direction.change),
ScheduleElement(startHours: 11, startMinutes: 0, duration: 115, direction: Direction.outCity),
ScheduleElement(startHours: 12, startMinutes: 55, duration: 5, direction: Direction.change),
ScheduleElement(startHours: 13, startMinutes: 0, duration: 145, direction: Direction.inCity),
ScheduleElement(startHours: 15, startMinutes: 25, duration: 5, direction: Direction.change),
ScheduleElement(startHours: 15, startMinutes: 30, duration: 205, direction: Direction.outCity),
ScheduleElement(startHours: 18, startMinutes: 55, duration: 5, direction: Direction.change),
ScheduleElement(startHours: 19, startMinutes: 0, duration: 55, direction: Direction.inCity),
ScheduleElement(startHours: 19, startMinutes: 55, duration: 5, direction: Direction.change),
ScheduleElement(startHours: 20, startMinutes: 0, duration: 475, direction: Direction.outCity),
ScheduleElement(startHours: 3, startMinutes: 55, duration: 5, direction: Direction.change),
]),
friday(elements: [
ScheduleElement(startHours: 4, startMinutes: 0, duration: 415, direction: Direction.inCity),
ScheduleElement(startHours: 10, startMinutes: 55, duration: 5, direction: Direction.change),
ScheduleElement(startHours: 11, startMinutes: 0, duration: 115, direction: Direction.outCity),
ScheduleElement(startHours: 12, startMinutes: 55, duration: 5, direction: Direction.change),
ScheduleElement(startHours: 13, startMinutes: 0, duration: 55, direction: Direction.inCity),
ScheduleElement(startHours: 13, startMinutes: 55, duration: 5, direction: Direction.change),
ScheduleElement(startHours: 14, startMinutes: 0, duration: 840, direction: Direction.outCity),
]),
saturday(elements: [
ScheduleElement(startHours: 4, startMinutes: 0, duration: 655, direction: Direction.outCity),
ScheduleElement(startHours: 14, startMinutes: 55, duration: 5, direction: Direction.change),
ScheduleElement(startHours: 15, startMinutes: 0, duration: 175, direction: Direction.inCity),
ScheduleElement(startHours: 17, startMinutes: 55, duration: 5, direction: Direction.change),
ScheduleElement(startHours: 18, startMinutes: 0, duration: 115, direction: Direction.outCity),
ScheduleElement(startHours: 19, startMinutes: 55, duration: 5, direction: Direction.change),
ScheduleElement(startHours: 20, startMinutes: 0, duration: 115, direction: Direction.inCity),
ScheduleElement(startHours: 21, startMinutes: 55, duration: 5, direction: Direction.change),
ScheduleElement(startHours: 22, startMinutes: 0, duration: 355, direction: Direction.outCity),
ScheduleElement(startHours: 3, startMinutes: 55, duration: 5, direction: Direction.change),
]),
sunday(elements: [
ScheduleElement(startHours: 4, startMinutes: 0, duration: 235, direction: Direction.inCity),
ScheduleElement(startHours: 7, startMinutes: 55, duration: 5, direction: Direction.change),
ScheduleElement(startHours: 8, startMinutes: 0, duration: 175, direction: Direction.outCity),
ScheduleElement(startHours: 10, startMinutes: 55, duration: 5, direction: Direction.change),
ScheduleElement(startHours: 11, startMinutes: 0, duration: 1020, direction: Direction.inCity),
]);
const Schedule({required this.elements});
final List<ScheduleElement> elements;
}
class ScheduleElement {
final int startHours;
final int startMinutes;
final int duration;
final Direction direction;
const ScheduleElement({required this.startHours, required this.startMinutes, required this.duration, required this.direction});
String getStartedTimeString() { return sprintf('%02i:%02i', [startHours, startMinutes]); }
String getEndedTimeString() {
DateTime e = DateTime(2024, 8, 1, startHours, startMinutes).add(Duration(minutes: duration)).subtract(const Duration(microseconds: 1));
return sprintf('%02i:%02i', [e.hour, e.minute]);
}
}

View File

@@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:get_storage/get_storage.dart';
import 'package:reverse_nn/application.dart';
import 'package:reverse_nn/application/services/remote_config/service.dart';
void main() async {
await GetStorage.init();
ReverseNNRuStoreRemoteConfig().create();
runApp(const ReversNNApplication());
}

View File

@@ -1,18 +0,0 @@
import 'package:flutter/material.dart';
import 'package:reverse_nn/widgets/schedule_widget.dart';
class HomePage extends StatelessWidget {
const HomePage({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('Реверс НН'),
),
body: const ScheduleWidget()
);
}
}

View File

@@ -1,9 +1,9 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart' show DateFormat;
import 'package:reverse_nn/application/services/schedule.dart';
import 'package:reverse_nn/application/dto/schedule.dart';
class ScheduleItemComponent extends StatelessWidget {
final Map<String, dynamic> item;
final ScheduleItem item;
const ScheduleItemComponent({super.key, required this.item});
@override
@@ -20,7 +20,7 @@ class ScheduleItemComponent extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(ScheduleService.getIconByDirection(item['direction'] as String), size: 56),
Icon(item.direction.icon(), size: 56),
const SizedBox(width: 16),
Expanded(child: Column(
mainAxisAlignment: MainAxisAlignment.start,
@@ -41,17 +41,17 @@ class ScheduleItemComponent extends StatelessWidget {
}
Widget _directionText(BuildContext context) => Text(
ScheduleService.formatDirection(item['direction'] ?? ''),
item.direction.displayName(),
style: Theme.of(context).textTheme.headlineLarge
);
Widget _startText(BuildContext context) => Text(
'C\t\t\t\t${formatDate(item['start'], showDate: item['show_start_date'] ?? false)}',
'C\t\t\t\t${formatDate(item.start!, showDate: item.showStartDate ?? false)}',
style: Theme.of(context).textTheme.titleLarge
);
Widget _endText(BuildContext context) => Text(
'До\t${formatDate(item['end'], showDate: item['show_end_date'] ?? false)}',
'До\t${formatDate(item.end!, showDate: item.showEndDate ?? false)}',
style: Theme.of(context).textTheme.titleLarge
);
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:reverse_nn/application/controllers/calendar_schedule_controller.dart';
import 'package:reverse_nn/application/services/schedule.dart';
import 'package:reverse_nn/application/services/remote_config/service.dart';
import 'package:reverse_nn/ui/components/schedule_item_component.dart';
import 'package:reverse_nn/ui/layouts/application_layout.dart';
import 'package:intl/intl.dart' show DateFormat;
@@ -114,11 +114,14 @@ class _SelectedDateWidget extends StatelessWidget {
void _openCalendar(BuildContext context) async {
DateTime now = DateTime.now();
DateTime firstDate = await ReverseNNRuStoreRemoteConfig().getStartScheduleDate();
DateTime lastDate = await ReverseNNRuStoreRemoteConfig().getEndScheduleDate();
DateTime? picked = await showDatePicker(
context: context,
initialDate: now,
firstDate: DateTime(2024, 8, 1, 12),
lastDate: DateTime(2025, 12, 31, 12),
firstDate: firstDate,
lastDate: lastDate,
locale: const Locale('ru')
);

View File

@@ -1,6 +1,8 @@
import 'dart:developer';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_rustore_remoteconfig/flutter_rustore_remoteconfig.dart';
import 'package:get/get.dart';
import 'package:reverse_nn/application/controllers/calendar_schedule_controller.dart';
import 'package:reverse_nn/application/controllers/home_controller.dart';
@@ -8,6 +10,7 @@ import 'package:reverse_nn/application/services/schedule.dart';
import 'package:reverse_nn/ui/components/current_status_component.dart';
import 'package:reverse_nn/ui/components/grid_menu_item.dart';
import 'package:reverse_nn/ui/layouts/application_layout.dart';
import 'package:reverse_nn/application/services/remote_config/service.dart';
class HomeScreen extends GetWidget<HomeController> {
const HomeScreen({super.key});
@@ -29,8 +32,9 @@ class HomeScreen extends GetWidget<HomeController> {
mainAxisSpacing: 10,
crossAxisSpacing: 10,
),
children: const [
GridMenuItem(icon: Icons.calendar_month, label: 'Расписание', onTap: CalendarScheduleController.openScreen),
children: [
const GridMenuItem(icon: Icons.calendar_month, label: 'Расписание', onTap: CalendarScheduleController.openScreen),
if(kDebugMode) GridMenuItem(icon: Icons.android, label: 'DEBUG', onTap: _debugRuStore),
// const GridMenuItem(icon: Icons.monetization_on, label: 'Поддержать автора'),
// GridMenuItem(),
// GridMenuItem(),
@@ -41,4 +45,10 @@ class HomeScreen extends GetWidget<HomeController> {
),
));
}
void _debugRuStore() async {
ReverseNNRuStoreRemoteConfig().getEndScheduleDate().then((value) {
log(value.toIso8601String());
});
}
}

View File

@@ -1,36 +0,0 @@
import 'package:reverse_nn/enums/schedule.dart';
class Scheduler {
static Schedule getSchedule(DateTime date) {
switch(date.weekday) {
case DateTime.friday: return Schedule.friday;
case DateTime.saturday: return Schedule.saturday;
case DateTime.sunday: return Schedule.sunday;
default: return Schedule.daily;
}
}
static List<ScheduleElement> getScheduleList() {
List<ScheduleElement> result = List<ScheduleElement>.empty(growable: true);
DateTime now = DateTime.now();
DateTime startSchedule = now.hour < 4 ? now.subtract(const Duration(days: 1)) : now;
bool currentReached = false;
for (ScheduleElement element in [...Scheduler.getSchedule(startSchedule).elements, ...Scheduler.getSchedule(startSchedule.add(const Duration(days: 1))).elements]) {
if(!currentReached) {
DateTime s = DateTime(now.year, now.month, now.day, element.startHours, element.startMinutes, 0);
if(now.compareTo(startSchedule) != 0 && element.startHours >= 4) { s = s.subtract(const Duration(days: 1)); }
DateTime e = s.add(Duration(minutes: element.duration)).subtract(const Duration(microseconds: 1));
if(now.isAfter(s) && now.isBefore(e)) { currentReached = true; }
else { continue; }
}
result.add(element);
}
return result;
}
}

View File

@@ -1,16 +0,0 @@
import 'package:flutter/material.dart';
class LoaderWidget extends StatelessWidget {
const LoaderWidget({super.key});
@override
Widget build(BuildContext context) {
return const Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CircularProgressIndicator()
],
);
}
}

View File

@@ -1,79 +0,0 @@
import 'package:flutter/material.dart';
import 'package:reverse_nn/enums/direction.dart';
import 'package:reverse_nn/enums/schedule.dart';
import 'package:sprintf/sprintf.dart';
class CurrentReversWidget extends StatelessWidget {
const CurrentReversWidget({super.key, required this.scheduleElement});
final ScheduleElement scheduleElement;
IconData _getIconData() {
switch(scheduleElement.direction) {
case Direction.inCity: return Icons.arrow_upward;
case Direction.outCity: return Icons.arrow_downward;
case Direction.change: return Icons.cancel_outlined;
}
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
color: Theme.of(context).colorScheme.primaryContainer
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(_getIconData(), size: 56.0),
const SizedBox(width: 10),
Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Реверс: ${scheduleElement.direction.displayName}', style: const TextStyle(fontSize: 20)),
Text('${scheduleElement.getStartedTimeString()} - ${scheduleElement.getEndedTimeString()}', style: const TextStyle(fontSize: 36)),
],
)
],
),
);
}
}
class ScheduledReversWidget extends StatelessWidget {
const ScheduledReversWidget({super.key, required this.scheduleElement});
final ScheduleElement scheduleElement;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(8),
margin: const EdgeInsets.symmetric(horizontal: 8),
decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(8.0)),
color: Theme.of(context).colorScheme.secondaryContainer
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text(sprintf('Следующий статус в %02i:%02i - %s', [
scheduleElement.startHours,
scheduleElement.startMinutes,
scheduleElement.direction.displayName
])),
],
),
);
}
}

View File

@@ -1,45 +0,0 @@
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:reverse_nn/enums/schedule.dart';
import 'package:reverse_nn/utils/scheduler.dart';
import 'package:reverse_nn/widgets/loader_widget.dart';
import 'package:reverse_nn/widgets/schedule_element_widget.dart';
class ScheduleWidget extends StatefulWidget {
const ScheduleWidget({super.key});
@override
State<ScheduleWidget> createState() => _ScheduleWidgetState();
}
class _ScheduleWidgetState extends State<ScheduleWidget> {
bool _loading = true;
List<ScheduleElement> _schedule = List.empty();
void loadScheduleData() async {
setState(() { _loading = true; });
setState(() { _schedule = Scheduler.getScheduleList(); });
setState(() { _loading = false; });
}
@override
Widget build(BuildContext context) {
loadScheduleData();
Future.delayed(const Duration(seconds: 15), () => loadScheduleData());
return _loading && _schedule.isNotEmpty ? const LoaderWidget() : ListView.separated(
scrollDirection: Axis.vertical,
shrinkWrap: true,
padding: const EdgeInsets.all(10),
separatorBuilder: (BuildContext context, int index) {
return SizedBox(height: index == 0 ? 10 : 4);
},
itemCount: _schedule.length,
itemBuilder: (ctx, i) {
if(i == 0) { return CurrentReversWidget(scheduleElement: _schedule[i]); }
return ScheduledReversWidget(scheduleElement: _schedule[i]);
},
);
}
}