Merge branch 'master' into dev

This commit is contained in:
Kingkor Roy Tirtho 2023-04-26 12:31:33 +06:00
commit 8fe9e4d245
16 changed files with 114 additions and 321 deletions

View File

@ -1,3 +1,7 @@
POCKETBASE_URL=
USERNAME=
PASSWORD=
PASSWORD=
# The format:
# SPOTIFY_SECRETS=clintId1:clientSecret1,clientId2:clientSecret2
SPOTIFY_SECRETS=

View File

@ -61,7 +61,7 @@ jobs:
run: |
flutter config --enable-windows-desktop
flutter pub get
dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
flutter pub run build_runner build --delete-conflicting-outputs
- name: Build Windows Executable
run: |
@ -99,7 +99,7 @@ jobs:
- name: Install Dependencies
run: |
sudo apt-get update -y
sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools patchelf desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp1 libsecret-1-dev libjsoncpp-dev
sudo apt-get install -y tar clang cmake ninja-build pkg-config libgtk-3-dev make python3-pip python3-setuptools desktop-file-utils libgdk-pixbuf2.0-dev fakeroot strace fuse libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libunwind-dev locate patchelf gir1.2-appindicator3-0.1 libappindicator3-1 libappindicator3-dev libsecret-1-0 libjsoncpp1 libsecret-1-dev libjsoncpp-dev
- name: Install AppImage Tool
run: |
@ -136,7 +136,7 @@ jobs:
run: |
flutter config --enable-linux-desktop
flutter pub get
dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
flutter pub run build_runner build --delete-conflicting-outputs
- name: Build Linux Packages
run: |
@ -200,10 +200,10 @@ jobs:
if: ${{ inputs.channel == 'nightly' }}
run: echo '${{ secrets.DOTENV_NIGHTLY }}' > .env
- name: Generate Secrets and Build apk
- name: Generate Secrets
run: |
flutter pub get
dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
flutter pub run build_runner build --delete-conflicting-outputs
- name: Sign Apk
run: |
@ -253,7 +253,7 @@ jobs:
- name: Generate Secrets
run: |
flutter pub get
dart bin/create-secrets.dart '${{ secrets.LYRICS_SECRET }}' '${{ secrets.SPOTIFY_SECRET }}'
flutter pub run build_runner build --delete-conflicting-outputs
- name: Build Macos App
run: |

5
.gitignore vendored
View File

@ -66,9 +66,8 @@ installer.exe
# secrets
*.env
lib/models/generated_secrets.dart
lib/collections/env.g.dart
help.txt
secrets.json
dist
appimage-build
@ -76,4 +75,4 @@ appimage-build
android/key.properties
.fvm/flutter_sdk
**/pb_data
**/pb_data

View File

@ -120,38 +120,28 @@ Do the following:
- Download the latest Flutter SDK (>=2.15.1) & enable desktop support
- Install Development dependencies in linux
- `libappindicator3-1 gir1.2-appindicator3-0.1 libappindicator3-dev libsecret-1-0 libjsoncpp1 libsecret-1-dev libjsoncpp-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev` (for Debian/Ubuntu)
- `libappindicator-gtk3 libsecret jsoncpp gstreamer gst-libav gst-plugins-base gst-plugins-good` (for Arch/Manjaro)
- `libappindicator libsecret libsecret-devel jsoncpp gstreamer1-devel gstreamer1-plugins-base-tools gstreamer1-doc gstreamer1-plugins-base-devel gstreamer1-plugins-good gstreamer1-plugins-good-extras` (for Fedora)
- Clone the Repo & Run `flutter pub get` in the Terminal
- Create a `secrets.json` in root of the project. The structure should be similar to the following example:
```jsoc name="secrets.json"
{
"LYRICS_SECRET": [
"Bo3LQEMcL2xUAJ6yCfQowV6f8K78s9J9FLa67AsyWmvhkP9LWikkgcEyFrzvs7jsR",
"HiLHxLj8uv2VhBZfq9BQ9HVrWQk5Jc8aneMZX8RV4KjTmC387K692xrbNK35c8Qe4"
],
"SPOTIFY_SECRET": [
{
"clientId": "9ed19daf-c7a2-4c28-91ac-2c5283ad86cf",
"clientSecret": "236d5822-820e-457e-b18c-10e258c9386b"
},
{
"clientId": "b4769027-e048-4485-8f0b-b8a336f2cd97",
"clientSecret": "41df6ea4-eba2-4d42-b7be-6f727555fccc"
}
]
}
```
> You can add more clientId/clientSecret/genius-access-token if you want. The credentials used in the example are dummy (fake). You've to use your own secrets
- Debian/Ubuntu
```bash
$ apt-get install libappindicator3-1 gir1.2-appindicator3-0.1 libappindicator3-dev libsecret-1-0 libjsoncpp1 libsecret-1-dev libjsoncpp-dev libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev
```
- Use `libjsoncpp25` instead of `libjsoncpp1` (for Ubuntu >= 22.04)
- Arch/Manjaro
```bash
yay -S libappindicator-gtk3 libsecret jsoncpp gstreamer gst-libav gst-plugins-base gst-plugins-good
```
- Fedora
```bash
dnf install libappindicator libsecret libsecret-devel jsoncpp gstreamer1-devel gstreamer1-plugins-base-tools gstreamer1-doc gstreamer1-plugins-base-devel gstreamer1-plugins-good gstreamer1-plugins-good-extras
```
- Clone the Repo
- Create a `.env` in root of the project following the `.env.example` template
- Now run the following to bootstrap the project
```bash
flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs
```
- Finally run these following commands in the root of the project to start the Spotube Locally
```bash
$ dart bin/create-secrets.dart --local
$ flutter run -d <window|macos|linux|(<android-device-id>)>
```
```bash
flutter run -d <window|macos|linux|(<android-device-id>)>
```
Do debugging/testing/build etc then submit to us with PR against the development branch (master) & we'll review your code

View File

@ -0,0 +1,27 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="762"
android:viewportHeight="762">
<path
android:pathData="M309.08,370.99L309.08,479.87C309.08,486.36 314.33,491.6 320.83,491.6C327.31,491.6 332.58,486.36 332.58,479.87L332.58,370.99C332.58,364.51 327.31,359.26 320.83,359.26C314.33,359.26 309.08,364.51 309.08,370.99Z"
android:strokeLineJoin="miter"
android:strokeWidth="14"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="butt"/>
<path
android:pathData="M254.59,491.73L280.46,491.73L280.46,362.47C280.53,361.85 280.64,361.23 280.64,360.6C280.64,304.83 325.72,259.46 381.12,259.46C436.51,259.46 481.59,304.83 481.59,360.6C481.59,361.45 481.71,362.27 481.84,363.1L481.84,491.73L507.71,491.73C525.72,491.73 540.33,476.65 540.33,458.03L540.33,390.62C540.33,375.26 530.37,362.33 516.78,358.26C515.53,284.17 455.17,224.26 381.12,224.26C307.05,224.26 246.69,284.18 245.45,358.29C231.88,362.36 221.96,375.29 221.96,390.63L221.96,458.03C221.96,476.64 236.56,491.73 254.59,491.73Z"
android:strokeLineJoin="miter"
android:strokeWidth="20"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="butt"/>
<path
android:pathData="M431.08,370.99L431.08,479.87C431.08,486.36 436.33,491.6 442.83,491.6C449.31,491.6 454.58,486.36 454.58,479.87L454.58,370.99C454.58,364.51 449.31,359.26 442.83,359.26C436.33,359.26 431.08,364.51 431.08,370.99Z"
android:strokeLineJoin="miter"
android:strokeWidth="14"
android:fillColor="#00000000"
android:strokeColor="#000000"
android:strokeLineCap="butt"/>
</vector>

View File

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome"/>
</adaptive-icon>

View File

@ -1,177 +0,0 @@
import 'dart:convert';
import 'dart:io';
import 'package:path/path.dart' as path;
// blob metadata for de-stringifying
const randHash = [
49,
111,
98,
72,
78,
122,
98,
48,
112,
73,
81,
50,
112,
89,
90,
50,
116,
83,
84,
110,
99,
105,
76,
67,
74,
67,
89,
121,
48,
119,
77,
106,
69,
50,
86,
69,
53,
107,
77,
69,
86,
71,
101,
68,
66,
113,
78,
110,
66,
119
];
const sugarCarbonator = [
81,
119,
79,
71,
85,
53,
78,
50,
69,
52,
90,
68,
107,
120,
77,
87,
89,
52,
89,
84,
73
];
const randomSalt = [
70,
117,
67,
75,
117,
116,
72,
101,
105,
102,
65,
110,
68,
87,
72,
97,
84,
85,
82,
100,
79,
73,
110,
103,
83,
117,
75,
115
];
const algorithmicSugar = [
70,
117,
67,
75,
117,
116,
72,
101,
105,
102,
65,
78,
100,
102,
68,
114,
79,
105,
100,
115,
85,
99,
107,
83
];
void main(List<String> args) async {
List<String> val;
List<Map> val2;
final cwd = Directory.current.path;
final binSafe = cwd.endsWith("/bin") ? ".." : "";
if (args.isEmpty) {
throw ArgumentError("Expected 1-3 arguments but passed none");
}
if (args.contains("--local")) {
final secretFilePath = path.join(cwd, binSafe, "secrets.json");
final file = File(secretFilePath);
if (!file.existsSync()) throw Exception("secrets.json file not found");
final data = jsonDecode(await file.readAsString());
val = List.castFrom<dynamic, String>(data["LYRICS_SECRET"]);
val2 = List.castFrom<dynamic, Map>(data["SPOTIFY_SECRET"]);
} else if (args.contains("--fdroid")) {
final decodedLyricSecret = utf8.decode(base64Decode(
args[1].replaceAll(
String.fromCharCodes(randomSalt), String.fromCharCodes(randHash)),
));
final decodedSpotifySecret = utf8.decode(base64Decode(
args.last.replaceAll(String.fromCharCodes(algorithmicSugar),
String.fromCharCodes(sugarCarbonator)),
));
val = List.castFrom<dynamic, String>(jsonDecode(decodedLyricSecret));
val2 = List.castFrom<dynamic, Map>(jsonDecode(decodedSpotifySecret));
} else {
final decodedLyricSecret = utf8.decode(base64Decode(args.first));
final decodedSpotifySecret = utf8.decode(base64Decode(args.last));
val = List.castFrom<dynamic, String>(jsonDecode(decodedLyricSecret));
val2 = List.castFrom<dynamic, Map>(jsonDecode(decodedSpotifySecret));
}
await File(path.join(cwd, binSafe, "lib/models/generated_secrets.dart"))
.writeAsString(
"final List<String> lyricsSecrets = ${jsonEncode(val)};\nfinal List<Map<String, dynamic>> spotifySecrets = ${jsonEncode(val2)};",
);
}

View File

@ -5,11 +5,9 @@
// coverage:ignore-file
// ignore_for_file: type=lint
// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal
// ignore_for_file: directives_ordering,unnecessary_import,implicit_dynamic_list_literal,deprecated_member_use
import 'package:flutter/widgets.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:flutter/services.dart';
class $AssetsTutorialGen {
const $AssetsTutorialGen();
@ -39,8 +37,7 @@ class Assets {
AssetGenImage('assets/spotube-logo-foreground.jpg');
static const AssetGenImage spotubeLogoPng =
AssetGenImage('assets/spotube-logo.png');
static const SvgGenImage spotubeLogoSvg =
SvgGenImage('assets/spotube-logo.svg');
static const String spotubeLogoSvg = 'assets/spotube-logo.svg';
static const AssetGenImage spotubeScreenshot =
AssetGenImage('assets/spotube-screenshot.jpg');
static const AssetGenImage spotubeBanner =
@ -129,54 +126,3 @@ class AssetGenImage {
String get keyName => _assetName;
}
class SvgGenImage {
const SvgGenImage(this._assetName);
final String _assetName;
SvgPicture svg({
Key? key,
bool matchTextDirection = false,
AssetBundle? bundle,
String? package,
double? width,
double? height,
BoxFit fit = BoxFit.contain,
AlignmentGeometry alignment = Alignment.center,
bool allowDrawingOutsideViewBox = false,
WidgetBuilder? placeholderBuilder,
Color? color,
BlendMode colorBlendMode = BlendMode.srcIn,
String? semanticsLabel,
bool excludeFromSemantics = false,
Clip clipBehavior = Clip.hardEdge,
bool cacheColorFilter = false,
SvgTheme? theme,
}) {
return SvgPicture.asset(
_assetName,
key: key,
matchTextDirection: matchTextDirection,
bundle: bundle,
package: package,
width: width,
height: height,
fit: fit,
alignment: alignment,
allowDrawingOutsideViewBox: allowDrawingOutsideViewBox,
placeholderBuilder: placeholderBuilder,
color: color,
colorBlendMode: colorBlendMode,
semanticsLabel: semanticsLabel,
excludeFromSemantics: excludeFromSemantics,
clipBehavior: clipBehavior,
cacheColorFilter: cacheColorFilter,
theme: theme,
);
}
String get path => _assetName;
String get keyName => _assetName;
}

View File

@ -1,17 +1,24 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'package:envied/envied.dart';
part 'env.g.dart';
@Envied(obfuscate: true, requireEnvFile: true, path: ".env")
abstract class Env {
static final String pocketbaseUrl =
dotenv.get('POCKETBASE_URL', fallback: 'http://127.0.0.1:8090');
static final String username = dotenv.get('USERNAME', fallback: 'root');
static final String password = dotenv.get('PASSWORD', fallback: '12345678');
@EnviedField(varName: 'POCKETBASE_URL', defaultValue: 'http://127.0.0.1:8090')
static final pocketbaseUrl = _Env.pocketbaseUrl;
static configure() async {
if (kReleaseMode) {
await dotenv.load(fileName: ".env");
} else {
dotenv.testLoad();
}
}
@EnviedField(varName: 'USERNAME', defaultValue: 'root')
static final username = _Env.username;
@EnviedField(varName: 'PASSWORD', defaultValue: '12345678')
static final password = _Env.password;
@EnviedField(varName: 'SPOTIFY_SECRETS')
static final spotifySecrets = _Env.spotifySecrets.split(',').map((e) {
final secrets = e.trim().split(":").map((e) => e.trim());
return {
"clientId": secrets.first,
"clientSecret": secrets.last,
};
}).toList();
}

View File

@ -13,7 +13,6 @@ import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:metadata_god/metadata_god.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:spotube/collections/env.dart';
import 'package:spotube/components/shared/dialogs/replace_downloaded_dialog.dart';
import 'package:spotube/entities/cache_track.dart';
import 'package:spotube/collections/routes.dart';
@ -87,7 +86,6 @@ Future<void> main(List<String> rawArgs) async {
Hive.registerAdapter(CacheTrackAdapter());
Hive.registerAdapter(CacheTrackEngagementAdapter());
Hive.registerAdapter(CacheTrackSkipSegmentAdapter());
await Env.configure();
Catcher(
enableLogger: arguments["verbose"],

View File

@ -1,12 +1,13 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:spotify/spotify.dart';
import 'package:spotube/models/generated_secrets.dart';
import 'package:spotube/collections/env.dart';
import 'package:spotube/provider/authentication_provider.dart';
import 'package:spotube/utils/primitive_utils.dart';
final spotifyProvider = Provider<SpotifyApi>((ref) {
final authState = ref.watch(AuthenticationNotifier.provider);
final anonCred = PrimitiveUtils.getRandomElement(spotifySecrets);
final anonCred = PrimitiveUtils.getRandomElement(Env.spotifySecrets);
if (authState == null) {
return SpotifyApi(

View File

@ -4,10 +4,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path_provider/path_provider.dart';
import 'package:spotube/components/settings/color_scheme_picker_dialog.dart';
import 'package:spotube/models/generated_secrets.dart';
import 'package:spotube/utils/persisted_change_notifier.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/primitive_utils.dart';
import 'package:path/path.dart' as path;
enum LayoutMode {
@ -30,7 +29,6 @@ class UserPreferences extends PersistedChangeNotifier {
ThemeMode themeMode;
String recommendationMarket;
bool saveTrackLyrics;
String geniusAccessToken;
bool checkUpdate;
AudioQuality audioQuality;
@ -48,7 +46,6 @@ class UserPreferences extends PersistedChangeNotifier {
bool showSystemTrayIcon;
UserPreferences({
required this.geniusAccessToken,
required this.recommendationMarket,
required this.themeMode,
required this.layoutMode,
@ -95,12 +92,6 @@ class UserPreferences extends PersistedChangeNotifier {
updatePersistence();
}
void setGeniusAccessToken(String token) {
geniusAccessToken = token;
notifyListeners();
updatePersistence();
}
void setAccentColorScheme(SpotubeColor color) {
accentColorScheme = color;
notifyListeners();
@ -172,8 +163,6 @@ class UserPreferences extends PersistedChangeNotifier {
saveTrackLyrics = map["saveTrackLyrics"] ?? false;
recommendationMarket = map["recommendationMarket"] ?? recommendationMarket;
checkUpdate = map["checkUpdate"] ?? checkUpdate;
geniusAccessToken = map["geniusAccessToken"] ??
PrimitiveUtils.getRandomElement(lyricsSecrets);
themeMode = ThemeMode.values[map["themeMode"] ?? 0];
accentColorScheme = map["accentColorScheme"] != null
@ -205,7 +194,6 @@ class UserPreferences extends PersistedChangeNotifier {
return {
"saveTrackLyrics": saveTrackLyrics,
"recommendationMarket": recommendationMarket,
"geniusAccessToken": geniusAccessToken,
"themeMode": themeMode.index,
"accentColorScheme": accentColorScheme.toString(),
"checkUpdate": checkUpdate,
@ -223,7 +211,6 @@ class UserPreferences extends PersistedChangeNotifier {
final userPreferencesProvider = ChangeNotifierProvider(
(_) => UserPreferences(
accentColorScheme: SpotubeColor(Colors.blue.value, name: "Blue"),
geniusAccessToken: "",
recommendationMarket: 'US',
themeMode: ThemeMode.system,
layoutMode: kIsMobile ? LayoutMode.compact : LayoutMode.adaptive,

View File

@ -9,7 +9,7 @@ import 'package:spotube/models/logger.dart';
import 'package:http/http.dart' as http;
import 'package:spotube/models/lyrics.dart';
import 'package:spotube/models/spotube_track.dart';
import 'package:spotube/models/generated_secrets.dart';
import 'package:spotube/utils/primitive_utils.dart';
import 'package:collection/collection.dart';
import 'package:html/parser.dart' as parser;
@ -23,6 +23,7 @@ abstract class ServiceUtils {
.trim();
}
@Deprecated("In favor spotify lyrics api, this isn't needed anymore")
static String getTitle(
String title, {
List<String> artists = const [],
@ -77,6 +78,7 @@ abstract class ServiceUtils {
return lyrics;
}
@Deprecated("In favor spotify lyrics api, this isn't needed anymore")
static Future<List?> searchSong(
String title,
List<String> artist, {
@ -85,7 +87,7 @@ abstract class ServiceUtils {
bool authHeader = false,
}) async {
if (apiKey == "" || apiKey == null) {
apiKey = PrimitiveUtils.getRandomElement(lyricsSecrets);
apiKey = PrimitiveUtils.getRandomElement(/* lyricsSecrets */ []);
}
const searchUrl = 'https://api.genius.com/search?q=';
String song =
@ -111,6 +113,7 @@ abstract class ServiceUtils {
return results;
}
@Deprecated("In favor spotify lyrics api, this isn't needed anymore")
static Future<String?> getLyrics(
String title,
List<String> artists, {
@ -158,8 +161,10 @@ abstract class ServiceUtils {
return lyrics;
}
@Deprecated("In favor spotify lyrics api, this isn't needed anymore")
static const baseUri = "https://www.rentanadviser.com/subtitles";
@Deprecated("In favor spotify lyrics api, this isn't needed anymore")
static Future<SubtitleSimple?> getTimedLyrics(SpotubeTrack track) async {
final artistNames =
track.artists?.map((artist) => artist.name!).toList() ?? [];

View File

@ -15,7 +15,6 @@ dependencies:
- libappindicator3-1
- gir1.2-appindicator3-0.1
- libsecret-1-0
- libjsoncpp1
essential: false
icon: assets/spotube-logo.png

View File

@ -506,6 +506,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
envied:
dependency: "direct main"
description:
name: envied
sha256: d5d978fbd578b5c00123003609c39185e0b1ddf9d2ac460d710dd0eb2fc223d7
url: "https://pub.dev"
source: hosted
version: "0.3.0"
envied_generator:
dependency: "direct dev"
description:
name: envied_generator
sha256: "6c5a98c27c5eae925807692eb252ccac2b8e81f09bace1f07207c47dfb6a4eb0"
url: "https://pub.dev"
source: hosted
version: "0.3.0"
fake_async:
dependency: transitive
description:
@ -624,14 +640,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.0.2"
flutter_dotenv:
dependency: "direct main"
description:
name: flutter_dotenv
sha256: d9283d92059a22e9834bc0a31336658ffba77089fb6f3cc36751f1fc7c6661a3
url: "https://pub.dev"
source: hosted
version: "5.0.2"
flutter_driver:
dependency: transitive
description: flutter

View File

@ -25,6 +25,7 @@ dependencies:
cupertino_icons: ^1.0.5
curved_navigation_bar: ^1.0.3
dbus: ^0.7.8
envied: ^0.3.0
file_picker: ^5.2.2
fl_query: ^1.0.0-alpha.2
fl_query_hooks: ^1.0.0-alpha.2
@ -32,7 +33,6 @@ dependencies:
flutter:
sdk: flutter
flutter_cache_manager: ^3.3.0
flutter_dotenv: ^5.0.2
flutter_feather_icons: ^2.0.0+1
flutter_hooks: ^0.18.2+1
flutter_inappwebview: ^5.7.2+3
@ -92,6 +92,7 @@ dependencies:
dev_dependencies:
build_runner: ^2.3.2
envied_generator: ^0.3.0
flutter_distributor: ^0.0.2
flutter_gen_runner: ^5.1.0+1
flutter_launcher_icons: ^0.11.0
@ -112,7 +113,6 @@ flutter:
assets:
- assets/
- assets/tutorial/
- .env
- LICENSE
flutter_icons:
@ -130,5 +130,3 @@ flutter_icons:
flutter_gen:
output: lib/collections
integrations:
flutter_svg: true