(
- offsets[2],
+ offsets[3],
GameDataSchema.deserialize,
allOffsets,
GameData(),
) ??
[];
object.id = id;
- object.lastUpdate = reader.readDateTime(offsets[3]);
- object.playerExtra = reader.readStringOrNull(offsets[4]);
- object.playerPackageId = reader.readStringOrNull(offsets[5]);
+ object.lastUpdate = reader.readDateTime(offsets[4]);
+ object.playerExtra = reader.readStringOrNull(offsets[5]);
+ object.playerPackageId = reader.readStringOrNull(offsets[6]);
return object;
}
@@ -148,6 +161,8 @@ P _platformDataDeserializeProp(
case 1:
return (reader.readString(offset)) as P;
case 2:
+ return (reader.readStringOrNull(offset)) as P;
+ case 3:
return (reader.readObjectList(
offset,
GameDataSchema.deserialize,
@@ -155,12 +170,12 @@ P _platformDataDeserializeProp(
GameData(),
) ??
[]) as P;
- case 3:
- return (reader.readDateTime(offset)) as P;
case 4:
- return (reader.readStringOrNull(offset)) as P;
+ return (reader.readDateTime(offset)) as P;
case 5:
return (reader.readStringOrNull(offset)) as P;
+ case 6:
+ return (reader.readStringOrNull(offset)) as P;
default:
throw IsarError('Unknown property with id $propertyId');
}
@@ -531,6 +546,160 @@ extension PlatformDataQueryFilter
});
}
+ QueryBuilder
+ folderCoverIsNull() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(const FilterCondition.isNull(
+ property: r'folderCover',
+ ));
+ });
+ }
+
+ QueryBuilder
+ folderCoverIsNotNull() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(const FilterCondition.isNotNull(
+ property: r'folderCover',
+ ));
+ });
+ }
+
+ QueryBuilder
+ folderCoverEqualTo(
+ String? value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.equalTo(
+ property: r'folderCover',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ folderCoverGreaterThan(
+ String? value, {
+ bool include = false,
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.greaterThan(
+ include: include,
+ property: r'folderCover',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ folderCoverLessThan(
+ String? value, {
+ bool include = false,
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.lessThan(
+ include: include,
+ property: r'folderCover',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ folderCoverBetween(
+ String? lower,
+ String? upper, {
+ bool includeLower = true,
+ bool includeUpper = true,
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.between(
+ property: r'folderCover',
+ lower: lower,
+ includeLower: includeLower,
+ upper: upper,
+ includeUpper: includeUpper,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ folderCoverStartsWith(
+ String value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.startsWith(
+ property: r'folderCover',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ folderCoverEndsWith(
+ String value, {
+ bool caseSensitive = true,
+ }) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.endsWith(
+ property: r'folderCover',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ folderCoverContains(String value, {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.contains(
+ property: r'folderCover',
+ value: value,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ folderCoverMatches(String pattern, {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.matches(
+ property: r'folderCover',
+ wildcard: pattern,
+ caseSensitive: caseSensitive,
+ ));
+ });
+ }
+
+ QueryBuilder
+ folderCoverIsEmpty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.equalTo(
+ property: r'folderCover',
+ value: '',
+ ));
+ });
+ }
+
+ QueryBuilder
+ folderCoverIsNotEmpty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addFilterCondition(FilterCondition.greaterThan(
+ property: r'folderCover',
+ value: '',
+ ));
+ });
+ }
+
QueryBuilder
gamesLengthEqualTo(int length) {
return QueryBuilder.apply(this, (query) {
@@ -1077,6 +1246,19 @@ extension PlatformDataQuerySortBy
});
}
+ QueryBuilder sortByFolderCover() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'folderCover', Sort.asc);
+ });
+ }
+
+ QueryBuilder
+ sortByFolderCoverDesc() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'folderCover', Sort.desc);
+ });
+ }
+
QueryBuilder sortByLastUpdate() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'lastUpdate', Sort.asc);
@@ -1144,6 +1326,19 @@ extension PlatformDataQuerySortThenBy
});
}
+ QueryBuilder thenByFolderCover() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'folderCover', Sort.asc);
+ });
+ }
+
+ QueryBuilder
+ thenByFolderCoverDesc() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addSortBy(r'folderCover', Sort.desc);
+ });
+ }
+
QueryBuilder thenById() {
return QueryBuilder.apply(this, (query) {
return query.addSortBy(r'id', Sort.asc);
@@ -1213,6 +1408,13 @@ extension PlatformDataQueryWhereDistinct
});
}
+ QueryBuilder distinctByFolderCover(
+ {bool caseSensitive = true}) {
+ return QueryBuilder.apply(this, (query) {
+ return query.addDistinctBy(r'folderCover', caseSensitive: caseSensitive);
+ });
+ }
+
QueryBuilder distinctByLastUpdate() {
return QueryBuilder.apply(this, (query) {
return query.addDistinctBy(r'lastUpdate');
@@ -1255,6 +1457,12 @@ extension PlatformDataQueryProperty
});
}
+ QueryBuilder folderCoverProperty() {
+ return QueryBuilder.apply(this, (query) {
+ return query.addPropertyName(r'folderCover');
+ });
+ }
+
QueryBuilder, QQueryOperations> gamesProperty() {
return QueryBuilder.apply(this, (query) {
return query.addPropertyName(r'games');
diff --git a/lib/app/data/repositories/uno_sync_repository.dart b/lib/app/data/repositories/uno_sync_repository.dart
index 14dbbfb..ce8c598 100644
--- a/lib/app/data/repositories/uno_sync_repository.dart
+++ b/lib/app/data/repositories/uno_sync_repository.dart
@@ -1,7 +1,11 @@
import 'dart:io';
+import 'package:collection/collection.dart';
+import 'package:flutter/cupertino.dart';
+import 'package:media_store_plus/media_store_plus.dart';
import 'package:path/path.dart';
import 'package:uno/uno.dart';
+import 'package:yuno/app/interactor/actions/platform_action.dart';
import 'package:yuno/app/interactor/models/embeds/game.dart';
import '../../core/constants/env.dart';
@@ -14,49 +18,109 @@ class UnoSyncRepository implements SyncRepository {
UnoSyncRepository({required this.uno});
@override
- Future syncIGDB(Game game) async {
- final response = await uno.post(
- 'https://api.igdb.com/v4/games',
- headers: {
- 'content-type': 'text/plain',
- 'client-id': igdbClientId,
- 'authorization': 'Bearer $igdbToken',
- },
- data:
- 'fields artworks,collection,cover.*, first_release_date,genres.*,name,summary; search "${game.name}"; limit 2;',
- );
-
- // prevent multiples calls
- await Future.delayed(const Duration(seconds: 1));
-
- if (response.data.isEmpty) {
+ Future syncLocalFolder(Game game, String coverFolder) async {
+ try {
+ final media = MediaStore();
+ final documents = await media.getDocumentTree(uriString: coverFolder);
+
+ if (documents == null) {
+ return game;
+ }
+
+ final files = documents //
+ .children
+ .where(
+ (doc) {
+ final name = doc.name?.toLowerCase();
+ if (name == null) {
+ return false;
+ }
+ return name.endsWith('.png') ||
+ name.endsWith('.jpg') ||
+ name.endsWith('.jpeg');
+ },
+ ).toList();
+
+ if (files.isEmpty) {
+ return game;
+ }
+
+ final gameName = basenameWithoutExtension(convertContentUriToFilePath(game.path));
+
+ final file = files.firstWhereOrNull((doc) {
+ final name = doc.name!;
+ return name.startsWith('${gameName}.');
+ });
+
+ if (file == null) {
+ return game;
+ }
+
+ final dirPath = await pathProvider.getApplicationDocumentsDirectory();
+ final imageName = file.name!;
+ final pathSeparator = Platform.pathSeparator;
+ final imageFile = File('${dirPath.path}${pathSeparator}local_$imageName');
+ if (imageFile.existsSync()) {
+ await imageFile.delete();
+ }
+ await media.readFileUsingUri(
+ uriString: file.uriString, tempFilePath: imageFile.path);
+ return game.copyWith(image: imageFile.path, isSynced: true);
+ } catch (e) {
+ debugPrint(e.toString());
return game;
}
+ }
- final json = response.data[0];
+ @override
+ Future syncIGDB(Game game) async {
+ try {
+ final response = await uno.post(
+ 'https://api.igdb.com/v4/games',
+ headers: {
+ 'content-type': 'text/plain',
+ 'client-id': igdbClientId,
+ 'authorization': 'Bearer $igdbToken',
+ },
+ data:
+ 'fields artworks,collection,cover.*, first_release_date,genres.*,name,summary; search "${game.name}"; limit 2;',
+ );
- var image = "https:${json['cover']['url']}";
- image = image.replaceAll('t_thumb', 't_cover_big');
+ // prevent multiples calls
+ await Future.delayed(const Duration(seconds: 1));
- final dirPath = await pathProvider.getApplicationDocumentsDirectory();
- final imageName = basename(image);
- final pathSeparator = Platform.pathSeparator;
- final imageFile = File('${dirPath.path}$pathSeparator$imageName');
+ if (response.data.isEmpty) {
+ return game;
+ }
- if (!imageFile.existsSync()) {
- final imageData =
- await uno.get(image, responseType: ResponseType.arraybuffer);
- await imageFile.writeAsBytes(imageData.data);
- }
+ final json = response.data[0];
+
+ var image = "https:${json['cover']['url']}";
+ image = image.replaceAll('t_thumb', 't_cover_big');
- final metaGame = game.copyWith(
- isSynced: true,
- description: json['summary'],
- image: imageFile.path,
- genre: json['genres'][0]['name'],
- );
+ final dirPath = await pathProvider.getApplicationDocumentsDirectory();
+ final imageName = basename(image);
+ final pathSeparator = Platform.pathSeparator;
+ final imageFile = File('${dirPath.path}${pathSeparator}igdb_$imageName');
- return metaGame;
+ if (!imageFile.existsSync()) {
+ final imageData =
+ await uno.get(image, responseType: ResponseType.arraybuffer);
+ await imageFile.writeAsBytes(imageData.data);
+ }
+
+ final metaGame = game.copyWith(
+ isSynced: true,
+ description: json['summary'],
+ image: imageFile.path,
+ genre: json['genres'][0]['name'],
+ );
+
+ return metaGame;
+ } catch (e) {
+ debugPrint(e.toString());
+ return game;
+ }
}
@override
diff --git a/lib/app/interactor/actions/config_action.dart b/lib/app/interactor/actions/config_action.dart
index 658ea25..3aaed43 100644
--- a/lib/app/interactor/actions/config_action.dart
+++ b/lib/app/interactor/actions/config_action.dart
@@ -8,6 +8,7 @@ import 'package:yuno/injector.dart';
import '../atoms/config_atom.dart';
import '../repositories/config_repository.dart';
+import 'platform_action.dart';
Future saveConfig(GameConfig config) async {
final repository = injector.get();
@@ -25,6 +26,11 @@ Future openUrl(Uri uri) async {
await repository.openUrl(uri);
}
+String beautifyPath(String dir) {
+ final path = convertContentUriToFilePath(dir);
+ return path.replaceAll('/storage/emulated/0', '');
+}
+
final _battery = Battery();
StreamSubscription? _batterySubscription;
diff --git a/lib/app/interactor/actions/platform_action.dart b/lib/app/interactor/actions/platform_action.dart
index 1802d40..17de62a 100644
--- a/lib/app/interactor/actions/platform_action.dart
+++ b/lib/app/interactor/actions/platform_action.dart
@@ -2,8 +2,10 @@
import 'dart:io';
+import 'package:shared_storage/shared_storage.dart' as shared_storage;
import 'package:flutter/material.dart';
import 'package:media_store_plus/media_store_plus.dart';
+import 'package:yuno/app/interactor/atoms/config_atom.dart';
import 'package:yuno/app/interactor/models/platform_model.dart';
import 'package:yuno/app/interactor/repositories/platform_repository.dart';
import 'package:yuno/app/interactor/repositories/sync_repository.dart';
@@ -32,6 +34,12 @@ Future createPlatform(PlatformModel platform) async {
await fetchPlatforms();
}
+Future getDirectory([String? initialFolder]) async {
+ final uri = await shared_storage.openDocumentTree(
+ initialUri: initialFolder == null ? null : Uri.parse(initialFolder));
+ return uri?.toString();
+}
+
Future> _getGames(PlatformModel platform) async {
if (platform.category.id == 'android') {
return platform.games;
@@ -40,6 +48,13 @@ Future> _getGames(PlatformModel platform) async {
final games = [];
final media = MediaStore();
+ await shared_storage.persistedUriPermissions();
+
+ final canRead = await shared_storage.canRead(Uri.parse(platform.folder));
+ if (canRead != true) {
+ await getDirectory(platform.folder);
+ }
+
final documents = await media.getDocumentTree(uriString: platform.folder);
if (documents == null) {
@@ -86,14 +101,41 @@ Future syncPlatform(PlatformModel platform) async {
final color = await getDominatingColor(platform.games[i].image);
platform.games[i] = platform.games[i].copyWith(imageColor: color);
} else {
- try {
- var metaGame = await repository.syncIGDB(platform.games[i]);
- final color = await getDominatingColor(metaGame.image);
- metaGame = metaGame.copyWith(imageColor: color);
- platform.games[i] = metaGame;
- } catch (e) {
- continue;
+ Game metaGame = platform.games[i];
+
+ final coverFolder = platform.folderCover ?? platform.folder;
+
+ var canRead = await shared_storage.canRead(Uri.parse(coverFolder));
+ if (canRead != true) {
+ await getDirectory(coverFolder);
+ }
+
+ metaGame = await repository.syncLocalFolder(
+ metaGame,
+ coverFolder,
+ );
+
+ if (gameConfigState.value.coverFolder != null && !metaGame.isSynced) {
+ canRead = await shared_storage
+ .canRead(Uri.parse(gameConfigState.value.coverFolder!));
+ if (canRead != true) {
+ await getDirectory(gameConfigState.value.coverFolder!);
+ }
+ metaGame = await repository.syncLocalFolder(
+ metaGame,
+ gameConfigState.value.coverFolder!,
+ );
}
+
+ if (gameConfigState.value.enableIGDB && !metaGame.isSynced) {
+ metaGame = await repository.syncIGDB(
+ platform.games[i],
+ );
+ }
+
+ final color = await getDominatingColor(metaGame.image);
+ metaGame = metaGame.copyWith(imageColor: color);
+ platform.games[i] = metaGame;
}
}
diff --git a/lib/app/interactor/actions/player_action.dart b/lib/app/interactor/actions/player_action.dart
index 4b1b32f..de92206 100644
--- a/lib/app/interactor/actions/player_action.dart
+++ b/lib/app/interactor/actions/player_action.dart
@@ -38,14 +38,13 @@ final _defaultAppIntent = {
arguments: {
'ROM': convertContentUriToFilePath(g.path),
'LIBRETRO':
- '/data/data/com.retroarch/cores/${p.extra}_libretro_android.so',
+ '/data/data/com.retroarch/cores/${p.extra}_libretro_android.so',
'CONFIGFILE':
- '/storage/emulated/0/Android/data/com.retroarch/files/retroarch.cfg',
+ '/storage/emulated/0/Android/data/com.retroarch/files/retroarch.cfg',
'DATADIR': '/data/data/com.retroarch',
'APK': '/data/app/com.retroarch-1/base.apk',
'SDCARD': '/storage/emulated/0',
- 'EXTERNAL':
- '/storage/emulated/0/Android/data/com.retroarch/files',
+ 'EXTERNAL': '/storage/emulated/0/Android/data/com.retroarch/files',
'IME': 'com.android.inputmethod.latin/.LatinIME',
},
);
diff --git a/lib/app/interactor/models/game_config.dart b/lib/app/interactor/models/game_config.dart
index ee29203..c4a07d9 100644
--- a/lib/app/interactor/models/game_config.dart
+++ b/lib/app/interactor/models/game_config.dart
@@ -14,28 +14,35 @@ class GameConfig {
final bool swapABXY;
final bool menuSounds;
final LanguageModel? language;
+ final bool enableIGDB;
+ final String? coverFolder;
GameConfig({
this.themeMode = ThemeMode.system,
this.swapABXY = false,
this.menuSounds = true,
this.language,
+ this.enableIGDB = true,
this.backgroundType = BackgroundType.bubble,
+ this.coverFolder,
});
- GameConfig copyWith({
- ThemeMode? themeMode,
- BackgroundType? backgroundType,
- bool? swapABXY,
- bool? menuSounds,
- LanguageModel? language,
- }) {
+ GameConfig copyWith(
+ {ThemeMode? themeMode,
+ BackgroundType? backgroundType,
+ bool? swapABXY,
+ bool? menuSounds,
+ LanguageModel? language,
+ bool? enableIGDB,
+ String? coverFolder}) {
return GameConfig(
themeMode: themeMode ?? this.themeMode,
backgroundType: backgroundType ?? this.backgroundType,
language: language ?? this.language,
swapABXY: swapABXY ?? this.swapABXY,
menuSounds: menuSounds ?? this.menuSounds,
+ enableIGDB: enableIGDB ?? this.enableIGDB,
+ coverFolder: coverFolder ?? this.coverFolder,
);
}
@@ -45,6 +52,8 @@ class GameConfig {
'backgroundType': backgroundType.name,
'swapABXY': swapABXY,
'menuSounds': menuSounds,
+ 'enableIGDB': enableIGDB,
+ if (coverFolder != null) 'coverFolder': coverFolder,
if (language != null) 'locale': language!.locale.toString(),
};
}
@@ -64,6 +73,8 @@ class GameConfig {
),
swapABXY: map['swapABXY'] as bool,
menuSounds: map['menuSounds'] as bool,
+ enableIGDB: map['enableIGDB'] ?? true,
+ coverFolder: map['coverFolder'],
);
}
diff --git a/lib/app/interactor/models/platform_model.dart b/lib/app/interactor/models/platform_model.dart
index 85cf20d..eaa5363 100644
--- a/lib/app/interactor/models/platform_model.dart
+++ b/lib/app/interactor/models/platform_model.dart
@@ -8,6 +8,7 @@ class PlatformModel {
final GameCategory category;
final Player? player;
final String folder;
+ final String? folderCover;
final List games;
final DateTime lastUpdate;
@@ -16,6 +17,7 @@ class PlatformModel {
required this.id,
required this.folder,
this.player,
+ this.folderCover,
required this.lastUpdate,
required this.category,
required this.games,
@@ -65,10 +67,12 @@ class PlatformModel {
DateTime? lastUpdate,
GameCategory? category,
String? playerArguments,
+ String? folderCover,
List? games}) {
return PlatformModel(
id: id,
category: category ?? this.category,
+ folderCover: folderCover ?? this.folderCover,
player: player ?? this.player,
games: games ?? this.games,
folder: folder ?? this.folder,
diff --git a/lib/app/interactor/repositories/sync_repository.dart b/lib/app/interactor/repositories/sync_repository.dart
index 2e96b8f..fc4b39c 100644
--- a/lib/app/interactor/repositories/sync_repository.dart
+++ b/lib/app/interactor/repositories/sync_repository.dart
@@ -2,5 +2,6 @@ import 'package:yuno/app/interactor/models/embeds/game.dart';
abstract class SyncRepository {
Future syncIGDB(Game game);
+ Future syncLocalFolder(Game game, String coverFolder);
Future syncRAWG(Game game);
}
diff --git a/pubspec.yaml b/pubspec.yaml
index 636a725..b2a29bc 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
-version: 0.0.9+17
+version: 0.1.0+20
environment:
sdk: '>=3.2.4 <4.0.0'
@@ -69,6 +69,7 @@ dependencies:
battery_plus: ^5.0.2
intl: ^0.18.1
based_battery_indicator: ^1.0.3
+ shared_storage: ^0.8.0
install_or_uninstall_app_listener:
path: plugins/install_or_uninstall_app_listener
localization: ^2.1.1