Skip to content

Commit

Permalink
Move autocomplete methods to index
Browse files Browse the repository at this point in the history
  • Loading branch information
violet-dev committed Dec 30, 2024
1 parent eb4b1ea commit 91a7339
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 186 deletions.
172 changes: 0 additions & 172 deletions violet/lib/component/hitomi/hitomi.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,6 @@
// This source code is a part of Project Violet.
// Copyright (C) 2020-2024. violet-team. Licensed under the Apache-2.0 License.

import 'package:violet/algorithm/distance.dart';
import 'package:violet/component/hitomi/displayed_tag.dart';
import 'package:violet/component/hitomi/tag_translate.dart';
import 'package:violet/component/index.dart';
import 'package:violet/script/script_manager.dart';
import 'package:violet/settings/settings.dart';

Expand All @@ -28,174 +24,6 @@ class HitomiManager {
return const ImageList(urls: [], bigThumbnails: []);
}

static String normalizeTagPrefix(String pp) {
switch (pp) {
case 'tags':
return 'tag';

case 'language':
case 'languages':
return 'lang';

case 'artists':
return 'artist';

case 'groups':
return 'group';

case 'types':
return 'type';

case 'characters':
return 'character';

case 'classes':
return 'class';
}

return pp;
}

static Future<List<(DisplayedTag, int)>> queryAutoComplete(String prefix,
[bool useTranslated = false]) async {
await HentaiIndex.loadCountMapIfRequired();

prefix = prefix.toLowerCase().replaceAll('_', ' ');

if (prefix.contains(':') && prefix.split(':')[0] != 'random') {
return _queryAutoCompleteWithTagmap(prefix, useTranslated);
}

return _queryAutoCompleteFullSearch(prefix, useTranslated);
}

static List<(DisplayedTag, int)> _queryAutoCompleteWithTagmap(
String prefix, bool useTranslated) {
final groupOrig = prefix.split(':')[0];
final group = normalizeTagPrefix(groupOrig);
final name = prefix.split(':').last;

final results = <(DisplayedTag, int)>[];
if (!HentaiIndex.tagCount!.containsKey(group)) return results;

final nameCountsMap = HentaiIndex.tagCount![group] as Map<dynamic, dynamic>;
if (!useTranslated) {
results.addAll(nameCountsMap.entries
.where((e) => e.key.toString().toLowerCase().contains(name))
.map((e) => (DisplayedTag(group: group, name: e.key), e.value)));
} else {
results.addAll(TagTranslate.containsTotal(name)
.where((e) => e.group! == group && nameCountsMap.containsKey(e.name))
.map((e) => (e, nameCountsMap[e.name])));
}
results.sort((a, b) => b.$2.compareTo(a.$2));
return results;
}

static List<(DisplayedTag, int)> _queryAutoCompleteFullSearch(
String prefix, bool useTranslated) {
if (useTranslated) {
final results = TagTranslate.containsTotal(prefix)
.where((e) => HentaiIndex.tagCount![e.group].containsKey(e.name))
.map((e) => (e, HentaiIndex.tagCount![e.group][e.name] as int))
.toList();
results.sort((a, b) => b.$2.compareTo(a.$2));
return results;
}

final results = <(DisplayedTag, int)>[];

HentaiIndex.tagCount!['tag'].forEach((group, count) {
if (group.contains(':')) {
final subGroup = group.split(':');
if (subGroup[1].contains(prefix)) {
results.add((DisplayedTag(group: subGroup[0], name: group), count));
}
} else if (group.contains(prefix)) {
results.add((DisplayedTag(group: 'tag', name: group), count));
}
});

HentaiIndex.tagCount!.forEach((group, value) {
if (group != 'tag') {
value.forEach((name, count) {
if (name.toLowerCase().contains(prefix)) {
results.add((DisplayedTag(group: group, name: name), count));
}
});
}
});

results.sort((a, b) => b.$2.compareTo(a.$2));
return results;
}

static Future<List<(DisplayedTag, int)>> queryAutoCompleteFuzzy(String prefix,
[bool useTranslated = false]) async {
await HentaiIndex.loadCountMapIfRequired();

prefix = prefix.toLowerCase().replaceAll('_', ' ');

if (prefix.contains(':')) {
final groupOrig = prefix.split(':')[0];
final group = normalizeTagPrefix(groupOrig);
final name = prefix.split(':').last;

// <Tag, Similarity, Count>
final results = <(DisplayedTag, int, int)>[];
if (!HentaiIndex.tagCount!.containsKey(group)) {
return <(DisplayedTag, int)>[];
}

final nameCountsMap = HentaiIndex.tagCount![group];
if (!useTranslated) {
nameCountsMap.forEach((key, value) {
results.add((
DisplayedTag(group: group, name: key),
Distance.levenshteinDistance(
name.runes.toList(), key.runes.toList()),
value
));
});
} else {
results.addAll(TagTranslate.containsFuzzingTotal(name)
.where((e) =>
e.$1.group! == group && nameCountsMap.containsKey(e.$1.name))
.map((e) => (e.$1, e.$2, nameCountsMap[e.$1.name])));
}
results.sort((a, b) => a.$2.compareTo(b.$2));
return results.map((e) => (e.$1, e.$3)).toList();
} else {
if (!useTranslated) {
final results = <(DisplayedTag, int, int)>[];
HentaiIndex.tagCount!.forEach((group, value) {
value.forEach((name, count) {
results.add((
DisplayedTag(group: group, name: name),
Distance.levenshteinDistance(
prefix.runes.toList(), name.runes.toList()),
count
));
});
});
results.sort((a, b) => a.$2.compareTo(b.$2));
return results.map((e) => (e.$1, e.$3)).toList();
} else {
final results = TagTranslate.containsFuzzingTotal(prefix)
.where(
(e) => HentaiIndex.tagCount![e.$1.group].containsKey(e.$1.name))
.map((e) => (
e.$1,
HentaiIndex.tagCount![e.$1.group][e.$1.name] as int,
e.$2
))
.toList();
results.sort((a, b) => a.$3.compareTo(b.$3));
return results.map((e) => (e.$1, e.$2)).toList();
}
}
}

static List<String> splitTokens(String tokens) {
final result = <String>[];
final builder = StringBuffer();
Expand Down
164 changes: 164 additions & 0 deletions violet/lib/component/index.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import 'package:flutter/services.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:violet/algorithm/distance.dart';
import 'package:violet/component/hitomi/displayed_tag.dart';
import 'package:violet/component/hitomi/tag_translate.dart';
import 'package:violet/log/log.dart';
import 'package:violet/variables.dart';
Expand Down Expand Up @@ -139,6 +140,169 @@ class HentaiIndex {
return tagCount![classification][name];
}

static String normalizeTagPrefix(String pp) {
switch (pp) {
case 'tags':
return 'tag';

case 'language':
case 'languages':
return 'lang';

case 'artists':
return 'artist';

case 'groups':
return 'group';

case 'types':
return 'type';

case 'characters':
return 'character';

case 'classes':
return 'class';
}

return pp;
}

static Future<List<(DisplayedTag, int)>> queryAutoComplete(String prefix,
[bool useTranslated = false]) async {
await loadCountMapIfRequired();

prefix = prefix.toLowerCase().replaceAll('_', ' ');

if (prefix.contains(':') && prefix.split(':')[0] != 'random') {
return _queryAutoCompleteWithTagmap(prefix, useTranslated);
}

return _queryAutoCompleteFullSearch(prefix, useTranslated);
}

static List<(DisplayedTag, int)> _queryAutoCompleteWithTagmap(
String prefix, bool useTranslated) {
final groupOrig = prefix.split(':')[0];
final group = normalizeTagPrefix(groupOrig);
final name = prefix.split(':').last;

final results = <(DisplayedTag, int)>[];
if (!tagCount!.containsKey(group)) return results;

final nameCountsMap = tagCount![group] as Map<dynamic, dynamic>;
if (!useTranslated) {
results.addAll(nameCountsMap.entries
.where((e) => e.key.toString().toLowerCase().contains(name))
.map((e) => (DisplayedTag(group: group, name: e.key), e.value)));
} else {
results.addAll(TagTranslate.containsTotal(name)
.where((e) => e.group! == group && nameCountsMap.containsKey(e.name))
.map((e) => (e, nameCountsMap[e.name])));
}
results.sort((a, b) => b.$2.compareTo(a.$2));
return results;
}

static List<(DisplayedTag, int)> _queryAutoCompleteFullSearch(
String prefix, bool useTranslated) {
if (useTranslated) {
final results = TagTranslate.containsTotal(prefix)
.where((e) => tagCount![e.group].containsKey(e.name))
.map((e) => (e, tagCount![e.group][e.name] as int))
.toList();
results.sort((a, b) => b.$2.compareTo(a.$2));
return results;
}

final results = <(DisplayedTag, int)>[];

tagCount!['tag'].forEach((group, count) {
if (group.contains(':')) {
final subGroup = group.split(':');
if (subGroup[1].contains(prefix)) {
results.add((DisplayedTag(group: subGroup[0], name: group), count));
}
} else if (group.contains(prefix)) {
results.add((DisplayedTag(group: 'tag', name: group), count));
}
});

tagCount!.forEach((group, value) {
if (group != 'tag') {
value.forEach((name, count) {
if (name.toLowerCase().contains(prefix)) {
results.add((DisplayedTag(group: group, name: name), count));
}
});
}
});

results.sort((a, b) => b.$2.compareTo(a.$2));
return results;
}

static Future<List<(DisplayedTag, int)>> queryAutoCompleteFuzzy(String prefix,
[bool useTranslated = false]) async {
await loadCountMapIfRequired();

prefix = prefix.toLowerCase().replaceAll('_', ' ');

if (prefix.contains(':')) {
final groupOrig = prefix.split(':')[0];
final group = normalizeTagPrefix(groupOrig);
final name = prefix.split(':').last;

// <Tag, Similarity, Count>
final results = <(DisplayedTag, int, int)>[];
if (!tagCount!.containsKey(group)) {
return <(DisplayedTag, int)>[];
}

final nameCountsMap = tagCount![group];
if (!useTranslated) {
nameCountsMap.forEach((key, value) {
results.add((
DisplayedTag(group: group, name: key),
Distance.levenshteinDistance(
name.runes.toList(), key.runes.toList()),
value
));
});
} else {
results.addAll(TagTranslate.containsFuzzingTotal(name)
.where((e) =>
e.$1.group! == group && nameCountsMap.containsKey(e.$1.name))
.map((e) => (e.$1, e.$2, nameCountsMap[e.$1.name])));
}
results.sort((a, b) => a.$2.compareTo(b.$2));
return results.map((e) => (e.$1, e.$3)).toList();
} else {
if (!useTranslated) {
final results = <(DisplayedTag, int, int)>[];
tagCount!.forEach((group, value) {
value.forEach((name, count) {
results.add((
DisplayedTag(group: group, name: name),
Distance.levenshteinDistance(
prefix.runes.toList(), name.runes.toList()),
count
));
});
});
results.sort((a, b) => a.$2.compareTo(b.$2));
return results.map((e) => (e.$1, e.$3)).toList();
} else {
final results = TagTranslate.containsFuzzingTotal(prefix)
.where((e) => tagCount![e.$1.group].containsKey(e.$1.name))
.map((e) => (e.$1, tagCount![e.$1.group][e.$1.name] as int, e.$2))
.toList();
results.sort((a, b) => a.$3.compareTo(b.$3));
return results.map((e) => (e.$1, e.$2)).toList();
}
}
}

static List<(String, double)> _calculateSimilars(
Map<String, dynamic> map, String artist) {
var rr = map[artist];
Expand Down
5 changes: 2 additions & 3 deletions violet/lib/pages/search/search_bar_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import 'package:flutter/material.dart';
import 'package:material_design_icons_flutter/material_design_icons_flutter.dart';
import 'package:violet/algorithm/distance.dart';
import 'package:violet/component/hitomi/displayed_tag.dart';
import 'package:violet/component/hitomi/hitomi.dart';
import 'package:violet/component/index.dart';
import 'package:violet/context/modal_bottom_sheet_context.dart';
import 'package:violet/database/user/search.dart';
Expand Down Expand Up @@ -704,14 +703,14 @@ class _SearchBarPageState extends State<SearchBarPage>
}

if (!Settings.searchUseFuzzy) {
final searchResult = (await HitomiManager.queryAutoComplete(
final searchResult = (await HentaiIndex.queryAutoComplete(
token, Settings.searchUseTranslated))
.take(_searchResultMaximum)
.toList();
if (searchResult.isEmpty) _nothing = true;
result.addAll(searchResult);
} else {
final searchResult = (await HitomiManager.queryAutoCompleteFuzzy(
final searchResult = (await HentaiIndex.queryAutoCompleteFuzzy(
token, Settings.searchUseTranslated))
.take(_searchResultMaximum)
.toList();
Expand Down
Loading

0 comments on commit 91a7339

Please sign in to comment.