Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found
Select Git revision
  • master
  • 82-reword-settings
  • 67-improve-app-metadata
  • 54-improve-discoveries-page
  • 7-add-lastfm-link
  • Release_0.0.10_10
  • Release_0.0.11_11
  • Release_0.0.12_12
  • Release_0.0.13_13
  • Release_0.0.14_14
  • Release_0.0.15_15
  • Release_0.0.16_16
  • Release_0.0.17_17
  • Release_0.0.18_18
  • Release_0.0.19_19
  • Release_0.0.1_1
  • Release_0.0.20_20
  • Release_0.0.21_21
  • Release_0.0.22_22
  • Release_0.0.23_23
  • Release_0.0.24_24
  • Release_0.0.25_25
  • Release_0.0.26_26
  • Release_0.0.27_27
  • Release_0.0.28_28
  • Release_0.0.29_29
  • Release_0.0.2_2
  • Release_0.0.30_30
  • Release_0.0.31_31
  • Release_0.0.32_32
  • Release_0.0.33_33
  • Release_0.0.34_34
  • Release_0.0.35_35
  • Release_0.0.36_36
  • Release_0.0.37_37
  • Release_0.0.38_38
  • Release_0.0.39_39
  • Release_0.0.3_3
  • Release_0.0.40_40
  • Release_0.0.41_41
  • Release_0.0.42_42
  • Release_0.0.43_43
  • Release_0.0.44_44
  • Release_0.0.45_45
  • Release_0.0.46_46
  • Release_0.0.47_47
  • Release_0.0.48_48
  • Release_0.0.49_49
  • Release_0.0.4_4
  • Release_0.0.50_50
  • Release_0.0.51_51
  • Release_0.0.52_52
  • Release_0.0.53_53
  • Release_0.0.54_54
  • Release_0.0.55_55
  • Release_0.0.56_56
  • Release_0.0.57_57
  • Release_0.0.5_5
  • Release_0.0.6_6
  • Release_0.0.7_7
  • Release_0.0.8_8
  • Release_0.0.9_9
  • Release_0.1.0_58
  • Release_0.1.1_59
  • Release_0.1.2_60
  • Release_0.2.0_61
  • Release_0.2.1_62
  • Release_0.2.2_63
  • Release_0.3.0_64
  • Release_0.3.1_65
  • Release_0.4.0_66
  • Release_0.4.1_67
  • Release_0.4.2_68
  • Release_0.5.0_69
  • Release_0.6.0_70
  • Release_0.7.0_71
  • Release_0.8.0_72
  • Release_0.8.1_73
  • Release_0.8.2_74
  • Release_0.8.3_75
  • Release_0.8.4_76
  • Release_0.9.0_77
  • Release_0.9.1_78
  • Release_0.9.2_79
84 results

Target

Select target project
  • android / org.benoitharrault.scrobbles
1 result
Select Git revision
  • master
  • 82-reword-settings
  • 67-improve-app-metadata
  • 54-improve-discoveries-page
  • 7-add-lastfm-link
  • Release_0.0.10_10
  • Release_0.0.11_11
  • Release_0.0.12_12
  • Release_0.0.13_13
  • Release_0.0.14_14
  • Release_0.0.15_15
  • Release_0.0.16_16
  • Release_0.0.17_17
  • Release_0.0.18_18
  • Release_0.0.19_19
  • Release_0.0.1_1
  • Release_0.0.20_20
  • Release_0.0.21_21
  • Release_0.0.22_22
  • Release_0.0.23_23
  • Release_0.0.24_24
  • Release_0.0.25_25
  • Release_0.0.26_26
  • Release_0.0.27_27
  • Release_0.0.28_28
  • Release_0.0.29_29
  • Release_0.0.2_2
  • Release_0.0.30_30
  • Release_0.0.31_31
  • Release_0.0.32_32
  • Release_0.0.33_33
  • Release_0.0.34_34
  • Release_0.0.35_35
  • Release_0.0.36_36
  • Release_0.0.37_37
  • Release_0.0.38_38
  • Release_0.0.39_39
  • Release_0.0.3_3
  • Release_0.0.40_40
  • Release_0.0.41_41
  • Release_0.0.42_42
  • Release_0.0.43_43
  • Release_0.0.44_44
  • Release_0.0.45_45
  • Release_0.0.46_46
  • Release_0.0.47_47
  • Release_0.0.48_48
  • Release_0.0.49_49
  • Release_0.0.4_4
  • Release_0.0.50_50
  • Release_0.0.51_51
  • Release_0.0.52_52
  • Release_0.0.53_53
  • Release_0.0.54_54
  • Release_0.0.55_55
  • Release_0.0.56_56
  • Release_0.0.57_57
  • Release_0.0.5_5
  • Release_0.0.6_6
  • Release_0.0.7_7
  • Release_0.0.8_8
  • Release_0.0.9_9
  • Release_0.1.0_58
  • Release_0.1.1_59
  • Release_0.1.2_60
  • Release_0.2.0_61
  • Release_0.2.1_62
  • Release_0.2.2_63
  • Release_0.3.0_64
  • Release_0.3.1_65
  • Release_0.4.0_66
  • Release_0.4.1_67
  • Release_0.4.2_68
  • Release_0.5.0_69
  • Release_0.6.0_70
  • Release_0.7.0_71
  • Release_0.8.0_72
  • Release_0.8.1_73
  • Release_0.8.2_74
  • Release_0.8.3_75
  • Release_0.8.4_76
  • Release_0.9.0_77
  • Release_0.9.1_78
  • Release_0.9.2_79
84 results
Show changes
121 files
+ 3153
857
Compare changes
  • Side-by-side
  • Inline

Files

+1 −1
Original line number Original line Diff line number Diff line
@@ -44,7 +44,7 @@ android {


    defaultConfig {
    defaultConfig {
        applicationId "org.benoitharrault.scrobbles"
        applicationId "org.benoitharrault.scrobbles"
        minSdkVersion 16
        minSdkVersion flutter.minSdkVersion
        targetSdkVersion 30
        targetSdkVersion 30
        versionCode appVersionCode.toInteger()
        versionCode appVersionCode.toInteger()
        versionName appVersionName
        versionName appVersionName
+2 −2
Original line number Original line Diff line number Diff line
org.gradle.jvmargs=-Xmx1536M
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.useAndroidX=true
android.enableJetifier=true
android.enableJetifier=true
app.versionName=0.0.10
app.versionName=0.0.35
app.versionCode=10
app.versionCode=35
+20 −3
Original line number Original line Diff line number Diff line
{
{
  "app_name": "Scrobbles",
  "app_name": "Scrobbles",
  "": "",

  "bottom_nav_home": "Home",
  "bottom_nav_discoveries": "Discoveries",
  "bottom_nav_repartition": "Statistics",
  "bottom_nav_settings": "Settings",


  "global_statistics": "Global statistics",
  "global_statistics": "Global statistics",
  "statistics_total_scrobbles_count": "Total scrobbles count: {count}",
  "statistics_total_scrobbles_count": "Total scrobbles count: {count}",
  "statistics_last_scrobble": "Last scrobble: {datetime}",
  "statistics_last_scrobble": "Last scrobble: {datetime}",
  "statistics_selected_period": "On last {daysCount} days:",

  "statistics_recent_scrobbles_count": "Scrobbles: {count}",
  "recent_statistics": "Recent statistics ({daysCount} days)",
  "statistics_discoveries": "Discoveries: {artistsCount} artists / {tracksCount} tracks",
  "statistics_recent_scrobbles_count_and_discoveries": "{count} scrobbles and {artistsCount} artists / {tracksCount} tracks discovered.",


  "timeline_title": "Recent scrobbles ({daysCount} days)",
  "timeline_title": "Recent scrobbles ({daysCount} days)",
  "counts_by_day": "Counts by day ({daysCount} days)",
  "counts_by_day": "Counts by day ({daysCount} days)",
  "counts_by_hour": "Counts by hour ({daysCount} days)",
  "counts_by_hour": "Counts by hour ({daysCount} days)",


  "discoveries_title": "Discoveries ({daysCount} days)",
  "discoveries_artists_title": "Artists",
  "discoveries_tracks_title": "Tracks",

  "top_artists_title": "Top artists ({daysCount} days)",

  "settings_title": "Settings",
  "settings_label_username": "Username: ",
  "settings_label_security_token": "Security token: ",
  "settings_button_save": "Save",

  "MON": "MON",
  "MON": "MON",
  "TUE": "TUE",
  "TUE": "TUE",
  "WED": "WED",
  "WED": "WED",
+20 −3
Original line number Original line Diff line number Diff line
{
{
  "app_name": "Scrobbles",
  "app_name": "Scrobbles",
  "": "",

  "bottom_nav_home": "Accueil",
  "bottom_nav_discoveries": "Découvertes",
  "bottom_nav_repartition": "Statistiques",
  "bottom_nav_settings": "Paramètres",


  "global_statistics": "Statistiques globales d'écoutes",
  "global_statistics": "Statistiques globales d'écoutes",
  "statistics_total_scrobbles_count": "Nombre total d'écoutes : {count}",
  "statistics_total_scrobbles_count": "Nombre total d'écoutes : {count}",
  "statistics_last_scrobble": "Dernière écoute : {datetime}",
  "statistics_last_scrobble": "Dernière écoute : {datetime}",
  "statistics_selected_period": "Sur les {daysCount} derniers jours:",

  "statistics_recent_scrobbles_count": "Écoutes : {count}",
  "recent_statistics": "Statistiques récentes ({daysCount} jours)",
  "statistics_discoveries": "Découvertes : {artistsCount} artistes / {tracksCount} morceaux",
  "statistics_recent_scrobbles_count_and_discoveries": "{count} écoutes et {artistsCount} artistes / {tracksCount} morceaux découverts.",


  "timeline_title": "Écoutes récentes ({daysCount} jours)",
  "timeline_title": "Écoutes récentes ({daysCount} jours)",
  "counts_by_day": "Écoutes par jour ({daysCount} jours)",
  "counts_by_day": "Écoutes par jour ({daysCount} jours)",
  "counts_by_hour": "Écoutes par heure ({daysCount} jours)",
  "counts_by_hour": "Écoutes par heure ({daysCount} jours)",


  "discoveries_title": "Découvertes ({daysCount} jours)",
  "discoveries_artists_title": "Artistes",
  "discoveries_tracks_title": "Morceaux",

  "top_artists_title": "Top artistes ({daysCount} jours)",

  "settings_title": "Paramètres",
  "settings_label_username": "Utilisateur : ",
  "settings_label_security_token": "Jeton de sécurité : ",
  "settings_button_save": "Enregistrer",

  "MON": "LUN",
  "MON": "LUN",
  "TUE": "MAR",
  "TUE": "MAR",
  "WED": "MER",
  "WED": "MER",
+1 −0
Original line number Original line Diff line number Diff line
Improve title bar, add refresh button.
+1 −0
Original line number Original line Diff line number Diff line
Improve charts colors.
+1 −0
Original line number Original line Diff line number Diff line
Center charts displaying left titles.
+1 −0
Original line number Original line Diff line number Diff line
Improve/merge charts code.
+1 −0
Original line number Original line Diff line number Diff line
Gradients uniformisation on bars of same charts.
+1 −0
Original line number Original line Diff line number Diff line
Add "%" in repartition charts, reduce font size in axis titles.
+1 −0
Original line number Original line Diff line number Diff line
Stack global counts and eclecticism charts.
+1 −0
Original line number Original line Diff line number Diff line
Use date parameter in global stats API call.
+1 −0
Original line number Original line Diff line number Diff line
Remove tooltips on charts.
+1 −0
Original line number Original line Diff line number Diff line
Add discoveries chart.
+1 −0
Original line number Original line Diff line number Diff line
Minor layout improvments.
+1 −0
Original line number Original line Diff line number Diff line
Add navigation bar, split charts in pages.
+1 −0
Original line number Original line Diff line number Diff line
Allow swipe to navigate between pages.
+1 −0
Original line number Original line Diff line number Diff line
Split global and recent statistics.
+1 −0
Original line number Original line Diff line number Diff line
Add "top artists" chart.
+1 −0
Original line number Original line Diff line number Diff line
Fix/improve charts display.
+1 −0
Original line number Original line Diff line number Diff line
Improve "discoveries" chart.
+1 −0
Original line number Original line Diff line number Diff line
Clean/improve/refactor code.
+1 −0
Original line number Original line Diff line number Diff line
Save scrobbles data from API.
+1 −0
Original line number Original line Diff line number Diff line
Update dependencies.
+1 −0
Original line number Original line Diff line number Diff line
Improve swipe navigation.
+1 −0
Original line number Original line Diff line number Diff line
Improve / uniformize days counts on charts.
+1 −0
Original line number Original line Diff line number Diff line
Improve loading charts data.
+1 −0
Original line number Original line Diff line number Diff line
Improve update data and charts.
+1 −0
Original line number Original line Diff line number Diff line
Add settings page (username, security token).
+1 −0
Original line number Original line Diff line number Diff line
Amélioration de la barre de titre, ajout d'un bouton de mise à jour.
+1 −0
Original line number Original line Diff line number Diff line
Amélioration des couleurs des graphiques.
+1 −0
Original line number Original line Diff line number Diff line
Centrage des graphiques avec affichage des graduations à gauche.
+1 −0
Original line number Original line Diff line number Diff line
Amélioration / mutualisation du code pour les graphiques.
+1 −0
Original line number Original line Diff line number Diff line
Uniformisation des dégradés de couleur entre les barres du même graphique.
+1 −0
Original line number Original line Diff line number Diff line
Ajout du "%" dans les graphiques de réparition, réduction de la taille des textes sur les axes.
+1 −0
Original line number Original line Diff line number Diff line
Superposition des graphiques d'écoutes global et d'éclectisme.
+1 −0
Original line number Original line Diff line number Diff line
Utilisation d'un paramètre de date dans l'appel à l'API des statistiques globales.
+1 −0
Original line number Original line Diff line number Diff line
Suppression des infobulles sur les graphiques.
+1 −0
Original line number Original line Diff line number Diff line
Ajout du graphique des découvertes.
+1 −0
Original line number Original line Diff line number Diff line
Améliorations visuelles mineures.
+1 −0
Original line number Original line Diff line number Diff line
Ajout d'une barre de navigation, séparation des graphiques en plusieurs pages.
+1 −0
Original line number Original line Diff line number Diff line
Navigation entre pages par mouvement de "swipe".
+1 −0
Original line number Original line Diff line number Diff line
Séparation des statistiques globales et récentes.
+1 −0
Original line number Original line Diff line number Diff line
Ajout du graphique "top artistes".
+1 −0
Original line number Original line Diff line number Diff line
Améliorations visuelles des graphiques.
+1 −0
Original line number Original line Diff line number Diff line
Amélioration du graphique des découvertes.
+1 −0
Original line number Original line Diff line number Diff line
Nettoyage/améliorations/refactorisations de code.
+1 −0
Original line number Original line Diff line number Diff line
Sauvegarde des données d'écoutes de l'API.
+1 −0
Original line number Original line Diff line number Diff line
Mise à jour des dépendances.
+1 −0
Original line number Original line Diff line number Diff line
Amélioration de la navigation par swipe.
+1 −0
Original line number Original line Diff line number Diff line
Amélioration / uniformisation des nombres de jours sur les graphiques.
+1 −0
Original line number Original line Diff line number Diff line
Amélioration du chargement des données des graphiques.
+1 −0
Original line number Original line Diff line number Diff line
Amélioration de la mise à jour des données et des graphiques.
+1 −0
Original line number Original line Diff line number Diff line
Ajout d'une page de paramètres (nom d'utilisateur, jeton de sécurité).
+8 −0
Original line number Original line Diff line number Diff line
class Settings {
  static const int countsByDayDaysCount = 21;
  static const int countsByHourDaysCount = 21;
  static const int discoveriesDaysCount = 14;
  static const int statisticsRecentDaysCount = 21;
  static const int timelineDaysCount = 14;
  static const int topArtistsDaysCount = 14;
}
+31 −0
Original line number Original line Diff line number Diff line
import 'package:hydrated_bloc/hydrated_bloc.dart';

class BottomNavCubit extends HydratedCubit<int> {
  BottomNavCubit() : super(0);

  int pagesCount = 4;

  void updateIndex(int index) {
    if (isIndexAllowed(index)) {
      emit(index);
    } else {
      goToHomePage();
    }
  }

  bool isIndexAllowed(int index) {
    return (index >= 0) && (index < pagesCount);
  }

  void goToHomePage() => emit(0);

  @override
  int? fromJson(Map<String, dynamic> json) {
    return 0;
  }

  @override
  Map<String, dynamic>? toJson(int state) {
    return <String, int>{'pageIndex': state};
  }
}
+45 −0
Original line number Original line Diff line number Diff line
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';

import 'package:scrobbles/models/counts_by_day.dart';

part 'data_counts_by_day_state.dart';

class DataCountsByDayCubit extends HydratedCubit<DataCountsByDayState> {
  DataCountsByDayCubit() : super(const DataCountsByDayState());

  void getData(DataCountsByDayState state) {
    emit(state);
  }

  CountsByDayData? getValue() {
    return state.countsByDay;
  }

  void update(CountsByDayData? countsByDay) {
    if (state.countsByDay.toString() != countsByDay.toString()) {
      setValue(countsByDay);
    }
  }

  void setValue(CountsByDayData? countsByDay) {
    emit(DataCountsByDayState(
      countsByDay: countsByDay,
    ));
  }

  @override
  DataCountsByDayState? fromJson(Map<String, dynamic> json) {
    return DataCountsByDayState(
      countsByDay: CountsByDayData.fromJson(json['countsByDay']),
    );
  }

  @override
  Map<String, Object?>? toJson(DataCountsByDayState state) {
    return <String, Object?>{
      'countsByDay': state.countsByDay?.toJson(),
    };
  }
}
+15 −0
Original line number Original line Diff line number Diff line
part of 'data_counts_by_day_cubit.dart';

@immutable
class DataCountsByDayState extends Equatable {
  const DataCountsByDayState({
    this.countsByDay,
  });

  final CountsByDayData? countsByDay;

  @override
  List<Object?> get props => <Object?>[
        countsByDay,
      ];
}
+45 −0
Original line number Original line Diff line number Diff line
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';

import 'package:scrobbles/models/counts_by_hour.dart';

part 'data_counts_by_hour_state.dart';

class DataCountsByHourCubit extends HydratedCubit<DataCountsByHourState> {
  DataCountsByHourCubit() : super(const DataCountsByHourState());

  void getData(DataCountsByHourState state) {
    emit(state);
  }

  CountsByHourData? getValue() {
    return state.countsByHour;
  }

  void update(CountsByHourData? countsByHour) {
    if (state.countsByHour.toString() != countsByHour.toString()) {
      setValue(countsByHour);
    }
  }

  void setValue(CountsByHourData? countsByHour) {
    emit(DataCountsByHourState(
      countsByHour: countsByHour,
    ));
  }

  @override
  DataCountsByHourState? fromJson(Map<String, dynamic> json) {
    return DataCountsByHourState(
      countsByHour: CountsByHourData.fromJson(json['countsByHour']),
    );
  }

  @override
  Map<String, Object?>? toJson(DataCountsByHourState state) {
    return <String, Object?>{
      'countsByHour': state.countsByHour?.toJson(),
    };
  }
}
+15 −0
Original line number Original line Diff line number Diff line
part of 'data_counts_by_hour_cubit.dart';

@immutable
class DataCountsByHourState extends Equatable {
  const DataCountsByHourState({
    this.countsByHour,
  });

  final CountsByHourData? countsByHour;

  @override
  List<Object?> get props => <Object?>[
        countsByHour,
      ];
}
+45 −0
Original line number Original line Diff line number Diff line
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';

import 'package:scrobbles/models/discoveries.dart';

part 'data_discoveries_state.dart';

class DataDiscoveriesCubit extends HydratedCubit<DataDiscoveriesState> {
  DataDiscoveriesCubit() : super(const DataDiscoveriesState());

  void getData(DataDiscoveriesState state) {
    emit(state);
  }

  DiscoveriesData? getValue() {
    return state.discoveries;
  }

  void update(DiscoveriesData? discoveries) {
    if (state.discoveries.toString() != discoveries.toString()) {
      setValue(discoveries);
    }
  }

  void setValue(DiscoveriesData? discoveries) {
    emit(DataDiscoveriesState(
      discoveries: discoveries,
    ));
  }

  @override
  DataDiscoveriesState? fromJson(Map<String, dynamic> json) {
    return DataDiscoveriesState(
      discoveries: DiscoveriesData.fromJson(json['discoveries']),
    );
  }

  @override
  Map<String, Object?>? toJson(DataDiscoveriesState state) {
    return <String, Object?>{
      'discoveries': state.discoveries?.toJson(),
    };
  }
}
+15 −0
Original line number Original line Diff line number Diff line
part of 'data_discoveries_cubit.dart';

@immutable
class DataDiscoveriesState extends Equatable {
  const DataDiscoveriesState({
    this.discoveries,
  });

  final DiscoveriesData? discoveries;

  @override
  List<Object?> get props => <Object?>[
        discoveries,
      ];
}
+45 −0
Original line number Original line Diff line number Diff line
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';

import 'package:scrobbles/models/statistics_global.dart';

part 'data_statistics_global_state.dart';

class DataStatisticsGlobalCubit extends HydratedCubit<DataStatisticsGlobalState> {
  DataStatisticsGlobalCubit() : super(const DataStatisticsGlobalState());

  void getData(DataStatisticsGlobalState state) {
    emit(state);
  }

  StatisticsGlobalData? getValue() {
    return state.statisticsGlobal;
  }

  void update(StatisticsGlobalData? statisticsGlobal) {
    if (state.statisticsGlobal.toString() != statisticsGlobal.toString()) {
      setValue(statisticsGlobal);
    }
  }

  void setValue(StatisticsGlobalData? statisticsGlobal) {
    emit(DataStatisticsGlobalState(
      statisticsGlobal: statisticsGlobal,
    ));
  }

  @override
  DataStatisticsGlobalState? fromJson(Map<String, dynamic> json) {
    return DataStatisticsGlobalState(
      statisticsGlobal: StatisticsGlobalData.fromJson(json['statisticsGlobal']),
    );
  }

  @override
  Map<String, Object?>? toJson(DataStatisticsGlobalState state) {
    return <String, Object?>{
      'statisticsGlobal': state.statisticsGlobal?.toJson(),
    };
  }
}
+15 −0
Original line number Original line Diff line number Diff line
part of 'data_statistics_global_cubit.dart';

@immutable
class DataStatisticsGlobalState extends Equatable {
  const DataStatisticsGlobalState({
    this.statisticsGlobal,
  });

  final StatisticsGlobalData? statisticsGlobal;

  @override
  List<Object?> get props => <Object?>[
        statisticsGlobal,
      ];
}
+45 −0
Original line number Original line Diff line number Diff line
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';

import 'package:scrobbles/models/statistics_recent.dart';

part 'data_statistics_recent_state.dart';

class DataStatisticsRecentCubit extends HydratedCubit<DataStatisticsRecentState> {
  DataStatisticsRecentCubit() : super(const DataStatisticsRecentState());

  void getData(DataStatisticsRecentState state) {
    emit(state);
  }

  StatisticsRecentData? getValue() {
    return state.statisticsRecent;
  }

  void update(StatisticsRecentData? statisticsRecent) {
    if (state.statisticsRecent.toString() != statisticsRecent.toString()) {
      setValue(statisticsRecent);
    }
  }

  void setValue(StatisticsRecentData? statisticsRecent) {
    emit(DataStatisticsRecentState(
      statisticsRecent: statisticsRecent,
    ));
  }

  @override
  DataStatisticsRecentState? fromJson(Map<String, dynamic> json) {
    return DataStatisticsRecentState(
      statisticsRecent: StatisticsRecentData.fromJson(json['statisticsRecent']),
    );
  }

  @override
  Map<String, Object?>? toJson(DataStatisticsRecentState state) {
    return <String, Object?>{
      'statisticsRecent': state.statisticsRecent?.toJson(),
    };
  }
}
+15 −0
Original line number Original line Diff line number Diff line
part of 'data_statistics_recent_cubit.dart';

@immutable
class DataStatisticsRecentState extends Equatable {
  const DataStatisticsRecentState({
    this.statisticsRecent,
  });

  final StatisticsRecentData? statisticsRecent;

  @override
  List<Object?> get props => <Object?>[
        statisticsRecent,
      ];
}
+45 −0
Original line number Original line Diff line number Diff line
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';

import 'package:scrobbles/models/timeline.dart';

part 'data_timeline_state.dart';

class DataTimelineCubit extends HydratedCubit<DataTimelineState> {
  DataTimelineCubit() : super(const DataTimelineState());

  void getData(DataTimelineState state) {
    emit(state);
  }

  TimelineData? getValue() {
    return state.timeline;
  }

  void update(TimelineData? timeline) {
    if (state.timeline.toString() != timeline.toString()) {
      setValue(timeline);
    }
  }

  void setValue(TimelineData? timeline) {
    emit(DataTimelineState(
      timeline: timeline,
    ));
  }

  @override
  DataTimelineState? fromJson(Map<String, dynamic> json) {
    return DataTimelineState(
      timeline: TimelineData.fromJson(json['timeline']),
    );
  }

  @override
  Map<String, Object?>? toJson(DataTimelineState state) {
    return <String, Object?>{
      'timeline': state.timeline?.toJson(),
    };
  }
}
+15 −0
Original line number Original line Diff line number Diff line
part of 'data_timeline_cubit.dart';

@immutable
class DataTimelineState extends Equatable {
  const DataTimelineState({
    this.timeline,
  });

  final TimelineData? timeline;

  @override
  List<Object?> get props => <Object?>[
        timeline,
      ];
}
+45 −0
Original line number Original line Diff line number Diff line
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';

import 'package:scrobbles/models/topartists.dart';

part 'data_top_artists_state.dart';

class DataTopArtistsCubit extends HydratedCubit<DataTopArtistsState> {
  DataTopArtistsCubit() : super(const DataTopArtistsState());

  void getData(DataTopArtistsState state) {
    emit(state);
  }

  TopArtistsData? getValue(key) {
    return state.topArtists;
  }

  void update(TopArtistsData? topArtists) {
    if (state.topArtists.toString() != topArtists.toString()) {
      setValue(topArtists);
    }
  }

  void setValue(TopArtistsData? topArtists) {
    emit(DataTopArtistsState(
      topArtists: topArtists,
    ));
  }

  @override
  DataTopArtistsState? fromJson(Map<String, dynamic> json) {
    return DataTopArtistsState(
      topArtists: TopArtistsData.fromJson(json['topArtists']),
    );
  }

  @override
  Map<String, Object?>? toJson(DataTopArtistsState state) {
    return <String, Object?>{
      'topArtists': state.topArtists?.toJson(),
    };
  }
}
+15 −0
Original line number Original line Diff line number Diff line
part of 'data_top_artists_cubit.dart';

@immutable
class DataTopArtistsState extends Equatable {
  const DataTopArtistsState({
    this.topArtists,
  });

  final TopArtistsData? topArtists;

  @override
  List<Object?> get props => <Object?>[
        topArtists,
      ];
}
+46 −0
Original line number Original line Diff line number Diff line
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';

part 'settings_state.dart';

class SettingsCubit extends HydratedCubit<SettingsState> {
  SettingsCubit() : super(const SettingsState());

  String getUsername() {
    return state.username ?? '';
  }

  String getSecurityToken() {
    return state.securityToken ?? '';
  }

  void setValues({
    String? username,
    String? securityToken,
  }) {
    emit(SettingsState(
      username: username != null ? username : state.username,
      securityToken: securityToken != null ? securityToken : state.securityToken,
    ));
  }

  @override
  SettingsState? fromJson(Map<String, dynamic> json) {
    String username = json['username'] as String;
    String securityToken = json['securityToken'] as String;

    return SettingsState(
      username: username,
      securityToken: securityToken,
    );
  }

  @override
  Map<String, String>? toJson(SettingsState state) {
    return <String, String>{
      'username': state.username ?? '',
      'securityToken': state.securityToken ?? '',
    };
  }
}
+23 −0
Original line number Original line Diff line number Diff line
part of 'settings_cubit.dart';

@immutable
class SettingsState extends Equatable {
  const SettingsState({
    this.username,
    this.securityToken,
  });

  final String? username;
  final String? securityToken;

  @override
  List<String?> get props => <String?>[
        username,
        securityToken,
      ];

  Map<String, String?> get values => <String, String?>{
        'username': username,
        'securityToken': securityToken,
      };
}
+47 −11
Original line number Original line Diff line number Diff line
import 'dart:io';

import 'package:easy_localization/easy_localization.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:hive/hive.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:path_provider/path_provider.dart';


import 'config/theme.dart';
import 'package:scrobbles/config/theme.dart';
import 'ui/screens/skeleton_screen.dart';
import 'package:scrobbles/cubit/bottom_nav_cubit.dart';
import 'package:scrobbles/cubit/data_counts_by_day_cubit.dart';
import 'package:scrobbles/cubit/data_counts_by_hour_cubit.dart';
import 'package:scrobbles/cubit/data_discoveries_cubit.dart';
import 'package:scrobbles/cubit/data_statistics_global_cubit.dart';
import 'package:scrobbles/cubit/data_statistics_recent_cubit.dart';
import 'package:scrobbles/cubit/data_timeline_cubit.dart';
import 'package:scrobbles/cubit/data_top_artists_cubit.dart';
import 'package:scrobbles/cubit/settings_cubit.dart';
import 'package:scrobbles/ui/skeleton.dart';


void main() async {
void main() async {
  /// Initialize packages
  WidgetsFlutterBinding.ensureInitialized();
  WidgetsFlutterBinding.ensureInitialized();
  await EasyLocalization.ensureInitialized();
  await EasyLocalization.ensureInitialized();
  final Directory tmpDir = await getTemporaryDirectory();
  Hive.init(tmpDir.toString());
  HydratedBloc.storage = await HydratedStorage.build(
    storageDirectory: tmpDir,
  );


  runApp(
  runApp(
    EasyLocalization(
    EasyLocalization(
@@ -27,7 +48,21 @@ class MyApp extends StatelessWidget {


  @override
  @override
  Widget build(BuildContext context) {
  Widget build(BuildContext context) {
    return MaterialApp(
    return MultiBlocProvider(
      providers: [
        BlocProvider<SettingsCubit>(create: (context) => SettingsCubit()),
        BlocProvider<BottomNavCubit>(create: (context) => BottomNavCubit()),
        BlocProvider<DataCountsByDayCubit>(create: (context) => DataCountsByDayCubit()),
        BlocProvider<DataCountsByHourCubit>(create: (context) => DataCountsByHourCubit()),
        BlocProvider<DataDiscoveriesCubit>(create: (context) => DataDiscoveriesCubit()),
        BlocProvider<DataStatisticsGlobalCubit>(
            create: (context) => DataStatisticsGlobalCubit()),
        BlocProvider<DataStatisticsRecentCubit>(
            create: (context) => DataStatisticsRecentCubit()),
        BlocProvider<DataTimelineCubit>(create: (context) => DataTimelineCubit()),
        BlocProvider<DataTopArtistsCubit>(create: (context) => DataTopArtistsCubit()),
      ],
      child: MaterialApp(
        title: 'Scrobbles',
        title: 'Scrobbles',
        theme: appTheme,
        theme: appTheme,
        home: const SkeletonScreen(),
        home: const SkeletonScreen(),
@@ -37,6 +72,7 @@ class MyApp extends StatelessWidget {
        supportedLocales: context.supportedLocales,
        supportedLocales: context.supportedLocales,
        locale: context.locale,
        locale: context.locale,
        debugShowCheckedModeBanner: false,
        debugShowCheckedModeBanner: false,
      ),
    );
    );
  }
  }
}
}
+9 −9
Original line number Original line Diff line number Diff line
@@ -7,11 +7,11 @@ class CountsByDayData {
    required this.data,
    required this.data,
  });
  });


  factory CountsByDayData.fromJson(Map<String, dynamic> json) {
  factory CountsByDayData.fromJson(Map<String, dynamic>? json) {
    Map<int, double> data = {};
    Map<int, double> data = {};


    if (json['counts-by-day'] != null) {
    if (json?['counts-by-day'] != null) {
      json['counts-by-day'].keys.forEach((day) {
      json?['counts-by-day'].keys.forEach((day) {
        data[int.parse(day)] = double.parse(json['counts-by-day'][day].toString());
        data[int.parse(day)] = double.parse(json['counts-by-day'][day].toString());
      });
      });
    }
    }
@@ -19,11 +19,7 @@ class CountsByDayData {
    return CountsByDayData(data: data);
    return CountsByDayData(data: data);
  }
  }


  factory CountsByDayData.createEmpty() {
  Map<String, Object?>? toJson() {
    return CountsByDayData.fromJson({});
  }

  String toString() {
    Map<String, double> map = {};
    Map<String, double> map = {};


    this.data.keys.forEach((day) {
    this.data.keys.forEach((day) {
@@ -31,6 +27,10 @@ class CountsByDayData {
      map[day.toString()] = value != null ? value.toDouble() : 0.0;
      map[day.toString()] = value != null ? value.toDouble() : 0.0;
    });
    });


    return jsonEncode({'counts-by-day': map});
    return {'counts-by-day': map};
  }

  String toString() {
    return jsonEncode(this.toJson());
  }
  }
}
}
+9 −9
Original line number Original line Diff line number Diff line
@@ -7,11 +7,11 @@ class CountsByHourData {
    required this.data,
    required this.data,
  });
  });


  factory CountsByHourData.fromJson(Map<String, dynamic> json) {
  factory CountsByHourData.fromJson(Map<String, dynamic>? json) {
    Map<int, double> data = {};
    Map<int, double> data = {};


    if (json['counts-by-hour'] != null) {
    if (json?['counts-by-hour'] != null) {
      json['counts-by-hour'].keys.forEach((day) {
      json?['counts-by-hour'].keys.forEach((day) {
        if (int.parse(day) != 24) {
        if (int.parse(day) != 24) {
          data[int.parse(day)] = double.parse(json['counts-by-hour'][day].toString());
          data[int.parse(day)] = double.parse(json['counts-by-hour'][day].toString());
        }
        }
@@ -21,11 +21,7 @@ class CountsByHourData {
    return CountsByHourData(data: data);
    return CountsByHourData(data: data);
  }
  }


  factory CountsByHourData.createEmpty() {
  Map<String, Object?>? toJson() {
    return CountsByHourData.fromJson({});
  }

  String toString() {
    Map<String, double> map = {};
    Map<String, double> map = {};


    this.data.keys.forEach((day) {
    this.data.keys.forEach((day) {
@@ -33,6 +29,10 @@ class CountsByHourData {
      map[day.toString()] = value != null ? value.toDouble() : 0.0;
      map[day.toString()] = value != null ? value.toDouble() : 0.0;
    });
    });


    return jsonEncode({'counts-by-hour': map});
    return {'counts-by-hour': map};
  }

  String toString() {
    return jsonEncode(this.toJson());
  }
  }
}
}
+56 −0
Original line number Original line Diff line number Diff line
import 'dart:convert';

class DiscoveriesDataValue {
  final int newArtistsCount;
  final int newTracksCount;

  const DiscoveriesDataValue({required this.newArtistsCount, required this.newTracksCount});

  factory DiscoveriesDataValue.fromJson(Map<String, dynamic>? json) {
    return DiscoveriesDataValue(
      newArtistsCount: json?['new-artists'] as int,
      newTracksCount: json?['new-tracks'] as int,
    );
  }
}

class DiscoveriesData {
  final Map<String, DiscoveriesDataValue> data;

  const DiscoveriesData({
    required this.data,
  });

  factory DiscoveriesData.fromJson(Map<String, dynamic>? json) {
    Map<String, DiscoveriesDataValue> data = {};

    json?.keys.forEach((date) {
      DiscoveriesDataValue value = DiscoveriesDataValue(
        newArtistsCount: json[date]['new-artists'] as int,
        newTracksCount: json[date]['new-tracks'] as int,
      );

      data[date] = value;
    });

    return DiscoveriesData(data: data);
  }

  Map<String, Object?>? toJson() {
    Map<String, Map<String, int>> map = {};

    this.data.keys.forEach((element) {
      DiscoveriesDataValue? item = this.data[element];
      map[element] = {
        'new-artists': item != null ? item.newArtistsCount : 0,
        'new-tracks': item != null ? item.newTracksCount : 0,
      };
    });

    return map;
  }

  String toString() {
    return jsonEncode(this.toJson());
  }
}

lib/models/statistics.dart

deleted100644 → 0
+0 −55
Original line number Original line Diff line number Diff line
import 'dart:convert';

class StatisticsData {
  final int totalCount;
  final int recentCount;
  final int firstPlayedArtistsCount;
  final int firstPlayedTracksCount;
  final int selectedPeriod;
  final DateTime lastScrobble;

  const StatisticsData({
    required this.totalCount,
    required this.recentCount,
    required this.firstPlayedArtistsCount,
    required this.firstPlayedTracksCount,
    required this.selectedPeriod,
    required this.lastScrobble,
  });

  factory StatisticsData.fromJson(Map<String, dynamic> json) {
    return StatisticsData(
      totalCount: json['totalCount'] != null ? json['totalCount'] as int : 0,
      recentCount: json['recentCount'] != null ? json['recentCount'] as int : 0,
      firstPlayedArtistsCount:
          json['firstPlayedArtistsCount'] != null ? json['firstPlayedArtistsCount'] as int : 0,
      firstPlayedTracksCount:
          json['firstPlayedTracksCount'] != null ? json['firstPlayedTracksCount'] as int : 0,
      selectedPeriod: json['selectedPeriod'] != null ? json['selectedPeriod'] as int : 0,
      lastScrobble: (json['lastScrobble'] != null && json['lastScrobble']['date'] != null)
          ? DateTime.parse(
              json['lastScrobble']['date'],
            )
          : DateTime.now(),
    );
  }

  factory StatisticsData.createEmpty() {
    return StatisticsData.fromJson({});
  }

  String toString() {
    Map<String, dynamic> map = {
      'totalCount': this.totalCount,
      'recentCount': this.recentCount,
      'firstPlayedArtistsCount': this.firstPlayedArtistsCount,
      'firstPlayedTracksCount': this.firstPlayedTracksCount,
      'selectedPeriod': this.selectedPeriod,
      'lastScrobble': {
        'date': this.lastScrobble.toString(),
      },
    };

    return jsonEncode(map);
  }
}
+35 −0
Original line number Original line Diff line number Diff line
import 'dart:convert';

class StatisticsGlobalData {
  final int? totalCount;
  final DateTime? lastScrobble;

  const StatisticsGlobalData({
    required this.totalCount,
    required this.lastScrobble,
  });

  factory StatisticsGlobalData.fromJson(Map<String, dynamic>? json) {
    return StatisticsGlobalData(
      totalCount: (json?['totalCount'] != null) ? (json?['totalCount'] as int) : null,
      lastScrobble: (json?['lastScrobble'] != null && json?['lastScrobble']['date'] != null)
          ? DateTime.parse(
              json?['lastScrobble']['date'],
            )
          : null,
    );
  }

  Map<String, Object?>? toJson() {
    return <String, Object?>{
      'totalCount': this.totalCount,
      'lastScrobble': {
        'date': this.lastScrobble != null ? this.lastScrobble.toString() : null,
      },
    };
  }

  String toString() {
    return jsonEncode(this.toJson());
  }
}
+42 −0
Original line number Original line Diff line number Diff line
import 'dart:convert';

class StatisticsRecentData {
  final int? recentCount;
  final int? firstPlayedArtistsCount;
  final int? firstPlayedTracksCount;
  final int? selectedPeriod;

  const StatisticsRecentData({
    required this.recentCount,
    required this.firstPlayedArtistsCount,
    required this.firstPlayedTracksCount,
    required this.selectedPeriod,
  });

  factory StatisticsRecentData.fromJson(Map<String, dynamic>? json) {
    return StatisticsRecentData(
      recentCount: (json?['recentCount'] != null) ? (json?['recentCount'] as int) : null,
      firstPlayedArtistsCount: (json?['firstPlayedArtistsCount'] != null)
          ? (json?['firstPlayedArtistsCount'] as int)
          : null,
      firstPlayedTracksCount: (json?['firstPlayedTracksCount'] != null)
          ? (json?['firstPlayedTracksCount'] as int)
          : null,
      selectedPeriod:
          (json?['selectedPeriod'] != null) ? (json?['selectedPeriod'] as int) : null,
    );
  }

  Map<String, Object?>? toJson() {
    return <String, Object?>{
      'recentCount': this.recentCount,
      'firstPlayedArtistsCount': this.firstPlayedArtistsCount,
      'firstPlayedTracksCount': this.firstPlayedTracksCount,
      'selectedPeriod': this.selectedPeriod,
    };
  }

  String toString() {
    return jsonEncode(this.toJson());
  }
}
+11 −11
Original line number Original line Diff line number Diff line
@@ -6,10 +6,10 @@ class TimelineDataValue {


  const TimelineDataValue({required this.counts, required this.eclecticism});
  const TimelineDataValue({required this.counts, required this.eclecticism});


  factory TimelineDataValue.fromJson(Map<String, dynamic> json) {
  factory TimelineDataValue.fromJson(Map<String, dynamic>? json) {
    return TimelineDataValue(
    return TimelineDataValue(
      counts: json['counts'] as int,
      counts: json?['counts'] as int,
      eclecticism: json['eclecticism'] as int,
      eclecticism: json?['eclecticism'] as int,
    );
    );
  }
  }
}
}
@@ -21,10 +21,10 @@ class TimelineData {
    required this.data,
    required this.data,
  });
  });


  factory TimelineData.fromJson(Map<String, dynamic> json) {
  factory TimelineData.fromJson(Map<String, dynamic>? json) {
    Map<String, TimelineDataValue> data = {};
    Map<String, TimelineDataValue> data = {};


    json.keys.forEach((date) {
    json?.keys.forEach((date) {
      TimelineDataValue value = TimelineDataValue(
      TimelineDataValue value = TimelineDataValue(
        counts: json[date]['counts'] as int,
        counts: json[date]['counts'] as int,
        eclecticism: json[date]['eclecticism'] as int,
        eclecticism: json[date]['eclecticism'] as int,
@@ -36,11 +36,7 @@ class TimelineData {
    return TimelineData(data: data);
    return TimelineData(data: data);
  }
  }


  factory TimelineData.createEmpty() {
  Map<String, Object?>? toJson() {
    return TimelineData.fromJson({});
  }

  String toString() {
    Map<String, Map<String, int>> map = {};
    Map<String, Map<String, int>> map = {};


    this.data.keys.forEach((element) {
    this.data.keys.forEach((element) {
@@ -51,6 +47,10 @@ class TimelineData {
      };
      };
    });
    });


    return jsonEncode(map);
    return map;
  }

  String toString() {
    return jsonEncode(this.toJson());
  }
  }
}
}
+59 −0
Original line number Original line Diff line number Diff line
import 'dart:convert';

class TopArtistsDataValue {
  final String artistName;
  final int count;

  const TopArtistsDataValue({required this.artistName, required this.count});

  factory TopArtistsDataValue.fromJson(Map<String, dynamic>? json) {
    return TopArtistsDataValue(
      artistName: json?['artistName'] as String,
      count: json?['count'] as int,
    );
  }
}

class TopArtistsData {
  final List<TopArtistsDataValue> topArtists;

  const TopArtistsData({
    required this.topArtists,
  });

  factory TopArtistsData.fromJson(Map<String, dynamic>? json) {
    List<TopArtistsDataValue> topArtists = [];

    json?['top-artists'].forEach((element) {
      TopArtistsDataValue value = TopArtistsDataValue(
        artistName: element['artistName'] as String,
        count: element['count'] as int,
      );

      topArtists.add(value);
    });

    return TopArtistsData(
      topArtists: topArtists,
    );
  }

  Map<String, Object?>? toJson() {
    List<Map<String, Object>> listArtists = [];

    this.topArtists.forEach((TopArtistsDataValue? item) {
      listArtists.add({
        'artistName': item != null ? item.artistName : '',
        'count': item != null ? item.count : 0,
      });
    });

    return {
      'top-artists': listArtists,
    };
  }

  String toString() {
    return jsonEncode(this.toJson());
  }
}
+105 −0
Original line number Original line Diff line number Diff line
import 'dart:convert';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:http/http.dart' as http;


import '../models/counts_by_day.dart';
import 'package:scrobbles/models/counts_by_day.dart';
import '../models/counts_by_hour.dart';
import 'package:scrobbles/models/counts_by_hour.dart';
import '../models/statistics.dart';
import 'package:scrobbles/models/discoveries.dart';
import '../models/timeline.dart';
import 'package:scrobbles/models/statistics_global.dart';
import 'package:scrobbles/models/statistics_recent.dart';
import 'package:scrobbles/models/timeline.dart';
import 'package:scrobbles/models/topartists.dart';


class ScrobblesApi {
class ScrobblesApi {
  static String baseUrl = 'https://scrobble.harrault.fr';
  static String baseUrl = 'https://scrobble.harrault.fr';


  static Future<StatisticsData> fetchStatistics() async {
  static Future<StatisticsGlobalData> fetchGlobalStatistics() async {
    final response = await http.get(Uri.parse(baseUrl + '/stats'));
    final String url = baseUrl + '/stats';
    print('fetching ' + url);
    final response = await http.get(Uri.parse(url));


    if (response.statusCode == 200) {
    if (response.statusCode == 200) {
      return StatisticsData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
      print('ok - fetched ' + url);
      return StatisticsGlobalData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
    } else {
      throw Exception('Failed to get data from API.');
    }
  }

  static Future<StatisticsRecentData> fetchRecentStatistics(int daysCount) async {
    final String url = baseUrl + '/' + daysCount.toString() + '/stats';
    print('fetching ' + url);
    final response = await http.get(Uri.parse(url));

    if (response.statusCode == 200) {
      print('ok - fetched ' + url);
      return StatisticsRecentData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
    } else {
    } else {
      throw Exception('Failed to get data from API.');
      throw Exception('Failed to get data from API.');
    }
    }
@@ -21,9 +40,11 @@ class ScrobblesApi {


  static Future<TimelineData> fetchTimeline(int daysCount) async {
  static Future<TimelineData> fetchTimeline(int daysCount) async {
    final String url = baseUrl + '/data/' + daysCount.toString() + '/timeline';
    final String url = baseUrl + '/data/' + daysCount.toString() + '/timeline';
    print('fetching ' + url);
    final response = await http.get(Uri.parse(url));
    final response = await http.get(Uri.parse(url));


    if (response.statusCode == 200) {
    if (response.statusCode == 200) {
      print('ok - fetched ' + url);
      return TimelineData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
      return TimelineData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
    } else {
    } else {
      throw Exception('Failed to get data from API.');
      throw Exception('Failed to get data from API.');
@@ -32,9 +53,11 @@ class ScrobblesApi {


  static Future<CountsByDayData> fetchCountsByDay(int daysCount) async {
  static Future<CountsByDayData> fetchCountsByDay(int daysCount) async {
    final String url = baseUrl + '/data/' + daysCount.toString() + '/counts-by-day';
    final String url = baseUrl + '/data/' + daysCount.toString() + '/counts-by-day';
    print('fetching ' + url);
    final response = await http.get(Uri.parse(url));
    final response = await http.get(Uri.parse(url));


    if (response.statusCode == 200) {
    if (response.statusCode == 200) {
      print('ok - fetched ' + url);
      return CountsByDayData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
      return CountsByDayData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
    } else {
    } else {
      throw Exception('Failed to get data from API.');
      throw Exception('Failed to get data from API.');
@@ -43,12 +66,40 @@ class ScrobblesApi {


  static Future<CountsByHourData> fetchCountsByHour(int daysCount) async {
  static Future<CountsByHourData> fetchCountsByHour(int daysCount) async {
    final String url = baseUrl + '/data/' + daysCount.toString() + '/counts-by-hour';
    final String url = baseUrl + '/data/' + daysCount.toString() + '/counts-by-hour';
    print('fetching ' + url);
    final response = await http.get(Uri.parse(url));
    final response = await http.get(Uri.parse(url));


    if (response.statusCode == 200) {
    if (response.statusCode == 200) {
      print('ok - fetched ' + url);
      return CountsByHourData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
      return CountsByHourData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
    } else {
    } else {
      throw Exception('Failed to get data from API.');
      throw Exception('Failed to get data from API.');
    }
    }
  }
  }

  static Future<DiscoveriesData> fetchDiscoveries(int daysCount) async {
    final String url = baseUrl + '/data/' + daysCount.toString() + '/news';
    print('fetching ' + url);
    final response = await http.get(Uri.parse(url));

    if (response.statusCode == 200) {
      print('ok - fetched ' + url);
      return DiscoveriesData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
    } else {
      throw Exception('Failed to get data from API.');
    }
  }

  static Future<TopArtistsData> fetchTopArtists(int daysCount) async {
    final String url = baseUrl + '/data/' + daysCount.toString() + '/top-artists';
    print('fetching ' + url);
    final response = await http.get(Uri.parse(url));

    if (response.statusCode == 200) {
      print('ok - fetched ' + url);
      return TopArtistsData.fromJson(jsonDecode(response.body) as Map<String, dynamic>);
    } else {
      throw Exception('Failed to get data from API.');
    }
  }
}
}
+23 −0
Original line number Original line Diff line number Diff line
import 'package:flutter/material.dart';
import 'package:flutter/material.dart';


import '../widgets/header.dart';
import 'package:scrobbles/ui/widgets/cards/discoveries.dart';
import '../widgets/main_screen/counts_by_day_card.dart';
import '../widgets/main_screen/counts_by_hour_card.dart';
import '../widgets/main_screen/statistics_card.dart';
import '../widgets/main_screen/timeline_card.dart';


class MainScreen extends StatelessWidget {
class ScreenDiscoveries extends StatelessWidget {
  const MainScreen({super.key});
  const ScreenDiscoveries({super.key});


  @override
  @override
  Widget build(BuildContext context) {
  Widget build(BuildContext context) {
    return Material(
    return Material(
      color: Theme.of(context).colorScheme.background,
      color: Theme.of(context).colorScheme.background,
      child: ListView(
      child: ListView(
        padding: const EdgeInsets.symmetric(horizontal: 16),
        padding: const EdgeInsets.symmetric(horizontal: 4),
        physics: const BouncingScrollPhysics(),
        physics: const BouncingScrollPhysics(),
        children: <Widget>[
        children: <Widget>[
          const Header(text: 'app_name'),
          const StatisticsCard(),
          const SizedBox(height: 8),
          const SizedBox(height: 8),
          const ChartTimelineCard(),
          const CardDiscoveries(),
          const SizedBox(height: 8),
          const ChartCountsByDayCard(),
          const SizedBox(height: 8),
          const ChartCountsByHourCard(),
          const SizedBox(height: 36),
          const SizedBox(height: 36),
        ],
        ],
      ),
      ),
+32 −0
Original line number Original line Diff line number Diff line
import 'package:flutter/material.dart';

import 'package:scrobbles/ui/widgets/cards/statistics_global.dart';
import 'package:scrobbles/ui/widgets/cards/statistics_recent.dart';
import 'package:scrobbles/ui/widgets/cards/timeline.dart';
import 'package:scrobbles/ui/widgets/cards/top_artists.dart';

class ScreenHome extends StatelessWidget {
  const ScreenHome({super.key});

  @override
  Widget build(BuildContext context) {
    return Material(
      color: Theme.of(context).colorScheme.background,
      child: ListView(
        padding: const EdgeInsets.symmetric(horizontal: 4),
        physics: const BouncingScrollPhysics(),
        children: <Widget>[
          const SizedBox(height: 8),
          const CardStatisticsGlobal(),
          const SizedBox(height: 6),
          const CardStatisticsRecent(),
          const SizedBox(height: 6),
          const CardTimeline(),
          const SizedBox(height: 6),
          const CardTopArtists(),
          const SizedBox(height: 36),
        ],
      ),
    );
  }
}
+23 −0
Original line number Original line Diff line number Diff line
import 'package:flutter/material.dart';

import 'package:scrobbles/ui/widgets/header_app.dart';
import 'package:scrobbles/ui/widgets/settings_form.dart';

class ScreenSettings extends StatelessWidget {
  const ScreenSettings({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.start,
      crossAxisAlignment: CrossAxisAlignment.start,
      mainAxisSize: MainAxisSize.max,
      children: <Widget>[
        SizedBox(height: 8),
        AppHeader(text: 'settings_title'),
        SizedBox(height: 8),
        SettingsForm(),
      ],
    );
  }
}
+0 −16
Original line number Original line Diff line number Diff line
import 'package:flutter/material.dart';

import 'main_screen.dart';

class SkeletonScreen extends StatelessWidget {
  const SkeletonScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      extendBodyBehindAppBar: true,
      body: MainScreen(),
      backgroundColor: Theme.of(context).colorScheme.background,
    );
  }
}
+26 −0
Original line number Original line Diff line number Diff line
import 'package:flutter/material.dart';

import 'package:scrobbles/ui/widgets/cards/counts_by_day.dart';
import 'package:scrobbles/ui/widgets/cards/counts_by_hour.dart';

class ScreenStatistics extends StatelessWidget {
  const ScreenStatistics({super.key});

  @override
  Widget build(BuildContext context) {
    return Material(
      color: Theme.of(context).colorScheme.background,
      child: ListView(
        padding: const EdgeInsets.symmetric(horizontal: 4),
        physics: const BouncingScrollPhysics(),
        children: <Widget>[
          const SizedBox(height: 8),
          const CardCountsByDay(),
          const SizedBox(height: 6),
          const CardCountsByHour(),
          const SizedBox(height: 36),
        ],
      ),
    );
  }
}

lib/ui/skeleton.dart

0 → 100644
+58 −0
Original line number Original line Diff line number Diff line
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_swipe/flutter_swipe.dart';

import 'package:scrobbles/cubit/bottom_nav_cubit.dart';
import 'package:scrobbles/ui/screens/discoveries.dart';
import 'package:scrobbles/ui/screens/home.dart';
import 'package:scrobbles/ui/screens/settings.dart';
import 'package:scrobbles/ui/screens/statistics.dart';
import 'package:scrobbles/ui/widgets/app_bar.dart';
import 'package:scrobbles/ui/widgets/bottom_nav_bar.dart';

class SkeletonScreen extends StatefulWidget {
  const SkeletonScreen({super.key});

  @override
  State<SkeletonScreen> createState() => _SkeletonScreenState();
}

class _SkeletonScreenState extends State<SkeletonScreen> {
  @override
  Widget build(BuildContext context) {
    const List<Widget> pageNavigation = <Widget>[
      const ScreenHome(),
      const ScreenDiscoveries(),
      const ScreenStatistics(),
      const ScreenSettings(),
    ];

    return Scaffold(
      appBar: StandardAppBar(notifyParent: refresh),
      extendBodyBehindAppBar: false,
      body: Swiper(
        itemCount: BlocProvider.of<BottomNavCubit>(context).pagesCount,
        itemBuilder: (BuildContext context, int index) {
          return pageNavigation.elementAt(index);
        },
        pagination: SwiperPagination(
          builder: SwiperCustomPagination(
            builder: (BuildContext context, SwiperPluginConfig config) {
              return BottomNavBar(swipeController: config.controller);
            },
          ),
        ),
        onIndexChanged: (newPageIndex) {
          BlocProvider.of<BottomNavCubit>(context).updateIndex(newPageIndex);
        },
        outer: true,
        loop: false,
      ),
      backgroundColor: Theme.of(context).colorScheme.background,
    );
  }

  refresh() {
    setState(() {});
  }
}
+212 −0
Original line number Original line Diff line number Diff line
import 'package:easy_localization/easy_localization.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';


import '../../../config/app_colors.dart';
import 'package:scrobbles/config/app_colors.dart';
import '../../../models/timeline.dart';
import 'package:scrobbles/utils/color_extensions.dart';
import '../../../utils/color_extensions.dart';


class ChartTimelineCounts extends StatelessWidget {
class CustomBarChart extends StatelessWidget {
  final TimelineData chartData;
  CustomBarChart({super.key});


  const ChartTimelineCounts({super.key, required this.chartData});
  final Widget placeholder = Text('⏳');
  final double chartHeight = 150.0;
  final double verticalTicksInterval = 10;
  final String verticalAxisTitleSuffix = '';
  final double titleFontSize = 10;


  @override
  @override
  Widget build(BuildContext context) {
  Widget build(BuildContext context) {
    return Container(
    return Container(
      height: 150.0,
      child: SizedBox(
      child: LayoutBuilder(builder: (context, constraints) {
        height: this.chartHeight,
        final double maxWidth = constraints.maxWidth;
      ),
        final int barsCount = this.chartData.data.keys.length;
    );
        final double barWidth = 0.7 * (maxWidth / barsCount);
  }


  BarChart getBarChart({barWidth, backgroundColor}) {
    return BarChart(
    return BarChart(
      BarChartData(
      BarChartData(
        barGroups: getDataCounts(barWidth),
        barGroups: getDataCounts(barWidth),
            backgroundColor: Theme.of(context).colorScheme.onBackground,
        backgroundColor: backgroundColor,
        borderData: getBorderData(),
        borderData: getBorderData(),
        gridData: getGridData(),
        gridData: getGridData(),
        titlesData: getTitlesData(),
        titlesData: getTitlesData(),
            barTouchData: getBarTouchData(),
        barTouchData: BarTouchData(enabled: false),
            maxY: getNextRoundNumber(getMaxCountsValue(), 50),
        maxY: getNextRoundNumber(getMaxCountsValue(), this.verticalTicksInterval),
      ),
      ),
    );
    );
      }),
    );
  }
  }


  double getMaxCountsValue() {
  double getBarWidth(double containerWidth, int barsCount) {
    double maxValue = 0;
    return 0.65 * (containerWidth / barsCount);

    this.chartData.data.keys.forEach((key) {
      TimelineDataValue? value = this.chartData.data[key];
      if (value != null) {
        double counts = value.counts.toDouble();
        if (counts > maxValue) {
          maxValue = counts;
  }
  }

  List<BarChartGroupData> getDataCounts(double barWidth) {
    return [];
  }
  }
    });


    return maxValue;
  double getMaxCountsValue() {
    return 0.0;
  }
  }


  double getNextRoundNumber(double number, int scale) {
  double getNextRoundNumber(double number, double scale) {
    return scale * ((number ~/ scale).toInt() + 1);
    return scale * ((number ~/ scale).toInt() + 1);
  }
  }


  List<BarChartGroupData> getDataCounts(double barWidth) {
  BarChartGroupData getBarItem(
    List<BarChartGroupData> data = [];
      {required int x,
      required List<double> values,
      required List<Color> barColors,
      required double barWidth}) {
    List<BarChartRodData> barRods = [];

    for (int i = 0; i < values.length; i++) {
      double value = values[i];
      Color barColor = barColors[i];


    this.chartData.data.keys.forEach((key) {
      final gradient = this.getGradient(barColor, value, this.getMaxCountsValue());
      TimelineDataValue? value = this.chartData.data[key];
      final borderColor = barColor.darken(20);
      if (value != null) {
        final int date = DateTime.parse(key).millisecondsSinceEpoch;
        final double counts = value.counts.toDouble();


        data.add(BarChartGroupData(
      barRods.add(
          x: date,
          barRods: [
        BarChartRodData(
        BarChartRodData(
              toY: counts,
          toY: value,
              color: AppColors.contentColorOrange,
          color: barColor,
          gradient: gradient,
          width: barWidth,
          width: barWidth,
          borderRadius: BorderRadius.all(Radius.zero),
          borderRadius: BorderRadius.all(Radius.zero),
          borderSide: BorderSide(
          borderSide: BorderSide(
                color: AppColors.contentColorOrange.darken(20),
            color: borderColor,
          ),
          ),
        ),
        ),
          ],
      );
        ));
    }
    }
    });


    return data;
    return BarChartGroupData(
      x: x,
      barRods: barRods,
    );
  }

  LinearGradient getGradient(Color baseColor, double value, double maxValue) {
    double alignmentTopValue = value != 0.0 ? -2 * maxValue / value + 1 : 0;

    return LinearGradient(
      begin: Alignment(-1, alignmentTopValue),
      end: Alignment(1, 1),
      colors: <Color>[
        baseColor.lighten(30),
        baseColor,
        baseColor.darken(30),
      ],
      tileMode: TileMode.mirror,
    );
  }
  }


  FlBorderData getBorderData() {
  FlBorderData getBorderData() {
@@ -97,6 +115,8 @@ class ChartTimelineCounts extends StatelessWidget {
  FlGridData getGridData() {
  FlGridData getGridData() {
    return const FlGridData(
    return const FlGridData(
      show: true,
      show: true,
      drawHorizontalLine: true,
      drawVerticalLine: false,
    );
    );
  }
  }


@@ -108,10 +128,21 @@ class ChartTimelineCounts extends StatelessWidget {
    final AxisTitles verticalTitles = AxisTitles(
    final AxisTitles verticalTitles = AxisTitles(
      sideTitles: SideTitles(
      sideTitles: SideTitles(
        showTitles: true,
        showTitles: true,
        reservedSize: 30,
        reservedSize: 35,
        getTitlesWidget: getVerticalTitlesWidget,
        getTitlesWidget: getVerticalTitlesWidget,
        interval: this.verticalTicksInterval,
      ),
    );

    final AxisTitles verticalSpacer = AxisTitles(
      sideTitles: SideTitles(
        showTitles: true,
        reservedSize: 35,
        getTitlesWidget: getVerticalTitlesSpacerWidget,
        interval: this.verticalTicksInterval,
      ),
      ),
    );
    );

    final AxisTitles horizontalTitles = AxisTitles(
    final AxisTitles horizontalTitles = AxisTitles(
      sideTitles: SideTitles(
      sideTitles: SideTitles(
        showTitles: true,
        showTitles: true,
@@ -123,26 +154,37 @@ class ChartTimelineCounts extends StatelessWidget {
    return FlTitlesData(
    return FlTitlesData(
      show: true,
      show: true,
      bottomTitles: horizontalTitles,
      bottomTitles: horizontalTitles,
      leftTitles: none,
      leftTitles: verticalTitles,
      topTitles: none,
      topTitles: none,
      rightTitles: verticalTitles,
      rightTitles: verticalSpacer,
    );
    );
  }
  }


  Widget getVerticalTitlesWidget(double value, TitleMeta meta) {
  Widget getVerticalTitlesWidget(double value, TitleMeta meta) {
    String suffix =
        this.verticalAxisTitleSuffix != '' ? ' ' + this.verticalAxisTitleSuffix : '';

    return SideTitleWidget(
    return SideTitleWidget(
      axisSide: meta.axisSide,
      axisSide: meta.axisSide,
      space: 4,
      space: 4,
      child: Text(
      child: Text(
        value.toInt().toString(),
        value.toInt().toString() + suffix,
        style: const TextStyle(
        style: TextStyle(
          color: AppColors.mainTextColor1,
          color: AppColors.mainTextColor1,
          fontSize: 12,
          fontSize: this.titleFontSize,
        ),
        ),
      ),
      ),
    );
    );
  }
  }


  Widget getVerticalTitlesSpacerWidget(double value, TitleMeta meta) {
    return SideTitleWidget(
      axisSide: meta.axisSide,
      space: 4,
      child: Text(''),
    );
  }

  Widget getHorizontalTitlesWidget(double value, TitleMeta meta) {
  Widget getHorizontalTitlesWidget(double value, TitleMeta meta) {
    final DateFormat formatter = DateFormat('dd/MM');
    final DateFormat formatter = DateFormat('dd/MM');


@@ -152,41 +194,18 @@ class ChartTimelineCounts extends StatelessWidget {
    return SideTitleWidget(
    return SideTitleWidget(
      axisSide: meta.axisSide,
      axisSide: meta.axisSide,
      space: 4,
      space: 4,
      child: Padding(
        padding: EdgeInsets.only(right: 10),
        child: RotationTransition(
        child: RotationTransition(
          turns: new AlwaysStoppedAnimation(-30 / 360),
          turns: new AlwaysStoppedAnimation(-30 / 360),
          child: Text(
          child: Text(
            text,
            text,
          style: const TextStyle(
            style: TextStyle(
              color: AppColors.mainTextColor1,
              color: AppColors.mainTextColor1,
            fontSize: 11,
              fontSize: this.titleFontSize,
            ),
            ),
          ),
          ),
        ),
        ),
    );
  }

  BarTouchData getBarTouchData() {
    return BarTouchData(
      enabled: true,
      touchTooltipData: BarTouchTooltipData(
        tooltipBgColor: Colors.transparent,
        tooltipPadding: EdgeInsets.zero,
        tooltipMargin: 2,
        getTooltipItem: (
          BarChartGroupData group,
          int groupIndex,
          BarChartRodData rod,
          int rodIndex,
        ) {
          return BarTooltipItem(
            rod.toY.round().toString(),
            const TextStyle(
              color: AppColors.mainTextColor2,
              fontWeight: FontWeight.bold,
              fontSize: 10,
            ),
          );
        },
      ),
      ),
    );
    );
  }
  }
+98 −0
Original line number Original line Diff line number Diff line
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:fl_chart/fl_chart.dart';
import 'package:flutter/material.dart';


import '../../../config/app_colors.dart';
import 'package:scrobbles/config/app_colors.dart';
import '../../../models/timeline.dart';


class ChartTimelineEclecticism extends StatelessWidget {
class CustomLineChart extends StatelessWidget {
  final TimelineData chartData;
  CustomLineChart({super.key});


  const ChartTimelineEclecticism({super.key, required this.chartData});
  final Widget placeholder = Text('⏳');
  final double chartHeight = 150.0;
  final double titleFontSize = 10;


  @override
  @override
  Widget build(BuildContext context) {
  Widget build(BuildContext context) {
    return Container(
    return Container(
      height: 100.0,
      child: SizedBox(
      child: LineChart(
        height: this.chartHeight,
        LineChartData(
          lineBarsData: getDataEclecticism(),
          backgroundColor: Theme.of(context).colorScheme.onBackground,
          borderData: getBorderData(),
          gridData: getGridData(),
          titlesData: getTitlesData(),
          lineTouchData: getLineTouchDataEclecticism(),
          maxY: 100,
          minY: 0,
        ),
        duration: const Duration(milliseconds: 250),
      ),
      ),
    );
    );
  }
  }


  List<LineChartBarData> getDataEclecticism() {
    List<FlSpot> spots = [];

    this.chartData.data.keys.forEach((element) {
      TimelineDataValue? value = this.chartData.data[element];
      if (value != null) {
        final double date = DateTime.parse(element).millisecondsSinceEpoch.toDouble();
        final double eclecticism = value.eclecticism.toDouble();

        spots.add(FlSpot(date, eclecticism));
      }
    });

    return [
      LineChartBarData(
        isCurved: true,
        color: AppColors.contentColorCyan,
        barWidth: 3,
        isStrokeCapRound: false,
        dotData: const FlDotData(show: false),
        belowBarData: BarAreaData(show: true),
        spots: spots,
      ),
    ];
  }

  FlBorderData getBorderData() {
  FlBorderData getBorderData() {
    return FlBorderData(
    return FlBorderData(
      show: true,
      show: false,
      border: Border.all(
        color: AppColors.borderColor,
        width: 2,
      ),
    );
    );
  }
  }


  FlGridData getGridData() {
  FlGridData getGridData() {
    return const FlGridData(
    return const FlGridData(
      show: true,
      show: false,
    );
    );
  }
  }


@@ -80,11 +39,20 @@ class ChartTimelineEclecticism extends StatelessWidget {
    final AxisTitles verticalTitles = AxisTitles(
    final AxisTitles verticalTitles = AxisTitles(
      sideTitles: SideTitles(
      sideTitles: SideTitles(
        showTitles: true,
        showTitles: true,
        reservedSize: 30,
        reservedSize: 35,
        getTitlesWidget: getVerticalTitlesWidget,
        getTitlesWidget: getVerticalTitlesWidget,
        interval: 25,
        interval: 25,
      ),
      ),
    );
    );

    final AxisTitles verticalSpacer = AxisTitles(
      sideTitles: SideTitles(
        showTitles: true,
        reservedSize: 35,
        getTitlesWidget: getVerticalTitlesSpacerWidget,
      ),
    );

    final AxisTitles horizontalTitles = AxisTitles(
    final AxisTitles horizontalTitles = AxisTitles(
      sideTitles: SideTitles(
      sideTitles: SideTitles(
        showTitles: true,
        showTitles: true,
@@ -96,7 +64,7 @@ class ChartTimelineEclecticism extends StatelessWidget {
    return FlTitlesData(
    return FlTitlesData(
      show: true,
      show: true,
      bottomTitles: horizontalTitles,
      bottomTitles: horizontalTitles,
      leftTitles: none,
      leftTitles: verticalSpacer,
      topTitles: none,
      topTitles: none,
      rightTitles: verticalTitles,
      rightTitles: verticalTitles,
    );
    );
@@ -107,58 +75,24 @@ class ChartTimelineEclecticism extends StatelessWidget {
      axisSide: meta.axisSide,
      axisSide: meta.axisSide,
      space: 4,
      space: 4,
      child: Text(
      child: Text(
        value.toInt().toString(),
        value.toInt().toString() + ' %',
        style: const TextStyle(
        style: TextStyle(
          color: AppColors.mainTextColor1,
          color: AppColors.mainTextColor1,
          fontSize: 12,
          fontSize: this.titleFontSize,
        ),
        ),
      ),
      ),
    );
    );
  }
  }


  Widget getHorizontalTitlesWidget(double value, TitleMeta meta) {
  Widget getVerticalTitlesSpacerWidget(double value, TitleMeta meta) {
    final DateFormat formatter = DateFormat('dd/MM');

    final DateTime date = DateTime.fromMillisecondsSinceEpoch(value.toInt());
    final String text = formatter.format(date);

    return SideTitleWidget(
    return SideTitleWidget(
      axisSide: meta.axisSide,
      axisSide: meta.axisSide,
      space: 4,
      space: 4,
      child: RotationTransition(
      child: Text(''),
        turns: new AlwaysStoppedAnimation(-30 / 360),
        child: Text(
          text,
          style: const TextStyle(
            color: AppColors.mainTextColor1,
            fontSize: 11,
          ),
        ),
      ),
    );
    );
  }
  }


  LineTouchData getLineTouchDataEclecticism() {
  Widget getHorizontalTitlesWidget(double value, TitleMeta meta) {
    return LineTouchData(
    return Text('');
      handleBuiltInTouches: true,
      touchTooltipData: LineTouchTooltipData(
        tooltipBgColor: Colors.transparent,
        tooltipPadding: EdgeInsets.all(8),
        tooltipMargin: 2,
        getTooltipItems: (List<LineBarSpot> touchedSpots) {
          return touchedSpots.map((LineBarSpot touchedSpot) {
            final textStyle = TextStyle(
              color: AppColors.mainTextColor2,
              fontWeight: FontWeight.bold,
              fontSize: 10,
            );
            return LineTooltipItem(
              touchedSpot.y.toString(),
              textStyle,
            );
          }).toList();
        },
      ),
    );
  }
  }
}
}
+28 −0
Original line number Original line Diff line number Diff line
import 'package:flutter/material.dart';
import 'package:unicons/unicons.dart';

import 'package:scrobbles/ui/widgets/header_app.dart';

class StandardAppBar extends StatelessWidget implements PreferredSizeWidget {
  final Function() notifyParent;

  const StandardAppBar({super.key, required this.notifyParent});

  @override
  Widget build(BuildContext context) {
    return AppBar(
      title: const AppHeader(text: 'app_name'),
      actions: [
        IconButton(
          onPressed: () {
            this.notifyParent();
          },
          icon: const Icon(UniconsSolid.refresh),
        ),
      ],
    );
  }

  @override
  Size get preferredSize => const Size.fromHeight(50);
}
+67 −0
Original line number Original line Diff line number Diff line
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_swipe/flutter_swipe.dart';
import 'package:ionicons/ionicons.dart';

import 'package:scrobbles/cubit/bottom_nav_cubit.dart';

class BottomNavBar extends StatelessWidget {
  const BottomNavBar({super.key, required this.swipeController});

  final SwiperController swipeController;

  @override
  Widget build(BuildContext context) {
    return Card(
      margin: const EdgeInsets.only(
        top: 1,
        right: 0,
        left: 0,
      ),
      elevation: 4,
      shadowColor: Theme.of(context).colorScheme.shadow,
      color: Theme.of(context).colorScheme.surfaceVariant,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.only(
          topLeft: Radius.circular(16),
          topRight: Radius.circular(16),
        ),
      ),
      child: BlocBuilder<BottomNavCubit, int>(
        builder: (BuildContext context, int state) {
          return BottomNavigationBar(
            currentIndex: state,
            onTap: (int index) {
              context.read<BottomNavCubit>().updateIndex(index);
              swipeController.move(index);
            },
            type: BottomNavigationBarType.fixed,
            elevation: 0,
            backgroundColor: Colors.transparent,
            selectedItemColor: Theme.of(context).colorScheme.primary,
            unselectedItemColor: Theme.of(context).textTheme.bodySmall!.color,
            items: <BottomNavigationBarItem>[
              BottomNavigationBarItem(
                icon: const Icon(Ionicons.home_outline),
                label: tr('bottom_nav_home'),
              ),
              BottomNavigationBarItem(
                icon: const Icon(Ionicons.star_outline),
                label: tr('bottom_nav_discoveries'),
              ),
              BottomNavigationBarItem(
                icon: const Icon(Ionicons.bar_chart_outline),
                label: tr('bottom_nav_repartition'),
              ),
              BottomNavigationBarItem(
                icon: const Icon(Ionicons.settings_outline),
                label: tr('bottom_nav_settings'),
              ),
            ],
          );
        },
      ),
    );
  }
}
+53 −0
Original line number Original line Diff line number Diff line
import 'package:flutter/material.dart';

class CardContent extends StatelessWidget {
  const CardContent({
    super.key,
    required this.title,
    required this.color,
    required this.loader,
    required this.content,
  });

  final String title;
  final Color color;
  final Widget loader;
  final Widget content;

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 2,
      shadowColor: Theme.of(context).colorScheme.shadow,
      color: this.color,
      shape: const RoundedRectangleBorder(
        borderRadius: BorderRadius.all(
          Radius.circular(8),
        ),
      ),
      child: Padding(
        padding: const EdgeInsets.all(8.0),
        child: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Row(
              crossAxisAlignment: CrossAxisAlignment.start,
              mainAxisAlignment: MainAxisAlignment.start,
              children: [
                Text(
                  this.title,
                  style:
                      Theme.of(context).primaryTextTheme.titleLarge!.apply(fontWeightDelta: 2),
                ),
                this.loader,
              ],
            ),
            const SizedBox(height: 8),
            this.content,
          ],
        ),
      ),
    );
  }
}
+64 −0
Original line number Original line Diff line number Diff line
import 'dart:convert';

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

import 'package:scrobbles/config/settings.dart';
import 'package:scrobbles/cubit/data_counts_by_day_cubit.dart';
import 'package:scrobbles/models/counts_by_day.dart';
import 'package:scrobbles/network/scrobbles.dart';
import 'package:scrobbles/ui/widgets/card_content.dart';
import 'package:scrobbles/ui/widgets/charts/counts_by_day.dart';
import 'package:scrobbles/ui/widgets/error.dart';

class CardCountsByDay extends StatelessWidget {
  const CardCountsByDay({super.key});

  @override
  Widget build(BuildContext context) {
    final int daysCount = Settings.countsByDayDaysCount;

    return BlocBuilder<DataCountsByDayCubit, DataCountsByDayState>(
      builder: (BuildContext context, DataCountsByDayState state) {
        return CardContent(
          color: Theme.of(context).colorScheme.surface,
          title: 'counts_by_day'.tr(
            namedArgs: {
              'daysCount': daysCount.toString(),
            },
          ),
          loader: updateCountsByDay(Settings.countsByDayDaysCount),
          content: ChartCountsByDay(
            chartData: CountsByDayData.fromJson(jsonDecode(state.countsByDay.toString())),
            isLoading: false,
          ),
        );
      },
    );
  }

  Widget updateCountsByDay(int daysCount) {
    final Widget loading = const Text('⏳');
    final Widget done = const Text('');

    late Future<CountsByDayData> futureCountsByDay = ScrobblesApi.fetchCountsByDay(daysCount);

    return BlocBuilder<DataCountsByDayCubit, DataCountsByDayState>(
      builder: (BuildContext context, DataCountsByDayState state) {
        return FutureBuilder<CountsByDayData>(
          future: futureCountsByDay,
          builder: (context, snapshot) {
            if (snapshot.hasError) {
              return ShowErrorWidget(message: '${snapshot.error}');
            }

            BlocProvider.of<DataCountsByDayCubit>(context).update(snapshot.data);

            return !snapshot.hasData ? loading : done;
          },
        );
      },
    );
  }
}
+65 −0
Original line number Original line Diff line number Diff line
import 'dart:convert';

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

import 'package:scrobbles/config/settings.dart';
import 'package:scrobbles/cubit/data_counts_by_hour_cubit.dart';
import 'package:scrobbles/models/counts_by_hour.dart';
import 'package:scrobbles/network/scrobbles.dart';
import 'package:scrobbles/ui/widgets/card_content.dart';
import 'package:scrobbles/ui/widgets/charts/counts_by_hour.dart';
import 'package:scrobbles/ui/widgets/error.dart';

class CardCountsByHour extends StatelessWidget {
  const CardCountsByHour({super.key});

  @override
  Widget build(BuildContext context) {
    final int daysCount = Settings.countsByHourDaysCount;

    return BlocBuilder<DataCountsByHourCubit, DataCountsByHourState>(
      builder: (BuildContext context, DataCountsByHourState state) {
        return CardContent(
          color: Theme.of(context).colorScheme.surface,
          title: 'counts_by_hour'.tr(
            namedArgs: {
              'daysCount': daysCount.toString(),
            },
          ),
          loader: updateCountsByHour(Settings.countsByHourDaysCount),
          content: ChartCountsByHour(
            chartData: CountsByHourData.fromJson(jsonDecode(state.countsByHour.toString())),
            isLoading: false,
          ),
        );
      },
    );
  }

  Widget updateCountsByHour(int daysCount) {
    final Widget loading = const Text('⏳');
    final Widget done = const Text('');

    late Future<CountsByHourData> futureCountsByHour =
        ScrobblesApi.fetchCountsByHour(daysCount);

    return BlocBuilder<DataCountsByHourCubit, DataCountsByHourState>(
      builder: (BuildContext context, DataCountsByHourState state) {
        return FutureBuilder<CountsByHourData>(
          future: futureCountsByHour,
          builder: (context, snapshot) {
            if (snapshot.hasError) {
              return ShowErrorWidget(message: '${snapshot.error}');
            }

            BlocProvider.of<DataCountsByHourCubit>(context).update(snapshot.data);

            return !snapshot.hasData ? loading : done;
          },
        );
      },
    );
  }
}
+88 −0
Original line number Original line Diff line number Diff line
import 'dart:convert';

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

import 'package:scrobbles/config/settings.dart';
import 'package:scrobbles/cubit/data_discoveries_cubit.dart';
import 'package:scrobbles/models/discoveries.dart';
import 'package:scrobbles/network/scrobbles.dart';
import 'package:scrobbles/ui/widgets/card_content.dart';
import 'package:scrobbles/ui/widgets/charts/discoveries_artists.dart';
import 'package:scrobbles/ui/widgets/charts/discoveries_tracks.dart';
import 'package:scrobbles/ui/widgets/error.dart';

class CardDiscoveries extends StatelessWidget {
  const CardDiscoveries({super.key});

  @override
  Widget build(BuildContext context) {
    final int daysCount = Settings.discoveriesDaysCount;

    return BlocBuilder<DataDiscoveriesCubit, DataDiscoveriesState>(
      builder: (BuildContext context, DataDiscoveriesState state) {
        final TextTheme textTheme = Theme.of(context).primaryTextTheme;

        return CardContent(
          color: Theme.of(context).colorScheme.surface,
          title: 'discoveries_title'.tr(
            namedArgs: {
              'daysCount': daysCount.toString(),
            },
          ),
          loader: updateDiscoveries(Settings.discoveriesDaysCount),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(
                'discoveries_artists_title',
                style: textTheme.titleMedium!.apply(fontWeightDelta: 2),
              ).tr(),
              const SizedBox(height: 8),
              ChartDiscoveriesArtists(
                chartData: DiscoveriesData.fromJson(jsonDecode(state.discoveries.toString())),
                isLoading: false,
              ),
              const SizedBox(height: 8),
              Text(
                'discoveries_tracks_title',
                style: textTheme.titleMedium!.apply(fontWeightDelta: 2),
              ).tr(),
              const SizedBox(height: 8),
              ChartDiscoveriesTracks(
                chartData: DiscoveriesData.fromJson(jsonDecode(state.discoveries.toString())),
                isLoading: false,
              ),
            ],
          ),
        );
      },
    );
  }

  Widget updateDiscoveries(int daysCount) {
    final Widget loading = const Text('⏳');
    final Widget done = const Text('');

    late Future<DiscoveriesData> futureDiscoveries = ScrobblesApi.fetchDiscoveries(daysCount);

    return BlocBuilder<DataDiscoveriesCubit, DataDiscoveriesState>(
      builder: (BuildContext context, DataDiscoveriesState state) {
        return FutureBuilder<DiscoveriesData>(
          future: futureDiscoveries,
          builder: (context, snapshot) {
            if (snapshot.hasError) {
              return ShowErrorWidget(message: '${snapshot.error}');
            }

            BlocProvider.of<DataDiscoveriesCubit>(context).update(snapshot.data);

            return !snapshot.hasData ? loading : done;
          },
        );
      },
    );
  }
}
+59 −0
Original line number Original line Diff line number Diff line
import 'dart:convert';

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

import 'package:scrobbles/cubit/data_statistics_global_cubit.dart';
import 'package:scrobbles/models/statistics_global.dart';
import 'package:scrobbles/network/scrobbles.dart';
import 'package:scrobbles/ui/widgets/card_content.dart';
import 'package:scrobbles/ui/widgets/content/statistics_global.dart';
import 'package:scrobbles/ui/widgets/error.dart';

class CardStatisticsGlobal extends StatelessWidget {
  const CardStatisticsGlobal({super.key});

  @override
  Widget build(BuildContext context) {
    return BlocBuilder<DataStatisticsGlobalCubit, DataStatisticsGlobalState>(
      builder: (BuildContext context, DataStatisticsGlobalState state) {
        return CardContent(
          color: Theme.of(context).colorScheme.primary,
          title: 'global_statistics'.tr(),
          loader: updateStatisticsGlobal(),
          content: ContentStatisticsGlobal(
            statistics:
                StatisticsGlobalData.fromJson(jsonDecode(state.statisticsGlobal.toString())),
            isLoading: false,
          ),
        );
      },
    );
  }

  Widget updateStatisticsGlobal() {
    final Widget loading = const Text('⏳');
    final Widget done = const Text('');

    late Future<StatisticsGlobalData> futureStatisticsGlobal =
        ScrobblesApi.fetchGlobalStatistics();

    return BlocBuilder<DataStatisticsGlobalCubit, DataStatisticsGlobalState>(
      builder: (BuildContext context, DataStatisticsGlobalState dataState) {
        return FutureBuilder<StatisticsGlobalData>(
          future: futureStatisticsGlobal,
          builder: (context, snapshot) {
            if (snapshot.hasError) {
              return ShowErrorWidget(message: '${snapshot.error}');
            }

            BlocProvider.of<DataStatisticsGlobalCubit>(context).update(snapshot.data);

            return !snapshot.hasData ? loading : done;
          },
        );
      },
    );
  }
}
+66 −0
Original line number Original line Diff line number Diff line
import 'dart:convert';

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

import 'package:scrobbles/config/settings.dart';
import 'package:scrobbles/cubit/data_statistics_recent_cubit.dart';
import 'package:scrobbles/models/statistics_recent.dart';
import 'package:scrobbles/network/scrobbles.dart';
import 'package:scrobbles/ui/widgets/card_content.dart';
import 'package:scrobbles/ui/widgets/content/statistics_recent.dart';
import 'package:scrobbles/ui/widgets/error.dart';

class CardStatisticsRecent extends StatelessWidget {
  const CardStatisticsRecent({super.key});

  @override
  Widget build(BuildContext context) {
    final int daysCount = Settings.statisticsRecentDaysCount;

    return BlocBuilder<DataStatisticsRecentCubit, DataStatisticsRecentState>(
      builder: (BuildContext context, DataStatisticsRecentState dataState) {
        return CardContent(
          color: Theme.of(context).colorScheme.primary,
          title: 'recent_statistics'.tr(
            namedArgs: {
              'daysCount': daysCount.toString(),
            },
          ),
          loader: updateStatisticsRecent(Settings.statisticsRecentDaysCount),
          content: ContentStatisticsRecent(
            statistics: StatisticsRecentData.fromJson(
                jsonDecode(dataState.statisticsRecent.toString())),
            isLoading: false,
          ),
        );
      },
    );
  }

  Widget updateStatisticsRecent(int daysCount) {
    final Widget loading = const Text('⏳');
    final Widget done = const Text('');

    late Future<StatisticsRecentData> futureStatisticsRecent =
        ScrobblesApi.fetchRecentStatistics(daysCount);

    return BlocBuilder<DataStatisticsRecentCubit, DataStatisticsRecentState>(
      builder: (BuildContext context, DataStatisticsRecentState state) {
        return FutureBuilder<StatisticsRecentData>(
          future: futureStatisticsRecent,
          builder: (context, snapshot) {
            if (snapshot.hasError) {
              return ShowErrorWidget(message: '${snapshot.error}');
            }

            BlocProvider.of<DataStatisticsRecentCubit>(context).update(snapshot.data);

            return !snapshot.hasData ? loading : done;
          },
        );
      },
    );
  }
}
+73 −0
Original line number Original line Diff line number Diff line
import 'dart:convert';

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

import 'package:scrobbles/config/settings.dart';
import 'package:scrobbles/cubit/data_timeline_cubit.dart';
import 'package:scrobbles/models/timeline.dart';
import 'package:scrobbles/network/scrobbles.dart';
import 'package:scrobbles/ui/widgets/card_content.dart';
import 'package:scrobbles/ui/widgets/charts/timeline_counts.dart';
import 'package:scrobbles/ui/widgets/charts/timeline_eclecticism.dart';
import 'package:scrobbles/ui/widgets/error.dart';

class CardTimeline extends StatelessWidget {
  const CardTimeline({super.key});

  @override
  Widget build(BuildContext context) {
    final int daysCount = Settings.timelineDaysCount;

    return BlocBuilder<DataTimelineCubit, DataTimelineState>(
      builder: (BuildContext context, DataTimelineState state) {
        return CardContent(
          color: Theme.of(context).colorScheme.surface,
          title: 'timeline_title'.tr(
            namedArgs: {
              'daysCount': daysCount.toString(),
            },
          ),
          loader: updateTimeline(Settings.timelineDaysCount),
          content: Stack(
            children: [
              ChartTimelineCounts(
                chartData: TimelineData.fromJson(jsonDecode(state.timeline.toString())),
                isLoading: false,
              ),
              ChartTimelineEclecticism(
                chartData: TimelineData.fromJson(jsonDecode(state.timeline.toString())),
                isLoading: false,
              ),
            ],
          ),
        );
      },
    );
  }

  Widget updateTimeline(int daysCount) {
    final Widget loading = const Text('⏳');
    final Widget done = const Text('');

    late Future<TimelineData> futureTimeline = ScrobblesApi.fetchTimeline(daysCount);

    return BlocBuilder<DataTimelineCubit, DataTimelineState>(
      builder: (BuildContext context, DataTimelineState dataTimelineState) {
        return FutureBuilder<TimelineData>(
          future: futureTimeline,
          builder: (context, snapshot) {
            if (snapshot.hasError) {
              return ShowErrorWidget(message: '${snapshot.error}');
            }

              BlocProvider.of<DataTimelineCubit>(context).update(snapshot.data);

            return !snapshot.hasData ? loading : done;
          },
        );
      },
    );
  }
}
+64 −0
Original line number Original line Diff line number Diff line
import 'dart:convert';

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

import 'package:scrobbles/config/settings.dart';
import 'package:scrobbles/cubit/data_top_artists_cubit.dart';
import 'package:scrobbles/models/topartists.dart';
import 'package:scrobbles/network/scrobbles.dart';
import 'package:scrobbles/ui/widgets/card_content.dart';
import 'package:scrobbles/ui/widgets/charts/top_artists.dart';
import 'package:scrobbles/ui/widgets/error.dart';

class CardTopArtists extends StatelessWidget {
  const CardTopArtists({super.key});

  @override
  Widget build(BuildContext context) {
    final int daysCount = Settings.topArtistsDaysCount;

    return BlocBuilder<DataTopArtistsCubit, DataTopArtistsState>(
      builder: (BuildContext context, DataTopArtistsState state) {
        return CardContent(
          color: Theme.of(context).colorScheme.surface,
          title: 'top_artists_title'.tr(
            namedArgs: {
              'daysCount': daysCount.toString(),
            },
          ),
          loader: updateTopArtists(Settings.topArtistsDaysCount),
          content: ChartTopArtists(
            chartData: TopArtistsData.fromJson(jsonDecode(state.topArtists.toString())),
            isLoading: false,
          ),
        );
      },
    );
  }

  Widget updateTopArtists(int daysCount) {
    final Widget loading = const Text('⏳');
    final Widget done = const Text('');

    late Future<TopArtistsData> futureTopArtists = ScrobblesApi.fetchTopArtists(daysCount);

    return BlocBuilder<DataTopArtistsCubit, DataTopArtistsState>(
      builder: (BuildContext context, DataTopArtistsState state) {
        return FutureBuilder<TopArtistsData>(
          future: futureTopArtists,
          builder: (context, snapshot) {
            if (snapshot.hasError) {
              return ShowErrorWidget(message: '${snapshot.error}');
            }

            BlocProvider.of<DataTopArtistsCubit>(context).update(snapshot.data);

            return !snapshot.hasData ? loading : done;
          },
        );
      },
    );
  }
}
+149 −0

File added.

Preview size limit exceeded, changes collapsed.

+99 −0

File added.

Preview size limit exceeded, changes collapsed.

+83 −0

File added.

Preview size limit exceeded, changes collapsed.

+83 −0

File added.

Preview size limit exceeded, changes collapsed.

+80 −0

File added.

Preview size limit exceeded, changes collapsed.

+100 −0

File added.

Preview size limit exceeded, changes collapsed.

+143 −0

File added.

Preview size limit exceeded, changes collapsed.

+17 −0

File changed and moved.

Preview size limit exceeded, changes collapsed.

+0 −48

File deleted.

Preview size limit exceeded, changes collapsed.

+0 −238

File deleted.

Preview size limit exceeded, changes collapsed.

+0 −49

File deleted.

Preview size limit exceeded, changes collapsed.

+0 −192

File deleted.

Preview size limit exceeded, changes collapsed.

+0 −43

File deleted.

Preview size limit exceeded, changes collapsed.

+0 −46

File deleted.

Preview size limit exceeded, changes collapsed.

+0 −48

File deleted.

Preview size limit exceeded, changes collapsed.

+0 −54

File deleted.

Preview size limit exceeded, changes collapsed.

+76 −0

File added.

Preview size limit exceeded, changes collapsed.

+129 −17

File changed.

Preview size limit exceeded, changes collapsed.

+9 −2

File changed.

Preview size limit exceeded, changes collapsed.