Compare commits

..

1 Commits

Author SHA1 Message Date
Guanciottaman
bb299ee738
Merge ff252d6b14 into 0e48b7a337 2025-09-18 21:59:54 +06:00
38 changed files with 197 additions and 469 deletions

View File

@ -1,69 +0,0 @@
import 'package:flutter/services.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/context.dart';
import 'package:url_launcher/url_launcher_string.dart';
class LinkOpenPermissionDialog extends StatelessWidget {
final String? href;
const LinkOpenPermissionDialog({super.key, this.href});
@override
Widget build(BuildContext context) {
return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 450),
child: AlertDialog(
title: Row(
spacing: 8,
children: [
const Icon(SpotubeIcons.warning),
Text(context.l10n.open_link_in_browser),
],
),
content: Text.rich(
TextSpan(
children: [
TextSpan(
text:
"${context.l10n.do_you_want_to_open_the_following_link}:\n",
),
if (href != null)
TextSpan(
text: "$href\n\n",
style: const TextStyle(color: Colors.blue),
),
TextSpan(text: context.l10n.unsafe_url_warning),
],
),
),
actions: [
Button.ghost(
onPressed: () => Navigator.of(context).pop(false),
child: Text(context.l10n.cancel),
),
Button.ghost(
onPressed: () {
if (href != null) {
Clipboard.setData(ClipboardData(text: href!));
}
Navigator.of(context).pop(false);
},
child: Text(context.l10n.copy_link),
),
Button.destructive(
onPressed: () {
if (href != null) {
launchUrlString(
href!,
mode: LaunchMode.externalApplication,
);
}
Navigator.of(context).pop(true);
},
child: Text(context.l10n.open),
),
],
),
);
}
}

View File

@ -1,7 +1,9 @@
import 'package:cached_network_image/cached_network_image.dart'; import 'package:cached_network_image/cached_network_image.dart';
import 'package:flutter/services.dart';
import 'package:flutter_markdown_plus/flutter_markdown_plus.dart'; import 'package:flutter_markdown_plus/flutter_markdown_plus.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:spotube/components/dialogs/link_open_permission_dialog.dart'; import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/extensions/context.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class AppMarkdown extends StatelessWidget { class AppMarkdown extends StatelessWidget {
@ -26,7 +28,61 @@ class AppMarkdown extends StatelessWidget {
final allowOpeningLink = await showDialog<bool>( final allowOpeningLink = await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) {
return LinkOpenPermissionDialog(href: href); return ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 450),
child: AlertDialog(
title: Row(
spacing: 8,
children: [
const Icon(SpotubeIcons.warning),
Text(context.l10n.open_link_in_browser),
],
),
content: Text.rich(
TextSpan(
children: [
TextSpan(
text:
"${context.l10n.do_you_want_to_open_the_following_link}:\n",
),
if (href != null)
TextSpan(
text: "$href\n\n",
style: const TextStyle(color: Colors.blue),
),
TextSpan(text: context.l10n.unsafe_url_warning),
],
),
),
actions: [
Button.ghost(
onPressed: () => Navigator.of(context).pop(false),
child: Text(context.l10n.cancel),
),
Button.ghost(
onPressed: () {
if (href != null) {
Clipboard.setData(ClipboardData(text: href));
}
Navigator.of(context).pop(false);
},
child: Text(context.l10n.copy_link),
),
Button.destructive(
onPressed: () {
if (href != null) {
launchUrlString(
href,
mode: LaunchMode.externalApplication,
);
}
Navigator.of(context).pop(true);
},
child: Text(context.l10n.open),
),
],
),
);
}, },
); );

View File

@ -461,6 +461,5 @@
"available_plugins": "Available plugins", "available_plugins": "Available plugins",
"configure_your_own_metadata_plugin": "Configure your own playlist/album/artist/feed metadata provider", "configure_your_own_metadata_plugin": "Configure your own playlist/album/artist/feed metadata provider",
"audio_scrobblers": "Audio Scrobblers", "audio_scrobblers": "Audio Scrobblers",
"scrobbling": "Scrobbling", "scrobbling": "Scrobbling"
"source": "Source: "
} }

View File

@ -2930,12 +2930,6 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Scrobbling'** /// **'Scrobbling'**
String get scrobbling; String get scrobbling;
/// No description provided for @source.
///
/// In en, this message translates to:
/// **'Source: '**
String get source;
} }
class _AppLocalizationsDelegate class _AppLocalizationsDelegate

View File

@ -1537,7 +1537,4 @@ class AppLocalizationsAr extends AppLocalizations {
@override @override
String get scrobbling => 'التتبع'; String get scrobbling => 'التتبع';
@override
String get source => 'Source: ';
} }

View File

@ -1538,7 +1538,4 @@ class AppLocalizationsBn extends AppLocalizations {
@override @override
String get scrobbling => 'স্ক্রোব্বলিং'; String get scrobbling => 'স্ক্রোব্বলিং';
@override
String get source => 'Source: ';
} }

View File

@ -1548,7 +1548,4 @@ class AppLocalizationsCa extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1538,7 +1538,4 @@ class AppLocalizationsCs extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1550,7 +1550,4 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1536,7 +1536,4 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1551,7 +1551,4 @@ class AppLocalizationsEs extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1548,7 +1548,4 @@ class AppLocalizationsEu extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1536,7 +1536,4 @@ class AppLocalizationsFa extends AppLocalizations {
@override @override
String get scrobbling => 'اسکراب‌بلینگ'; String get scrobbling => 'اسکراب‌بلینگ';
@override
String get source => 'Source: ';
} }

View File

@ -1536,7 +1536,4 @@ class AppLocalizationsFi extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1556,7 +1556,4 @@ class AppLocalizationsFr extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1542,7 +1542,4 @@ class AppLocalizationsHi extends AppLocalizations {
@override @override
String get scrobbling => 'स्क्रॉबलिंग'; String get scrobbling => 'स्क्रॉबलिंग';
@override
String get source => 'Source: ';
} }

View File

@ -1544,7 +1544,4 @@ class AppLocalizationsId extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1543,7 +1543,4 @@ class AppLocalizationsIt extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1507,7 +1507,4 @@ class AppLocalizationsJa extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1545,7 +1545,4 @@ class AppLocalizationsKa extends AppLocalizations {
@override @override
String get scrobbling => 'სქრობლინგი'; String get scrobbling => 'სქრობლინგი';
@override
String get source => 'Source: ';
} }

View File

@ -1511,7 +1511,4 @@ class AppLocalizationsKo extends AppLocalizations {
@override @override
String get scrobbling => '스크로블링'; String get scrobbling => '스크로블링';
@override
String get source => 'Source: ';
} }

View File

@ -1548,7 +1548,4 @@ class AppLocalizationsNe extends AppLocalizations {
@override @override
String get scrobbling => 'स्क्रब्बलिंग'; String get scrobbling => 'स्क्रब्बलिंग';
@override
String get source => 'Source: ';
} }

View File

@ -1542,7 +1542,4 @@ class AppLocalizationsNl extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1544,7 +1544,4 @@ class AppLocalizationsPl extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1541,7 +1541,4 @@ class AppLocalizationsPt extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1544,7 +1544,4 @@ class AppLocalizationsRu extends AppLocalizations {
@override @override
String get scrobbling => 'Скробблинг'; String get scrobbling => 'Скробблинг';
@override
String get source => 'Source: ';
} }

View File

@ -1550,7 +1550,4 @@ class AppLocalizationsTa extends AppLocalizations {
@override @override
String get scrobbling => 'ஸ்க்ரோப்ளிங்'; String get scrobbling => 'ஸ்க்ரோப்ளிங்';
@override
String get source => 'Source: ';
} }

View File

@ -1533,7 +1533,4 @@ class AppLocalizationsTh extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1551,7 +1551,4 @@ class AppLocalizationsTl extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1544,7 +1544,4 @@ class AppLocalizationsTr extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1540,7 +1540,4 @@ class AppLocalizationsUk extends AppLocalizations {
@override @override
String get scrobbling => 'Скроблінг'; String get scrobbling => 'Скроблінг';
@override
String get source => 'Source: ';
} }

View File

@ -1546,7 +1546,4 @@ class AppLocalizationsVi extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }

View File

@ -1500,9 +1500,6 @@ class AppLocalizationsZh extends AppLocalizations {
@override @override
String get scrobbling => 'Scrobbling'; String get scrobbling => 'Scrobbling';
@override
String get source => 'Source: ';
} }
/// The translations for Chinese, as used in Taiwan (`zh_TW`). /// The translations for Chinese, as used in Taiwan (`zh_TW`).

View File

@ -1,4 +1,3 @@
import 'package:flutter/gestures.dart';
import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart'; import 'package:shadcn_flutter/shadcn_flutter.dart';
@ -9,7 +8,6 @@ import 'package:spotube/extensions/context.dart';
import 'package:spotube/models/metadata/metadata.dart'; import 'package:spotube/models/metadata/metadata.dart';
import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart'; import 'package:spotube/provider/metadata_plugin/metadata_plugin_provider.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
import 'package:change_case/change_case.dart';
class MetadataPluginRepositoryItem extends HookConsumerWidget { class MetadataPluginRepositoryItem extends HookConsumerWidget {
final MetadataPluginRepository pluginRepo; final MetadataPluginRepository pluginRepo;
@ -28,180 +26,144 @@ class MetadataPluginRepositoryItem extends HookConsumerWidget {
final isInstalling = useState(false); final isInstalling = useState(false);
return Card( return Card(
child: Column( child: Basic(
mainAxisSize: MainAxisSize.min, title: Text(
crossAxisAlignment: CrossAxisAlignment.stretch, "${pluginRepo.owner == "KRTirtho" ? "" : "${pluginRepo.owner}/"}${pluginRepo.name}"),
spacing: 8, subtitle: Column(
children: [ mainAxisSize: MainAxisSize.min,
Basic( crossAxisAlignment: CrossAxisAlignment.start,
title: Text( spacing: 8,
pluginRepo.name.startsWith("spotube-plugin") children: [
? pluginRepo.name Text(pluginRepo.description),
.replaceFirst("spotube-plugin-", "") Row(
.trim() spacing: 8,
.toCapitalCase() children: [
: pluginRepo.name.toCapitalCase(), if (pluginRepo.owner == "KRTirtho") ...[
), PrimaryBadge(
subtitle: Text(pluginRepo.description), leading: Icon(SpotubeIcons.done),
trailing: Button.primary( child: Text(context.l10n.official),
enabled: !isInstalling.value,
onPressed: () async {
try {
isInstalling.value = true;
final pluginConfig = await pluginsNotifier
.downloadAndCachePlugin(pluginRepo.repoUrl);
if (!context.mounted) return;
final isOfficialPlugin = pluginRepo.owner == "KRTirtho";
final isAllowed = isOfficialPlugin
? true
: await showDialog<bool>(
context: context,
builder: (context) {
final pluginAbilities = pluginConfig.apis
.map((e) =>
context.l10n.can_access_name_api(e.name))
.join("\n\n");
return AlertDialog(
title: Text(context
.l10n.do_you_want_to_install_this_plugin),
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.third_party_plugin_warning),
const Gap(8),
FutureBuilder(
future: pluginsNotifier
.getLogoPath(pluginConfig),
builder: (context, snapshot) {
return Basic(
leading: snapshot.hasData
? Image.file(
snapshot.data!,
width: 36,
height: 36,
)
: Container(
height: 36,
width: 36,
alignment: Alignment.center,
decoration: BoxDecoration(
color: context.theme
.colorScheme.secondary,
borderRadius:
BorderRadius.circular(8),
),
child: const Icon(
SpotubeIcons.plugin),
),
title: Text(pluginConfig.name),
subtitle:
Text(pluginConfig.description),
);
},
),
const Gap(8),
AppMarkdown(
data:
"**${context.l10n.author}**: ${pluginConfig.author}\n\n"
"**${context.l10n.repository}**: [${pluginConfig.repository ?? 'N/A'}](${pluginConfig.repository})\n\n\n\n"
"${context.l10n.this_plugin_can_do_following}:\n\n"
"$pluginAbilities",
),
],
),
actions: [
Button.secondary(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text(context.l10n.decline),
),
Button.primary(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text(context.l10n.accept),
),
],
);
},
);
if (isAllowed != true) return;
await pluginsNotifier.addPlugin(pluginConfig);
} finally {
if (context.mounted) {
isInstalling.value = false;
}
}
},
leading: isInstalling.value
? SizedBox.square(
dimension: 20,
child: CircularProgressIndicator(
color: context.theme.colorScheme.primaryForeground,
),
)
: const Icon(SpotubeIcons.add),
child: Text(context.l10n.install),
),
),
if (pluginRepo.owner != "KRTirtho")
Text.rich(
TextSpan(
children: [
TextSpan(text: context.l10n.source),
TextSpan(
text: pluginRepo.repoUrl.replaceAll("https://", ""),
style: const TextStyle(
color: Colors.blue,
decoration: TextDecoration.underline,
),
recognizer: TapGestureRecognizer()
..onTap = () async {
launchUrlString(pluginRepo.repoUrl);
},
), ),
], SecondaryBadge(
), leading: host == "github.com"
style: context.theme.typography.xSmall, ? const Icon(SpotubeIcons.github)
), : null,
Wrap( child: Text(host),
spacing: 8, onPressed: () {
runSpacing: 8, launchUrlString(pluginRepo.repoUrl);
children: [ },
if (pluginRepo.owner == "KRTirtho") ),
PrimaryBadge( ] else ...[
leading: const Icon(SpotubeIcons.done), Text(context.l10n.author_name(pluginRepo.owner)),
child: Text(context.l10n.official), DestructiveBadge(
) leading: const Icon(SpotubeIcons.warning),
else ...[ child: Text(context.l10n.third_party),
Text( )
context.l10n.author_name(pluginRepo.owner), ]
style: context.theme.typography.xSmall,
),
DestructiveBadge(
leading: const Icon(SpotubeIcons.warning),
child: Text(context.l10n.third_party),
),
], ],
SecondaryBadge( ),
leading: host == "github.com" ],
? const Icon(SpotubeIcons.github) ),
: null, trailing: Button.primary(
child: Text(host), enabled: !isInstalling.value,
onPressed: () { onPressed: () async {
launchUrlString(pluginRepo.repoUrl); try {
}, isInstalling.value = true;
), final pluginConfig = await pluginsNotifier
], .downloadAndCachePlugin(pluginRepo.repoUrl);
),
], if (!context.mounted) return;
final isOfficialPlugin = pluginRepo.owner == "KRTirtho";
final isAllowed = isOfficialPlugin
? true
: await showDialog<bool>(
context: context,
builder: (context) {
final pluginAbilities = pluginConfig.apis
.map(
(e) => context.l10n.can_access_name_api(e.name))
.join("\n\n");
return AlertDialog(
title: Text(
context.l10n.do_you_want_to_install_this_plugin),
content: Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(context.l10n.third_party_plugin_warning),
const Gap(8),
FutureBuilder(
future:
pluginsNotifier.getLogoPath(pluginConfig),
builder: (context, snapshot) {
return Basic(
leading: snapshot.hasData
? Image.file(
snapshot.data!,
width: 36,
height: 36,
)
: Container(
height: 36,
width: 36,
alignment: Alignment.center,
decoration: BoxDecoration(
color: context
.theme.colorScheme.secondary,
borderRadius:
BorderRadius.circular(8),
),
child:
const Icon(SpotubeIcons.plugin),
),
title: Text(pluginConfig.name),
subtitle: Text(pluginConfig.description),
);
},
),
const Gap(8),
AppMarkdown(
data:
"**${context.l10n.author}**: ${pluginConfig.author}\n\n"
"**${context.l10n.repository}**: [${pluginConfig.repository ?? 'N/A'}](${pluginConfig.repository})\n\n\n\n"
"${context.l10n.this_plugin_can_do_following}:\n\n"
"$pluginAbilities",
),
],
),
actions: [
Button.secondary(
onPressed: () {
Navigator.of(context).pop(false);
},
child: Text(context.l10n.decline),
),
Button.primary(
onPressed: () {
Navigator.of(context).pop(true);
},
child: Text(context.l10n.accept),
),
],
);
},
);
if (isAllowed != true) return;
await pluginsNotifier.addPlugin(pluginConfig);
} finally {
if (context.mounted) {
isInstalling.value = false;
}
}
},
leading: isInstalling.value
? const CircularProgressIndicator()
: const Icon(SpotubeIcons.add),
child: Text(context.l10n.install),
),
), ),
); );
} }

View File

@ -350,8 +350,6 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
abilities: plugin.abilities.map((e) => e.name).toList(), abilities: plugin.abilities.map((e) => e.name).toList(),
pluginApiVersion: Value(plugin.pluginApiVersion), pluginApiVersion: Value(plugin.pluginApiVersion),
repository: Value(plugin.repository), repository: Value(plugin.repository),
// Setting the very first plugin as the default plugin
selected: Value(state.valueOrNull?.plugins.isEmpty ?? true),
), ),
); );
} }
@ -364,17 +362,6 @@ class MetadataPluginNotifier extends AsyncNotifier<MetadataPluginState> {
} }
await database.metadataPluginsTable.deleteWhere((tbl) => await database.metadataPluginsTable.deleteWhere((tbl) =>
tbl.name.equals(plugin.name) & tbl.author.equals(plugin.author)); tbl.name.equals(plugin.name) & tbl.author.equals(plugin.author));
// Same here, if the removed plugin is the default plugin
// set the first available plugin as the default plugin
// only when there is 1 remaining plugin
if (state.valueOrNull?.defaultPluginConfig == plugin) {
final remainingPlugins =
state.valueOrNull?.plugins.where((p) => p != plugin) ?? [];
if (remainingPlugins.length == 1) {
await setDefaultPlugin(remainingPlugins.first);
}
}
} }
Future<void> updatePlugin( Future<void> updatePlugin(

View File

@ -315,7 +315,7 @@ packages:
source: hosted source: hosted
version: "1.3.1" version: "1.3.1"
change_case: change_case:
dependency: "direct main" dependency: transitive
description: description:
name: change_case name: change_case
sha256: f4e08feaa845e75e4f5ad2b0e15f24813d7ea6c27e7b78252f0c17f752cf1157 sha256: f4e08feaa845e75e4f5ad2b0e15f24813d7ea6c27e7b78252f0c17f752cf1157

View File

@ -163,7 +163,6 @@ dependencies:
get_it: ^8.0.3 get_it: ^8.0.3
flutter_markdown_plus: ^1.0.3 flutter_markdown_plus: ^1.0.3
pub_semver: ^2.2.0 pub_semver: ^2.2.0
change_case: ^1.1.0
dev_dependencies: dev_dependencies:
build_runner: ^2.4.13 build_runner: ^2.4.13

View File

@ -1,118 +1,5 @@
{ {
"ar": [
"source"
],
"bn": [
"source"
],
"ca": [
"source"
],
"cs": [
"source"
],
"de": [
"source"
],
"es": [
"source"
],
"eu": [
"source"
],
"fa": [
"source"
],
"fi": [
"source"
],
"fr": [
"source"
],
"hi": [
"source"
],
"id": [
"source"
],
"it": [
"source"
],
"ja": [
"source"
],
"ka": [
"source"
],
"ko": [
"source"
],
"ne": [
"source"
],
"nl": [ "nl": [
"audio_source", "audio_source"
"source"
],
"pl": [
"source"
],
"pt": [
"source"
],
"ru": [
"source"
],
"ta": [
"source"
],
"th": [
"source"
],
"tl": [
"source"
],
"tr": [
"source"
],
"uk": [
"source"
],
"vi": [
"source"
],
"zh": [
"source"
],
"zh_TW": [
"source"
] ]
} }