Compare commits

...

6 Commits

Author SHA1 Message Date
Richard Hajek
af8067bd92
Merge 42e954428b into ca6924f5a9 2025-09-18 23:44:59 +06:00
Kingkor Roy Tirtho
ca6924f5a9 feat: show plugin source and set the only plugin as default if no plugin is there 2025-09-18 23:28:56 +06:00
Richard Hajek
42e954428b feat: added filtering duplicates in recent 2025-01-18 18:01:26 +01:00
Kingkor Roy Tirtho
8c1337d1fc
Merge pull request #2118 from KRTirtho/dev
chore: release 3.9.0
2024-12-09 00:04:29 +06:00
Kingkor Roy Tirtho
94e704087f Merge branch 'dev' 2024-10-09 16:38:23 +06:00
Kingkor Roy Tirtho
8e287ab1e5
Merge pull request #1981 from KRTirtho/dev
Release 3.8.3
2024-10-09 15:39:31 +06:00
39 changed files with 475 additions and 200 deletions

View File

@ -0,0 +1,69 @@
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,9 +1,7 @@
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/collections/spotube_icons.dart'; import 'package:spotube/components/dialogs/link_open_permission_dialog.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 {
@ -28,61 +26,7 @@ class AppMarkdown extends StatelessWidget {
final allowOpeningLink = await showDialog<bool>( final allowOpeningLink = await showDialog<bool>(
context: context, context: context,
builder: (context) { builder: (context) {
return ConstrainedBox( return LinkOpenPermissionDialog(href: href);
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,5 +461,6 @@
"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,6 +2930,12 @@ 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,4 +1537,7 @@ class AppLocalizationsAr extends AppLocalizations {
@override @override
String get scrobbling => 'التتبع'; String get scrobbling => 'التتبع';
@override
String get source => 'Source: ';
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1500,6 +1500,9 @@ 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

@ -20,17 +20,20 @@ class HomeRecentlyPlayedSection extends HookConsumerWidget {
return const SizedBox(); return const SizedBox();
} }
final uniqueItems = <dynamic>{};
final filteredItems = [
for (final item in historyData)
if (item.playlist != null && item.playlist?.id != null && uniqueItems.add(item.playlist!.id!))
item.playlist
else if (item.album != null && item.album?.id != null && uniqueItems.add(item.album?.id))
item.album
];
return Skeletonizer( return Skeletonizer(
enabled: history.isLoading, enabled: history.isLoading,
child: HorizontalPlaybuttonCardView( child: HorizontalPlaybuttonCardView(
title: Text(context.l10n.recently_played), title: Text(context.l10n.recently_played),
items: [ items: filteredItems,
for (final item in historyData)
if (item.playlist != null)
item.playlist
else if (item.album != null)
item.album
],
hasNextPage: false, hasNextPage: false,
isLoadingNextPage: false, isLoadingNextPage: false,
onFetchMore: () {}, onFetchMore: () {},

View File

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

View File

@ -350,6 +350,8 @@ 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),
), ),
); );
} }
@ -362,6 +364,17 @@ 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: transitive dependency: "direct main"
description: description:
name: change_case name: change_case
sha256: f4e08feaa845e75e4f5ad2b0e15f24813d7ea6c27e7b78252f0c17f752cf1157 sha256: f4e08feaa845e75e4f5ad2b0e15f24813d7ea6c27e7b78252f0c17f752cf1157

View File

@ -163,6 +163,7 @@ 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,5 +1,118 @@
{ {
"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"
] ]
} }