Skip to content

Commit

Permalink
feat: refactor appstream code
Browse files Browse the repository at this point in the history
This is a first pass to refactor and simplify the appstream code. Some
of the main changes:

- Removed _late_ class variables by using a factory to compute values
- Extracted AppstreamComponent localization logic to extension methods
- Removed some dead code and comments
- Enable prefer single quotes lint
  • Loading branch information
Tim Holmes-Mitra committed Sep 8, 2023
1 parent ca54b38 commit 8da843a
Show file tree
Hide file tree
Showing 4 changed files with 109 additions and 96 deletions.
1 change: 1 addition & 0 deletions analysis_options.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@ linter:
rules:
- directives_ordering
- prefer_relative_imports
- prefer_single_quotes
- type_annotate_public_apis
- unawaited_futures
97 changes: 53 additions & 44 deletions lib/src/appstream/appstream_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,55 +7,61 @@ import 'package:snowball_stemmer/snowball_stemmer.dart';
import '/l10n.dart';
import 'appstream_utils.dart';

extension _Tokenizer on String {
List<String> tokenize() => split(RegExp('\\W'));
}

class _CachedComponent {
final AppstreamComponent component;
final String id;
late String name;
late List<String> keywords;
late List<String> summary;
late List<String> description;
late String origin;
late String package;
late List<String> mediaTypes;

_CachedComponent(this.component) : id = component.id.toLowerCase() {
name = _getLocalizedComponentAttribute(component.name)?.toLowerCase() ?? '';
keywords = _getLocalizedComponentAttribute(component.keywords)
?.map((e) => e.toLowerCase())
.toList() ??
[];
summary = _getLocalizedComponentAttribute(component.summary)
?.toLowerCase()
.tokenize() ??
[];
description = _getLocalizedComponentAttribute(component.description)
?.toLowerCase()
.tokenize() ??
[];
origin = ''; // XXX: https://github.com/canonical/appstream.dart/issues/25
package = component.package?.toLowerCase() ?? '';
mediaTypes = [];
for (final provider in component.provides) {
if (provider is AppstreamProvidesMediatype) {
mediaTypes.add(provider.mediaType.toLowerCase());
}
}
}

static T? _getLocalizedComponentAttribute<T>(Map<String, T> attribute) {
final languageKey = bestLanguageKey(attribute.keys);
if (languageKey == null) return null;
return attribute[languageKey];
final String name;
final List<String> keywords;
final List<String> summary;
final List<String> description;
final String origin;
final String package;
final List<String> mediaTypes;

_CachedComponent(
this.component,
this.id,
this.name,
this.keywords,
this.summary,
this.description,
this.origin,
this.package,
this.mediaTypes);

factory _CachedComponent.fromAppstream(AppstreamComponent component) {
final id = component.getId();
final name = component.getLocalizedName();
const origin = '';
final package = component.getPackage();
final mediaTypes = component.getLocalizedMediaTypes();

final keywords =
component.getLocalizedKeywords().map((e) => e.toLowerCase()).toList();

final nonWordCharacters = RegExp('\\W');

final summary = component
.getLocalizedSummary()
.toLowerCase()
.split(nonWordCharacters)
.toList();

final description = component
.getLocalizedDescription()
.toLowerCase()
.split(nonWordCharacters)
.toList();

return _CachedComponent(component, id, name, keywords, summary, description,
origin, package, mediaTypes);
}

int match(List<String> tokens) {
int score = _MatchScore.none.value;

for (final token in tokens) {
if (id.toLowerCase().contains(token)) {
if (id.contains(token)) {
score |= _MatchScore.id.value;
}
if (name.contains(token)) {
Expand Down Expand Up @@ -105,12 +111,14 @@ enum _MatchScore {
all(1 << 0 | 1 << 1 | 1 << 2 | 1 << 3 | 1 << 4 | 1 << 5 | 1 << 6 | 1 << 7);

final int value;

const _MatchScore(this.value);
}

class _ScoredComponent {
final int score;
final AppstreamComponent component;

const _ScoredComponent(this.score, this.component);
}

Expand All @@ -128,17 +136,18 @@ class AppstreamService {
}

final HashSet<_CachedComponent> _cache = HashSet<_CachedComponent>();

@visibleForTesting
int get cacheSize => _cache.length;

void _populateCache() {
_cache.clear();
for (final component in _pool.components) {
_cache.add(_CachedComponent(component));
_cache.add(_CachedComponent.fromAppstream(component));
}
}

List<String> get _greylist =>
List<String> get _greyList =>
lookupAppLocalizations(PlatformDispatcher.instance.locale)
.appstreamSearchGreylist
.split(';');
Expand Down Expand Up @@ -181,7 +190,7 @@ class AppstreamService {
List<String> _buildSearchTokens(String search) {
final words = search.toLowerCase().split(RegExp(r'\s'));
// Filter out too generic search terms
words.removeWhere((element) => _greylist.contains(element));
words.removeWhere((element) => _greyList.contains(element));
if (words.isEmpty) {
words.addAll(search.toLowerCase().split(RegExp(r'\s')));
}
Expand Down
105 changes: 54 additions & 51 deletions lib/src/appstream/appstream_utils.dart
Original file line number Diff line number Diff line change
@@ -1,66 +1,69 @@
import 'dart:io';
import 'dart:ui';

import 'package:appstream/appstream.dart';
import 'package:collection/collection.dart';

// TODO: uncomment once we re-add packagekit
// import 'package:packagekit/packagekit.dart';
// import 'package:ubuntu_service/ubuntu_service.dart';

String? bestLanguageKey(Iterable<String> keys, {Locale? locale}) {
locale ??= PlatformDispatcher.instance.locale;
if (locale.toLanguageTag() != 'und') {
var key = '${locale.languageCode}_${locale.countryCode}';
if (keys.contains(key)) return key;
key = locale.languageCode;
if (keys.contains(key)) return key;

extension _GetOrDefault<K, V> on Map<K, V> {
V getOrDefault(K? key, V fallback) {
if (key == null) {
return fallback;
}

return this[key] ?? fallback;
}
const fallback = 'C';
if (keys.contains(fallback)) return fallback;
return null;
}

extension LocalizedComponent on AppstreamComponent {
String localizedName({Locale? locale}) =>
name[bestLanguageKey(name.keys, locale: locale)] ?? '';
String localizedSummary({Locale? locale}) =>
summary[bestLanguageKey(summary.keys, locale: locale)] ?? '';
String localizedDescription({Locale? locale}) =>
description[bestLanguageKey(description.keys, locale: locale)] ?? '';
}
String getId() => id.toLowerCase();
String getPackage() => package?.toLowerCase() ?? '';

String getLocalizedName() {
final key = bestLanguageKey(name);
return name.getOrDefault(key, '');
}

List<String> getLocalizedKeywords() {
final key = bestLanguageKey(keywords);
return keywords.getOrDefault(key, []);
}

String getLocalizedSummary() {
final key = bestLanguageKey(summary);
return summary.getOrDefault(key, '');
}

String getLocalizedDescription() {
final key = bestLanguageKey(description);
return description.getOrDefault(key, '');
}

// TODO: uncomment once we re-add packagekit
// extension PackageKitId on AppstreamComponent {
// Future<PackageKitPackageId> get packageKitId =>
// getService<PackageService>().resolve(package ?? id);
// }

int _sizeComparison(int? a, int? b) => a?.compareTo(b ?? 0) ?? 0;

extension Icons on AppstreamComponent {
String? get icon {
final cached = icons.whereType<AppstreamCachedIcon>().toList();
cached.sort((a, b) => _sizeComparison(b.width, a.width));
//for (final icon in cached) {
// XXX: we need the origin to determine if a cached icon exists on disk
// (https://github.com/canonical/appstream.dart/issues/25)
//}

final local = icons.whereType<AppstreamLocalIcon>().toList();
local.sort((a, b) => _sizeComparison(b.width, a.width));
for (final icon in local) {
if (File(icon.filename).existsSync()) {
return icon.filename;
List<String> getLocalizedMediaTypes() {
final List<String> mediaTypes = [];

for (final provider in provides) {
if (provider is AppstreamProvidesMediatype) {
mediaTypes.add(provider.mediaType.toLowerCase());
}
}

final stock = icons.whereType<AppstreamStockIcon>().firstOrNull;
if (stock != null) {
// TODO: check whether the stock icon exists on disk, and return it
return mediaTypes;
}

String? bestLanguageKey<T>(Map<String, T> keyedByLanguage) {
final locale = PlatformDispatcher.instance.locale;

if (locale.toLanguageTag() == 'und') return null;

final countryCode = locale.countryCode;
final languageCode = locale.languageCode;
final fullLocale = '${languageCode}_$countryCode';
const fallback = 'C';
final candidates = [fullLocale, languageCode, fallback];
final keys = keyedByLanguage.keys;

for (final String candidate in candidates) {
if (keys.contains(candidate)) return candidate;
}

final remote = icons.whereType<AppstreamRemoteIcon>().firstOrNull;
return remote?.url;
return null;
}
}
2 changes: 1 addition & 1 deletion lib/src/detail/detail_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ class _ChannelDropdown extends StatelessWidget {
),
itemStyle: MenuItemButton.styleFrom(),
child: Text(
"${model.selectedChannel} ${model.availableChannels![model.selectedChannel]!.version}",
'${model.selectedChannel} ${model.availableChannels![model.selectedChannel]!.version}',
),
),
),
Expand Down

0 comments on commit 8da843a

Please sign in to comment.