mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-14 07:00:19 +02:00
feat: add built-in Tidal/Qobuz search with recommended service picker
- Add SearchAll() for Tidal and Qobuz in Go backend (tracks, artists, albums) - Add searchTidalAll/searchQobuzAll platform routing for Android and iOS - Add Tidal/Qobuz options to search provider dropdown in home tab - Show (Recommended) label and auto-select service in download picker
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:spotiflac_android/services/download_request_payload.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
@@ -14,6 +15,11 @@ class PlatformBridge {
|
||||
'com.zarz.spotiflac/library_scan_progress_stream',
|
||||
);
|
||||
|
||||
static bool get supportsCoreBackend => Platform.isAndroid || Platform.isIOS;
|
||||
|
||||
static bool get supportsExtensionSystem =>
|
||||
Platform.isAndroid || Platform.isIOS;
|
||||
|
||||
static Future<Map<String, dynamic>> parseSpotifyUrl(String url) async {
|
||||
_log.d('parseSpotifyUrl: $url');
|
||||
final result = await _channel.invokeMethod('parseSpotifyUrl', {'url': url});
|
||||
@@ -503,6 +509,36 @@ class PlatformBridge {
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> searchTidalAll(
|
||||
String query, {
|
||||
int trackLimit = 15,
|
||||
int artistLimit = 2,
|
||||
String? filter,
|
||||
}) async {
|
||||
final result = await _channel.invokeMethod('searchTidalAll', {
|
||||
'query': query,
|
||||
'track_limit': trackLimit,
|
||||
'artist_limit': artistLimit,
|
||||
'filter': filter ?? '',
|
||||
});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> searchQobuzAll(
|
||||
String query, {
|
||||
int trackLimit = 15,
|
||||
int artistLimit = 2,
|
||||
String? filter,
|
||||
}) async {
|
||||
final result = await _channel.invokeMethod('searchQobuzAll', {
|
||||
'query': query,
|
||||
'track_limit': trackLimit,
|
||||
'artist_limit': artistLimit,
|
||||
'filter': filter ?? '',
|
||||
});
|
||||
return jsonDecode(result as String) as Map<String, dynamic>;
|
||||
}
|
||||
|
||||
static Future<Map<String, dynamic>> getDeezerRelatedArtists(
|
||||
String artistId, {
|
||||
int limit = 12,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
|
||||
@@ -10,8 +11,9 @@ class ShareIntentService {
|
||||
ShareIntentService._internal();
|
||||
|
||||
// Spotify patterns
|
||||
static final RegExp _spotifyUriPattern =
|
||||
RegExp(r'spotify:(track|album|playlist|artist):[a-zA-Z0-9]+');
|
||||
static final RegExp _spotifyUriPattern = RegExp(
|
||||
r'spotify:(track|album|playlist|artist):[a-zA-Z0-9]+',
|
||||
);
|
||||
static final RegExp _spotifyUrlPattern = RegExp(
|
||||
r'https?://open\.spotify\.com/(track|album|playlist|artist)/[a-zA-Z0-9]+(\?[^\s]*)?',
|
||||
);
|
||||
@@ -56,6 +58,11 @@ class ShareIntentService {
|
||||
if (_initialized) return;
|
||||
_initialized = true;
|
||||
|
||||
if (!Platform.isAndroid && !Platform.isIOS) {
|
||||
_log.i('Share intent is not supported on this platform');
|
||||
return;
|
||||
}
|
||||
|
||||
_mediaSubscription = ReceiveSharingIntent.instance.getMediaStream().listen(
|
||||
_handleSharedMedia,
|
||||
onError: (err) => _log.e('Error: $err'),
|
||||
@@ -68,14 +75,14 @@ class ShareIntentService {
|
||||
}
|
||||
}
|
||||
|
||||
void _handleSharedMedia(List<SharedMediaFile> files, {bool isInitial = false}) {
|
||||
void _handleSharedMedia(
|
||||
List<SharedMediaFile> files, {
|
||||
bool isInitial = false,
|
||||
}) {
|
||||
for (final file in files) {
|
||||
// Check both path and message - apps may share URL in either field
|
||||
final textsToCheck = [
|
||||
file.path,
|
||||
if (file.message != null) file.message!,
|
||||
];
|
||||
|
||||
final textsToCheck = [file.path, if (file.message != null) file.message!];
|
||||
|
||||
for (final textToCheck in textsToCheck) {
|
||||
final url = _extractMusicUrl(textToCheck);
|
||||
if (url != null) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
import 'package:http/http.dart' as http;
|
||||
import 'package:spotiflac_android/constants/app_info.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
@@ -24,20 +25,28 @@ class UpdateInfo {
|
||||
}
|
||||
|
||||
class UpdateChecker {
|
||||
static const String _latestApiUrl = 'https://api.github.com/repos/${AppInfo.githubRepo}/releases/latest';
|
||||
static const String _allReleasesApiUrl = 'https://api.github.com/repos/${AppInfo.githubRepo}/releases';
|
||||
static const String _latestApiUrl =
|
||||
'https://api.github.com/repos/${AppInfo.githubRepo}/releases/latest';
|
||||
static const String _allReleasesApiUrl =
|
||||
'https://api.github.com/repos/${AppInfo.githubRepo}/releases';
|
||||
|
||||
/// Check for updates based on channel preference
|
||||
/// [channel] can be 'stable' or 'preview'
|
||||
static Future<UpdateInfo?> checkForUpdate({String channel = 'stable'}) async {
|
||||
if (!Platform.isAndroid) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
Map<String, dynamic>? releaseData;
|
||||
|
||||
|
||||
if (channel == 'preview') {
|
||||
final response = await http.get(
|
||||
Uri.parse('$_allReleasesApiUrl?per_page=10'),
|
||||
headers: {'Accept': 'application/vnd.github.v3+json'},
|
||||
).timeout(const Duration(seconds: 10));
|
||||
final response = await http
|
||||
.get(
|
||||
Uri.parse('$_allReleasesApiUrl?per_page=10'),
|
||||
headers: {'Accept': 'application/vnd.github.v3+json'},
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
_log.w('GitHub API returned ${response.statusCode}');
|
||||
@@ -49,13 +58,15 @@ class UpdateChecker {
|
||||
_log.i('No releases found');
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
releaseData = releases.first as Map<String, dynamic>;
|
||||
} else {
|
||||
final response = await http.get(
|
||||
Uri.parse(_latestApiUrl),
|
||||
headers: {'Accept': 'application/vnd.github.v3+json'},
|
||||
).timeout(const Duration(seconds: 10));
|
||||
final response = await http
|
||||
.get(
|
||||
Uri.parse(_latestApiUrl),
|
||||
headers: {'Accept': 'application/vnd.github.v3+json'},
|
||||
)
|
||||
.timeout(const Duration(seconds: 10));
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
_log.w('GitHub API returned ${response.statusCode}');
|
||||
@@ -68,19 +79,24 @@ class UpdateChecker {
|
||||
final tagName = releaseData['tag_name'] as String? ?? '';
|
||||
final latestVersion = tagName.replaceFirst('v', '');
|
||||
final isPrerelease = releaseData['prerelease'] as bool? ?? false;
|
||||
|
||||
|
||||
if (!_isNewerVersion(latestVersion, AppInfo.version)) {
|
||||
_log.i('No update available (current: ${AppInfo.version}, latest: $latestVersion, channel: $channel)');
|
||||
_log.i(
|
||||
'No update available (current: ${AppInfo.version}, latest: $latestVersion, channel: $channel)',
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
final body = releaseData['body'] as String? ?? 'No changelog available';
|
||||
final htmlUrl = releaseData['html_url'] as String? ?? '${AppInfo.githubUrl}/releases';
|
||||
final publishedAt = DateTime.tryParse(releaseData['published_at'] as String? ?? '') ?? DateTime.now();
|
||||
final htmlUrl =
|
||||
releaseData['html_url'] as String? ?? '${AppInfo.githubUrl}/releases';
|
||||
final publishedAt =
|
||||
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();
|
||||
@@ -98,12 +114,14 @@ class UpdateChecker {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Only arm64 is supported; fall back to universal if available
|
||||
final apkUrl = arm64Url ?? universalUrl;
|
||||
|
||||
_log.i('Update available: $latestVersion (prerelease: $isPrerelease), APK URL: $apkUrl');
|
||||
|
||||
_log.i(
|
||||
'Update available: $latestVersion (prerelease: $isPrerelease), APK URL: $apkUrl',
|
||||
);
|
||||
|
||||
return UpdateInfo(
|
||||
version: latestVersion,
|
||||
changelog: body,
|
||||
@@ -122,7 +140,7 @@ class UpdateChecker {
|
||||
try {
|
||||
final latestBase = latest.split('-').first;
|
||||
final currentBase = current.split('-').first;
|
||||
|
||||
|
||||
final latestParts = latestBase.split('.').map(int.parse).toList();
|
||||
final currentParts = currentBase.split('.').map(int.parse).toList();
|
||||
|
||||
@@ -137,12 +155,12 @@ class UpdateChecker {
|
||||
if (latestParts[i] > currentParts[i]) return true;
|
||||
if (latestParts[i] < currentParts[i]) return false;
|
||||
}
|
||||
|
||||
|
||||
final latestHasSuffix = latest.contains('-');
|
||||
final currentHasSuffix = current.contains('-');
|
||||
|
||||
|
||||
if (!latestHasSuffix && currentHasSuffix) return true;
|
||||
|
||||
|
||||
return false;
|
||||
} catch (e) {
|
||||
_log.e('Error comparing versions: $e');
|
||||
|
||||
Reference in New Issue
Block a user