mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-08-18 00:47:16 +02:00
feat(settings): add extension verification browser mode preference
Let users choose whether signed-session verification opens in the external browser or in-app browser first, with automatic fallback to the other mode when launch fails.
This commit is contained in:
+12
-10
@@ -43,14 +43,15 @@ class AppSettings {
|
|||||||
final String singleFilenameFormat;
|
final String singleFilenameFormat;
|
||||||
final String albumFolderStructure;
|
final String albumFolderStructure;
|
||||||
final bool showExtensionStore;
|
final bool showExtensionStore;
|
||||||
|
final String
|
||||||
|
extensionVerificationBrowserMode; // 'external_first' or 'in_app_first'
|
||||||
final String locale;
|
final String locale;
|
||||||
final String lyricsMode;
|
final String lyricsMode;
|
||||||
final String
|
final String
|
||||||
tidalHighFormat; // Legacy key for 320kbps lossy output format: 'mp3_320', 'aac_320', 'opus_256', or 'opus_128'
|
tidalHighFormat; // Legacy key for 320kbps lossy output format: 'mp3_320', 'aac_320', 'opus_256', or 'opus_128'
|
||||||
final bool
|
final bool
|
||||||
useAllFilesAccess; // Android 13+ only: enable MANAGE_EXTERNAL_STORAGE
|
useAllFilesAccess; // Android 13+ only: enable MANAGE_EXTERNAL_STORAGE
|
||||||
final bool
|
final bool autoExportFailedDownloads;
|
||||||
autoExportFailedDownloads;
|
|
||||||
final String
|
final String
|
||||||
downloadNetworkMode; // 'any' = WiFi + Mobile, 'wifi_only' = WiFi only
|
downloadNetworkMode; // 'any' = WiFi + Mobile, 'wifi_only' = WiFi only
|
||||||
final bool
|
final bool
|
||||||
@@ -66,16 +67,13 @@ class AppSettings {
|
|||||||
final String localLibraryPath;
|
final String localLibraryPath;
|
||||||
final String
|
final String
|
||||||
localLibraryBookmark; // Base64-encoded iOS security-scoped bookmark
|
localLibraryBookmark; // Base64-encoded iOS security-scoped bookmark
|
||||||
final bool
|
final bool localLibraryShowDuplicates;
|
||||||
localLibraryShowDuplicates;
|
|
||||||
final String
|
final String
|
||||||
localLibraryAutoScan; // Auto-scan mode: 'off', 'on_open', 'daily', 'weekly'
|
localLibraryAutoScan; // Auto-scan mode: 'off', 'on_open', 'daily', 'weekly'
|
||||||
|
|
||||||
final bool
|
final bool hasCompletedTutorial;
|
||||||
hasCompletedTutorial;
|
|
||||||
|
|
||||||
final List<String>
|
final List<String> lyricsProviders;
|
||||||
lyricsProviders;
|
|
||||||
final bool
|
final bool
|
||||||
lyricsIncludeTranslationNetease; // Append translated lyrics (Netease)
|
lyricsIncludeTranslationNetease; // Append translated lyrics (Netease)
|
||||||
final bool
|
final bool
|
||||||
@@ -90,8 +88,7 @@ class AppSettings {
|
|||||||
final String
|
final String
|
||||||
lastSeenVersion; // Last app version the user has acknowledged (e.g. '3.7.0')
|
lastSeenVersion; // Last app version the user has acknowledged (e.g. '3.7.0')
|
||||||
|
|
||||||
final bool
|
final bool deduplicateDownloads;
|
||||||
deduplicateDownloads;
|
|
||||||
final bool saveDownloadHistory;
|
final bool saveDownloadHistory;
|
||||||
|
|
||||||
final String playerMode;
|
final String playerMode;
|
||||||
@@ -132,6 +129,7 @@ class AppSettings {
|
|||||||
this.singleFilenameFormat = '{title} - {artist}',
|
this.singleFilenameFormat = '{title} - {artist}',
|
||||||
this.albumFolderStructure = 'artist_album',
|
this.albumFolderStructure = 'artist_album',
|
||||||
this.showExtensionStore = true,
|
this.showExtensionStore = true,
|
||||||
|
this.extensionVerificationBrowserMode = 'external_first',
|
||||||
this.locale = 'system',
|
this.locale = 'system',
|
||||||
this.lyricsMode = 'embed',
|
this.lyricsMode = 'embed',
|
||||||
this.tidalHighFormat = 'mp3_320',
|
this.tidalHighFormat = 'mp3_320',
|
||||||
@@ -199,6 +197,7 @@ class AppSettings {
|
|||||||
String? singleFilenameFormat,
|
String? singleFilenameFormat,
|
||||||
String? albumFolderStructure,
|
String? albumFolderStructure,
|
||||||
bool? showExtensionStore,
|
bool? showExtensionStore,
|
||||||
|
String? extensionVerificationBrowserMode,
|
||||||
String? locale,
|
String? locale,
|
||||||
String? lyricsMode,
|
String? lyricsMode,
|
||||||
String? tidalHighFormat,
|
String? tidalHighFormat,
|
||||||
@@ -274,6 +273,9 @@ class AppSettings {
|
|||||||
singleFilenameFormat: singleFilenameFormat ?? this.singleFilenameFormat,
|
singleFilenameFormat: singleFilenameFormat ?? this.singleFilenameFormat,
|
||||||
albumFolderStructure: albumFolderStructure ?? this.albumFolderStructure,
|
albumFolderStructure: albumFolderStructure ?? this.albumFolderStructure,
|
||||||
showExtensionStore: showExtensionStore ?? this.showExtensionStore,
|
showExtensionStore: showExtensionStore ?? this.showExtensionStore,
|
||||||
|
extensionVerificationBrowserMode:
|
||||||
|
extensionVerificationBrowserMode ??
|
||||||
|
this.extensionVerificationBrowserMode,
|
||||||
locale: locale ?? this.locale,
|
locale: locale ?? this.locale,
|
||||||
lyricsMode: lyricsMode ?? this.lyricsMode,
|
lyricsMode: lyricsMode ?? this.lyricsMode,
|
||||||
tidalHighFormat: tidalHighFormat ?? this.tidalHighFormat,
|
tidalHighFormat: tidalHighFormat ?? this.tidalHighFormat,
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ AppSettings _$AppSettingsFromJson(Map<String, dynamic> json) => AppSettings(
|
|||||||
albumFolderStructure:
|
albumFolderStructure:
|
||||||
json['albumFolderStructure'] as String? ?? 'artist_album',
|
json['albumFolderStructure'] as String? ?? 'artist_album',
|
||||||
showExtensionStore: json['showExtensionStore'] as bool? ?? true,
|
showExtensionStore: json['showExtensionStore'] as bool? ?? true,
|
||||||
|
extensionVerificationBrowserMode:
|
||||||
|
json['extensionVerificationBrowserMode'] as String? ?? 'external_first',
|
||||||
locale: json['locale'] as String? ?? 'system',
|
locale: json['locale'] as String? ?? 'system',
|
||||||
lyricsMode: json['lyricsMode'] as String? ?? 'embed',
|
lyricsMode: json['lyricsMode'] as String? ?? 'embed',
|
||||||
tidalHighFormat: json['tidalHighFormat'] as String? ?? 'mp3_320',
|
tidalHighFormat: json['tidalHighFormat'] as String? ?? 'mp3_320',
|
||||||
@@ -125,6 +127,7 @@ Map<String, dynamic> _$AppSettingsToJson(
|
|||||||
'singleFilenameFormat': instance.singleFilenameFormat,
|
'singleFilenameFormat': instance.singleFilenameFormat,
|
||||||
'albumFolderStructure': instance.albumFolderStructure,
|
'albumFolderStructure': instance.albumFolderStructure,
|
||||||
'showExtensionStore': instance.showExtensionStore,
|
'showExtensionStore': instance.showExtensionStore,
|
||||||
|
'extensionVerificationBrowserMode': instance.extensionVerificationBrowserMode,
|
||||||
'locale': instance.locale,
|
'locale': instance.locale,
|
||||||
'lyricsMode': instance.lyricsMode,
|
'lyricsMode': instance.lyricsMode,
|
||||||
'tidalHighFormat': instance.tidalHighFormat,
|
'tidalHighFormat': instance.tidalHighFormat,
|
||||||
|
|||||||
@@ -2098,6 +2098,7 @@ class DownloadQueueNotifier extends Notifier<DownloadQueueState> {
|
|||||||
|
|
||||||
final opened = await openPendingExtensionVerification(
|
final opened = await openPendingExtensionVerification(
|
||||||
normalizedExtensionId,
|
normalizedExtensionId,
|
||||||
|
browserMode: ref.read(settingsProvider).extensionVerificationBrowserMode,
|
||||||
);
|
);
|
||||||
if (!opened) return false;
|
if (!opened) return false;
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
|||||||
'album',
|
'album',
|
||||||
'playlist',
|
'playlist',
|
||||||
};
|
};
|
||||||
|
static const Set<String> _extensionVerificationBrowserModeValues = {
|
||||||
|
'external_first',
|
||||||
|
'in_app_first',
|
||||||
|
};
|
||||||
|
|
||||||
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
|
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
|
||||||
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
|
final FlutterSecureStorage _secureStorage = const FlutterSecureStorage();
|
||||||
@@ -79,6 +83,10 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
|||||||
defaultSearchTab: sanitizedDefaultSearchTab,
|
defaultSearchTab: sanitizedDefaultSearchTab,
|
||||||
defaultService: loaded.defaultService,
|
defaultService: loaded.defaultService,
|
||||||
searchProvider: loaded.searchProvider,
|
searchProvider: loaded.searchProvider,
|
||||||
|
extensionVerificationBrowserMode:
|
||||||
|
_normalizeExtensionVerificationBrowserMode(
|
||||||
|
loaded.extensionVerificationBrowserMode,
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
await _runMigrations(prefs);
|
await _runMigrations(prefs);
|
||||||
@@ -270,6 +278,14 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
|||||||
return 'all';
|
return 'all';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _normalizeExtensionVerificationBrowserMode(String value) {
|
||||||
|
final normalized = value.trim().toLowerCase();
|
||||||
|
if (_extensionVerificationBrowserModeValues.contains(normalized)) {
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
return 'external_first';
|
||||||
|
}
|
||||||
|
|
||||||
String? _sanitizeRetiredBuiltInProviderId(String? providerId) {
|
String? _sanitizeRetiredBuiltInProviderId(String? providerId) {
|
||||||
final normalized = providerId?.trim().toLowerCase();
|
final normalized = providerId?.trim().toLowerCase();
|
||||||
if (normalized == null || normalized.isEmpty) return providerId;
|
if (normalized == null || normalized.isEmpty) return providerId;
|
||||||
@@ -557,6 +573,14 @@ class SettingsNotifier extends Notifier<AppSettings> {
|
|||||||
_saveSettings();
|
_saveSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void setExtensionVerificationBrowserMode(String mode) {
|
||||||
|
state = state.copyWith(
|
||||||
|
extensionVerificationBrowserMode:
|
||||||
|
_normalizeExtensionVerificationBrowserMode(mode),
|
||||||
|
);
|
||||||
|
_saveSettings();
|
||||||
|
}
|
||||||
|
|
||||||
void setLocale(String locale) {
|
void setLocale(String locale) {
|
||||||
state = state.copyWith(locale: locale);
|
state = state.copyWith(locale: locale);
|
||||||
_saveSettings();
|
_saveSettings();
|
||||||
|
|||||||
@@ -686,6 +686,9 @@ class TrackNotifier extends Notifier<TrackState> {
|
|||||||
try {
|
try {
|
||||||
final opened = await openPendingExtensionVerification(
|
final opened = await openPendingExtensionVerification(
|
||||||
normalizedExtensionId,
|
normalizedExtensionId,
|
||||||
|
browserMode: ref
|
||||||
|
.read(settingsProvider)
|
||||||
|
.extensionVerificationBrowserMode,
|
||||||
);
|
);
|
||||||
if (!opened) return false;
|
if (!opened) return false;
|
||||||
|
|
||||||
|
|||||||
@@ -75,6 +75,12 @@ class AppSettingsPage extends ConsumerWidget {
|
|||||||
.read(settingsProvider.notifier)
|
.read(settingsProvider.notifier)
|
||||||
.setShowExtensionStore(v),
|
.setShowExtensionStore(v),
|
||||||
),
|
),
|
||||||
|
_VerificationBrowserModeSelector(
|
||||||
|
currentMode: settings.extensionVerificationBrowserMode,
|
||||||
|
onChanged: (mode) => ref
|
||||||
|
.read(settingsProvider.notifier)
|
||||||
|
.setExtensionVerificationBrowserMode(mode),
|
||||||
|
),
|
||||||
SettingsSwitchItem(
|
SettingsSwitchItem(
|
||||||
icon: Icons.system_update,
|
icon: Icons.system_update,
|
||||||
title: context.l10n.optionsCheckUpdates,
|
title: context.l10n.optionsCheckUpdates,
|
||||||
@@ -374,6 +380,90 @@ class _UpdateChannelSelector extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _VerificationBrowserModeSelector extends StatelessWidget {
|
||||||
|
final String currentMode;
|
||||||
|
final ValueChanged<String> onChanged;
|
||||||
|
|
||||||
|
const _VerificationBrowserModeSelector({
|
||||||
|
required this.currentMode,
|
||||||
|
required this.onChanged,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final colorScheme = Theme.of(context).colorScheme;
|
||||||
|
final normalizedMode = currentMode == 'in_app_first'
|
||||||
|
? 'in_app_first'
|
||||||
|
: 'external_first';
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
Icons.open_in_browser,
|
||||||
|
color: colorScheme.onSurfaceVariant,
|
||||||
|
size: 24,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Verification browser',
|
||||||
|
style: Theme.of(context).textTheme.bodyLarge,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 2),
|
||||||
|
Text(
|
||||||
|
normalizedMode == 'external_first'
|
||||||
|
? 'Open challenges in the default browser first'
|
||||||
|
: 'Open challenges in the in-app browser first',
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium
|
||||||
|
?.copyWith(color: colorScheme.onSurfaceVariant),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
_ChannelChip(
|
||||||
|
label: 'External',
|
||||||
|
isSelected: normalizedMode == 'external_first',
|
||||||
|
onTap: () => onChanged('external_first'),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_ChannelChip(
|
||||||
|
label: 'In-app',
|
||||||
|
isSelected: normalizedMode == 'in_app_first',
|
||||||
|
onTap: () => onChanged('in_app_first'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Divider(
|
||||||
|
height: 1,
|
||||||
|
thickness: 1,
|
||||||
|
indent: 56,
|
||||||
|
endIndent: 20,
|
||||||
|
color: colorScheme.outlineVariant.withValues(alpha: 0.3),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
class _ChannelChip extends StatelessWidget {
|
class _ChannelChip extends StatelessWidget {
|
||||||
final String label;
|
final String label;
|
||||||
final bool isSelected;
|
final bool isSelected;
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ bool _containsHttpStatusCode(String message, String code) {
|
|||||||
message.contains('$code;');
|
message.contains('$code;');
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> openPendingExtensionVerification(String extensionId) async {
|
Future<bool> openPendingExtensionVerification(
|
||||||
|
String extensionId, {
|
||||||
|
String browserMode = 'external_first',
|
||||||
|
}) async {
|
||||||
final normalizedExtensionId = extensionId.trim();
|
final normalizedExtensionId = extensionId.trim();
|
||||||
if (normalizedExtensionId.isEmpty) return false;
|
if (normalizedExtensionId.isEmpty) return false;
|
||||||
|
|
||||||
@@ -41,9 +44,17 @@ Future<bool> openPendingExtensionVerification(String extensionId) async {
|
|||||||
final uri = Uri.tryParse(authUrl);
|
final uri = Uri.tryParse(authUrl);
|
||||||
if (uri == null) return false;
|
if (uri == null) return false;
|
||||||
|
|
||||||
var launched = await launchUrl(uri, mode: LaunchMode.inAppBrowserView);
|
final preferInApp = browserMode.trim().toLowerCase() == 'in_app_first';
|
||||||
|
final firstMode = preferInApp
|
||||||
|
? LaunchMode.inAppBrowserView
|
||||||
|
: LaunchMode.externalApplication;
|
||||||
|
final fallbackMode = preferInApp
|
||||||
|
? LaunchMode.externalApplication
|
||||||
|
: LaunchMode.inAppBrowserView;
|
||||||
|
|
||||||
|
var launched = await launchUrl(uri, mode: firstMode);
|
||||||
if (!launched) {
|
if (!launched) {
|
||||||
launched = await launchUrl(uri, mode: LaunchMode.externalApplication);
|
launched = await launchUrl(uri, mode: fallbackMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (launched) {
|
if (launched) {
|
||||||
|
|||||||
Reference in New Issue
Block a user