Skip to content

Commit

Permalink
feat: integrate ratings service (#1371)
Browse files Browse the repository at this point in the history
* feat: get ratings from new ratings service
* test: fix all failing tests
* test: ratings tests on details_page
* refactor: removing ratingsListModelProvider

Removed the ratingsListModelProvider, opting to use ratingsModelProvider
instead and nesting the calls deeper in the widget tree.

* style: formatting and small code tweaks
* fix: relative imports
* feat: remove loading spinner from snap card
* chore: update mocks

---------

Co-authored-by: Dennis Loose <[email protected]>
  • Loading branch information
2 people authored and Tim Holmes-Mitra committed Sep 17, 2023
1 parent 6c5e11b commit 56d8668
Show file tree
Hide file tree
Showing 16 changed files with 868 additions and 389 deletions.
5 changes: 5 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:yaru_widgets/yaru_widgets.dart';

import 'appstream.dart';
import 'l10n.dart';
import 'ratings.dart';
import 'snapd.dart';
import 'store.dart';

Expand All @@ -25,6 +26,10 @@ Future<void> main(List<String> args) async {
await launcher.connect();
registerServiceInstance(launcher);

// TODO: Dev/prod url's, determine on .env var
final ratings = RatingsService('localhost', 8080);
registerServiceInstance(ratings);

registerService(() => GitHub());
registerService(() => GtkApplicationNotifier(args));

Expand Down
4 changes: 4 additions & 0 deletions lib/ratings.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export 'src/ratings/exports.dart';
export 'src/ratings/ratings_l10n.dart';
export 'src/ratings/ratings_model.dart';
export 'src/ratings/ratings_service.dart';
74 changes: 46 additions & 28 deletions lib/src/detail/detail_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,12 @@ import 'package:intl/intl.dart';
import 'package:snapd/snapd.dart';
import 'package:ubuntu_widgets/ubuntu_widgets.dart';
import 'package:url_launcher/url_launcher_string.dart';
import 'package:yaru/yaru.dart';
import 'package:yaru_icons/yaru_icons.dart';
import 'package:yaru_widgets/yaru_widgets.dart';

import '/l10n.dart';
import '/layout.dart';
import '/ratings.dart';
import '/snapd.dart';
import '/widgets.dart';

Expand Down Expand Up @@ -55,13 +55,6 @@ class _SnapView extends ConsumerWidget {
final l10n = AppLocalizations.of(context);

final snapInfos = <SnapInfo>[
(
label: '123 Ratings',
value: Text(
'Positive',
style: TextStyle(color: Theme.of(context).colorScheme.success),
)
), // Placeholder
(
label: l10n.detailPageConfinementLabel,
value: Row(
Expand All @@ -86,7 +79,7 @@ class _SnapView extends ConsumerWidget {
snapModel.channelInfo != null
? context.formatByteSize(snapModel.channelInfo!.size)
: '',
)
),
),
(
label: l10n.detailPagePublishedLabel,
Expand Down Expand Up @@ -139,7 +132,11 @@ class _SnapView extends ConsumerWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_SnapInfos(snapInfos: snapInfos, layout: layout),
_SnapInfos(
snapInfos: snapInfos,
snapId: snapModel.snap.id,
layout: layout,
),
const Divider(),
if (snapModel.hasGallery)
_Section(
Expand Down Expand Up @@ -172,36 +169,57 @@ class _SnapView extends ConsumerWidget {
}
}

class _SnapInfos extends StatelessWidget {
class _SnapInfos extends ConsumerWidget {
const _SnapInfos({
required this.snapInfos,
required this.snapId,
required this.layout,
});

final List<SnapInfo> snapInfos;
final ResponsiveLayout layout;
final String snapId;

@override
Widget build(BuildContext context) {
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final ratingsModel = ref.watch(ratingsModelProvider(snapId));

final ratings = ratingsModel.state.whenOrNull(
data: (_) => (
label: l10n.snapRatingsVotes(ratingsModel.snapRating?.totalVotes ?? 0),
value: Text(
ratingsModel.snapRating?.ratingsBand.localize(l10n) ?? '',
style: TextStyle(
color: ratingsModel.snapRating?.ratingsBand.getColor(context)),
),
),
);

return Wrap(
spacing: kPagePadding,
runSpacing: 8,
children: snapInfos
.map((info) => SizedBox(
width: (layout.totalWidth -
(layout.snapInfoColumnCount - 1) * kPagePadding) /
layout.snapInfoColumnCount,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(info.label),
DefaultTextStyle.merge(
style: const TextStyle(fontWeight: FontWeight.w500),
child: info.value,
),
],
),
))
children: [
if (ratings != null) ratings,
...snapInfos,
]
.map(
(info) => SizedBox(
width: (layout.totalWidth -
(layout.snapInfoColumnCount - 1) * kPagePadding) /
layout.snapInfoColumnCount,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(info.label),
DefaultTextStyle.merge(
style: const TextStyle(fontWeight: FontWeight.w500),
child: info.value,
),
],
),
),
)
.toList(),
);
}
Expand Down
18 changes: 17 additions & 1 deletion lib/src/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -149,5 +149,21 @@
"snapSortOrderInstalledSizeDesc": "Size (Largest to smallest)",
"snapSortOrderInstalledDateAsc": "Least recently updated",
"snapSortOrderInstalledDateDesc": "Most recently updated",
"snapSortOrderRelevance": "Relevance"
"snapSortOrderRelevance": "Relevance",
"snapRatingsBandVeryGood": "Very Good",
"snapRatingsBandGood": "Good",
"snapRatingsBandNeutral": "Neutral",
"snapRatingsBandPoor": "Poor",
"snapRatingsBandVeryPoor": "Very Poor",
"snapRatingsBandInsufficientVotes": "Insufficient Votes",
"snapRatingsVotes": "{n} votes",
"@snapRatingsVotes": {
"placeholders": {
"n": {
"type": "int"
}
}
}


}
2 changes: 2 additions & 0 deletions lib/src/ratings/exports.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export 'package:app_center_ratings_client/src/app.dart';
export 'package:app_center_ratings_client/src/user.dart';
28 changes: 28 additions & 0 deletions lib/src/ratings/ratings_l10n.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:flutter/material.dart';

import '/l10n.dart';
import 'exports.dart';

extension RatingsBandL10n on RatingsBand {
String localize(AppLocalizations l10n) {
return switch (this) {
RatingsBand.veryGood => l10n.snapRatingsBandVeryGood,
RatingsBand.good => l10n.snapRatingsBandGood,
RatingsBand.neutral => l10n.snapRatingsBandNeutral,
RatingsBand.poor => l10n.snapRatingsBandPoor,
RatingsBand.veryPoor => l10n.snapRatingsBandVeryPoor,
RatingsBand.insufficientVotes => l10n.snapRatingsBandInsufficientVotes,
};
}

Color getColor(BuildContext context) {
return switch (this) {
RatingsBand.veryGood => const Color(0xFF0E8420),
RatingsBand.good => const Color(0xFF0E8420),
RatingsBand.neutral => const Color(0xFFC75A00),
RatingsBand.poor => const Color(0xFFC7162B),
RatingsBand.veryPoor => const Color(0xFFC7162B),
RatingsBand.insufficientVotes => Colors.grey,
};
}
}
40 changes: 40 additions & 0 deletions lib/src/ratings/ratings_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:ubuntu_service/ubuntu_service.dart';
import 'exports.dart';

import 'ratings_service.dart';

final ratingsModelProvider =
ChangeNotifierProvider.family.autoDispose<RatingsModel, String>(
(ref, snapId) => RatingsModel(
ratings: getService<RatingsService>(),
snapId: snapId,
)..init(),
);

class RatingsModel extends ChangeNotifier {
RatingsModel({
required this.ratings,
required this.snapId,
}) : _state = const AsyncValue.loading();
final RatingsService ratings;
final String snapId;

Rating? snapRating;

AsyncValue<void> get state => _state;
AsyncValue<void> _state;

Future<void> init() async {
_state = await AsyncValue.guard(() async {
final rating = await ratings.getRating(snapId);
_setSnapRating(rating);
notifyListeners();
});
}

void _setSnapRating(Rating? rating) {
snapRating = rating;
}
}
52 changes: 52 additions & 0 deletions lib/src/ratings/ratings_service.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import 'dart:convert';
import 'dart:io';

import 'package:app_center_ratings_client/ratings_client.dart';
import 'package:crypto/crypto.dart';
import 'package:flutter/material.dart';
import 'package:glib/glib.dart';
import 'package:jwt_decode/jwt_decode.dart';
import 'exports.dart';

class RatingsService {
RatingsService(String url, int port,
[@visibleForTesting RatingsClient? client])
: _client = client ?? RatingsClient(url, port),
_id = _generateId();

final RatingsClient _client;
String? _jwt;
final String _id;

static String _generateId() {
final username = glib.getUserName();
final machineId = File('/etc/machine-id').readAsStringSync().trim();
return sha256.convert(utf8.encode('[$username:$machineId]')).toString();
}

Future<void> _ensureValidToken() async {
if (_jwt == null || Jwt.isExpired(_jwt!)) {
_jwt = await _client.authenticate(_id);
}
}

Future<Rating?> getRating(String snapId) async {
await _ensureValidToken();
return _client.getRating(snapId, _jwt!);
}

Future<void> vote(Vote vote) async {
await _ensureValidToken();
await _client.vote(vote.snapId, vote.snapRevision, vote.voteUp, _jwt!);
}

Future<void> delete() async {
await _ensureValidToken();
await _client.delete(_jwt!);
}

Future<List<Vote>> listMyVotes(String snapFilter) async {
await _ensureValidToken();
return await _client.listMyVotes(snapFilter, _jwt!);
}
}
50 changes: 37 additions & 13 deletions lib/src/widgets/snap_card.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:snapd/snapd.dart';
import 'package:yaru/yaru.dart';
import 'package:yaru_widgets/yaru_widgets.dart';

import '/l10n.dart';
import '/layout.dart';
import '/ratings.dart';
import '/snapd.dart';
import '/widgets.dart';

Expand Down Expand Up @@ -103,25 +105,47 @@ class _SnapCardBody extends StatelessWidget {
),
),
const SizedBox(height: 8),
// TODO: ratings
Wrap(
_RatingsInfo(snapId: snap.id),
],
);
}
}

class _RatingsInfo extends ConsumerWidget {
const _RatingsInfo({required this.snapId});

final String snapId;

@override
Widget build(BuildContext context, WidgetRef ref) {
final ratingsModel = ref.watch(ratingsModelProvider(snapId));
final l10n = AppLocalizations.of(context);

return ratingsModel.state.when(
data: (ratingsData) {
final rating = ratingsModel.snapRating;
return Wrap(
children: [
Text(
'Positive',
rating?.ratingsBand.localize(l10n) ?? ' ',
style: Theme.of(context).textTheme.bodySmall!.copyWith(
color: Theme.of(context).colorScheme.success,
color: rating?.ratingsBand.getColor(context),
fontSize: 12,
),
),
const SizedBox(width: 2),
Text('|', style: Theme.of(context).textTheme.bodySmall),
const SizedBox(width: 2),
Text(
'200 Ratings',
style: Theme.of(context).textTheme.bodySmall,
),
if (rating?.totalVotes != null) ...[
const SizedBox(width: 2),
Text(
' | ${l10n.snapRatingsVotes(rating?.totalVotes ?? 0)}',
style: Theme.of(context).textTheme.bodySmall,
),
],
],
),
],
);
},
error: (error, stackTrace) => const SizedBox.shrink(),
loading: () => const SizedBox.shrink(),
);
}
}
7 changes: 7 additions & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ dependencies:
cached_network_image: ^3.2.3
collection: ^1.17.0
dbus: ^0.7.8
app_center_ratings_client:
git:
url: https://github.com/matthew-hagemann/app_center_ratings_client.dart.git
ref: register-auth-merge
crypto: ^3.0.3
file: ^6.1.0
flutter:
sdk: flutter
Expand All @@ -23,9 +28,11 @@ dependencies:
flutter_markdown: ^0.6.15+1
flutter_riverpod: ^2.3.6
github: ^9.15.1
glib: ^0.0.1
gtk: ^2.1.0
handy_window: ^0.3.1
intl: ^0.18.0
jwt_decode: ^0.3.1
meta: ^1.9.1
package_info_plus: ^4.0.2
packagekit: ^0.2.6
Expand Down
Loading

0 comments on commit 56d8668

Please sign in to comment.