Skip to content

Commit

Permalink
Merge branch 'dev' into main
Browse files Browse the repository at this point in the history
Gioele Bigini committed Mar 11, 2021
2 parents 9dd0948 + ccc5ca3 commit 6cfe32d
Showing 40 changed files with 1,096 additions and 229 deletions.
Binary file added assets/app_logo_circle_broken.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/drinking.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/eating.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/reading.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/sleeping.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/sport.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/walking.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/images/working.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/sounds/finish_bigdsc.mp3
Binary file not shown.
Binary file removed assets/sounds/light_pop.mp3
Binary file not shown.
Binary file added assets/sounds/light_pop_bigdsc.mp3
Binary file not shown.
36 changes: 34 additions & 2 deletions assets/translations/en.json
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@
"open_source_txt": "Open Source",
"about_balance_txt": "About Balance",
"version_txt": "Version",
"build_txt": "alpha.",
"build_txt": "beta.",
"made_with_heart_txt": "Credits",
"easter_egg_txt": "Keep clicking! The new release will come out faster!",
"empty_txt": "Nothing to show here!",
@@ -74,7 +74,7 @@
"leave_dialog_title": "Are you sure you want to leave?",
"leave_dialog_msg": "If you leave the test will fail",
"never_show_again": "Never show again",
"tutorial_msg": "Before performing a test select eyes open or eyes closed.\nPlease maintain a straight posture and hold the phone with two hands in a vertical position at the navel level.\nThe test will last about 30 seconds, at the end of which you will feel a vibration.",
"tutorial_msg": "While performing a test, keep a straight posture and hold the phone with two hands in a vertical position at the navel level.\nThe correct positioning of the smartphone will be suggested by a beep! The analysis will start with a vibration and will last approximately 30 seconds, after which you will hear an additional beep. Remember to keep your eyes open or closed as you stated throughout the measurement!",
"your_personal_info_txt": "My personal data",
"general_title": "GENERAL",
"health_title": "HEALTH",
@@ -148,6 +148,38 @@
"about_balance_title": "About Balance",
"about_balance_msg": "Balance is an effort of Computer Science researchers at the University of Urbino with the aim of providing a tool to measure your posture in a few seconds, with your smartphone.",
"cool_btn": "Cool!",
"back_home_btn": "Back to Home Screen",
"report_title": "Support",
"report_issue_title": "Report an Issue",
"report_title_txt": "Report an Issue",
"report_description_txt": "Please describe...",
"report_snack_true_txt": "Report sent successfully!",
"report_snack_false_txt": "You should write something first!",
"report_send_txt": "Send Report",
"credits_description_txt": "Balance is a smartphone application developed by the researchers of the University of Urbino to measure your stability. It is designed to be easy to use and quick enough to give stabilometric indices in a few seconds. It is part of the iPhD funded by Regione Marche and led by Professor E. Lattanzi. The PhD is a joint initiative of the University of Urbino, Politecnica delle Marche, TU Wien and the company Digit Srl. All trademarks mentioned belong to their respective owners.",
"credits_authors_txt": "Authors",
"credits_authors_bigini_txt": "iPh.D Student @ University of Urbino",
"credits_authors_freschi_txt": "Researcher @ University of Urbino",
"credits_authors_lattanzi_txt": "Associate Professor @ University of Urbino",
"credits_developers_txt": "Developers",
"credits_developers_bigini_txt": "Software Developer @ University of Urbino",
"credits_developers_difrancesco_txt": "Development Support @ DIGIT Srl",
"credits_developers_calisti_txt": "Original Design & Code @ University of Urbino",
"credits_collaborators_txt": "Collaborators",
"credits_collaborators_klopfenstein_txt": "Infrastructure Provider @ DIGIT Srl",
"credits_collaborators_delpriori_txt": "Infrastructure Provider @ DIGIT Srl",
"credits_collaborators_bogliolo_txt": "Data Processor @ DIGIT Srl",
"credits_foundation_title": "Foundations",
"credits_sponsors_partners_txt": "Partners and Sponsors",
"test_measurement_ok_title": "Measurement performed correctly",
"test_measurement_ok_txt": "Congratulations! When you take a measurement we check your parameters. Some of these provide an indication of correct execution and it seems that everything went well!",
"test_measurement_wrong_title": "Suspicious Measurement",
"test_measurement_wrong_txt": "This measurement shows erratic results. If you did not execute the test correctly, try it again. If the problem persists, contact the support.",
"test_backend_ok_title": "Measurement successfully sent to the server",
"test_backend_ok_txt": "Your measurement has been successfully recorded. You are actively contributing to Balance's research and development!",
"test_backend_wrong_title": "Your measurement was not saved",
"test_backend_wrong_txt": "What a pity! The measurement did not reach our servers (maybe they were resting, never sleep a wink). By submitting the results you will help our researchers better understand stability. Please connect to the internet next time.",
"intro_backend_snack_txt": "Unable to retrieve the unique Token. Please activate your Internet connection and try again. If the problem persists, try again in a few hours.",
"privacy_title": "Balance Terms and Conditions and Privacy Policy",
"privacy_finality_title": "Purposes and Methods of Processing",
"privacy_finality_txt": "The processing of your personal data through the Balance application is carried out for the realization of the scientific purposes of the \"Smartphone-based Postural Stability Monitoring System for Falls Prevention in the Elderly\" project. The Project was drawn up in accordance with the methodological standards of the disciplinary sector concerned and is filed with the Department of Pure and Applied Sciences of the University of Urbino Carlo Bo, where it will be kept for five years from the scheduled conclusion of the research itself. Your personal data will be processed only to the extent that they are indispensable in relation to the objective of the project, in compliance with the provisions of current legislation on the protection of personal data and in accordance with the provisions of the general authorizations of the Guarantor Authority for the protection of personal data. Your data will be processed exclusively by the Data Controller, by the Scientific Manager and/or by authorized subjects in the context of the realization of the Project, with automated and non-automated tools, exclusively to allow the carrying out of the research in question and all the related operations and related activities, including administrative ones, conducted by the University of Urbino in collaboration with Digit srl. The data will be processed electronically by adopting pseudonymisation: the user's identity will never be requested but a unique identifier will be associated with each installation that will allow the data provided by the same user to be correlated over time. Furthermore, although the experimentation does not fall into medical devices, the use of Balance will constitute a service for users through personal monitoring of their progress.",
36 changes: 34 additions & 2 deletions assets/translations/it.json
Original file line number Diff line number Diff line change
@@ -18,7 +18,7 @@
"open_source_txt": "Open Source",
"about_balance_txt": "Riguardo a Balance",
"version_txt": "Versione",
"build_txt": "alpha.",
"build_txt": "beta.",
"made_with_heart_txt": "Crediti",
"easter_egg_txt": "Continua a cliccare! La nuova release uscirà più velocemente!",
"empty_txt": "Non c'è nulla da vedere qui!",
@@ -74,7 +74,7 @@
"leave_dialog_title": "Sei sicuro di voler uscire?",
"leave_dialog_msg": "Se esci il test fallirà",
"never_show_again": "Non mostrare più",
"tutorial_msg": "Prima di eseguire un test seleziona occhi aperti oppure occhi chiusi. \nPer favore mantieni una postura dritta e tieni il telefono con due mani in posizione verticale all'altezza dell'ombelico. \nIl test durerà circa 30 secondi, al termine dei quali sentirai una vibrazione.",
"tutorial_msg": "Durante l'esecuzione mantieni una postura dritta e tieni il telefono con due mani in posizione verticale all'altezza dell'ombelico.\nIl corretto posizionamento del telefono ti verrà suggerito da un segnale acustico! L'analisi inizierà con una vibrazione e durerà circa 30 secondi, al termine dei quali sentirai un'ulteriore segnale acustico. Ricorda di tenere gli occhi aperti o chiusi come hai dichiarato per tutto il tempo della misurazione!",
"your_personal_info_txt": "Le mie informazioni personali",
"general_title": "GENERALE",
"health_title": "SALUTE",
@@ -148,6 +148,38 @@
"about_balance_title": "Riguardo a Balance",
"about_balance_msg": "Balance nasce dallo sforzo dei ricercatori di Computer Science dell'Università di Urbino con l'obbiettivo di fornire uno strumento per misurare la propria postura in pochi secondi, con il proprio smartphone.",
"cool_btn": "Figo!",
"back_home_btn": "Torna alla Schermata Principale",
"report_title": "Supporto",
"report_issue_title": "Segnala un Problema",
"report_title_txt": "Segnala Un Malfunzionamento",
"report_description_txt": "Descrivici il problema...",
"report_snack_true_txt": "Segnalazione inviata con successo!",
"report_snack_false_txt": "Prima dovresti scriverci qualcosa!",
"report_send_txt": "Invia Segnalazione",
"credits_description_txt": "Balance è un'applicazione per smartphone sviluppata dai ricercatori dell'Università degli Studi di Urbino per misurare la tua stabilità. È progettato per essere facile da usare e abbastanza veloce da fornire indici stabilometrici in pochi secondi. Fa parte del percorso di Dottorato Innovativo finanziato dalla Regione Marche e guidato dal Professor E. Lattanzi. Il Dottorato di Ricerca è un'iniziativa congiunta dell'Università degli Studi di Urbino, Politecnica delle Marche, Università Tecnica di Vienna e la società Digit Srl. Tutti i marchi citati appartengono ai rispettivi proprietari.",
"credits_authors_txt": "Autori",
"credits_authors_bigini_txt": "Studente Dottorato Innovativo @ Università degli Studi di Urbino",
"credits_authors_freschi_txt": "Ricercatore @ Università degli Studi di Urbino",
"credits_authors_lattanzi_txt": "Professore Associato @ Università degli Studi di Urbino",
"credits_developers_txt": "Sviluppatori",
"credits_developers_bigini_txt": "Sviluppatore Software @ Università degli Studi di Urbino",
"credits_developers_difrancesco_txt": "Supporto allo Sviluppo @ DIGIT Srl",
"credits_developers_calisti_txt": "Design & Codice Originale @ Università degli Studi di Urbino",
"credits_collaborators_txt": "Collaboratori",
"credits_collaborators_klopfenstein_txt": "Fornitore Infrastrutture @ DIGIT Srl",
"credits_collaborators_delpriori_txt": "Fornitore Infrastrutture @ DIGIT Srl",
"credits_collaborators_bogliolo_txt": "Titolare del Trattamento Dati @ DIGIT Srl",
"credits_foundation_title": "Fondamenti",
"credits_sponsors_partners_txt": "Sponsor e Partner",
"test_measurement_ok_title": "Misurazione eseguita correttamente",
"test_measurement_ok_txt": "Congratulazioni! Quando esegui una misurazione verifichiamo i tuoi parametri. Alcuni di questi forniscono un'indicazione riguardante la corretta esecuzione e sembra sia andato tutto bene!",
"test_measurement_wrong_title": "Misurazione Sospetta",
"test_measurement_wrong_txt": "Questa misurazione presenta dei risultati irregolari. Se non hai eseguito il test correttamente, effettualo nuovamente. Se il problema persiste, contatta i ricercatori.",
"test_backend_ok_title": "Misurazione inviata correttamente al server",
"test_backend_ok_txt": "La tua misurazione é stata registrata correttamente. Stai contribuendo attivamente alla ricerca e sviluppo di Balance!",
"test_backend_wrong_title": "La tua misurazione non é stata salvata",
"test_backend_wrong_txt": "Che peccato! La misurazione non ha raggiunto i nostri server (forse si stavano riposando, non chiudono mai occhio). Inviando i risultati aiuterai i nostri ricercatori a comprendere meglio la stabilità. Per favore, attiva la connessione alla rete internet la prossima volta.",
"intro_backend_snack_txt": "Impossibile recuperare il Token univoco. Attivare la connessione Internet e riprovare. Se il problema persiste, riprovare fra qualche ora.",
"privacy_title": "Termini e Condizioni e Informativa sulla Privacy di Balance",
"privacy_finality_title": "Finalità e Modalità del Trattamento",
"privacy_finality_txt": "Il trattamento dei Suoi dati personali attraverso l'applicazione Balance è effettuato per la realizzazione delle finalità scientifiche del Progetto \"Sistema di Monitoraggio della Stabilità Posturale basato su Smartphone\". Il Progetto è stato redatto conformemente agli standard metodologici del settore disciplinare interessato ed è depositato presso il Dipartimento di Scienze Pure e Applicate dell’Università degli Studi di Urbino Carlo Bo, ove verrà conservato per cinque anni dalla conclusione programmata della ricerca stessa. I suoi dati personali saranno trattati soltanto nella misura in cui siano indispensabili in relazione all'obiettivo del progetto, nel rispetto di quanto previsto dalla normativa vigente in materia di protezione dei dati personali e conformemente alle disposizioni di cui alle autorizzazioni generali dell’Autorità Garante per la protezione dei dati personali.I Suoi dati saranno trattati esclusivamente dal Titolare, dal Responsabile scientifico e/o da soggetti autorizzati nell’ambito della realizzazione del Progetto, con strumenti automatizzati e non, esclusivamente per consentire lo svolgimento della ricerca in parola e di tutte le relative operazioni ed attività connesse, comprese quelle amministrative, condotte dal’Università di Urbino in collaborazione con Digit srl. I dati saranno trattati mediante strumenti elettronici adottando la pseudonimizzazione: l’identità dell’utente non verrà mai richiesta ma ad ogni installazione verrà associato un identificativo univoco che consentirà di correlare nel tempo i dati forniti dallo stesso utente. Inoltre, sebbene la sperimentazione non ricada nei dispositivi di tipo medico, l’utilizzo di Balance costituirà un servizio per i soggetti utilizzatori attraverso un monitoraggio personale dei propri progressi.",
2 changes: 1 addition & 1 deletion ios/Flutter/.last_build_id
Original file line number Diff line number Diff line change
@@ -1 +1 @@
bdd717abda10237c2fa465b7a097dac7
f4a445f069a0b2de2c728ed6b4490eda
4 changes: 2 additions & 2 deletions ios/Podfile.lock
Original file line number Diff line number Diff line change
@@ -22,7 +22,7 @@ PODS:
- sqflite (0.0.2):
- Flutter
- FMDB (>= 2.7.5)
- vibration (1.7.1):
- vibration (1.7.3):
- Flutter
- wakelock (0.0.1):
- Flutter
@@ -83,7 +83,7 @@ SPEC CHECKSUMS:
sensors: 84eb7a30e47a649e4172b71d6e81be614c280336
shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d
sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904
vibration: 4f4ded27b9d0e21e4110ca43978f057412d28c44
vibration: b5a33e764c3f609a975b9dca73dce20fdde627dc
wakelock: bfc7955c418d0db797614075aabbc58a39ab5107

PODFILE CHECKSUM: aafe91acc616949ddb318b77800a7f51bffa2a4c
2 changes: 1 addition & 1 deletion lib/bloc/main/home/countdown_bloc_impl.dart
Original file line number Diff line number Diff line change
@@ -46,7 +46,7 @@ class CountdownBloc extends Bloc<CountdownEvents, CountdownState> {
print("CountdownBloc.mapEventToState: startPreMeasure");
_isCountdownCancelled = false;
_countdownTimer = CountdownTimer(
Duration(milliseconds: 6000),
Duration(milliseconds: 2000),
Duration(milliseconds: 1000)
)..listen((event) { /*No-Op*/ },
onDone: () {
12 changes: 8 additions & 4 deletions lib/floor/test_database_view.dart
Original file line number Diff line number Diff line change
@@ -14,7 +14,7 @@ import 'package:floor/floor.dart';
///
/// See also:
/// * [Measurement]
@DatabaseView("SELECT id, creation_date, eyes_open, invalid FROM measurements", viewName: "tests")
@DatabaseView("SELECT id, creation_date, eyes_open, invalid, sent FROM measurements", viewName: "tests")
class Test {
@ColumnInfo(name: "id")
final int id;
@@ -24,15 +24,18 @@ class Test {
final bool eyesOpen;
@ColumnInfo(name: "invalid")
final bool invalid;
@ColumnInfo(name: "sent")
final bool sent;

Test({this.id, this.creationDate, this.eyesOpen, this.invalid});
Test({this.id, this.creationDate, this.eyesOpen, this.invalid, this.sent});

@override
bool operator ==(other) => other is Test &&
other.id == id &&
other.creationDate == creationDate &&
other.eyesOpen == eyesOpen &&
other.invalid == invalid;
other.invalid == invalid &&
other.sent == sent;

@override
int get hashCode {
@@ -42,9 +45,10 @@ class Test {
result = prime * result + creationDate.hashCode;
result = prime * result + eyesOpen.hashCode;
result = prime * result + invalid.hashCode;
result = prime * result + sent.hashCode;
return result;
}

@override
String toString() => "Test(id=$id, creationDate=$creationDate, eyesOpen=$eyesOpen, invalid=$invalid)";
String toString() => "Test(id=$id, creationDate=$creationDate, eyesOpen=$eyesOpen, invalid=$invalid, sent=$sent)";
}
6 changes: 4 additions & 2 deletions lib/main.dart
Original file line number Diff line number Diff line change
@@ -3,6 +3,7 @@ import 'dart:io';
import 'package:balance_app/bloc/intro_state/on_boarding_data_bloc.dart';
import 'package:balance_app/bloc/main/home/countdown_bloc_impl.dart';
import 'package:balance_app/screens/calibration/quick_calibration_screen.dart';
import 'package:balance_app/screens/issues/issues_screen.dart';
import 'package:balance_app/screens/res/colors.dart';
import 'package:balance_app/screens/res/theme.dart';
import 'package:device_info/device_info.dart';
@@ -41,7 +42,7 @@ Future<void> main() async {
var sdkInt = androidInfo.version.sdkInt;
var manufacturer = androidInfo.manufacturer;
var model = androidInfo.model;
PreferenceManager.updateSystemInfo(producer: manufacturer, model: model, appVersion: "alpha.5", osVersion: "Android "+release+" SDK "+sdkInt.toString());
PreferenceManager.updateSystemInfo(producer: manufacturer, model: model, appVersion: "beta.5", osVersion: "Android "+release+" SDK "+sdkInt.toString());
}

if (Platform.isIOS) {
@@ -50,7 +51,7 @@ Future<void> main() async {
var version = iosInfo.systemVersion;
var name = iosInfo.name;
//var model = iosInfo.model;
PreferenceManager.updateSystemInfo(producer: "Apple", model: name, appVersion: "alpha.5", osVersion: systemName+" "+version);
PreferenceManager.updateSystemInfo(producer: "Apple", model: name, appVersion: "beta.5", osVersion: systemName+" "+version);
}

runApp(
@@ -95,6 +96,7 @@ class BalanceApp extends StatelessWidget {
Routes.info: (_) => UserInfoRecapScreen(),
Routes.slider: (_) => SliderScreen(),
Routes.credits: (_) => CreditsScreen(),
Routes.issues: (_) => IssuesScreen(),
Routes.result: (_) => ResultScreen(),
Routes.open_source: (_) => OpenSourceScreen(),
},
22 changes: 22 additions & 0 deletions lib/manager/preference_manager.dart
Original file line number Diff line number Diff line change
@@ -11,6 +11,8 @@ class PreferenceManager {
static const _showTutorial = "ShowTutorial";
// Calibration
static const _isDeviceCalibrated = "IsDeviceCalibrated";
// Initial Condition
static const _initialCondition = "initialCondition";
// Accelerometer
static const _accelerometerBiasX = "AccelerometerBiasX";
static const _accelerometerBiasY = "AccelerometerBiasY";
@@ -74,6 +76,16 @@ class PreferenceManager {
return pref.getBool(_isDeviceCalibrated) ?? false;
}

/// Update the all sensors biases with fresh values
///
/// Given an accelerometer [SensorBias] and a gyroscope
/// [SensorBias] update their values into the [SharedPreferences]
/// and set [_isDeviceCalibrated] to true.
static Future<void> updateInitialCondition(int condition) async {
var pref = await SharedPreferences.getInstance();
pref.setInt(_initialCondition, condition);
}

/// Update the all sensors biases with fresh values
///
/// Given an accelerometer [SensorBias] and a gyroscope
@@ -109,6 +121,16 @@ class PreferenceManager {
pref.setString(_osVersion, osVersion);
}

/// Return a [Future] with accelerometer [SensorBias]
///
/// This method will return the accelerometer
/// [SensorBias] stored in [SharedPreferences];
/// if the values are null [0.0] will be passed instead.
static Future<int> get initialCondition async {
var pref = await SharedPreferences.getInstance();
return pref.getInt(_initialCondition) ?? 0;
}

/// Return a [Future] with accelerometer [SensorBias]
///
/// This method will return the accelerometer
14 changes: 9 additions & 5 deletions lib/manager/vibration_manager.dart
Original file line number Diff line number Diff line change
@@ -4,7 +4,6 @@ import 'package:audioplayers/audioplayers.dart';
import 'package:vibration/vibration.dart';

class VibrationManager {
static const _vibrationPattern = [0, 300, 700, 300, 700, 300, 700, 300, 700, 300, 700, 300, 700];
static const _longVibrationTime = 800;
final AudioCache _audioCache;
AudioPlayer _playing;
@@ -14,12 +13,17 @@ class VibrationManager {
prefix: "sounds/",
respectSilence: true
) {
_audioCache.load("light_pop.mp3");
_audioCache.load("light_pop_bigdsc.mp3");
_audioCache.load("finish_bigdsc.mp3");
}

Future<void> playPattern() async{
_playing = await _audioCache.play("light_pop.mp3");
Vibration.vibrate(pattern: _vibrationPattern);
Future<void> measuring() async{
_playing = await _audioCache.play("light_pop_bigdsc.mp3");
return null;
}

Future<void> finish() async{
_playing = await _audioCache.play("finish_bigdsc.mp3");
return null;
}

71 changes: 67 additions & 4 deletions lib/model/measurement.dart
Original file line number Diff line number Diff line change
@@ -15,6 +15,12 @@ class Measurement {
// Flag the measurement if invalid
@ColumnInfo(name: "invalid")
final bool invalid;
// Flag the measurement if invalid
@ColumnInfo(name: "initCondition")
final int initCondition;
// Flag the measurement if invalid
@ColumnInfo(name: "sent")
final bool sent;
// General info about the measurement
@ColumnInfo(name: "eyes_open", nullable: false)
final bool eyesOpen;
@@ -80,6 +86,8 @@ class Measurement {
this.id,
this.token,
this.invalid = false,
this.initCondition = 0,
this.sent = true,
this.creationDate,
this.eyesOpen,
this.hasFeatures = false,
@@ -107,11 +115,13 @@ class Measurement {
/// This method will create a new [Measurement] form an existing one and
/// a [Statokinesigram].
/// The new [Measurement] will retain the id of the given one.
factory Measurement.from(Measurement m, String token, Statokinesigram s) =>
factory Measurement.from(Measurement m, String token, int initCondition, Statokinesigram s, bool delivered) =>
Measurement(
id: m.id,
token: token,
initCondition: initCondition,
invalid: s.outOfRange == 1.0 ? true : false,
sent: delivered ?? true,
creationDate: m.creationDate,
eyesOpen: m.eyesOpen,
hasFeatures: true,
@@ -150,10 +160,59 @@ class Measurement {
grZ: s.grZ,
);

/// Factory method to update the sending state of a [Measurement]
/// This method will create a new [Measurement] form an existing one.
/// The new [Measurement] will have the right bool for sent.
factory Measurement.sent(Measurement m, bool sent) =>
Measurement(
id: m.id,
token: m.token,
initCondition: m.initCondition,
invalid: m.invalid,
sent: sent,
creationDate: m.creationDate,
eyesOpen: m.eyesOpen,
hasFeatures: true,
swayPath: m.swayPath,
meanDisplacement: m.meanDisplacement,
stdDisplacement: m.stdDisplacement,
minDist: m.minDist,
maxDist: m.maxDist,
frequencyPeakAP: m.frequencyPeakAP,
frequencyPeakML: m.frequencyPeakML,
meanFrequencyML: m.meanFrequencyML,
meanFrequencyAP: m.meanFrequencyAP,
f80ML: m.f80ML,
f80AP: m.f80AP,
numMax: m.numMax,
meanTime: m.meanTime,
stdTime: m.stdTime,
meanDistance: m.meanDistance,
stdDistance: m.stdDistance,
meanPeaks: m.meanPeaks,
stdPeaks: m.stdPeaks,
gsX: m.gsX,
gsY: m.gsY,
gsZ: m.gsZ,
gkX: m.gkX,
gkY: m.gkY,
gkZ: m.gkZ,
gmX: m.gmX,
gmY: m.gmY,
gmZ: m.gmZ,
gvX: m.gvX,
gvY: m.gvY,
gvZ: m.gvZ,
grX: m.grX,
grY: m.grY,
grZ: m.grZ,
);

Map<String, dynamic> toJson() => {
'id': this.id,
'token': this.token,
'invalid': this.invalid,
'initCondition': this.initCondition,
'creationDate': this.creationDate,
'eyesOpen': this.eyesOpen,
'hasFeatures': this.hasFeatures,
@@ -191,6 +250,8 @@ class Measurement {
runtimeType == other.runtimeType &&
id == other.id &&
invalid == other.invalid &&
initCondition == other.initCondition &&
sent == other.sent &&
creationDate == other.creationDate &&
eyesOpen == other.eyesOpen &&
hasFeatures == other.hasFeatures &&
@@ -232,6 +293,8 @@ class Measurement {
int get hashCode =>
id.hashCode ^
invalid.hashCode ^
initCondition.hashCode ^
sent.hashCode ^
creationDate.hashCode ^
eyesOpen.hashCode ^
hasFeatures.hashCode ^
@@ -272,7 +335,7 @@ class Measurement {
@override
String toString() {
return 'Measurement('
'id: $id, invalid: $invalid, creationDate: $creationDate, eyesOpen: $eyesOpen, hasFeatures: $hasFeatures, '
'id: $id, invalid: $invalid, condition: $initCondition, creationDate: $creationDate, eyesOpen: $eyesOpen, hasFeatures: $hasFeatures, '
'swayPath: $swayPath, meanDisplacement: $meanDisplacement, '
'stdDisplacement: $stdDisplacement, minDist: $minDist, maxDist: $maxDist, '
'meanFrequencyAP: $meanFrequencyAP, meanFrequencyML: $meanFrequencyML, '
@@ -284,11 +347,11 @@ class Measurement {
}

String toCSV() {
return 'id;invalid;creationDate;eyesOpen;hasFeatures;swayPath;meanDisplacement;stdDisplacement;'
return 'id;invalid;condition;creationDate;eyesOpen;hasFeatures;swayPath;meanDisplacement;stdDisplacement;'
'minDist;maxDist;meanFrequencyAP;meanFrequencyML;frequencyPeakAP;frequencyPeakML;'
'f80AP;f80ML;np;meanTime;stdTime;meanDistance;stdDistance;meanPeaks;stdPeaks;'
'grX;grY;grZ;gmX;gmY;gmZ;gvX;gvY;gvZ;gkX;gkY;gkZ;gsX;gsY;gsZ\n'
'$id;$invalid;$creationDate;$eyesOpen;$hasFeatures;$swayPath;$meanDisplacement;'
'$id;$invalid;$initCondition;$creationDate;$eyesOpen;$hasFeatures;$swayPath;$meanDisplacement;'
'$stdDisplacement;$minDist;$maxDist;$meanFrequencyAP;$meanFrequencyML;'
'$frequencyPeakAP;$frequencyPeakML;$f80AP;$f80ML;$numMax;$meanTime;'
'$stdTime;$meanDistance;$stdDistance;$meanPeaks;$stdPeaks;$grX;$grY;'
38 changes: 23 additions & 15 deletions lib/repository/measure_countdown_repository.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'dart:async';
import 'dart:convert';
import 'dart:io';


import 'package:balance_app/floor/measurement_database.dart';
@@ -29,13 +31,12 @@ class MeasureCountdownRepository {
),
);

// Generate data as RawMeasurementData
var rawData = _generateRawData(rawSensorData, newMeasId).toList();
// Store the SensorData in database as RawMeasurementData
await rawMeasDataDao.insertRawMeasurements(
await _generateRawData(rawSensorData, newMeasId).toList()
);

// send data to server
_makePostRequest(await _generateRawData(rawSensorData, newMeasId).toList());
await rawMeasDataDao.insertRawMeasurements(await rawData);
// Send data to server
_makePostRequest(await rawData);

// return the newly added Test
return await measurementDao.findTestById(newMeasId);
@@ -45,21 +46,28 @@ class MeasureCountdownRepository {
}
}

_makePostRequest(var data) async {
// TODO: This stuff here is hardcode. Need changes
Future<bool> _makePostRequest(var data) async {
// set up POST request arguments
String url = 'https://www.balancemobile.it/api/v1/db/sway';
//String url = 'http://192.168.1.206:8000/api/v1/db/sway';
Map<String, String> headers = {"Content-type": "application/json"};
String json = jsonEncode(data);

// make POST request
Response response = await post(url, headers: headers, body: json);
try {
Response response = await post(url, headers: headers, body: jsonEncode(data)).timeout(Duration(seconds: 30));

// response
//int statusCode = response.statusCode;
//String body = response.body;
print("Response from backend: "+response.toString());
if (response.statusCode == 200) {
return true;
} else {
print("_SendingData.RawMeasurement: The server answered with: "+response.statusCode.toString());
return false;
}
} on TimeoutException catch (_) {
print("_SendingData.RawMeasurement: The connection dropped, maybe the server is congested");
return false;
} on SocketException catch (_) {
print("_SendingData.RawMeasurement: Communication failed. The server was not reachable");
return false;
}
}

/// Asynchronously generate the [RawMeasurementData] from the [SensorData]
42 changes: 29 additions & 13 deletions lib/repository/result_repository.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@

import 'dart:async';
import 'dart:convert';
import 'dart:io';

@@ -29,6 +30,7 @@ class ResultRepository {
final measurement = await database.measurementDao.findMeasurementById(measurementId);
final cogv = await database.cogvDataDao.findAllCogvDataForId(measurementId);
final token = (await PreferenceManager.userInfo).token;
final condition = await PreferenceManager.initialCondition;
// 2. Check if the features and the cogv data are present and compute them if not
if (!measurement.hasFeatures && cogv.isEmpty) {
print("ResultRepository.getResult: Computing Features...");
@@ -39,8 +41,15 @@ class ResultRepository {
final computed = await PostureProcessor.computeFromData(measurementId, rawMeasurementData);

// Update the measurement with the computed features
database.measurementDao.updateMeasurement(Measurement.from(measurement, token, computed));
_makePostRequest(Measurement.from(measurement, token, computed));
var created_measurement = Measurement.from(measurement, token, condition, computed, true);
print(jsonEncode(created_measurement));
bool result = await _makePostRequest(created_measurement);
if (result)
database.measurementDao.updateMeasurement(created_measurement);
else {
database.measurementDao.updateMeasurement(Measurement.from(measurement, token, condition, computed, false));
print("_SendingData.Measurement: BAD DELIVERY STATUS for test $measurementId");
}

// Store the computed CogvData
database.cogvDataDao.insertCogvData(computed.cogv);
@@ -50,21 +59,28 @@ class ResultRepository {
return Statokinesigram.from(measurement, cogv);
}

_makePostRequest(var data) async {
// TODO: This stuff here is hardcode. Need changes
// set up POST request arguments
Future<bool> _makePostRequest(var data) async {
// set up POST request argumentsq
String url = 'https://www.balancemobile.it/api/v1/db/measurement';
//String url = 'http://192.168.1.206:8000/api/v1/db/measurement';
Map<String, String> headers = {"Content-type": "application/json"};
String json = jsonEncode(data.toJson());

// make POST request
Response response = await post(url, headers: headers, body: json);

// response
//int statusCode = response.statusCode;
//String body = response.body;
print("Response from backend: "+response.toString());
try {
Response response = await post(url, headers: headers, body: jsonEncode(data)).timeout(Duration(seconds: 5));

if (response.statusCode == 200) {
return true;
} else {
print("_SendingData.Measurement: The server answered with: "+response.statusCode.toString());
return false;
}
} on TimeoutException catch (_) {
print("_SendingData.Measurement: The connection dropped, maybe the server is congested");
return false;
} on SocketException catch (_) {
print("_SendingData.Measurement: Communication failed. The server was not reachable");
return false;
}
}

/// Save all the measurement in a .json file
1 change: 1 addition & 0 deletions lib/routes.dart
Original file line number Diff line number Diff line change
@@ -12,4 +12,5 @@ class Routes {
static const String result = "/result_route";
static const String open_source = "/open_source";
static const String credits = "/credits";
static const String issues = "/issues";
}
12 changes: 2 additions & 10 deletions lib/screens/calibration/quick_calibration_screen.dart
Original file line number Diff line number Diff line change
@@ -46,18 +46,10 @@ class QuickCalibrationScreen extends StatelessWidget {
Align(
alignment: Alignment.center,
child: RaisedButton(
onPressed: () {
if (state == SensorController.listening)
null;
else if (state == SensorController.complete) {
Navigator.pop(context);
}
else
controller.listen();
},
onPressed: state == SensorController.listening ? null : (state == SensorController.complete ? () => Navigator.pop(context) : () => controller.listen()),
child: Text(
state == SensorController.complete
? 'Torna alla Schermata principale'
? 'back_home_btn'.tr()
: 'start_calibration_btn'.tr()
),
),
181 changes: 162 additions & 19 deletions lib/screens/credits/credits.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,26 @@

import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:package_info/package_info.dart';

class CreditsScreen extends StatefulWidget {
@override
_CreditsScreenState createState() => _CreditsScreenState();
}

/// Widget for displaying informations about open source dependencies
class CreditsScreen extends StatelessWidget {
class _CreditsScreenState extends State<CreditsScreen> {
PackageInfo packageInfo;

@override
void initState() {
super.initState();
PackageInfo.fromPlatform().then((value) => setState(() => packageInfo = value));
}

@override
Widget build(BuildContext context) {
PackageInfo.fromPlatform().then((value) => packageInfo = value);
return Scaffold(
appBar: AppBar(
title: Text('about_title'.tr()),
@@ -24,12 +39,27 @@ class CreditsScreen extends StatelessWidget {
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 16.0),
padding: const EdgeInsets.fromLTRB(0.0,16.0,0.0,0.0),
child: Text(
'Balance Mobile ©',
style: Theme.of(context).textTheme.headline4,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
child: Text(
"${'version_txt'.tr()} ${packageInfo?.version} (${'build_txt'.tr()}${packageInfo?.buildNumber})",
style: Theme.of(context).textTheme.bodyText1,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
child: Text(
"credits_description_txt".tr(),
style: Theme.of(context).textTheme.bodyText1,
textAlign: TextAlign.justify,
),
),
Card(
margin: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 4.0),
child: Padding(
@@ -38,14 +68,45 @@ class CreditsScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Authors',
'credits_authors_txt'.tr(),
style: Theme.of(context).textTheme.headline5,
),
Divider(),
SizedBox(height: 4.0),
SizedBox(height: 8.0),
RichText(
overflow: TextOverflow.clip,
text: TextSpan(
text: 'Gioele Bigini',
style: Theme.of(context).textTheme.headline6.copyWith(fontSize: 16),
),
),
Text(
'Emanuele Lattanzi, Valerio Freschi, Gioele Bigini',
style: Theme.of(context).textTheme.bodyText2,
'credits_authors_bigini_txt'.tr(),
style: Theme.of(context).textTheme.bodyText1,
),
SizedBox(height: 8.0),
RichText(
overflow: TextOverflow.clip,
text: TextSpan(
text: 'Valerio Freschi',
style: Theme.of(context).textTheme.headline6.copyWith(fontSize: 16),
),
),
Text(
'credits_authors_freschi_txt'.tr(),
style: Theme.of(context).textTheme.bodyText1,
),
SizedBox(height: 8.0),
RichText(
overflow: TextOverflow.clip,
text: TextSpan(
text: 'Emanuele Lattanzi',
style: Theme.of(context).textTheme.headline6.copyWith(fontSize: 16),
),
),
Text(
'credits_authors_lattanzi_txt'.tr(),
style: Theme.of(context).textTheme.bodyText1,
),
],
),
@@ -59,14 +120,45 @@ class CreditsScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Developers',
'credits_developers_txt'.tr(),
style: Theme.of(context).textTheme.headline5,
),
Divider(),
SizedBox(height: 4.0),
SizedBox(height: 8.0),
RichText(
overflow: TextOverflow.clip,
text: TextSpan(
text: 'Gioele Bigini',
style: Theme.of(context).textTheme.headline6.copyWith(fontSize: 16),
),
),
Text(
'credits_developers_bigini_txt'.tr(),
style: Theme.of(context).textTheme.bodyText1,
),
SizedBox(height: 8.0),
RichText(
overflow: TextOverflow.clip,
text: TextSpan(
text: 'Gian Marco di Francesco',
style: Theme.of(context).textTheme.headline6.copyWith(fontSize: 16),
),
),
Text(
'Gioele Bigini, Gianmarco di Francesco, Lorenzo Calisti',
style: Theme.of(context).textTheme.bodyText2,
'credits_developers_difrancesco_txt'.tr(),
style: Theme.of(context).textTheme.bodyText1,
),
SizedBox(height: 8.0),
RichText(
overflow: TextOverflow.clip,
text: TextSpan(
text: 'Lorenzo Calisti',
style: Theme.of(context).textTheme.headline6.copyWith(fontSize: 16),
),
),
Text(
'credits_developers_calisti_txt'.tr(),
style: Theme.of(context).textTheme.bodyText1,
),
],
),
@@ -80,14 +172,45 @@ class CreditsScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Collaboratori',
'credits_collaborators_txt'.tr(),
style: Theme.of(context).textTheme.headline5,
),
Divider(),
SizedBox(height: 4.0),
SizedBox(height: 8.0),
RichText(
overflow: TextOverflow.clip,
text: TextSpan(
text: 'Lorenz Cuno Klopfenstein',
style: Theme.of(context).textTheme.headline6.copyWith(fontSize: 16),
),
),
Text(
'Lorenz Cuno Klopfestein, Saverio Delpriori',
style: Theme.of(context).textTheme.bodyText2,
'credits_collaborators_klopfenstein_txt'.tr(),
style: Theme.of(context).textTheme.bodyText1,
),
SizedBox(height: 8.0),
RichText(
overflow: TextOverflow.clip,
text: TextSpan(
text: 'Saverio Delpriori',
style: Theme.of(context).textTheme.headline6.copyWith(fontSize: 16),
),
),
Text(
'credits_collaborators_delpriori_txt'.tr(),
style: Theme.of(context).textTheme.bodyText1,
),
SizedBox(height: 8.0),
RichText(
overflow: TextOverflow.clip,
text: TextSpan(
text: 'Alessandro Bogliolo',
style: Theme.of(context).textTheme.headline6.copyWith(fontSize: 16),
),
),
Text(
'credits_collaborators_bogliolo_txt'.tr(),
style: Theme.of(context).textTheme.bodyText1,
),
],
),
@@ -101,15 +224,35 @@ class CreditsScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Responsabile del Trattamento Dati',
'credits_foundation_title'.tr(),
style: Theme.of(context).textTheme.headline5,
),
Divider(),
SizedBox(height: 4.0),
SizedBox(height: 8.0),
RichText(
overflow: TextOverflow.clip,
text: TextSpan(
text: 'Standing Balance Assessment by Measurement of Body Center of Gravity Using Smartphones',
style: Theme.of(context).textTheme.headline6.copyWith(fontSize: 14),
),
),
Text(
'E. Lattanzi et al., IEEE Access, 2019',
style: Theme.of(context).textTheme.bodyText1,
),
SizedBox(height: 8.0),
RichText(
overflow: TextOverflow.clip,
text: TextSpan(
text: 'A Review on Blockchain for the Internet of Medical Things',
style: Theme.of(context).textTheme.headline6.copyWith(fontSize: 14),
),
),
Text(
'Alessandro Bogliolo',
style: Theme.of(context).textTheme.bodyText2,
'G. Bigini et al., Future Internet, 2020',
style: Theme.of(context).textTheme.bodyText1,
),
SizedBox(height: 8.0),
],
),
),
@@ -122,7 +265,7 @@ class CreditsScreen extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Sponsor e Partner',
'credits_sponsors_partners_txt'.tr(),
style: Theme.of(context).textTheme.headline5,
),
Divider(),
18 changes: 11 additions & 7 deletions lib/screens/intro/intro_screen.dart
Original file line number Diff line number Diff line change
@@ -26,6 +26,8 @@ import 'slider/posture.dart';
import 'slider/habits.dart';
import 'slider/sight.dart';

import 'package:easy_localization/easy_localization.dart';

/// This class show an introduction when the app is first open.
class IntroScreen extends StatefulWidget {
@override
@@ -121,10 +123,12 @@ class _IntroScreenState extends State<IntroScreen> {
bool result = await _makePostRequest(jsonEncode(await PreferenceManager.userInfo), true);

setState(() {
if (result) {
_isLoggedIn = true;
} else {
_isLoggedIn = false;
if (!result) {
Scaffold.of(context).showSnackBar(SnackBar(
duration: const Duration(seconds: 10),
behavior: SnackBarBehavior.floating,
content: Text('intro_backend_snack_txt'.tr())
));
}
_isInAsyncCall = false;
});
@@ -135,9 +139,9 @@ class _IntroScreenState extends State<IntroScreen> {
}
} else {
/*
* All the required data are stored... mark the
* first launch as done so we don't ask this data anymore
*/
* All the required data are stored... mark the
* first launch as done so we don't ask this data anymore
*/
print(await PreferenceManager.userInfo);
// Move to next page
_pageController.nextPage(
34 changes: 34 additions & 0 deletions lib/screens/intro/slider/widgets/turn_internet_on_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@

import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';

void showTurnInternetOnDialog(BuildContext context) {
showDialog(
context: context,
builder: (context) => AlertDialog(
backgroundColor: Colors.black45,
title:
Text('other_trauma_title'.tr(),
style: Theme.of(context).textTheme.subtitle2.copyWith(
fontSize: 16,
color: Colors.white,
),),
content:
Text('trauma_explained_txt'.tr(),
style: Theme.of(context).textTheme.subtitle2.copyWith(
fontSize: 12,
color: Colors.white,
),),
actions: [
FlatButton(
child: Text('close'.tr(),
style: Theme.of(context).textTheme.subtitle2.copyWith(
fontSize: 10,
color: Colors.white,
),),
onPressed: () => Navigator.pop(context),
)
],
),
);
}
153 changes: 153 additions & 0 deletions lib/screens/issues/issues_screen.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:balance_app/manager/preference_manager.dart';
import 'package:balance_app/screens/issues/widgets/custom_form_field.dart';
import 'package:flutter/material.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:http/http.dart';
import 'package:package_info/package_info.dart';

class IssuesScreen extends StatefulWidget {
@override
_IssuesScreenState createState() => _IssuesScreenState();
}

/// Widget for displaying informations about open source dependencies
class _IssuesScreenState extends State<IssuesScreen> {
PackageInfo packageInfo;
String token;
String _description;

@override
void initState() {
super.initState();
PackageInfo.fromPlatform().then((value) => setState(() => packageInfo = value));
_description = '';
}

@override
Widget build(BuildContext context) {
PackageInfo.fromPlatform().then((value) => packageInfo = value);
return Scaffold(
appBar: AppBar(
title: Text('report_title'.tr()),
),
body: Builder(
// Create an inner BuildContext so that the onPressed methods
// can refer to the Scaffold with Scaffold.of().
builder: (BuildContext context) {
return SingleChildScrollView(
padding: const EdgeInsets.only(
left: 8.0, top: 16.0, right: 8.0, bottom: 16.0),
child: Column(
children: <Widget>[
Container(
padding: const EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 16.0),
width: 150,
height: 150,
child: Center(
child: Image.asset("assets/app_logo_circle_broken.png"),
),
),
Padding(
padding: const EdgeInsets.fromLTRB(0.0, 16.0, 0.0, 0.0),
child: Text(
'report_title_txt'.tr(),
style: Theme
.of(context)
.textTheme
.headline5,
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8.0, vertical: 8.0),
child: Text(
"${'version_txt'.tr()} ${packageInfo
?.version} (${'build_txt'.tr()}${packageInfo
?.buildNumber})",
style: Theme
.of(context)
.textTheme
.bodyText1,
),
),
Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0, vertical: 16.0),
child: CustomFormField(
labelText: 'report_description_txt'.tr(),
initialValue: '',
onChanged: (value) {
setState(() {
_description = value;
});
},
validator: (value) {
if (value.isEmpty)
return 'too_short_error_txt'.tr();
},
),
),
RaisedButton(
onPressed: () async {
if (_description.length > 0) {
String token = (await PreferenceManager.userInfo).token;
String version = "${'version_txt'.tr()} ${packageInfo
?.version} (${'build_txt'.tr()}${packageInfo
?.buildNumber})";
String json = '{"token":"$token","version":"$version","description":"$_description"}';
_makePostRequest(json);
FocusScope.of(context).unfocus();
Scaffold.of(context).showSnackBar(SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('report_snack_true_txt'.tr()),
duration: Duration(seconds: 2),
));
} else {
FocusScope.of(context).unfocus();
Scaffold.of(context).showSnackBar(SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('report_snack_false_txt'.tr()),
duration: Duration(seconds: 2),
));
}
},
child: Text('report_send_txt'.tr())
),
SizedBox(height: 16),
],
),
);
},
),
);
}
}

Future<bool> _makePostRequest(var data) async {
// set up POST request arguments
String url = 'https://www.balancemobile.it/api/v1/db/reporting';
//String url = 'http://192.168.1.206:8000/api/v1/db/reporting';
Map<String, String> headers = {"Content-type": "application/json"};

try {
Response response = await post(url, headers: headers, body: data).timeout(Duration(seconds: 30));

if (response.statusCode == 200) {
return true;
} else {
print("_SendingData.Issues: The server answered with: "+response.statusCode.toString());
return false;
}
} on TimeoutException catch (_) {
print("_SendingData.Issues: The connection dropped, maybe the server is congested");
return false;
} on SocketException catch (_) {
print("_SendingData.Issues: Communication failed. The server was not reachable");
return false;
}
}
74 changes: 74 additions & 0 deletions lib/screens/issues/widgets/custom_form_field.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@

import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

/// StatelessWidget that implements the custom
/// style for a [TextFormField]
class CustomFormField extends StatelessWidget {
final String initialValue;
final String labelText;
final String suffix;
final bool decimal;
final ValueChanged<String> onChanged;
final FormFieldValidator<String> validator;
final FormFieldSetter<String> onSaved;

CustomFormField({
this.labelText,
this.suffix,
this.decimal = false,
this.initialValue,
this.onSaved,
this.validator,
this.onChanged,
});

@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
FocusScopeNode currentFocus = FocusScope.of(context);
},
child: Material(
elevation: 4,
color: Colors.white,
borderRadius: BorderRadius.circular(9),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 4),
child: TextFormField(
inputFormatters: [
new LengthLimitingTextInputFormatter(256),
],
keyboardType: TextInputType.multiline,
maxLines: null,
decoration: InputDecoration(
border: InputBorder.none,
hintText: labelText,
suffixText: suffix,
hintStyle: TextStyle(
color: Color(0xFFBFBFBF),
fontSize: 14,
fontWeight: FontWeight.w500,
),
suffixStyle: TextStyle(
color: Color(0xFFBFBFBF),
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
style: TextStyle(
color: Colors.black45,
fontSize: 14,
fontWeight: FontWeight.w500,
),
autocorrect: false,
initialValue: initialValue,
onChanged: (newValue) => onChanged?.call(newValue),
validator: validator,
onSaved: onSaved,
),
),
),
);
}
}
2 changes: 1 addition & 1 deletion lib/screens/main/home/widgets/circular_countdown.dart
Original file line number Diff line number Diff line change
@@ -64,7 +64,7 @@ class _CircularCounterState extends State<CircularCounter> with SingleTickerProv
Duration get _duration => Duration(
milliseconds: widget.state is CountdownMeasureState
? 30000
: 6000
: 2000
);

String get _timeString {
107 changes: 59 additions & 48 deletions lib/screens/main/home/widgets/measure_countdown.dart
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import 'dart:io';
import 'package:balance_app/manager/vibration_manager.dart';
import 'package:balance_app/routes.dart';
import 'package:balance_app/screens/main/home/widgets/device_not_ready_dialog.dart';
import 'package:balance_app/screens/main/home/widgets/measuring_condition_dialog.dart';
import 'package:balance_app/screens/main/home/widgets/targeting_game.dart';
import 'package:battery/battery.dart';
import 'package:flutter/material.dart';
@@ -93,11 +94,15 @@ class _MeasureCountdownState extends State<MeasureCountdown> with WidgetsBinding
// Start/Stop the vibration and sounds
if (state is CountdownPreMeasureState) {
Wakelock.enable();
vibrationManager.playPattern();
vibrationManager.playSingle();
}
else if (state is CountdownMeasureState || state is CountdownCompleteState) {
else if (state is CountdownMeasureState) {
Wakelock.enable();
vibrationManager.playSingle();
vibrationManager.measuring();
}
else if (state is CountdownCompleteState) {
Wakelock.enable();
vibrationManager.finish();
}
else {
Wakelock.disable();
@@ -121,55 +126,61 @@ class _MeasureCountdownState extends State<MeasureCountdown> with WidgetsBinding
SizedBox(height: 0),
RaisedButton(
onPressed: () async{
int batteryLevel = await _battery.batteryLevel;
if (batteryLevel >= 30) {
if (state is CountdownIdleState) {
/*
* Every time the user presses the start button we need to check
* two conditions:
* - the device is calibrated? if not ask the user to do it
* - we need to show the tutorial?
*/
bool isDeviceCalibrated = await PreferenceManager
.isDeviceCalibrated;
bool showTutorial = await PreferenceManager
.showMeasuringTutorial;
try {
if (await _battery.batteryLevel < 30)
return showDeviceNotReady(context);
} on Exception {
print("Cannot take battery level from smartphone");
}

if (!isDeviceCalibrated)
showCalibrateDeviceDialog(context);
else if (showTutorial)
showTutorialDialog(
context,
() =>
context.bloc<CountdownBloc>().add(
CountdownEvents.startTargeting)
);
else
context.bloc<CountdownBloc>().add(
CountdownEvents.startTargeting);
}
else if (state is CountdownPreMeasureState) {
// Stop the pre measure countdown
vibrationManager.cancel();
context.bloc<CountdownBloc>().add(
CountdownEvents.stopPreMeasure);
}
else if (state is CountdownMeasureState) {
// Stop the measurement
vibrationManager.cancel();
context.bloc<CountdownBloc>().add(
CountdownEvents.stopMeasure);
if (state is CountdownIdleState) {
/*
* Every time the user presses the start button we need to check
* two conditions:
* - the device is calibrated? if not ask the user to do it
* - we need to show the tutorial?
*/
bool isDeviceCalibrated = await PreferenceManager
.isDeviceCalibrated;
bool showTutorial = await PreferenceManager
.showMeasuringTutorial;

if (!isDeviceCalibrated)
showCalibrateDeviceDialog(context);
else if (showTutorial) {
showMeasuringConditionDialog(
context,
() => context.bloc<CountdownBloc>().add(CountdownEvents.startTargeting)
);
showTutorialDialog(
context,
() => null
);
}
else if (state is CountdownTargetingState) {
// Stop the measurement
vibrationManager.cancel();
context.bloc<CountdownBloc>().add(
CountdownEvents.stopTargeting);
else {
showMeasuringConditionDialog(context,
() => context.bloc<CountdownBloc>().add(CountdownEvents.startTargeting)
);
}
}
else {
showDeviceNotReady(context);
};
else if (state is CountdownPreMeasureState) {
// Stop the pre measure countdown
vibrationManager.cancel();
context.bloc<CountdownBloc>().add(
CountdownEvents.stopPreMeasure);
}
else if (state is CountdownMeasureState) {
// Stop the measurement
vibrationManager.cancel();
context.bloc<CountdownBloc>().add(
CountdownEvents.stopMeasure);
}
else if (state is CountdownTargetingState) {
// Stop the measurement
vibrationManager.cancel();
context.bloc<CountdownBloc>().add(
CountdownEvents.stopTargeting);
}
},
color: BColors.colorAccent,
child: Text(
195 changes: 195 additions & 0 deletions lib/screens/main/home/widgets/measuring_condition_dialog.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@

import 'package:balance_app/manager/preference_manager.dart';
import 'package:balance_app/screens/res/colors.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';

/// Show the [TutorialDialog]
///
/// The callback [onDone] is called every time the action button
/// is pressed and it lets the parent Widget start the measuring
void showMeasuringConditionDialog(BuildContext context, VoidCallback onDone) {
showDialog(
barrierDismissible: false,
context: context,
builder: (context) => MeasuringConditionDialog(onDone),
);
}

/// Widget that implements a tutorial dialog
///
/// This dialog has the purpose of teaching the user
/// how to correctly perform a measurement.
class MeasuringConditionDialog extends StatefulWidget {
final VoidCallback callback;

MeasuringConditionDialog(this.callback);

@override
_MeasuringConditionDialogState createState() => _MeasuringConditionDialogState();
}

class _MeasuringConditionDialogState extends State<MeasuringConditionDialog> {
int _value;

@override
Widget build(BuildContext context) {
return AlertDialog(
title: Text('Le tue condizioni'),
contentPadding: const EdgeInsets.all(0.0),
content: Container(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.all(24.0),
child: Text('Conoscere la tua condizione degli ultimi 15 minuti é estremamente importante. Seleziona le icone che meglio descrivono quello che stavi facendo!'),
),
SizedBox(width: 4),
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GestureDetector(
onTap: () => setState(() => _value = 1),
child: ClipRRect(
borderRadius: BorderRadius.circular(15.0),
child: Container(
height: 56,
width: 56,
color: _value == 1 ? BColors.colorPrimary : Colors.transparent,
child: Center(
child: Image.asset("assets/images/sleeping.png"),
),
),
),
),
SizedBox(width: 4),
GestureDetector(
onTap: () => setState(() => _value = 2),
child: ClipRRect(
borderRadius: BorderRadius.circular(15.0),
child: Container(
height: 56,
width: 56,
color: _value == 2 ? BColors.colorPrimary : Colors.transparent,
child: Center(
child: Image.asset("assets/images/working.png"),
),
),
),
),
SizedBox(width: 4),
GestureDetector(
onTap: () => setState(() => _value = 3),
child: ClipRRect(
borderRadius: BorderRadius.circular(15.0),
child: Container(
height: 56,
width: 56,
color: _value == 3 ? BColors.colorPrimary : Colors.transparent,
child: Center(
child: Image.asset("assets/images/walking.png"),
),
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GestureDetector(
onTap: () => setState(() => _value = 4),
child: ClipRRect(
borderRadius: BorderRadius.circular(15.0),
child: Container(
height: 56,
width: 56,
color: _value == 4 ? BColors.colorPrimary : Colors.transparent,
child: Center(
child: Image.asset("assets/images/reading.png"),
),
),
),
),
SizedBox(width: 4),
GestureDetector(
onTap: () => setState(() => _value = 5),
child: ClipRRect(
borderRadius: BorderRadius.circular(15.0),
child: Container(
height: 56,
width: 56,
color: _value == 5 ? BColors.colorPrimary : Colors.transparent,
child: Center(
child: Image.asset("assets/images/eating.png"),
),
),
),
),
SizedBox(width: 4),
GestureDetector(
onTap: () => setState(() => _value = 6),
child: ClipRRect(
borderRadius: BorderRadius.circular(15.0),
child: Container(
height: 56,
width: 56,
color: _value == 6 ? BColors.colorPrimary : Colors.transparent,
child: Center(
child: Image.asset("assets/images/drinking.png"),
),
),
),
),
],
),
),
Padding(
padding: const EdgeInsets.fromLTRB(24, 0, 24, 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
GestureDetector(
onTap: () => setState(() => _value = 7),
child: ClipRRect(
borderRadius: BorderRadius.circular(15.0),
child: Container(
height: 56,
width: 56,
color: _value == 7 ? BColors.colorPrimary : Colors.transparent,
child: Center(
child: Image.asset("assets/images/sport.png"),
),
),
),
),
],
),
),
],
),
),
),
actions: [
FlatButton(
onPressed: () {
widget.callback();
PreferenceManager.updateInitialCondition(_value ?? 0);
Navigator.pop(context);
},
child: Text('ok'.tr())
),
],
);
}
}
99 changes: 52 additions & 47 deletions lib/screens/main/home/widgets/measuring_tutorial_dialog.dart
Original file line number Diff line number Diff line change
@@ -11,6 +11,7 @@ import 'package:flutter/rendering.dart';
/// is pressed and it lets the parent Widget start the measuring
void showTutorialDialog(BuildContext context, VoidCallback onDone) {
showDialog(
barrierDismissible: false,
context: context,
builder: (context) => TutorialDialog(onDone),
);
@@ -36,55 +37,59 @@ class _TutorialDialogState extends State<TutorialDialog> {
Widget build(BuildContext context) {
return AlertDialog(
contentPadding: const EdgeInsets.all(0.0),
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.maxFinite,
height: 250,
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(9.0)),
child: Image.asset(
"assets/images/tutorial.png",
fit: BoxFit.fitHeight,
),
),
),
Padding(
padding: const EdgeInsets.all(24.0),
child: Text(
'tutorial_msg'.tr(),
textScaleFactor: 0.9,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: GestureDetector(
onTap: () {
setState(() => _neverShowAgainCheck = !_neverShowAgainCheck);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CircularCheckBox(
value: _neverShowAgainCheck,
onChanged: (value) {
setState(() => _neverShowAgainCheck = value);
},
activeColor: Colors.blue,
),
SizedBox(width: 8),
Text(
'never_show_again'.tr(),
textScaleFactor: 0.9,
content: Container(
child: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: double.maxFinite,
height: 190,
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(9.0)),
child: Image.asset(
"assets/images/tutorial.png",
fit: BoxFit.fitHeight,
),
],
),
),
),
)
],
Padding(
padding: const EdgeInsets.all(24.0),
child: Text(
'tutorial_msg'.tr(),
textScaleFactor: 0.9,
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 12.0),
child: GestureDetector(
onTap: () {
setState(() => _neverShowAgainCheck = !_neverShowAgainCheck);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
CircularCheckBox(
value: _neverShowAgainCheck,
onChanged: (value) {
setState(() => _neverShowAgainCheck = value);
},
activeColor: Colors.blue,
),
SizedBox(width: 8),
Text(
'never_show_again'.tr(),
textScaleFactor: 0.9,
),
],
),
),
)
],
),
),
),
actions: [
FlatButton(
2 changes: 1 addition & 1 deletion lib/screens/main/home/widgets/targeting_game.dart
Original file line number Diff line number Diff line change
@@ -104,7 +104,7 @@ class _TargetingGameState extends State<TargetingGame> {

timer2 = Timer.periodic(Duration(milliseconds: 1000), (_) {
if (count == 0) {
FlutterBeep.beep();
FlutterBeep.beep(false);
}
});

28 changes: 19 additions & 9 deletions lib/screens/main/measurements/measurements_screen.dart
Original file line number Diff line number Diff line change
@@ -13,7 +13,14 @@ import 'package:provider/provider.dart';
import 'package:balance_app/routes.dart';
import 'package:easy_localization/easy_localization.dart';

class MeasurementsScreen extends StatelessWidget {
class MeasurementsScreen extends StatefulWidget {
MeasurementsScreen();

@override
_MeasurementsScreenState createState() => _MeasurementsScreenState();
}

class _MeasurementsScreenState extends State<MeasurementsScreen> {
@override
Widget build(BuildContext context) {
return BlocProvider(
@@ -29,7 +36,7 @@ class MeasurementsScreen extends StatelessWidget {
return ListView.builder(
padding: EdgeInsets.symmetric(vertical: 8),
itemBuilder: (context, index) =>
_measurementItemTemplate(context, state.tests[index]),
_measurementItemTemplate(context,state.tests[index]),
itemCount: state.tests.length,
);
}
@@ -160,22 +167,25 @@ class MeasurementsScreen extends StatelessWidget {
FlatButton(
minWidth: 30.0,
height: 30.0,
color: (test?.invalid ?? false) ? Colors.red : Colors.green,
color: test.invalid ? Colors.red : Colors.green,
splashColor: Colors.grey,
onPressed: () {
showMeasurementDialog(context, test?.invalid ?? false);
showMeasurementDialog(context, test.invalid ?? false);
},
child: (test?.invalid ?? false) ? Icon(Icons.priority_high) : Icon(Icons.check),
child: test.invalid ? Icon(Icons.priority_high) : Icon(Icons.check),
),
FlatButton(
minWidth: 30.0,
height: 30.0,
color: (test?.invalid ?? false) ? Colors.red : Colors.green,
color: test.sent ? Colors.green : Colors.red ,
splashColor: Colors.grey,
onPressed: () {
showBackendStatusDialog(context, test?.invalid ?? false);
onPressed: () async {
bool result = await showBackendStatusDialog(context, test.sent, test.id);
if (result)
BlocProvider.of<MeasurementsBloc>(context).add(
MeasurementsEvents.fetch);
},
child: (test?.invalid ?? false) ? Icon(Icons.signal_wifi_off) : Icon(Icons.wifi),
child: test.sent ? Icon(Icons.wifi) : Icon(Icons.signal_wifi_off),
),
]
),
68 changes: 55 additions & 13 deletions lib/screens/main/measurements/widgets/backend_status_dialog.dart
Original file line number Diff line number Diff line change
@@ -1,18 +1,27 @@

import 'dart:async';
import 'dart:convert';
import 'dart:io';

import 'package:balance_app/floor/measurement_database.dart';
import 'package:balance_app/manager/preference_manager.dart';
import 'package:balance_app/model/measurement.dart';
import 'package:balance_app/model/raw_measurement_data.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:http/http.dart';

/// Show a dialog to ask the user if he wants to close the app during the test.
///
/// This method return a [Future] of [bool] used by the method [didPopRoute]
/// to know if the app should be closed or not.
Future<bool> showBackendStatusDialog(BuildContext context, bool valid) {
if (!valid)
Future<bool> showBackendStatusDialog(BuildContext context, bool valid, int id) {
if (valid)
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Misurazione inviata correttamente al server'),
content: Text('La tua misurazione é stata registrata correttamente nel server. Stai contribuendo attivamente alla ricerca!'),
title: Text('test_backend_ok_title'.tr()),
content: Text('test_backend_ok_txt'.tr()),
actions: [
// Stop the test and close the app
FlatButton(
@@ -29,18 +38,51 @@ Future<bool> showBackendStatusDialog(BuildContext context, bool valid) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('La misurazione non ha raggiunto il backend'),
content: Text('La misurazione non ha raggiunto il backend. Per farlo devi attivare la connessione alla rete internet. Se non invii i risultati della tua misurazione non contribuirai alla ricerca ma solo a te stesso.'),
title: Text('test_backend_wrong_title'.tr()),
content: Text('test_backend_wrong_txt'.tr()),
actions: [
// Stop the test and close the app
FlatButton(
onPressed: () {
// Close the dialog and the app
Navigator.pop(context, false);
},
child: Text('close'.tr()),
Row(
children: <Widget>[
// Try sending measurement
FlatButton(
onPressed: () async {
// Close the dialog and the app
Navigator.pop(context, await _makePostRequest(id));
},
child: Text('Ritenta invio'),
),
]
),
],
)
);
}

Future<bool> _makePostRequest(measurementId) async {
// set up POST request arguments
String url_measurement = 'https://www.balancemobile.it/api/v1/db/measurement';
//String url_measurement = 'http://192.168.1.206:8000/api/v1/db/sway';
Map<String, String> headers = {"Content-type": "application/json"};

final database = await MeasurementDatabase.getDatabase();
var measurement = await database.measurementDao.findMeasurementById(measurementId);

try {
Measurement newMeasurement = Measurement.sent(measurement, true);
Response response = await post(url_measurement, headers: headers, body: jsonEncode(newMeasurement)).timeout(Duration(seconds: 5));

if (response.statusCode == 200) {
await database.measurementDao.updateMeasurement(newMeasurement);
return true;
} else {
print("_SendingData.RawMeasurement: The server answered with: "+response.statusCode.toString());
return false;
}
} on TimeoutException catch (_) {
print("_SendingData.RawMeasurement: The connection dropped, maybe the server is congested");
return false;
} on SocketException catch (_) {
print("_SendingData.RawMeasurement: Communication failed. The server was not reachable");
return false;
}
}
Original file line number Diff line number Diff line change
@@ -11,8 +11,8 @@ Future<bool> showMeasurementDialog(BuildContext context, bool valid) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Misurazione eseguita correttamente'),
content: Text('Congratulazioni! Quando esegui una misurazione verifichiamo i tuoi parametri. Alcuni di questi forniscono un\'indicazione riguardante la corretta esecuzione e sembra sia andato tutto bene!'),
title: Text('test_measurement_ok_title'.tr()),
content: Text('test_measurement_ok_txt'.tr()),
actions: [
// Stop the test and close the app
FlatButton(
@@ -29,8 +29,8 @@ Future<bool> showMeasurementDialog(BuildContext context, bool valid) {
return showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Misurazione Sospetta'),
content: Text('Questa misurazione presenta dei risultati irregolari. Se non hai eseguito il test correttamente, effettualo nuovamente. Se il problema persiste, contatta i ricercatori.'),
title: Text('test_measurement_wrong_title'.tr()),
content: Text('test_measurement_wrong_txt'.tr()),
actions: [
// Stop the test and close the app
FlatButton(
45 changes: 28 additions & 17 deletions lib/screens/main/settings/settings_screen.dart
Original file line number Diff line number Diff line change
@@ -66,24 +66,35 @@ class _SettingsScreenState extends State<SettingsScreen> {
]
),
SettingsGroup(
title: 'version_txt'.tr(),
children: [
SettingsElement(
icon: Icon(BIcons.version),
text: "${'version_txt'.tr()} ${packageInfo?.version} (${'build_txt'.tr()}${packageInfo?.buildNumber})",
onLongPress: () {
Scaffold.of(context)
.showSnackBar(
SnackBar(
behavior: SnackBarBehavior.floating,
content: Text('easter_egg_txt'.tr()),
duration: Duration(seconds: 2),
)
);
}
),
]
title: 'report_title'.tr(),
children: [
SettingsElement(
icon: Icon(BIcons.version),
text: "report_issue_title".tr(),
//text: "${'version_txt'.tr()} ${packageInfo?.version} (${'build_txt'.tr()}${packageInfo?.buildNumber})",
onTap: () => Navigator.of(context).pushNamed(Routes.issues),
),
]
),
//SettingsGroup(
// title: 'version_txt'.tr(),
// children: [
// SettingsElement(
// icon: Icon(BIcons.version),
// text: "${'version_txt'.tr()} ${packageInfo?.version} (${'build_txt'.tr()}${packageInfo?.buildNumber})",
// onLongPress: () {
// Scaffold.of(context)
// .showSnackBar(
// SnackBar(
// behavior: SnackBarBehavior.floating,
// content: Text('easter_egg_txt'.tr()),
// duration: Duration(seconds: 2),
// )
// );
// }
// ),
// ]
//),
]
);
}
13 changes: 11 additions & 2 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Flutter app
name: balance_app
description: Flutter application to measure you posture!
version: 1.0.0+5
version: 1.0.0+1

environment:
sdk: ">=2.6.0 <3.0.0"
@@ -89,6 +89,7 @@ flutter:
assets:
# App Logo
- assets/app_logo_circle.png
- assets/app_logo_circle_broken.png
- assets/app_logo_circle_grey.png
- assets/app_logo.png
# Partners
@@ -105,8 +106,16 @@ flutter:
- assets/images/tutorial.png
- assets/images/appstore_logo.png
- assets/images/open_source.png
- assets/images/reading.png
- assets/images/walking.png
- assets/images/working.png
- assets/images/drinking.png
- assets/images/eating.png
- assets/images/sleeping.png
- assets/images/sport.png
# Sounds
- assets/sounds/light_pop.mp3
- assets/sounds/light_pop_bigdsc.mp3
- assets/sounds/finish_bigdsc.mp3
# Translations
- assets/translations/en.json
- assets/translations/it.json

0 comments on commit 6cfe32d

Please sign in to comment.