diff --git a/android/gradle.properties b/android/gradle.properties index 9afb4c689a0bff258d2a5403ef6a41e03d7faed5..007b41f87df662638a73e32c7ad937e5b08460a6 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,5 +1,5 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -app.versionName=0.0.48 -app.versionCode=48 +app.versionName=0.0.49 +app.versionCode=49 diff --git a/assets/translations/en.json b/assets/translations/en.json index 4ff99c8b7a7d1ca5abdb3ee305a0a56c684e7676..8ebe491d4fcc44fa1cb1ad5f4fb3af0ee7d49817 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -22,6 +22,8 @@ "discoveries_title": "Discoveries ({daysCount} days)", "discoveries_artists_title": "Artists", "discoveries_tracks_title": "Tracks", + "new_artists_title": "New artists", + "new_tracks_title": "New tracks", "top_artists_title": "Top artists ({daysCount} days)", @@ -35,6 +37,9 @@ "settings_label_statistics_recent_days_count": "Statistics: ", "settings_label_timeline_days_count": "Global timeline: ", "settings_label_top_artists_days_count": "Top Artists: ", + "settings_title_counts": "Counts: ", + "settings_label_new_artists_count": "New artists: ", + "settings_label_new_tracks_count": "New tracks: ", "settings_button_save": "Save", diff --git a/assets/translations/fr.json b/assets/translations/fr.json index bbf6d69cc6b098c4a4c61dd8de0aaab7adc8eb92..39625aecb52e201581a06a0727eff33b689daf17 100644 --- a/assets/translations/fr.json +++ b/assets/translations/fr.json @@ -22,6 +22,8 @@ "discoveries_title": "Découvertes ({daysCount} jours)", "discoveries_artists_title": "Artistes", "discoveries_tracks_title": "Morceaux", + "new_artists_title": "Nouveaux artistes", + "new_tracks_title": "Nouveaux morceaux", "top_artists_title": "Top artistes ({daysCount} jours)", @@ -35,6 +37,10 @@ "settings_label_statistics_recent_days_count": "Statistiques : ", "settings_label_timeline_days_count": "Timeline globale : ", "settings_label_top_artists_days_count": "Top Artistes : ", + "settings_title_counts": "Nombres :", + "settings_label_new_artists_count": "Nouveaux artistes :", + "settings_label_new_tracks_count": "Nouveaux morceaux :", + "settings_button_save": "Enregistrer", "MON": "LUN", diff --git a/fastlane/metadata/android/en-US/changelogs/49.txt b/fastlane/metadata/android/en-US/changelogs/49.txt new file mode 100644 index 0000000000000000000000000000000000000000..d54a19ed62988600ffa2848fcb6632647d2c7110 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/49.txt @@ -0,0 +1 @@ +Display new discovered tracks and artists. diff --git a/fastlane/metadata/android/fr-FR/changelogs/49.txt b/fastlane/metadata/android/fr-FR/changelogs/49.txt new file mode 100644 index 0000000000000000000000000000000000000000..c5f41784ffd999298dd1912d68efa9de31db8876 --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/49.txt @@ -0,0 +1 @@ +Affiche les nouveaux morceaux et artistes découverts. diff --git a/lib/config/default_settings.dart b/lib/config/default_settings.dart index e56460587678ed53759c14cc68e3a8ebd899e3b3..525784f01d4966e604ee67e5446e81fe5c778472 100644 --- a/lib/config/default_settings.dart +++ b/lib/config/default_settings.dart @@ -1,5 +1,5 @@ class DefaultSettings { - static const List<int> allowedValues = [ + static const List<int> allowedDaysCountValues = [ 7, 14, 21, @@ -7,9 +7,17 @@ class DefaultSettings { 60, 90, ]; + static const List<int> allowedCountValues = [ + 5, + 10, + 20, + ]; + static const int defaultDiscoveriesDaysCount = 14; static const int defaultDistributionDaysCount = 21; static const int defaultStatisticsRecentDaysCount = 21; static const int defaultTimelineDaysCount = 14; static const int defaultTopArtistsDaysCount = 14; + static const int defaultNewArtistsCount = 5; + static const int defaultNewTracksCount = 5; } diff --git a/lib/cubit/data_new_artists_cubit.dart b/lib/cubit/data_new_artists_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..6ef782b5dfa4b68124f4627951df2b82d6feae44 --- /dev/null +++ b/lib/cubit/data_new_artists_cubit.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:scrobbles/models/new_artists.dart'; + +part 'data_new_artists_state.dart'; + +class DataNewArtistsCubit extends HydratedCubit<DataNewArtistsState> { + DataNewArtistsCubit() : super(const DataNewArtistsState()); + + void getData(DataNewArtistsState state) { + emit(state); + } + + NewArtistsData? getValue() { + return state.newArtists; + } + + void update(NewArtistsData? newArtists) { + if ((newArtists != null) && (state.newArtists.toString() != newArtists.toString())) { + setValue(newArtists); + } + } + + void setValue(NewArtistsData? newArtists) { + emit(DataNewArtistsState( + newArtists: newArtists, + )); + } + + @override + DataNewArtistsState? fromJson(Map<String, dynamic> json) { + return DataNewArtistsState( + newArtists: NewArtistsData.fromJson(json['newArtists']), + ); + } + + @override + Map<String, Object?>? toJson(DataNewArtistsState state) { + return <String, Object?>{ + 'newArtists': state.newArtists?.toJson(), + }; + } +} diff --git a/lib/cubit/data_new_artists_state.dart b/lib/cubit/data_new_artists_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..eb6678c624e66563b534b788f959532d545ecd00 --- /dev/null +++ b/lib/cubit/data_new_artists_state.dart @@ -0,0 +1,15 @@ +part of 'data_new_artists_cubit.dart'; + +@immutable +class DataNewArtistsState extends Equatable { + const DataNewArtistsState({ + this.newArtists, + }); + + final NewArtistsData? newArtists; + + @override + List<Object?> get props => <Object?>[ + newArtists, + ]; +} diff --git a/lib/cubit/data_new_tracks_cubit.dart b/lib/cubit/data_new_tracks_cubit.dart new file mode 100644 index 0000000000000000000000000000000000000000..10923886b3f7ad976cf67a353e9855326c42bf6c --- /dev/null +++ b/lib/cubit/data_new_tracks_cubit.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:hydrated_bloc/hydrated_bloc.dart'; + +import 'package:scrobbles/models/new_tracks.dart'; + +part 'data_new_tracks_state.dart'; + +class DataNewTracksCubit extends HydratedCubit<DataNewTracksState> { + DataNewTracksCubit() : super(const DataNewTracksState()); + + void getData(DataNewTracksState state) { + emit(state); + } + + NewTracksData? getValue() { + return state.newTracks; + } + + void update(NewTracksData? newTracks) { + if ((newTracks != null) && (state.newTracks.toString() != newTracks.toString())) { + setValue(newTracks); + } + } + + void setValue(NewTracksData? newTracks) { + emit(DataNewTracksState( + newTracks: newTracks, + )); + } + + @override + DataNewTracksState? fromJson(Map<String, dynamic> json) { + return DataNewTracksState( + newTracks: NewTracksData.fromJson(json['newTracks']), + ); + } + + @override + Map<String, Object?>? toJson(DataNewTracksState state) { + return <String, Object?>{ + 'newTracks': state.newTracks?.toJson(), + }; + } +} diff --git a/lib/cubit/data_new_tracks_state.dart b/lib/cubit/data_new_tracks_state.dart new file mode 100644 index 0000000000000000000000000000000000000000..c23abe47f1c136b5a300957f6a2abdef76fa6b3b --- /dev/null +++ b/lib/cubit/data_new_tracks_state.dart @@ -0,0 +1,15 @@ +part of 'data_new_tracks_cubit.dart'; + +@immutable +class DataNewTracksState extends Equatable { + const DataNewTracksState({ + this.newTracks, + }); + + final NewTracksData? newTracks; + + @override + List<Object?> get props => <Object?>[ + newTracks, + ]; +} diff --git a/lib/cubit/settings_cubit.dart b/lib/cubit/settings_cubit.dart index 5b4374aa2b8ea5ec3450d8409b7d2ee2eb9b0fec..6c86444f3d23ddd6bf3e12a466b36f496dcc26aa 100644 --- a/lib/cubit/settings_cubit.dart +++ b/lib/cubit/settings_cubit.dart @@ -37,6 +37,14 @@ class SettingsCubit extends HydratedCubit<SettingsState> { return state.topArtistsDaysCount ?? DefaultSettings.defaultTopArtistsDaysCount; } + int getNewArtistsCount() { + return state.newArtistsCount ?? DefaultSettings.defaultNewArtistsCount; + } + + int getNewTracksCount() { + return state.newTracksCount ?? DefaultSettings.defaultNewTracksCount; + } + void setValues({ String? username, String? securityToken, @@ -45,6 +53,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> { int? statisticsRecentDaysCount, int? timelineDaysCount, int? topArtistsDaysCount, + int? newArtistsCount, + int? newTracksCount, }) { emit(SettingsState( username: username ?? state.username, @@ -54,6 +64,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> { statisticsRecentDaysCount: statisticsRecentDaysCount ?? state.statisticsRecentDaysCount, timelineDaysCount: timelineDaysCount ?? state.timelineDaysCount, topArtistsDaysCount: topArtistsDaysCount ?? state.topArtistsDaysCount, + newArtistsCount: newArtistsCount ?? state.newArtistsCount, + newTracksCount: newTracksCount ?? state.newTracksCount, )); } @@ -66,6 +78,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> { int statisticsRecentDaysCount = json['statisticsRecentDaysCount'] as int; int timelineDaysCount = json['timelineDaysCount'] as int; int topArtistsDaysCount = json['topArtistsDaysCount'] as int; + int newArtistsCount = json['newArtistsCount'] as int; + int newTracksCount = json['newTracksCount'] as int; return SettingsState( username: username, @@ -75,6 +89,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> { statisticsRecentDaysCount: statisticsRecentDaysCount, timelineDaysCount: timelineDaysCount, topArtistsDaysCount: topArtistsDaysCount, + newArtistsCount: newArtistsCount, + newTracksCount: newTracksCount, ); } @@ -92,6 +108,8 @@ class SettingsCubit extends HydratedCubit<SettingsState> { 'timelineDaysCount': state.timelineDaysCount ?? DefaultSettings.defaultTimelineDaysCount, 'topArtistsDaysCount': state.topArtistsDaysCount ?? DefaultSettings.defaultTopArtistsDaysCount, + 'newArtistsCount': state.newArtistsCount ?? DefaultSettings.defaultNewArtistsCount, + 'newTracksCount': state.newTracksCount ?? DefaultSettings.defaultNewTracksCount, }; } } diff --git a/lib/cubit/settings_state.dart b/lib/cubit/settings_state.dart index 46d965ebf1aa732b6bc6dff235c0451a8f95ba00..45ef6bb4eb11e57efbc584c77f3a311fe9090a7e 100644 --- a/lib/cubit/settings_state.dart +++ b/lib/cubit/settings_state.dart @@ -10,6 +10,8 @@ class SettingsState extends Equatable { this.statisticsRecentDaysCount, this.timelineDaysCount, this.topArtistsDaysCount, + this.newArtistsCount, + this.newTracksCount, }); final String? username; @@ -19,6 +21,8 @@ class SettingsState extends Equatable { final int? statisticsRecentDaysCount; final int? timelineDaysCount; final int? topArtistsDaysCount; + final int? newArtistsCount; + final int? newTracksCount; @override List<dynamic> get props => <dynamic>[ @@ -29,6 +33,8 @@ class SettingsState extends Equatable { statisticsRecentDaysCount, timelineDaysCount, topArtistsDaysCount, + newArtistsCount, + newTracksCount, ]; Map<String, dynamic> get values => <String, dynamic>{ @@ -39,5 +45,7 @@ class SettingsState extends Equatable { 'statisticsRecentDaysCount': statisticsRecentDaysCount, 'timelineDaysCount': timelineDaysCount, 'topArtistsDaysCount': topArtistsDaysCount, + 'newArtistsCount': newArtistsCount, + 'newTracksCount': newTracksCount, }; } diff --git a/lib/main.dart b/lib/main.dart index d5123a18c168b1c89afbbea128aa1a11e5924935..6d990ff5759cfe2e6942e40f2ea6045ef2d49e0f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,6 +13,8 @@ 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_heatmap_cubit.dart'; +import 'package:scrobbles/cubit/data_new_artists_cubit.dart'; +import 'package:scrobbles/cubit/data_new_tracks_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'; @@ -57,6 +59,8 @@ class MyApp extends StatelessWidget { BlocProvider<DataCountsByHourCubit>(create: (context) => DataCountsByHourCubit()), BlocProvider<DataDiscoveriesCubit>(create: (context) => DataDiscoveriesCubit()), BlocProvider<DataHeatmapCubit>(create: (context) => DataHeatmapCubit()), + BlocProvider<DataNewArtistsCubit>(create: (context) => DataNewArtistsCubit()), + BlocProvider<DataNewTracksCubit>(create: (context) => DataNewTracksCubit()), BlocProvider<DataStatisticsGlobalCubit>( create: (context) => DataStatisticsGlobalCubit()), BlocProvider<DataStatisticsRecentCubit>( diff --git a/lib/models/artists.dart b/lib/models/artists.dart new file mode 100644 index 0000000000000000000000000000000000000000..e9eaf5e698f862823478523ffd36ec28f9964dca --- /dev/null +++ b/lib/models/artists.dart @@ -0,0 +1,33 @@ +import 'dart:convert'; + +class Artist { + final int id; + final String name; + final String mbid; + + const Artist({ + required this.id, + required this.name, + required this.mbid, + }); + + factory Artist.fromJson(Map<String, dynamic> json) { + return Artist( + id: json['id'] as int, + name: json['name'] as String, + mbid: (json['mbid'] != null) ? (json['mbid'] as String) : '', + ); + } + + Map<String, Object?>? toJson() { + return { + 'id': this.id, + 'name': this.name, + 'mbid': this.mbid, + }; + } + + String toString() { + return jsonEncode(this.toJson()); + } +} diff --git a/lib/models/new_artists.dart b/lib/models/new_artists.dart new file mode 100644 index 0000000000000000000000000000000000000000..d0c4b6a69fab06b76f570cb9f711066147421fde --- /dev/null +++ b/lib/models/new_artists.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; + +import 'package:scrobbles/models/artists.dart'; + +class NewArtistData { + final DateTime? firstPlayed; + final Artist? artist; + + const NewArtistData({ + required this.firstPlayed, + required this.artist, + }); + + factory NewArtistData.fromJson(Map<String, dynamic>? json) { + return NewArtistData( + firstPlayed: (json?['firstPlayed'] != null && json?['firstPlayed']['date'] != null) + ? DateTime.parse( + json?['firstPlayed']['date'], + ) + : null, + artist: Artist.fromJson(json?['artist']), + ); + } +} + +class NewArtistsData { + final List<NewArtistData> data; + + const NewArtistsData({ + required this.data, + }); + + factory NewArtistsData.fromJson(List<dynamic>? json) { + List<NewArtistData> list = []; + + json?.forEach((item) { + list.add(NewArtistData.fromJson(item)); + }); + + return NewArtistsData(data: list); + } + + List<Map<String, dynamic>> toJson() { + List<Map<String, dynamic>> list = []; + + this.data.forEach((item) { + list.add({ + 'firstPlayed': { + 'date': item.firstPlayed != null ? item.firstPlayed.toString() : null, + }, + 'artist': item.artist?.toJson(), + }); + }); + + return list; + } + + String toString() { + return jsonEncode(this.toJson()); + } +} diff --git a/lib/models/new_tracks.dart b/lib/models/new_tracks.dart new file mode 100644 index 0000000000000000000000000000000000000000..101c4334df229ba85c56aa03a38ca5477af49c0e --- /dev/null +++ b/lib/models/new_tracks.dart @@ -0,0 +1,61 @@ +import 'dart:convert'; + +import 'package:scrobbles/models/track.dart'; + +class NewTrackData { + final DateTime? firstPlayed; + final Track? track; + + const NewTrackData({ + required this.firstPlayed, + required this.track, + }); + + factory NewTrackData.fromJson(Map<String, dynamic>? json) { + return NewTrackData( + firstPlayed: (json?['firstPlayed'] != null && json?['firstPlayed']['date'] != null) + ? DateTime.parse( + json?['firstPlayed']['date'], + ) + : null, + track: Track.fromJson(json?['track']), + ); + } +} + +class NewTracksData { + final List<NewTrackData> data; + + const NewTracksData({ + required this.data, + }); + + factory NewTracksData.fromJson(List<dynamic>? json) { + List<NewTrackData> list = []; + + json?.forEach((item) { + list.add(NewTrackData.fromJson(item)); + }); + + return NewTracksData(data: list); + } + + List<Map<String, dynamic>> toJson() { + List<Map<String, dynamic>> list = []; + + this.data.forEach((item) { + list.add({ + 'firstPlayed': { + 'date': item.firstPlayed != null ? item.firstPlayed.toString() : null, + }, + 'track': item.track?.toJson(), + }); + }); + + return list; + } + + String toString() { + return jsonEncode(this.toJson()); + } +} diff --git a/lib/models/track.dart b/lib/models/track.dart new file mode 100644 index 0000000000000000000000000000000000000000..4630b88d7bfe0abe2ecb27205a40f12d1fddecc7 --- /dev/null +++ b/lib/models/track.dart @@ -0,0 +1,39 @@ +import 'dart:convert'; + +import 'package:scrobbles/models/artists.dart'; + +class Track { + final int id; + final String name; + final String mbid; + final Artist artist; + + const Track({ + required this.id, + required this.name, + required this.mbid, + required this.artist, + }); + + factory Track.fromJson(Map<String, dynamic> json) { + return Track( + id: json['id'] as int, + name: json['name'] as String, + mbid: (json['mbid'] != null) ? (json['mbid'] as String) : '', + artist: Artist.fromJson(json['artist']), + ); + } + + Map<String, Object?>? toJson() { + return { + 'id': this.id, + 'name': this.name, + 'mbid': this.mbid, + 'artist': this.artist.toJson(), + }; + } + + String toString() { + return jsonEncode(this.toJson()); + } +} diff --git a/lib/network/scrobbles.dart b/lib/network/scrobbles.dart index 7f5c6f96f0276d7386147cf6197b34fd29ad1f27..3e58697bf0d9651ab06cf9d8a11c57d01ad00589 100644 --- a/lib/network/scrobbles.dart +++ b/lib/network/scrobbles.dart @@ -5,6 +5,8 @@ import 'package:scrobbles/models/counts_by_day.dart'; import 'package:scrobbles/models/counts_by_hour.dart'; import 'package:scrobbles/models/discoveries.dart'; import 'package:scrobbles/models/heatmap.dart'; +import 'package:scrobbles/models/new_artists.dart'; +import 'package:scrobbles/models/new_tracks.dart'; import 'package:scrobbles/models/statistics_global.dart'; import 'package:scrobbles/models/statistics_recent.dart'; import 'package:scrobbles/models/timeline.dart'; @@ -100,4 +102,26 @@ class ScrobblesApi { throw Exception('Failed to get data from API.'); } } + + static Future<NewArtistsData> fetchNewArtists(int count) async { + final String url = baseUrl + '/data/discoveries/artists/' + count.toString(); + final response = await http.get(Uri.parse(url)); + + if (response.statusCode == 200) { + return NewArtistsData.fromJson(jsonDecode(response.body) as List<dynamic>); + } else { + throw Exception('Failed to get data from API.'); + } + } + + static Future<NewTracksData> fetchNewTracks(int count) async { + final String url = baseUrl + '/data/discoveries/tracks/' + count.toString(); + final response = await http.get(Uri.parse(url)); + + if (response.statusCode == 200) { + return NewTracksData.fromJson(jsonDecode(response.body) as List<dynamic>); + } else { + throw Exception('Failed to get data from API.'); + } + } } diff --git a/lib/ui/screens/discoveries.dart b/lib/ui/screens/discoveries.dart index 7a039c7d5327bfc62679f1929e255fd5bb825b10..c30be6637b78d07536fe9747f953f49d247644d8 100644 --- a/lib/ui/screens/discoveries.dart +++ b/lib/ui/screens/discoveries.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:scrobbles/ui/widgets/cards/discoveries.dart'; +import 'package:scrobbles/ui/widgets/cards/new_artists.dart'; +import 'package:scrobbles/ui/widgets/cards/new_tracks.dart'; class ScreenDiscoveries extends StatelessWidget { final Function() notifyParent; @@ -21,6 +23,10 @@ class ScreenDiscoveries extends StatelessWidget { children: <Widget>[ const SizedBox(height: 8), const CardDiscoveries(), + const SizedBox(height: 6), + const CardNewArtists(), + const SizedBox(height: 6), + const CardNewTracks(), const SizedBox(height: 36), ], ), diff --git a/lib/ui/widgets/cards/new_artists.dart b/lib/ui/widgets/cards/new_artists.dart new file mode 100644 index 0000000000000000000000000000000000000000..d9e898b94a66112dbc51842f983f636f32304940 --- /dev/null +++ b/lib/ui/widgets/cards/new_artists.dart @@ -0,0 +1,63 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:scrobbles/cubit/data_new_artists_cubit.dart'; +import 'package:scrobbles/cubit/settings_cubit.dart'; +import 'package:scrobbles/models/new_artists.dart'; +import 'package:scrobbles/network/scrobbles.dart'; +import 'package:scrobbles/ui/widgets/card_content.dart'; +import 'package:scrobbles/ui/widgets/error.dart'; + +class CardNewArtists extends StatelessWidget { + const CardNewArtists({super.key}); + + @override + Widget build(BuildContext context) { + SettingsCubit settings = BlocProvider.of<SettingsCubit>(context); + + final int count = settings.getNewArtistsCount(); + + return BlocBuilder<DataNewArtistsCubit, DataNewArtistsState>( + builder: (BuildContext context, DataNewArtistsState state) { + return CardContent( + color: Theme.of(context).colorScheme.surface, + title: 'new_artists_title'.tr(), + loader: update(count), + content: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: state.newArtists?.data + .map((newArtist) => Text(newArtist.artist?.name ?? '')) + .toList() ?? + [], + ), + ); + }, + ); + } + + Widget update(int count) { + final Widget loading = const Text('â³'); + final Widget done = const Text(''); + + late Future<NewArtistsData> future = ScrobblesApi.fetchNewArtists(count); + + return BlocBuilder<DataNewArtistsCubit, DataNewArtistsState>( + builder: (BuildContext context, DataNewArtistsState state) { + return FutureBuilder<NewArtistsData>( + future: future, + builder: (context, snapshot) { + if (snapshot.hasError) { + return ShowErrorWidget(message: '${snapshot.error}'); + } + + BlocProvider.of<DataNewArtistsCubit>(context).update(snapshot.data); + + return !snapshot.hasData ? loading : done; + }, + ); + }, + ); + } +} diff --git a/lib/ui/widgets/cards/new_tracks.dart b/lib/ui/widgets/cards/new_tracks.dart new file mode 100644 index 0000000000000000000000000000000000000000..d2bcc0baa35f28f3e908276621d61b9234a521eb --- /dev/null +++ b/lib/ui/widgets/cards/new_tracks.dart @@ -0,0 +1,65 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import 'package:scrobbles/cubit/data_new_tracks_cubit.dart'; +import 'package:scrobbles/cubit/settings_cubit.dart'; +import 'package:scrobbles/models/new_tracks.dart'; +import 'package:scrobbles/network/scrobbles.dart'; +import 'package:scrobbles/ui/widgets/card_content.dart'; +import 'package:scrobbles/ui/widgets/error.dart'; + +class CardNewTracks extends StatelessWidget { + const CardNewTracks({super.key}); + + @override + Widget build(BuildContext context) { + SettingsCubit settings = BlocProvider.of<SettingsCubit>(context); + + final int count = settings.getNewTracksCount(); + + return BlocBuilder<DataNewTracksCubit, DataNewTracksState>( + builder: (BuildContext context, DataNewTracksState state) { + return CardContent( + color: Theme.of(context).colorScheme.surface, + title: 'new_tracks_title'.tr(), + loader: update(count), + content: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: state.newTracks?.data + .map((newTrack) => Text((newTrack.track?.artist.name ?? '') + + ' - ' + + (newTrack.track?.name ?? ''))) + .toList() ?? + [], + ), + ); + }, + ); + } + + Widget update(int count) { + final Widget loading = const Text('â³'); + final Widget done = const Text(''); + + late Future<NewTracksData> future = ScrobblesApi.fetchNewTracks(count); + + return BlocBuilder<DataNewTracksCubit, DataNewTracksState>( + builder: (BuildContext context, DataNewTracksState state) { + return FutureBuilder<NewTracksData>( + future: future, + builder: (context, snapshot) { + if (snapshot.hasError) { + return ShowErrorWidget(message: '${snapshot.error}'); + } + + BlocProvider.of<DataNewTracksCubit>(context).update(snapshot.data); + + return !snapshot.hasData ? loading : done; + }, + ); + }, + ); + } +} diff --git a/lib/ui/widgets/settings_form.dart b/lib/ui/widgets/settings_form.dart index aad22f578a877733d717d2ece111b6ca1fe94b0b..de09c91d03229cc7082e0bc8a7d7709419d4f915 100644 --- a/lib/ui/widgets/settings_form.dart +++ b/lib/ui/widgets/settings_form.dart @@ -23,12 +23,16 @@ class _SettingsFormState extends State<SettingsForm> { int statisticsRecentDaysCount = DefaultSettings.defaultStatisticsRecentDaysCount; int timelineDaysCount = DefaultSettings.defaultTimelineDaysCount; int topArtistsDaysCount = DefaultSettings.defaultTopArtistsDaysCount; + int newArtistsCount = DefaultSettings.defaultNewArtistsCount; + int newTracksCount = DefaultSettings.defaultNewTracksCount; List<bool> _selectedDiscoveriesDaysCount = []; List<bool> _selectedDistributionDaysCount = []; List<bool> _selectedStatisticsRecentDaysCount = []; List<bool> _selectedTimelineDaysCount = []; List<bool> _selectedTopArtistsDaysCount = []; + List<bool> _selectedNewArtistsCount = []; + List<bool> _selectedNewTracksCount = []; @override void didChangeDependencies() { @@ -42,17 +46,26 @@ class _SettingsFormState extends State<SettingsForm> { statisticsRecentDaysCount = settings.getStatisticsRecentDaysCount(); timelineDaysCount = settings.getTimelineDaysCount(); topArtistsDaysCount = settings.getTopArtistsDaysCount(); + newArtistsCount = settings.getNewArtistsCount(); + newTracksCount = settings.getNewTracksCount(); - _selectedDiscoveriesDaysCount = - DefaultSettings.allowedValues.map((e) => (e == discoveriesDaysCount)).toList(); - _selectedDistributionDaysCount = - DefaultSettings.allowedValues.map((e) => (e == distributionDaysCount)).toList(); - _selectedStatisticsRecentDaysCount = - DefaultSettings.allowedValues.map((e) => (e == statisticsRecentDaysCount)).toList(); + _selectedDiscoveriesDaysCount = DefaultSettings.allowedDaysCountValues + .map((e) => (e == discoveriesDaysCount)) + .toList(); + _selectedDistributionDaysCount = DefaultSettings.allowedDaysCountValues + .map((e) => (e == distributionDaysCount)) + .toList(); + _selectedStatisticsRecentDaysCount = DefaultSettings.allowedDaysCountValues + .map((e) => (e == statisticsRecentDaysCount)) + .toList(); _selectedTimelineDaysCount = - DefaultSettings.allowedValues.map((e) => (e == timelineDaysCount)).toList(); + DefaultSettings.allowedDaysCountValues.map((e) => (e == timelineDaysCount)).toList(); _selectedTopArtistsDaysCount = - DefaultSettings.allowedValues.map((e) => (e == topArtistsDaysCount)).toList(); + DefaultSettings.allowedDaysCountValues.map((e) => (e == topArtistsDaysCount)).toList(); + _selectedNewArtistsCount = + DefaultSettings.allowedCountValues.map((e) => (e == newArtistsCount)).toList(); + _selectedNewTracksCount = + DefaultSettings.allowedCountValues.map((e) => (e == newTracksCount)).toList(); super.didChangeDependencies(); } @@ -75,6 +88,8 @@ class _SettingsFormState extends State<SettingsForm> { statisticsRecentDaysCount: statisticsRecentDaysCount, timelineDaysCount: timelineDaysCount, topArtistsDaysCount: topArtistsDaysCount, + newArtistsCount: newArtistsCount, + newTracksCount: newTracksCount, ); } @@ -118,7 +133,7 @@ class _SettingsFormState extends State<SettingsForm> { ToggleButtons( onPressed: (int index) { setState(() { - statisticsRecentDaysCount = DefaultSettings.allowedValues[index]; + statisticsRecentDaysCount = DefaultSettings.allowedDaysCountValues[index]; for (int i = 0; i < _selectedStatisticsRecentDaysCount.length; i++) { _selectedStatisticsRecentDaysCount[i] = i == index; } @@ -128,7 +143,9 @@ class _SettingsFormState extends State<SettingsForm> { borderRadius: const BorderRadius.all(Radius.circular(8)), constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0), isSelected: _selectedStatisticsRecentDaysCount, - children: DefaultSettings.allowedValues.map((e) => Text(e.toString())).toList(), + children: DefaultSettings.allowedDaysCountValues + .map((e) => Text(e.toString())) + .toList(), ), ], ), @@ -142,7 +159,7 @@ class _SettingsFormState extends State<SettingsForm> { ToggleButtons( onPressed: (int index) { setState(() { - timelineDaysCount = DefaultSettings.allowedValues[index]; + timelineDaysCount = DefaultSettings.allowedDaysCountValues[index]; for (int i = 0; i < _selectedTimelineDaysCount.length; i++) { _selectedTimelineDaysCount[i] = i == index; } @@ -152,7 +169,9 @@ class _SettingsFormState extends State<SettingsForm> { borderRadius: const BorderRadius.all(Radius.circular(8)), constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0), isSelected: _selectedTimelineDaysCount, - children: DefaultSettings.allowedValues.map((e) => Text(e.toString())).toList(), + children: DefaultSettings.allowedDaysCountValues + .map((e) => Text(e.toString())) + .toList(), ), ], ), @@ -166,7 +185,7 @@ class _SettingsFormState extends State<SettingsForm> { ToggleButtons( onPressed: (int index) { setState(() { - topArtistsDaysCount = DefaultSettings.allowedValues[index]; + topArtistsDaysCount = DefaultSettings.allowedDaysCountValues[index]; for (int i = 0; i < _selectedTopArtistsDaysCount.length; i++) { _selectedTopArtistsDaysCount[i] = i == index; } @@ -176,7 +195,9 @@ class _SettingsFormState extends State<SettingsForm> { borderRadius: const BorderRadius.all(Radius.circular(8)), constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0), isSelected: _selectedTopArtistsDaysCount, - children: DefaultSettings.allowedValues.map((e) => Text(e.toString())).toList(), + children: DefaultSettings.allowedDaysCountValues + .map((e) => Text(e.toString())) + .toList(), ), ], ), @@ -190,7 +211,7 @@ class _SettingsFormState extends State<SettingsForm> { ToggleButtons( onPressed: (int index) { setState(() { - discoveriesDaysCount = DefaultSettings.allowedValues[index]; + discoveriesDaysCount = DefaultSettings.allowedDaysCountValues[index]; for (int i = 0; i < _selectedDiscoveriesDaysCount.length; i++) { _selectedDiscoveriesDaysCount[i] = i == index; } @@ -200,7 +221,9 @@ class _SettingsFormState extends State<SettingsForm> { borderRadius: const BorderRadius.all(Radius.circular(8)), constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0), isSelected: _selectedDiscoveriesDaysCount, - children: DefaultSettings.allowedValues.map((e) => Text(e.toString())).toList(), + children: DefaultSettings.allowedDaysCountValues + .map((e) => Text(e.toString())) + .toList(), ), ], ), @@ -214,7 +237,7 @@ class _SettingsFormState extends State<SettingsForm> { ToggleButtons( onPressed: (int index) { setState(() { - distributionDaysCount = DefaultSettings.allowedValues[index]; + distributionDaysCount = DefaultSettings.allowedDaysCountValues[index]; for (int i = 0; i < _selectedDistributionDaysCount.length; i++) { _selectedDistributionDaysCount[i] = i == index; } @@ -224,7 +247,62 @@ class _SettingsFormState extends State<SettingsForm> { borderRadius: const BorderRadius.all(Radius.circular(8)), constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0), isSelected: _selectedDistributionDaysCount, - children: DefaultSettings.allowedValues.map((e) => Text(e.toString())).toList(), + children: DefaultSettings.allowedDaysCountValues + .map((e) => Text(e.toString())) + .toList(), + ), + ], + ), + + SizedBox(height: 8), + AppTitle2(text: tr('settings_title_counts')), + + // New artists count + Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('settings_label_new_artists_count').tr(), + ToggleButtons( + onPressed: (int index) { + setState(() { + newArtistsCount = DefaultSettings.allowedCountValues[index]; + for (int i = 0; i < _selectedNewArtistsCount.length; i++) { + _selectedNewArtistsCount[i] = i == index; + } + }); + saveSettings(); + }, + borderRadius: const BorderRadius.all(Radius.circular(8)), + constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0), + isSelected: _selectedNewArtistsCount, + children: + DefaultSettings.allowedCountValues.map((e) => Text(e.toString())).toList(), + ), + ], + ), + + // New tracks count + Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text('settings_label_new_tracks_count').tr(), + ToggleButtons( + onPressed: (int index) { + setState(() { + newTracksCount = DefaultSettings.allowedCountValues[index]; + for (int i = 0; i < _selectedNewTracksCount.length; i++) { + _selectedNewTracksCount[i] = i == index; + } + }); + saveSettings(); + }, + borderRadius: const BorderRadius.all(Radius.circular(8)), + constraints: const BoxConstraints(minHeight: 30.0, minWidth: 30.0), + isSelected: _selectedNewTracksCount, + children: + DefaultSettings.allowedCountValues.map((e) => Text(e.toString())).toList(), ), ], ), diff --git a/pubspec.yaml b/pubspec.yaml index 2e058f8862eb87b6a01af482cd28233cd5ec3c3d..bb7e9233bab3fc8a013aed2ed4fa7332bd6651ac 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: Display scrobbles data and charts publish_to: 'none' -version: 0.0.48+48 +version: 0.0.49+49 environment: sdk: '^3.0.0'