mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-05-20 07:04:49 +02:00
fix: stabilize shared extension link handling
This commit is contained in:
@@ -483,6 +483,7 @@ class ExtensionState {
|
||||
class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
AppLifecycleListener? _appLifecycleListener;
|
||||
bool _cleanupInFlight = false;
|
||||
Completer<void>? _initializationCompleter;
|
||||
|
||||
@override
|
||||
ExtensionState build() {
|
||||
@@ -520,6 +521,13 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
|
||||
Future<void> initialize(String extensionsDir, String dataDir) async {
|
||||
if (state.isInitialized) return;
|
||||
if (_initializationCompleter != null) {
|
||||
await _initializationCompleter!.future;
|
||||
return;
|
||||
}
|
||||
|
||||
final completer = Completer<void>();
|
||||
_initializationCompleter = completer;
|
||||
|
||||
state = state.copyWith(isLoading: true, error: null);
|
||||
|
||||
@@ -531,6 +539,8 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
error: null,
|
||||
);
|
||||
_log.i('Extension system disabled on this platform');
|
||||
completer.complete();
|
||||
_initializationCompleter = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -544,6 +554,32 @@ class ExtensionNotifier extends Notifier<ExtensionState> {
|
||||
} catch (e) {
|
||||
_log.e('Failed to initialize extension system: $e');
|
||||
state = state.copyWith(isLoading: false, error: e.toString());
|
||||
} finally {
|
||||
if (!completer.isCompleted) {
|
||||
completer.complete();
|
||||
}
|
||||
if (identical(_initializationCompleter, completer)) {
|
||||
_initializationCompleter = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> waitForInitialization({
|
||||
Duration timeout = const Duration(seconds: 30),
|
||||
}) async {
|
||||
if (state.isInitialized || !PlatformBridge.supportsExtensionSystem) {
|
||||
return;
|
||||
}
|
||||
|
||||
final future = _initializationCompleter?.future;
|
||||
if (future == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await future.timeout(timeout);
|
||||
} on TimeoutException {
|
||||
_log.w('Timed out waiting for extension initialization after $timeout');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
|
||||
final _log = AppLogger('TrackProvider');
|
||||
const _extensionInitRetryTimeout = Duration(seconds: 30);
|
||||
|
||||
class TrackState {
|
||||
final List<Track> tracks;
|
||||
@@ -203,13 +204,36 @@ class TrackNotifier extends Notifier<TrackState> {
|
||||
|
||||
bool _isRequestValid(int requestId) => requestId == _currentRequestId;
|
||||
|
||||
bool _usesBuiltInUrlResolver(String url) {
|
||||
final normalized = url.toLowerCase();
|
||||
return normalized.contains('deezer.com') ||
|
||||
normalized.contains('deezer.page.link') ||
|
||||
normalized.contains('qobuz.com') ||
|
||||
normalized.startsWith('qobuzapp://') ||
|
||||
normalized.contains('tidal.com');
|
||||
}
|
||||
|
||||
Future<void> fetchFromUrl(String url, {bool useDeezerFallback = true}) async {
|
||||
final requestId = ++_currentRequestId;
|
||||
|
||||
state = TrackState(isLoading: true, hasSearchText: state.hasSearchText);
|
||||
|
||||
try {
|
||||
final extensionHandler = await PlatformBridge.findURLHandler(url);
|
||||
var extensionHandler = await PlatformBridge.findURLHandler(url);
|
||||
if (extensionHandler == null && !_usesBuiltInUrlResolver(url)) {
|
||||
final extensionState = ref.read(extensionProvider);
|
||||
if (!extensionState.isInitialized && extensionState.isLoading) {
|
||||
_log.i(
|
||||
'Extension URL handlers not ready yet, waiting for initialization...',
|
||||
);
|
||||
await ref
|
||||
.read(extensionProvider.notifier)
|
||||
.waitForInitialization(timeout: _extensionInitRetryTimeout);
|
||||
if (!_isRequestValid(requestId)) return;
|
||||
extensionHandler = await PlatformBridge.findURLHandler(url);
|
||||
}
|
||||
}
|
||||
|
||||
if (extensionHandler != null) {
|
||||
_log.i('Found extension URL handler: $extensionHandler for URL: $url');
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:spotiflac_android/l10n/l10n.dart';
|
||||
import 'package:spotiflac_android/providers/download_queue_provider.dart';
|
||||
import 'package:spotiflac_android/providers/extension_provider.dart';
|
||||
import 'package:spotiflac_android/providers/settings_provider.dart';
|
||||
import 'package:spotiflac_android/providers/store_provider.dart';
|
||||
import 'package:spotiflac_android/providers/track_provider.dart';
|
||||
@@ -94,20 +93,6 @@ class _MainShellState extends ConsumerState<MainShell>
|
||||
}
|
||||
|
||||
Future<void> _handleSharedUrl(String url) async {
|
||||
// Wait for extensions to be initialized before handling URL
|
||||
final extState = ref.read(extensionProvider);
|
||||
if (!extState.isInitialized) {
|
||||
_log.d('Waiting for extensions to initialize before handling URL...');
|
||||
for (int i = 0; i < 50; i++) {
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
if (!mounted) return;
|
||||
if (ref.read(extensionProvider).isInitialized) {
|
||||
_log.d('Extensions initialized, proceeding with URL handling');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
|
||||
@@ -13,27 +13,9 @@ class ShareIntentService {
|
||||
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]*)?',
|
||||
);
|
||||
|
||||
static final RegExp _deezerUrlPattern = RegExp(
|
||||
r'https?://(www\.)?deezer\.com/(track|album|playlist|artist)/\d+(\?[^\s]*)?',
|
||||
);
|
||||
static final RegExp _deezerShortLinkPattern = RegExp(
|
||||
r'https?://deezer\.page\.link/[a-zA-Z0-9]+',
|
||||
);
|
||||
|
||||
static final RegExp _tidalUrlPattern = RegExp(
|
||||
r'https?://(listen\.)?tidal\.com/(track|album|playlist|artist)/[a-zA-Z0-9-]+(\?[^\s]*)?',
|
||||
);
|
||||
|
||||
static final RegExp _ytMusicUrlPattern = RegExp(
|
||||
r'https?://music\.youtube\.com/(watch\?v=|playlist\?list=|channel/|browse/)[a-zA-Z0-9_-]+([?&][^\s]*)?',
|
||||
);
|
||||
|
||||
static final RegExp _youtubeUrlPattern = RegExp(
|
||||
r'https?://(youtu\.be/[a-zA-Z0-9_-]+|www\.youtube\.com/watch\?v=[a-zA-Z0-9_-]+)([?&][^\s]*)?',
|
||||
static final RegExp _genericHttpUrlPattern = RegExp(
|
||||
"https?://[^\\s<>\\\"']+",
|
||||
caseSensitive: false,
|
||||
);
|
||||
|
||||
final _sharedUrlController = StreamController<String>.broadcast();
|
||||
@@ -99,24 +81,17 @@ class ShareIntentService {
|
||||
return uriMatch.group(0);
|
||||
}
|
||||
|
||||
final patterns = [
|
||||
_spotifyUrlPattern,
|
||||
_deezerUrlPattern,
|
||||
_deezerShortLinkPattern,
|
||||
_tidalUrlPattern,
|
||||
_ytMusicUrlPattern,
|
||||
_youtubeUrlPattern,
|
||||
];
|
||||
// Keep share parsing generic and let manifest-based URL handlers decide
|
||||
// which installed extension can handle the incoming link.
|
||||
for (final match in _genericHttpUrlPattern.allMatches(text)) {
|
||||
final rawUrl = match.group(0);
|
||||
if (rawUrl == null || rawUrl.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (final pattern in patterns) {
|
||||
final match = pattern.firstMatch(text);
|
||||
if (match != null) {
|
||||
final fullUrl = match.group(0)!;
|
||||
if (pattern == _ytMusicUrlPattern || pattern == _youtubeUrlPattern) {
|
||||
return fullUrl;
|
||||
}
|
||||
final queryIndex = fullUrl.indexOf('?');
|
||||
return queryIndex > 0 ? fullUrl.substring(0, queryIndex) : fullUrl;
|
||||
final sanitizedUrl = rawUrl.replaceFirst(RegExp(r'[.,;:!?)\]}]+$'), '');
|
||||
if (sanitizedUrl.isNotEmpty) {
|
||||
return sanitizedUrl;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user