mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-15 15:40:20 +02:00
refactor: enable strict analysis options and fix type safety across codebase
Enable strict-casts, strict-inference, and strict-raw-types in analysis_options.yaml. Add custom_lint with riverpod_lint. Fix all resulting type warnings with explicit type parameters and safer casts. Also improves APK update checker to detect device ABIs for correct variant selection and fixes Deezer artist name parsing edge case.
This commit is contained in:
@@ -119,7 +119,7 @@ class AppStateDatabase {
|
||||
final db = await database;
|
||||
await db.transaction((txn) async {
|
||||
final batch = txn.batch();
|
||||
for (final entry in decoded.whereType<Map>()) {
|
||||
for (final entry in decoded.whereType<Map<Object?, Object?>>()) {
|
||||
final map = Map<String, dynamic>.from(entry);
|
||||
final id = map['id'] as String?;
|
||||
if (id == null || id.isEmpty) continue;
|
||||
@@ -179,7 +179,7 @@ class AppStateDatabase {
|
||||
final decoded = jsonDecode(rawRecent);
|
||||
if (decoded is List) {
|
||||
final batch = txn.batch();
|
||||
for (final entry in decoded.whereType<Map>()) {
|
||||
for (final entry in decoded.whereType<Map<Object?, Object?>>()) {
|
||||
final map = Map<String, dynamic>.from(entry);
|
||||
final type = map['type'] as String?;
|
||||
final id = map['id'] as String?;
|
||||
|
||||
@@ -124,7 +124,7 @@ class CsvImportService {
|
||||
);
|
||||
|
||||
if (i < tracks.length - 1) {
|
||||
await Future.delayed(const Duration(milliseconds: 100));
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -224,7 +224,7 @@ class HistoryDatabase {
|
||||
}
|
||||
|
||||
try {
|
||||
final List<dynamic> jsonList = jsonDecode(jsonStr);
|
||||
final jsonList = List<dynamic>.from(jsonDecode(jsonStr) as List);
|
||||
_log.i(
|
||||
'Migrating ${jsonList.length} items from SharedPreferences to SQLite',
|
||||
);
|
||||
@@ -233,7 +233,7 @@ class HistoryDatabase {
|
||||
final batch = db.batch();
|
||||
|
||||
for (final json in jsonList) {
|
||||
final map = json as Map<String, dynamic>;
|
||||
final map = Map<String, dynamic>.from(json as Map);
|
||||
batch.insert(
|
||||
'history',
|
||||
_jsonToDbRow(map),
|
||||
|
||||
@@ -155,11 +155,11 @@ class LibraryCollectionsDatabase {
|
||||
|
||||
final db = await database;
|
||||
await db.transaction((txn) async {
|
||||
for (final entry in wishlistRaw.whereType<Map>()) {
|
||||
for (final entry in wishlistRaw.whereType<Map<Object?, Object?>>()) {
|
||||
final map = Map<String, dynamic>.from(entry);
|
||||
final trackKey = map['key'] as String?;
|
||||
final track = map['track'];
|
||||
if (trackKey == null || track is! Map) continue;
|
||||
if (trackKey == null || track is! Map<Object?, Object?>) continue;
|
||||
final addedAt = (map['addedAt'] as String?) ?? nowIso;
|
||||
await txn.insert(_tableWishlist, {
|
||||
'track_key': trackKey,
|
||||
@@ -168,11 +168,11 @@ class LibraryCollectionsDatabase {
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
for (final entry in lovedRaw.whereType<Map>()) {
|
||||
for (final entry in lovedRaw.whereType<Map<Object?, Object?>>()) {
|
||||
final map = Map<String, dynamic>.from(entry);
|
||||
final trackKey = map['key'] as String?;
|
||||
final track = map['track'];
|
||||
if (trackKey == null || track is! Map) continue;
|
||||
if (trackKey == null || track is! Map<Object?, Object?>) continue;
|
||||
final addedAt = (map['addedAt'] as String?) ?? nowIso;
|
||||
await txn.insert(_tableLoved, {
|
||||
'track_key': trackKey,
|
||||
@@ -181,7 +181,8 @@ class LibraryCollectionsDatabase {
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
for (final playlistEntry in playlistsRaw.whereType<Map>()) {
|
||||
for (final playlistEntry
|
||||
in playlistsRaw.whereType<Map<Object?, Object?>>()) {
|
||||
final playlist = Map<String, dynamic>.from(playlistEntry);
|
||||
final playlistId = playlist['id'] as String?;
|
||||
if (playlistId == null || playlistId.isEmpty) continue;
|
||||
@@ -197,11 +198,12 @@ class LibraryCollectionsDatabase {
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
|
||||
final tracksRaw = (playlist['tracks'] as List?) ?? const [];
|
||||
for (final trackEntry in tracksRaw.whereType<Map>()) {
|
||||
for (final trackEntry
|
||||
in tracksRaw.whereType<Map<Object?, Object?>>()) {
|
||||
final trackMap = Map<String, dynamic>.from(trackEntry);
|
||||
final trackKey = trackMap['key'] as String?;
|
||||
final track = trackMap['track'];
|
||||
if (trackKey == null || track is! Map) continue;
|
||||
if (trackKey == null || track is! Map<Object?, Object?>) continue;
|
||||
final addedAt = (trackMap['addedAt'] as String?) ?? nowIso;
|
||||
await txn.insert(_tablePlaylistTracks, {
|
||||
'playlist_id': playlistId,
|
||||
|
||||
@@ -67,8 +67,8 @@ class PlatformBridge {
|
||||
if (response['success'] == true) {
|
||||
final service = response['service'] ?? payload.service;
|
||||
final filePath = response['file_path'] ?? '';
|
||||
final bitDepth = response['actual_bit_depth'];
|
||||
final sampleRate = response['actual_sample_rate'];
|
||||
final bitDepth = response['actual_bit_depth'] as num?;
|
||||
final sampleRate = response['actual_sample_rate'] as num?;
|
||||
final qualityStr = bitDepth != null && sampleRate != null
|
||||
? ' ($bitDepth-bit/${(sampleRate / 1000).toStringAsFixed(1)}kHz)'
|
||||
: '';
|
||||
|
||||
@@ -65,7 +65,7 @@ class ShareIntentService {
|
||||
|
||||
_mediaSubscription = ReceiveSharingIntent.instance.getMediaStream().listen(
|
||||
_handleSharedMedia,
|
||||
onError: (err) => _log.e('Error: $err'),
|
||||
onError: (Object err) => _log.e('Error: $err'),
|
||||
);
|
||||
|
||||
final initialMedia = await ReceiveSharingIntent.instance.getInitialMedia();
|
||||
|
||||
@@ -1,11 +1,26 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:spotiflac_android/constants/app_info.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
|
||||
final _log = AppLogger('UpdateChecker');
|
||||
|
||||
enum _ApkVariant { arm64, arm32, universal }
|
||||
|
||||
class _ApkAsset {
|
||||
final String name;
|
||||
final String url;
|
||||
final _ApkVariant variant;
|
||||
|
||||
const _ApkAsset({
|
||||
required this.name,
|
||||
required this.url,
|
||||
required this.variant,
|
||||
});
|
||||
}
|
||||
|
||||
class UpdateInfo {
|
||||
final String version;
|
||||
final String changelog;
|
||||
@@ -94,32 +109,15 @@ class UpdateChecker {
|
||||
DateTime.tryParse(releaseData['published_at'] as String? ?? '') ??
|
||||
DateTime.now();
|
||||
|
||||
String? arm64Url;
|
||||
String? universalUrl;
|
||||
|
||||
final assets = releaseData['assets'] as List<dynamic>? ?? [];
|
||||
for (final asset in assets) {
|
||||
final name = (asset['name'] as String? ?? '').toLowerCase();
|
||||
if (name.endsWith('.apk')) {
|
||||
final downloadUrl = asset['browser_download_url'] as String?;
|
||||
final uri = downloadUrl != null ? Uri.tryParse(downloadUrl) : null;
|
||||
if (uri == null || uri.scheme != 'https') {
|
||||
_log.w('Skipping non-HTTPS APK URL: $downloadUrl');
|
||||
continue;
|
||||
}
|
||||
if (name.contains('arm64') || name.contains('v8a')) {
|
||||
arm64Url = downloadUrl;
|
||||
} else if (name.contains('universal')) {
|
||||
universalUrl = downloadUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Only arm64 is supported; fall back to universal if available
|
||||
final apkUrl = arm64Url ?? universalUrl;
|
||||
final assets = _collectApkAssets(
|
||||
releaseData['assets'] as List<dynamic>? ?? const [],
|
||||
);
|
||||
final selectedAsset = await _selectApkForCurrentDevice(assets);
|
||||
final apkUrl = selectedAsset?.url;
|
||||
|
||||
_log.i(
|
||||
'Update available: $latestVersion (prerelease: $isPrerelease), APK URL: $apkUrl',
|
||||
'Update available: $latestVersion (prerelease: $isPrerelease), '
|
||||
'APK asset: ${selectedAsset?.name ?? 'none'}, APK URL: $apkUrl',
|
||||
);
|
||||
|
||||
return UpdateInfo(
|
||||
@@ -169,4 +167,128 @@ class UpdateChecker {
|
||||
}
|
||||
|
||||
static String get currentVersion => AppInfo.version;
|
||||
|
||||
static List<_ApkAsset> _collectApkAssets(List<dynamic> assets) {
|
||||
final apkAssets = <_ApkAsset>[];
|
||||
|
||||
for (final asset in assets.whereType<Map<Object?, Object?>>()) {
|
||||
final assetMap = Map<String, dynamic>.from(asset);
|
||||
final name = (assetMap['name'] as String? ?? '').trim();
|
||||
final normalizedName = name.toLowerCase();
|
||||
if (!normalizedName.endsWith('.apk')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final downloadUrl = assetMap['browser_download_url'] as String?;
|
||||
final uri = downloadUrl != null ? Uri.tryParse(downloadUrl) : null;
|
||||
if (uri == null || uri.scheme != 'https') {
|
||||
_log.w('Skipping non-HTTPS APK URL: $downloadUrl');
|
||||
continue;
|
||||
}
|
||||
|
||||
final variant = _apkVariantFromName(normalizedName);
|
||||
if (variant == null) {
|
||||
_log.w('Skipping APK with unknown variant: $name');
|
||||
continue;
|
||||
}
|
||||
|
||||
apkAssets.add(
|
||||
_ApkAsset(name: name, url: uri.toString(), variant: variant),
|
||||
);
|
||||
}
|
||||
|
||||
return apkAssets;
|
||||
}
|
||||
|
||||
static _ApkVariant? _apkVariantFromName(String name) {
|
||||
if (name.contains('universal')) {
|
||||
return _ApkVariant.universal;
|
||||
}
|
||||
if (name.contains('arm64') || name.contains('arm64-v8a')) {
|
||||
return _ApkVariant.arm64;
|
||||
}
|
||||
if (name.contains('arm32') ||
|
||||
name.contains('armeabi') ||
|
||||
name.contains('armv7') ||
|
||||
name.contains('v7a')) {
|
||||
return _ApkVariant.arm32;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<_ApkAsset?> _selectApkForCurrentDevice(
|
||||
List<_ApkAsset> assets,
|
||||
) async {
|
||||
if (assets.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
_ApkAsset? arm64Asset;
|
||||
_ApkAsset? arm32Asset;
|
||||
_ApkAsset? universalAsset;
|
||||
for (final asset in assets) {
|
||||
switch (asset.variant) {
|
||||
case _ApkVariant.arm64:
|
||||
arm64Asset ??= asset;
|
||||
break;
|
||||
case _ApkVariant.arm32:
|
||||
arm32Asset ??= asset;
|
||||
break;
|
||||
case _ApkVariant.universal:
|
||||
universalAsset ??= asset;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
final supportedAbis = await _getSupportedAndroidAbis();
|
||||
final hasArm64 = supportedAbis.any(_isArm64Abi);
|
||||
final hasArm32 = supportedAbis.any(_isArm32Abi);
|
||||
|
||||
if (hasArm64) {
|
||||
return arm64Asset ?? universalAsset ?? arm32Asset;
|
||||
}
|
||||
if (hasArm32) {
|
||||
return arm32Asset ?? universalAsset;
|
||||
}
|
||||
|
||||
if (universalAsset != null) {
|
||||
_log.w(
|
||||
'Could not match APK asset to supported ABIs ${supportedAbis.join(', ')}; '
|
||||
'falling back to universal APK.',
|
||||
);
|
||||
return universalAsset;
|
||||
}
|
||||
|
||||
_log.w(
|
||||
'Could not match APK asset to supported ABIs ${supportedAbis.join(', ')}; '
|
||||
'no universal APK available.',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
static Future<List<String>> _getSupportedAndroidAbis() async {
|
||||
if (!Platform.isAndroid) {
|
||||
return const [];
|
||||
}
|
||||
|
||||
try {
|
||||
final androidInfo = await DeviceInfoPlugin().androidInfo;
|
||||
final supportedAbis = androidInfo.supportedAbis
|
||||
.map((abi) => abi.toLowerCase())
|
||||
.where((abi) => abi.isNotEmpty)
|
||||
.toSet()
|
||||
.toList();
|
||||
_log.i('Detected supported Android ABIs: ${supportedAbis.join(', ')}');
|
||||
return supportedAbis;
|
||||
} catch (e) {
|
||||
_log.w('Failed to detect supported Android ABIs: $e');
|
||||
return const [];
|
||||
}
|
||||
}
|
||||
|
||||
static bool _isArm64Abi(String abi) =>
|
||||
abi.contains('arm64') || abi.contains('aarch64');
|
||||
|
||||
static bool _isArm32Abi(String abi) =>
|
||||
abi.contains('armeabi') || abi.contains('armv7') || abi.contains('arm');
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user