Compare commits

...

10 Commits

Author SHA1 Message Date
$urasek-805
f92e2a0b1d
Merge 053db0bbdc into bbe3394e9e 2025-03-13 22:21:12 +07:00
Kingkor Roy Tirtho
bbe3394e9e fix: lastfm form broken in other locales #2447 2025-03-12 15:05:32 +06:00
Kingkor Roy Tirtho
7cde803bee feat(local_library): add support for x-flac, opus and x-wav 2025-03-12 14:20:19 +06:00
Kingkor Roy Tirtho
cd475e93d0 chore: upgrade action flutter to 3.29.1 2025-03-12 13:48:07 +06:00
Kingkor Roy Tirtho
3d334d96fd fix(desktop): double titlebar in local library folders and massive space in overlay player 2025-03-11 21:28:57 +06:00
Kingkor Roy Tirtho
bd4cd22e4e fix(generate_playlist): create playlist not adding tracks nor navigating to playlist page 2025-03-11 00:16:53 +06:00
Kingkor Roy Tirtho
c709de6bf1 fix: language picker search broken 2025-03-10 20:44:52 +06:00
Kingkor Roy Tirtho
ccbac85171 chore: remove GeistMono 2025-03-10 20:15:28 +06:00
Kingkor Roy Tirtho
50123b235c fix: add to playlist not working in smaller screen devices 2025-03-10 20:07:51 +06:00
Kingkor Roy Tirtho
4072531c62 fix(android): navigation overlaying in app navigation 2025-03-09 10:05:02 +06:00
24 changed files with 142 additions and 274 deletions

View File

@ -1,3 +1,3 @@
{ {
"flutterSdkVersion": "3.29.0" "flutterSdkVersion": "3.29.1"
} }

2
.fvmrc
View File

@ -1,4 +1,4 @@
{ {
"flutter": "3.29.0", "flutter": "3.29.1",
"flavors": {} "flavors": {}
} }

View File

@ -4,7 +4,7 @@ on:
pull_request: pull_request:
env: env:
FLUTTER_VERSION: 3.29.0 FLUTTER_VERSION: 3.29.1
jobs: jobs:
lint: lint:

View File

@ -20,7 +20,7 @@ on:
description: Dry run without uploading to release description: Dry run without uploading to release
env: env:
FLUTTER_VERSION: 3.29.0 FLUTTER_VERSION: 3.29.1
FLUTTER_CHANNEL: master FLUTTER_CHANNEL: master
permissions: permissions:

View File

@ -28,5 +28,5 @@
"README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md", "README.md": "LICENSE,CODE_OF_CONDUCT.md,CONTRIBUTING.md,SECURITY.md,CONTRIBUTION.md,CHANGELOG.md,PRIVACY_POLICY.md",
"*.dart": "${capture}.g.dart,${capture}.freezed.dart" "*.dart": "${capture}.g.dart,${capture}.freezed.dart"
}, },
"dart.flutterSdkPath": ".fvm/versions/3.29.0" "dart.flutterSdkPath": ".fvm/versions/3.29.1"
} }

View File

@ -25,9 +25,9 @@
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:usesCleartextTraffic="true"> android:usesCleartextTraffic="true">
<!-- Enable Impeller --> <!-- Enable Impeller -->
<!-- <meta-data <meta-data
android:name="io.flutter.embedding.android.EnableImpeller" android:name="io.flutter.embedding.android.EnableImpeller"
android:value="false" /> --> android:value="false" />
<activity <activity
android:name="com.ryanheise.audioservice.AudioServiceActivity" android:name="com.ryanheise.audioservice.AudioServiceActivity"

View File

@ -75,17 +75,17 @@ class AppRouter extends RootStackRouter {
path: "local", path: "local",
page: UserLocalLibraryRoute.page, page: UserLocalLibraryRoute.page,
), ),
AutoRoute(
path: "local/folder",
page: LocalLibraryRoute.page,
// parentNavigatorKey: shellRouteNavigatorKey,
),
AutoRoute( AutoRoute(
path: "downloads", path: "downloads",
page: UserDownloadsRoute.page, page: UserDownloadsRoute.page,
), ),
], ],
), ),
AutoRoute(
path: "local/folder",
page: LocalLibraryRoute.page,
// parentNavigatorKey: shellRouteNavigatorKey,
),
AutoRoute( AutoRoute(
path: "library/generate", path: "library/generate",
page: PlaylistGeneratorRoute.page, page: PlaylistGeneratorRoute.page,

View File

@ -28,7 +28,7 @@ class AdaptiveMenuButton<T> extends MenuButton {
/// or equal to 640px /// or equal to 640px
/// In smaller screen, a [IconButton] with a [showModalBottomSheet] is shown /// In smaller screen, a [IconButton] with a [showModalBottomSheet] is shown
class AdaptivePopSheetList<T> extends StatelessWidget { class AdaptivePopSheetList<T> extends StatelessWidget {
final List<AdaptiveMenuButton<T>> children; final List<AdaptiveMenuButton<T>> Function(BuildContext context) items;
final Widget? icon; final Widget? icon;
final Widget? child; final Widget? child;
final bool useRootNavigator; final bool useRootNavigator;
@ -43,7 +43,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
const AdaptivePopSheetList({ const AdaptivePopSheetList({
super.key, super.key,
required this.children, required this.items,
this.icon, this.icon,
this.child, this.child,
this.useRootNavigator = true, this.useRootNavigator = true,
@ -59,27 +59,28 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
Future<void> showDropdownMenu(BuildContext context, Offset position) async { Future<void> showDropdownMenu(BuildContext context, Offset position) async {
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.of(context);
final childrenModified = children.map((s) { List<MenuButton> childrenModified(BuildContext context) =>
if (s.onPressed == null) { items(context).map((s) {
return MenuButton( if (s.onPressed == null) {
key: s.key, return MenuButton(
autoClose: s.autoClose, key: s.key,
enabled: s.enabled, autoClose: s.autoClose,
leading: s.leading, enabled: s.enabled,
focusNode: s.focusNode, leading: s.leading,
onPressed: (context) { focusNode: s.focusNode,
if (s.value != null) { onPressed: (context) {
onSelected?.call(s.value as T); if (s.value != null) {
} onSelected?.call(s.value as T);
}, }
popoverController: s.popoverController, },
subMenu: s.subMenu, popoverController: s.popoverController,
trailing: s.trailing, subMenu: s.subMenu,
child: s.child, trailing: s.trailing,
); child: s.child,
} );
return s; }
}).toList(); return s;
}).toList();
if (mediaQuery.mdAndUp) { if (mediaQuery.mdAndUp) {
await showDropdown<T?>( await showDropdown<T?>(
@ -92,7 +93,7 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
position: position, position: position,
builder: (context) { builder: (context) {
return DropdownMenu( return DropdownMenu(
children: childrenModified, children: childrenModified(context),
); );
}, },
).future; ).future;
@ -109,11 +110,12 @@ class AdaptivePopSheetList<T> extends StatelessWidget {
), ),
backgroundColor: context.theme.colorScheme.card, backgroundColor: context.theme.colorScheme.card,
builder: (context) { builder: (context) {
final children = childrenModified(context);
return ListView.builder( return ListView.builder(
itemCount: childrenModified.length, itemCount: children.length,
shrinkWrap: true, shrinkWrap: true,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final data = childrenModified[index]; final data = children[index];
return Button( return Button(
enabled: data.enabled, enabled: data.enabled,

View File

@ -1,126 +0,0 @@
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
class AnimateGradient extends HookWidget {
const AnimateGradient({
super.key,
required this.primaryColors,
required this.secondaryColors,
this.child,
this.primaryBegin,
this.primaryEnd,
this.secondaryBegin,
this.secondaryEnd,
AnimationController? controller,
this.duration = const Duration(seconds: 4),
this.animateAlignments = true,
this.reverse = true,
}) : assert(primaryColors.length >= 2),
assert(primaryColors.length == secondaryColors.length),
_controller = controller;
/// [controller]: pass this to have a fine control over the [Animation]
final AnimationController? _controller;
/// [duration]: Time to switch between [Gradient].
/// By default its value is [Duration(seconds:4)]
final Duration duration;
/// [primaryColors]: These will be the starting colors of the [Animation].
final List<Color> primaryColors;
/// [secondaryColors]: These Colors are those in which the [primaryColors] will transition into.
final List<Color> secondaryColors;
/// [primaryBegin]: This is begin [Alignment] for [primaryColors].
/// By default its value is [Alignment.topLeft]
final Alignment? primaryBegin;
/// [primaryBegin]: This is end [Alignment] for [primaryColors].
/// By default its value is [Alignment.topRight]
final Alignment? primaryEnd;
/// [secondaryBegin]: This is begin [Alignment] for [secondaryColors].
/// By default its value is [Alignment.bottomLeft]
final Alignment? secondaryBegin;
/// [secondaryEnd]: This is end [Alignment] for [secondaryColors].
/// By default its value is [Alignment.bottomRight]
final Alignment? secondaryEnd;
/// [animateAlignments]: set to false if you don't want to animate the alignments.
/// This can provide you way cooler animations
final bool animateAlignments;
/// [reverse]: set it to false if you don't want to reverse the animation.
/// using that it will go into one direction only
final bool reverse;
final Widget? child;
@override
Widget build(BuildContext context) {
// ignore: no_leading_underscores_for_local_identifiers
final __controller = useAnimationController(
duration: duration,
)..repeat(reverse: reverse);
final controller = _controller ?? __controller;
final animation = useMemoized(
() => CurvedAnimation(
parent: controller,
curve: Curves.easeInOut,
),
[controller]);
final colorTween = useMemoized(
() => primaryColors.map((color) {
return ColorTween(
begin: color,
end: color,
);
}).toList(),
[primaryColors]);
final colors = useMemoized(
() => colorTween.map((color) {
return color.evaluate(animation)!;
}).toList(),
[colorTween, animation]);
final begin = useMemoized(
() => AlignmentTween(
begin: primaryBegin ?? Alignment.topLeft,
end: primaryEnd ?? Alignment.topRight,
),
[primaryBegin, primaryEnd]);
final end = useMemoized(
() => AlignmentTween(
begin: secondaryBegin ?? Alignment.bottomLeft,
end: secondaryEnd ?? Alignment.bottomRight,
),
[secondaryBegin, secondaryEnd]);
return AnimatedBuilder(
animation: animation,
child: useMemoized(() => child, [child]),
builder: (BuildContext context, Widget? child) {
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: animateAlignments
? begin.evaluate(animation)
: (primaryBegin as Alignment),
end: animateAlignments
? end.evaluate(animation)
: primaryEnd as Alignment,
colors: colors,
),
),
child: child,
);
},
);
}
}

View File

@ -1,24 +0,0 @@
import 'package:shadcn_flutter/shadcn_flutter.dart';
class SpotubePage<T> extends MaterialPage<T> {
const SpotubePage({required super.child});
}
// class SpotubeSlidePage extends CustomTransitionPage {
// SpotubeSlidePage({
// required super.child,
// super.key,
// }) : super(
// reverseTransitionDuration: const Duration(milliseconds: 150),
// transitionDuration: const Duration(milliseconds: 150),
// transitionsBuilder: (context, animation, secondaryAnimation, child) {
// return SlideTransition(
// position: Tween<Offset>(
// begin: const Offset(1, 0),
// end: Offset.zero,
// ).animate(animation),
// child: child,
// );
// },
// );
// }

View File

@ -166,7 +166,7 @@ class TrackPresentationActionsSection extends HookConsumerWidget {
}, },
icon: const Icon(SpotubeIcons.moreVertical), icon: const Icon(SpotubeIcons.moreVertical),
variance: ButtonVariance.outline, variance: ButtonVariance.outline,
children: [ items: (context) => [
AdaptiveMenuButton( AdaptiveMenuButton(
value: "download", value: "download",
leading: const Icon(SpotubeIcons.download), leading: const Icon(SpotubeIcons.download),

View File

@ -23,7 +23,7 @@ class SortTracksDropdown extends StatelessWidget {
onSelected: onChanged, onSelected: onChanged,
tooltip: context.l10n.sort_tracks, tooltip: context.l10n.sort_tracks,
icon: const Icon(SpotubeIcons.sort), icon: const Icon(SpotubeIcons.sort),
children: [ items: (context) => [
AdaptiveMenuButton( AdaptiveMenuButton(
value: SortBy.none, value: SortBy.none,
enabled: value != SortBy.none, enabled: value != SortBy.none,

View File

@ -90,11 +90,25 @@ class TrackOptions extends HookConsumerWidget {
BuildContext context, BuildContext context,
Track track, Track track,
) { ) {
showDialog( /// showDialog doesn't work for some reason. So we have to
context: context, /// manually push a Dialog Route in the Navigator to get it working
builder: (context) => PlaylistAddTrackDialog( Navigator.push(
tracks: [track], context,
openFromPlaylist: playlistId, DialogRoute(
alignment: Alignment.bottomCenter,
transitionBuilder: (context, animation, secondaryAnimation, child) {
return FadeTransition(opacity: animation, child: child);
},
context: context,
barrierColor: Colors.black.withValues(alpha: 0.5),
builder: (context) {
return Center(
child: PlaylistAddTrackDialog(
tracks: [track],
openFromPlaylist: playlistId,
),
);
},
), ),
); );
} }
@ -352,7 +366,7 @@ class TrackOptions extends HookConsumerWidget {
), ),
), ),
], ],
children: [ items: (context) => [
if (isLocalTrack) if (isLocalTrack)
AdaptiveMenuButton( AdaptiveMenuButton(
value: TrackOptionValue.delete, value: TrackOptionValue.delete,

View File

@ -27,6 +27,7 @@ import 'package:spotube/provider/audio_player/audio_player.dart';
import 'package:spotube/provider/server/active_sourced_track.dart'; import 'package:spotube/provider/server/active_sourced_track.dart';
import 'package:spotube/provider/volume_provider.dart'; import 'package:spotube/provider/volume_provider.dart';
import 'package:spotube/services/sourced_track/sources/youtube.dart'; import 'package:spotube/services/sourced_track/sources/youtube.dart';
import 'package:spotube/utils/platform.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
@ -48,7 +49,7 @@ class PlayerView extends HookConsumerWidget {
ref.watch(audioPlayerProvider.select((s) => s.activeTrack)); ref.watch(audioPlayerProvider.select((s) => s.activeTrack));
final currentTrack = sourcedCurrentTrack ?? currentActiveTrack; final currentTrack = sourcedCurrentTrack ?? currentActiveTrack;
final isLocalTrack = currentTrack is LocalTrack; final isLocalTrack = currentTrack is LocalTrack;
final mediaQuery = MediaQuery.of(context); final mediaQuery = MediaQuery.sizeOf(context);
final shouldHide = useState(true); final shouldHide = useState(true);
@ -101,6 +102,9 @@ class PlayerView extends HookConsumerWidget {
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
headers: [ headers: [
SafeArea( SafeArea(
minimum:
kIsMobile ? const EdgeInsets.only(top: 80) : EdgeInsets.zero,
bottom: false,
child: TitleBar( child: TitleBar(
surfaceOpacity: 0, surfaceOpacity: 0,
surfaceBlur: 0, surfaceBlur: 0,

View File

@ -191,7 +191,7 @@ class PlayerActions extends HookConsumerWidget {
sleepTimerNotifier.setSleepTimer(value); sleepTimerNotifier.setSleepTimer(value);
} }
}, },
children: [ items: (context) => [
for (final entry in sleepTimerEntries.entries) for (final entry in sleepTimerEntries.entries)
AdaptiveMenuButton( AdaptiveMenuButton(
value: entry.value, value: entry.value,

View File

@ -101,11 +101,17 @@ class PlaylistCreateDialog extends HookConsumerWidget {
} else { } else {
await playlistNotifier.create(payload, onError); await playlistNotifier.create(payload, onError);
} }
if (trackIds.isNotEmpty) {
await playlistNotifier.addTracks(trackIds, onError);
}
} finally { } finally {
isSubmitting.value = false; isSubmitting.value = false;
if (context.mounted && if (context.mounted &&
!ref.read(playlistProvider(playlistId ?? "")).hasError) { !ref.read(playlistProvider(playlistId ?? "")).hasError) {
context.router.maybePop(); context.router.maybePop<Playlist>(
await ref.read(playlistProvider(playlistId ?? "").future),
);
} }
} }
} }

View File

@ -133,37 +133,38 @@ class GettingStartedPageLanguageRegionSection extends HookConsumerWidget {
popup: SelectPopup.builder( popup: SelectPopup.builder(
searchPlaceholder: Text(context.l10n.search), searchPlaceholder: Text(context.l10n.search),
builder: (context, searchQuery) { builder: (context, searchQuery) {
final filteredLocale = searchQuery?.isNotEmpty != true final hasNotQueried =
? L10n.all searchQuery == null || searchQuery.trim().isEmpty;
final filteredLocale = hasNotQueried
? [
const Locale("system", "system"),
...L10n.all,
]
: L10n.all : L10n.all
.where( .where(
(element) => (element) => filterLocale(
filterLocale(element, searchQuery!), element,
searchQuery.trim(),
),
) )
.toList(); .toList();
return SelectItemBuilder( return SelectItemBuilder(
childCount: filteredLocale.length + 1, childCount: filteredLocale.length,
builder: (context, index) { builder: (context, index) {
if (index == 0 && final locale = filteredLocale[index];
searchQuery?.isNotEmpty != true) { if (locale == const Locale("system", "system")) {
return SelectItemButton( return SelectItemButton(
value: const Locale("system", "system"), value: locale,
child: Text(context.l10n.system_default), child: Text(context.l10n.system_default),
); );
} }
final indexThen = searchQuery?.isNotEmpty != true
? index
: index - 1;
final locale = filteredLocale[indexThen];
return SelectItemButton( return SelectItemButton(
value: locale, value: locale,
child: Text( child: Text(
LanguageLocals.getDisplayLanguage( LanguageLocals.getDisplayLanguage(
locale.languageCode) locale.languageCode,
.toString(), ).toString(),
), ),
); );
}, },

View File

@ -96,7 +96,9 @@ class LastFMLoginPage extends HookConsumerWidget {
FormField( FormField(
label: Text(context.l10n.username), label: Text(context.l10n.username),
key: usernameKey, key: usernameKey,
validator: const NotEmptyValidator(), validator: const NotEmptyValidator(
message: "Username is required",
),
child: TextField( child: TextField(
autofillHints: const [ autofillHints: const [
AutofillHints.username, AutofillHints.username,
@ -107,7 +109,9 @@ class LastFMLoginPage extends HookConsumerWidget {
), ),
FormField( FormField(
key: passwordKey, key: passwordKey,
validator: const NotEmptyValidator(), validator: const NotEmptyValidator(
message: "Password is required",
),
label: Text(context.l10n.password), label: Text(context.l10n.password),
child: TextField( child: TextField(
autofillHints: const [ autofillHints: const [

View File

@ -81,6 +81,7 @@ class LyricsPage extends HookConsumerWidget {
title: tabbar, title: tabbar,
height: 58 * context.theme.scaling, height: 58 * context.theme.scaling,
surfaceBlur: 0, surfaceBlur: 0,
automaticallyImplyLeading: false,
) )
: tabbar : tabbar
], ],

View File

@ -43,13 +43,16 @@ class RootAppPage extends HookConsumerWidget {
final scaffold = MediaQuery.removeViewInsets( final scaffold = MediaQuery.removeViewInsets(
context: context, context: context,
removeBottom: true, removeBottom: true,
child: const Scaffold( child: const SafeArea(
footers: [ top: false,
BottomPlayer(), child: Scaffold(
SpotubeNavigationBar(), footers: [
], BottomPlayer(),
floatingFooter: true, SpotubeNavigationBar(),
child: Sidebar(child: AutoRouter()), ],
floatingFooter: true,
child: Sidebar(child: AutoRouter()),
),
), ),
); );

View File

@ -24,6 +24,9 @@ const supportedAudioTypes = [
"audio/opus", "audio/opus",
"audio/wav", "audio/wav",
"audio/aac", "audio/aac",
"audio/flac",
"audio/x-flac",
"audio/x-wav",
]; ];
const imgMimeToExt = { const imgMimeToExt = {
@ -68,13 +71,16 @@ final localTracksProvider =
await Directory(location).list(recursive: true).toList(); await Directory(location).list(recursive: true).toList();
entities.addAll( entities.addAll(
dirEntities dirEntities.where(
.where( (e) {
(e) => final mime = lookupMimeType(e.path) ??
e is File && (extension(e.path) == ".opus" ? "audio/opus" : null);
supportedAudioTypes.contains(lookupMimeType(e.path)),
) print("${basename(e.path)}: $mime");
.cast<File>(),
return e is File && supportedAudioTypes.contains(mime);
},
).cast<File>(),
); );
} catch (e, stack) { } catch (e, stack) {
AppLogger.reportError(e, stack); AppLogger.reportError(e, stack);

View File

@ -98,6 +98,23 @@ class PlaylistNotifier extends FamilyAsyncNotifier<Playlist, String> {
} }
}); });
} }
Future<void> addTracks(List<String> trackIds, [ValueChanged? onError]) async {
try {
if (state.value == null) return;
final spotify = ref.read(spotifyProvider);
await spotify.playlists.addTracks(
trackIds.map((id) => "spotify:track:$id").toList(),
state.value!.id!,
);
} catch (e, stack) {
onError?.call(e);
AppLogger.reportError(e, stack);
rethrow;
}
}
} }
final playlistProvider = final playlistProvider =

View File

@ -1539,10 +1539,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: mime name: mime
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.6" version: "2.0.0"
nm: nm:
dependency: transitive dependency: transitive
description: description:

View File

@ -88,7 +88,7 @@ dependencies:
media_kit: ^1.1.10+1 media_kit: ^1.1.10+1
media_kit_libs_audio: ^1.0.4 media_kit_libs_audio: ^1.0.4
metadata_god: ^1.0.0 metadata_god: ^1.0.0
mime: ^1.0.2 mime: ^2.0.0
open_file: ^3.5.10 open_file: ^3.5.10
package_info_plus: ^6.0.0 package_info_plus: ^6.0.0
palette_generator: ^0.3.3 palette_generator: ^0.3.3
@ -193,46 +193,6 @@ flutter:
- packages/flutter_undraw/assets/undraw/empty.svg - packages/flutter_undraw/assets/undraw/empty.svg
- packages/flutter_undraw/assets/undraw/no_data.svg - packages/flutter_undraw/assets/undraw/no_data.svg
fonts: fonts:
- family: GeistSans
fonts:
- asset: packages/shadcn_flutter/fonts/Geist-Black.otf
weight: 800
- asset: packages/shadcn_flutter/fonts/Geist-Bold.otf
weight: 700
- asset: packages/shadcn_flutter/fonts/Geist-Light.otf
weight: 300
- asset: packages/shadcn_flutter/fonts/Geist-Medium.otf
weight: 500
- asset: packages/shadcn_flutter/fonts/Geist-SemiBold.otf
weight: 600
- asset: packages/shadcn_flutter/fonts/Geist-Thin.otf
weight: 100
- asset: packages/shadcn_flutter/fonts/Geist-UltraBlack.otf
weight: 900
- asset: packages/shadcn_flutter/fonts/Geist-UltraLight.otf
weight: 200
- asset: packages/shadcn_flutter/fonts/Geist-Regular.otf
weight: 400
- family: GeistMono
fonts:
- asset: packages/shadcn_flutter/fonts/GeistMono-Black.otf
weight: 800
- asset: packages/shadcn_flutter/fonts/GeistMono-Bold.otf
weight: 700
- asset: packages/shadcn_flutter/fonts/GeistMono-Light.otf
weight: 300
- asset: packages/shadcn_flutter/fonts/GeistMono-Medium.otf
weight: 500
- asset: packages/shadcn_flutter/fonts/GeistMono-Regular.otf
weight: 400
- asset: packages/shadcn_flutter/fonts/GeistMono-SemiBold.otf
weight: 600
- asset: packages/shadcn_flutter/fonts/GeistMono-Thin.otf
weight: 100
- asset: packages/shadcn_flutter/fonts/GeistMono-UltraBlack.otf
weight: 900
- asset: packages/shadcn_flutter/fonts/GeistMono-UltraLight.otf
weight: 200
- family: RadixIcons - family: RadixIcons
fonts: fonts:
- asset: packages/shadcn_flutter/icons/RadixIcons.otf - asset: packages/shadcn_flutter/icons/RadixIcons.otf