feat: initial integration of shadcn-ui

This commit is contained in:
Kingkor Roy Tirtho 2024-12-21 09:22:30 +06:00
parent b52bf0f448
commit 5ad151932a
15 changed files with 549 additions and 542 deletions

View File

@ -3,7 +3,6 @@ import 'dart:ui';
import 'package:desktop_webview_window/desktop_webview_window.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_discord_rpc/flutter_discord_rpc.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
@ -34,7 +33,6 @@ import 'package:spotube/provider/server/server.dart';
import 'package:spotube/provider/tray_manager/tray_manager.dart';
import 'package:spotube/l10n/l10n.dart';
import 'package:spotube/provider/connect/clients.dart';
import 'package:spotube/provider/palette_provider.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/services/audio_player/audio_player.dart';
import 'package:spotube/services/cli/cli.dart';
@ -42,8 +40,6 @@ import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/services/wm_tools/wm_tools.dart';
import 'package:spotube/themes/theme.dart';
import 'package:spotube/utils/migrations/hive.dart';
import 'package:spotube/utils/migrations/sandbox.dart';
import 'package:spotube/utils/platform.dart';
import 'package:system_theme/system_theme.dart';
@ -53,6 +49,7 @@ import 'package:flutter_native_splash/flutter_native_splash.dart';
import 'package:flutter_displaymode/flutter_displaymode.dart';
import 'package:timezone/data/latest.dart' as tz;
import 'package:window_manager/window_manager.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
Future<void> main(List<String> rawArgs) async {
if (rawArgs.contains("web_view_title_bar")) {
@ -110,8 +107,6 @@ Future<void> main(List<String> rawArgs) async {
final database = AppDatabase();
await migrateFromHiveToDrift(database);
if (kIsDesktop) {
await localNotifier.setup(appName: "Spotube");
await WindowManagerTools.initialize();
@ -142,13 +137,13 @@ class Spotube extends HookConsumerWidget {
Widget build(BuildContext context, ref) {
final themeMode =
ref.watch(userPreferencesProvider.select((s) => s.themeMode));
final accentMaterialColor =
ref.watch(userPreferencesProvider.select((s) => s.accentColorScheme));
final isAmoledTheme =
ref.watch(userPreferencesProvider.select((s) => s.amoledDarkTheme));
final locale = ref.watch(userPreferencesProvider.select((s) => s.locale));
final paletteColor =
ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
// final accentMaterialColor =
// ref.watch(userPreferencesProvider.select((s) => s.accentColorScheme));
// final isAmoledTheme =
// ref.watch(userPreferencesProvider.select((s) => s.amoledDarkTheme));
// final paletteColor =
// ref.watch(paletteProvider.select((s) => s?.dominantColor?.color));
final router = ref.watch(routerProvider);
final hasTouchSupport = useHasTouch();
@ -178,20 +173,20 @@ class Spotube extends HookConsumerWidget {
};
}, []);
final lightTheme = useMemoized(
() => theme(paletteColor ?? accentMaterialColor, Brightness.light, false),
[paletteColor, accentMaterialColor],
);
final darkTheme = useMemoized(
() => theme(
paletteColor ?? accentMaterialColor,
Brightness.dark,
isAmoledTheme,
),
[paletteColor, accentMaterialColor, isAmoledTheme],
);
// final lightTheme = useMemoized(
// () => theme(paletteColor ?? accentMaterialColor, Brightness.light, false),
// [paletteColor, accentMaterialColor],
// );
// final darkTheme = useMemoized(
// () => theme(
// paletteColor ?? accentMaterialColor,
// Brightness.dark,
// isAmoledTheme,
// ),
// [paletteColor, accentMaterialColor, isAmoledTheme],
// );
return MaterialApp.router(
return ShadcnApp.router(
supportedLocales: L10n.all,
locale: locale.languageCode == "system" ? null : locale,
localizationsDelegates: const [
@ -221,9 +216,17 @@ class Spotube extends HookConsumerWidget {
return child;
},
theme: ThemeData(
radius: .5,
iconTheme: const IconThemeProperties(),
colorScheme: ColorSchemes.lightNeutral(),
),
darkTheme: ThemeData(
radius: .5,
iconTheme: const IconThemeProperties(),
colorScheme: ColorSchemes.darkNeutral(),
),
themeMode: themeMode,
theme: lightTheme,
darkTheme: darkTheme,
shortcuts: {
...WidgetsApp.defaultShortcuts.map((key, value) {
return MapEntry(

View File

@ -8,13 +8,14 @@ import 'package:encrypt/encrypt.dart';
import 'package:media_kit/media_kit.dart' hide Track;
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart' show ThemeMode, Colors;
import 'package:spotify/spotify.dart' hide Playlist;
import 'package:spotube/models/database/database.steps.dart';
import 'package:spotube/models/lyrics.dart';
import 'package:spotube/services/kv_store/encrypted_kv_store.dart';
import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/services/sourced_track/enums.dart';
import 'package:flutter/material.dart' hide Table, Key, View;
import 'package:flutter/widgets.dart' hide Table, Key, View;
import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';
import 'package:drift/native.dart';
import 'package:sqlite3/sqlite3.dart';

View File

@ -1,33 +1,28 @@
import 'package:collection/collection.dart';
import 'package:flutter/material.dart' show Badge;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:flutter/material.dart';
import 'package:sidebarx/sidebarx.dart';
import 'package:shadcn_flutter/shadcn_flutter.dart';
import 'package:shadcn_flutter/shadcn_flutter_extension.dart';
import 'package:spotube/collections/assets.gen.dart';
import 'package:spotube/collections/side_bar_tiles.dart';
import 'package:spotube/collections/spotube_icons.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/connect/connect_device.dart';
import 'package:spotube/components/image/universal_image.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/extensions/constrains.dart';
import 'package:spotube/extensions/context.dart';
import 'package:spotube/extensions/image.dart';
import 'package:spotube/hooks/utils/use_brightness_value.dart';
import 'package:spotube/hooks/controllers/use_sidebarx_controller.dart';
import 'package:spotube/modules/connect/connect_device.dart';
import 'package:spotube/pages/profile/profile.dart';
import 'package:spotube/pages/settings/settings.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/authentication/authentication.dart';
import 'package:spotube/provider/download_manager_provider.dart';
import 'package:spotube/provider/spotify/spotify.dart';
import 'package:spotube/provider/user_preferences/user_preferences_provider.dart';
import 'package:spotube/utils/platform.dart';
import 'package:spotube/utils/service_utils.dart';
import 'package:window_manager/window_manager.dart';
class Sidebar extends HookConsumerWidget {
final Widget child;
@ -66,56 +61,34 @@ class Sidebar extends HookConsumerWidget {
(e) => routerState.namedLocation(e.name) == routerState.matchedLocation,
);
final controller = useSidebarXController(
selectedIndex: selectedIndex,
extended: mediaQuery.lgAndUp,
);
final theme = Theme.of(context);
final bg = theme.colorScheme.surfaceContainerHighest;
final bgColor = useBrightnessValue(
Color.lerp(bg, Colors.white, 0.6),
Color.lerp(bg, Colors.black, 0.45)!,
);
useEffect(() {
if (!context.mounted) return;
if (mediaQuery.lgAndUp && !controller.extended) {
controller.setExtended(true);
} else if (mediaQuery.mdAndDown && controller.extended) {
controller.setExtended(false);
}
return null;
}, [mediaQuery, controller]);
useEffect(() {
if (controller.selectedIndex != selectedIndex) {
controller.selectIndex(selectedIndex);
}
return null;
}, [selectedIndex]);
if (layoutMode == LayoutMode.compact ||
(mediaQuery.smAndDown && layoutMode == LayoutMode.adaptive)) {
return Scaffold(body: child);
return Scaffold(child: child);
}
return LayoutBuilder(builder: (context, constrains) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SafeArea(
child: SidebarX(
controller: controller,
items: sidebarTileList.mapIndexed(
(index, e) {
return SidebarXItem(
onTap: () {
context.goNamed(e.name);
child: Column(
children: [
Expanded(
child: NavigationSidebar(
index: selectedIndex,
onSelected: (index) {
final tile = sidebarTileList[index];
ServiceUtils.pushNamed(context, tile.name);
},
iconBuilder: (selected, hovered) {
return Badge(
backgroundColor: theme.colorScheme.primary,
isLabelVisible: e.title == "Library" && downloadCount > 0,
children: [
const NavigationLabel(child: Text("Spotube")),
for (final tile in sidebarTileList)
NavigationButton(
label: Text(tile.title),
child: Badge(
backgroundColor: context.theme.colorScheme.primary,
isLabelVisible:
tile.title == "Library" && downloadCount > 0,
label: Text(
downloadCount.toString(),
style: const TextStyle(
@ -123,116 +96,30 @@ class Sidebar extends HookConsumerWidget {
fontSize: 10,
),
),
child: Icon(
e.icon,
color: selected || hovered
? theme.colorScheme.primary
: null,
child: Icon(tile.icon),
),
);
onChanged: (value) {
if (value) {
context.goNamed(tile.name);
}
},
label: e.title,
);
},
).toList(),
headerBuilder: (_, __) => const SidebarHeader(),
footerBuilder: (_, __) => const Padding(
padding: EdgeInsets.only(bottom: 5),
child: SidebarFooter(),
),
showToggleButton: false,
theme: SidebarXTheme(
width: 50,
margin: EdgeInsets.only(bottom: 10, top: kIsMacOS ? 35 : 5),
selectedItemDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: theme.colorScheme.primary.withOpacity(0.1),
),
selectedIconTheme: IconThemeData(
color: theme.colorScheme.primary,
],
),
),
extendedTheme: SidebarXTheme(
width: 250,
margin: EdgeInsets.only(
bottom: 10,
left: 0,
top: kIsMacOS ? 0 : 5,
),
padding: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(
color: bgColor?.withOpacity(0.8),
borderRadius: const BorderRadius.only(
topRight: Radius.circular(10),
bottomRight: Radius.circular(10),
const SidebarFooter(),
],
),
),
selectedItemDecoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
color: theme.colorScheme.primary.withOpacity(0.1),
),
selectedIconTheme: IconThemeData(
color: theme.colorScheme.primary,
),
selectedTextStyle: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.primary,
fontWeight: FontWeight.w600,
),
itemTextPadding: const EdgeInsets.only(left: 10),
selectedItemTextPadding: const EdgeInsets.only(left: 10),
hoverTextStyle: theme.textTheme.bodyMedium?.copyWith(
color: theme.colorScheme.primary,
),
),
),
),
Expanded(child: child)
const VerticalDivider(),
Expanded(child: child),
],
);
});
}
}
class SidebarHeader extends HookWidget {
const SidebarHeader({super.key});
@override
Widget build(BuildContext context) {
final mediaQuery = MediaQuery.of(context);
final theme = Theme.of(context);
if (mediaQuery.mdAndDown) {
return Container(
height: 40,
width: 40,
margin: const EdgeInsets.only(bottom: 5),
child: Sidebar.brandLogo(),
);
}
return DragToMoveArea(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
if (kIsMacOS) const SizedBox(height: 25),
Row(
children: [
Sidebar.brandLogo(),
const SizedBox(width: 10),
Text(
"Spotube",
style: theme.textTheme.titleLarge,
),
],
),
],
),
),
);
}
}
class SidebarFooter extends HookConsumerWidget {
class SidebarFooter extends HookConsumerWidget implements NavigationBarItem {
const SidebarFooter({
super.key,
});
@ -253,6 +140,7 @@ class SidebarFooter extends HookConsumerWidget {
if (mediaQuery.mdAndDown) {
return IconButton(
variance: ButtonVariance.ghost,
icon: const Icon(SpotubeIcons.settings),
onPressed: () => ServiceUtils.navigateNamed(context, SettingsPage.name),
);
@ -260,8 +148,9 @@ class SidebarFooter extends HookConsumerWidget {
return Container(
padding: const EdgeInsets.only(left: 12),
width: 250,
width: 180,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const ConnectDeviceButton.sidebar(),
const Gap(10),
@ -273,21 +162,16 @@ class SidebarFooter extends HookConsumerWidget {
const CircularProgressIndicator()
else if (data != null)
Flexible(
child: InkWell(
child: GestureDetector(
onTap: () {
ServiceUtils.pushNamed(context, ProfilePage.name);
},
borderRadius: BorderRadius.circular(30),
child: Row(
children: [
CircleAvatar(
backgroundImage:
UniversalImage.imageProvider(avatarImg),
onBackgroundImageError: (exception, stackTrace) =>
Assets.userPlaceholder.image(
height: 16,
width: 16,
),
Avatar(
initials:
Avatar.getInitials(data.displayName ?? "User"),
provider: UniversalImage.imageProvider(avatarImg),
),
const SizedBox(width: 10),
Flexible(
@ -296,8 +180,8 @@ class SidebarFooter extends HookConsumerWidget {
maxLines: 1,
softWrap: false,
overflow: TextOverflow.fade,
style: theme.textTheme.bodyMedium
?.copyWith(fontWeight: FontWeight.bold),
style: theme.typography.normal
.copyWith(fontWeight: FontWeight.bold),
),
),
],
@ -305,6 +189,7 @@ class SidebarFooter extends HookConsumerWidget {
),
),
IconButton(
variance: ButtonVariance.ghost,
icon: const Icon(SpotubeIcons.settings),
onPressed: () {
ServiceUtils.pushNamed(context, SettingsPage.name);
@ -316,4 +201,7 @@ class SidebarFooter extends HookConsumerWidget {
),
);
}
@override
bool get selectable => false;
}

View File

@ -1,4 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/material.dart' hide ThemeMode;
import 'package:shadcn_flutter/shadcn_flutter.dart' show ThemeMode;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:gap/gap.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

View File

@ -1,8 +1,9 @@
import 'package:drift/drift.dart';
import 'package:flutter/material.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart' as paths;
import 'package:shadcn_flutter/shadcn_flutter.dart' hide join;
import 'package:spotify/spotify.dart';
import 'package:spotube/models/database/database.dart';
import 'package:spotube/modules/settings/color_scheme_picker_dialog.dart';

View File

@ -1,319 +0,0 @@
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart';
import 'package:hive/hive.dart';
import 'package:path_provider/path_provider.dart';
import 'package:spotube/models/database/database.dart'
hide
SourceType,
AudioSource,
CloseBehavior,
MusicCodec,
LayoutMode,
SearchMode,
BlacklistedType;
import 'package:spotube/models/database/database.dart' as db;
import 'package:spotube/services/kv_store/kv_store.dart';
import 'package:spotube/services/logger/logger.dart';
import 'package:spotube/utils/migrations/adapters.dart';
import 'package:spotube/utils/migrations/cache_box.dart';
late AppDatabase _database;
Future<String?> getHiveCacheDir() async =>
kIsWeb ? null : (await getApplicationSupportDirectory()).path;
Future<void> migrateAuthenticationInfo() async {
AppLogger.log.i("🔵 Migrating authentication info..");
final box = PersistenceCacheBox<AuthenticationCredentials>(
"authentication",
encrypted: true,
fromJson: (json) => AuthenticationCredentials.fromJson(json),
);
final credentials = await box.getData();
if (credentials == null) return;
await _database.into(_database.authenticationTable).insert(
AuthenticationTableCompanion.insert(
accessToken: DecryptedText(credentials.accessToken),
cookie: DecryptedText(credentials.cookie),
expiration: credentials.expiration,
id: const Value(0),
),
mode: InsertMode.insertOrReplace,
);
AppLogger.log.i("✅ Migrated authentication info");
}
Future<void> migratePreferences() async {
AppLogger.log.i("🔵 Migrating preferences..");
final box = PersistenceCacheBox<UserPreferences>(
"preferences",
fromJson: (json) => UserPreferences.fromJson(json),
);
final preferences = await box.getData();
if (preferences == null) return;
await _database.into(_database.preferencesTable).insert(
PreferencesTableCompanion.insert(
id: const Value(0),
accentColorScheme: Value(preferences.accentColorScheme),
albumColorSync: Value(preferences.albumColorSync),
amoledDarkTheme: Value(preferences.amoledDarkTheme),
audioQuality: Value(preferences.audioQuality),
audioSource: Value(
switch (preferences.audioSource) {
AudioSource.youtube => db.AudioSource.youtube,
AudioSource.piped => db.AudioSource.piped,
AudioSource.jiosaavn => db.AudioSource.jiosaavn,
},
),
checkUpdate: Value(preferences.checkUpdate),
closeBehavior: Value(
switch (preferences.closeBehavior) {
CloseBehavior.minimizeToTray => db.CloseBehavior.minimizeToTray,
CloseBehavior.close => db.CloseBehavior.close,
},
),
discordPresence: Value(preferences.discordPresence),
downloadLocation: Value(preferences.downloadLocation),
downloadMusicCodec: Value(preferences.downloadMusicCodec),
enableConnect: Value(preferences.enableConnect),
endlessPlayback: Value(preferences.endlessPlayback),
layoutMode: Value(
switch (preferences.layoutMode) {
LayoutMode.adaptive => db.LayoutMode.adaptive,
LayoutMode.compact => db.LayoutMode.compact,
LayoutMode.extended => db.LayoutMode.extended,
},
),
localLibraryLocation: Value(preferences.localLibraryLocation),
locale: Value(preferences.locale),
market: Value(preferences.recommendationMarket),
normalizeAudio: Value(preferences.normalizeAudio),
pipedInstance: Value(preferences.pipedInstance),
searchMode: Value(
switch (preferences.searchMode) {
SearchMode.youtube => db.SearchMode.youtube,
SearchMode.youtubeMusic => db.SearchMode.youtubeMusic,
},
),
showSystemTrayIcon: Value(preferences.showSystemTrayIcon),
skipNonMusic: Value(preferences.skipNonMusic),
streamMusicCodec: Value(preferences.streamMusicCodec),
systemTitleBar: Value(preferences.systemTitleBar),
themeMode: Value(preferences.themeMode),
),
mode: InsertMode.replace,
);
AppLogger.log.i("✅ Migrated preferences");
}
Future<void> migrateSkipSegment() async {
AppLogger.log.i("🔵 Migrating skip segments..");
Hive.registerAdapter(SkipSegmentAdapter());
final box = await Hive.openLazyBox(
SkipSegment.boxName,
path: await getHiveCacheDir(),
);
final skipSegments = await Future.wait(
box.keys.map(
(key) async => (
id: key as String,
data: await box.get(key),
),
),
);
await _database.batch((batch) {
batch.insertAll(
_database.skipSegmentTable,
skipSegments
.where((element) => element.data != null)
.expand((element) => (element.data as List).map(
(segment) => SkipSegmentTableCompanion.insert(
trackId: element.id,
start: segment["start"],
end: segment["end"],
),
))
.toList(),
);
});
AppLogger.log.i("✅ Migrated skip segments");
}
Future<void> migrateSourceMatches() async {
AppLogger.log.i("🔵 Migrating source matches..");
Hive.registerAdapter(SourceMatchAdapter());
Hive.registerAdapter(SourceTypeAdapter());
final box = await Hive.openBox<SourceMatch>(
SourceMatch.boxName,
path: await getHiveCacheDir(),
);
final sourceMatches =
box.keys.map((key) => (data: box.get(key), trackId: key));
await _database.batch((batch) {
batch.insertAll(
_database.sourceMatchTable,
sourceMatches
.where((element) => element.data != null)
.map(
(sourceMatch) => SourceMatchTableCompanion.insert(
sourceId: sourceMatch.data!.sourceId,
trackId: sourceMatch.trackId,
sourceType: Value(
switch (sourceMatch.data!.sourceType) {
SourceType.jiosaavn => db.SourceType.jiosaavn,
SourceType.youtube => db.SourceType.youtube,
SourceType.youtubeMusic => db.SourceType.youtubeMusic,
},
),
),
)
.toList(),
);
});
AppLogger.log.i("✅ Migrated source matches");
}
Future<void> migrateBlacklist() async {
AppLogger.log.i("🔵 Migrating blacklist..");
final box = PersistenceCacheBox<Set<BlacklistedElement>>(
"blacklist",
fromJson: (json) => (json["blacklist"] as List)
.map((e) => BlacklistedElement.fromJson(e))
.toSet(),
);
final data = await box.getData();
if (data == null) return;
await _database.batch((batch) {
batch.insertAll(
_database.blacklistTable,
data.map(
(element) => BlacklistTableCompanion.insert(
name: element.name,
elementId: element.id,
elementType: switch (element.type) {
BlacklistedType.artist => db.BlacklistedType.artist,
BlacklistedType.track => db.BlacklistedType.track,
},
),
),
);
});
AppLogger.log.i("✅ Migrated blacklist");
}
Future<void> migrateLastFmCredentials() async {
AppLogger.log.i("🔵 Migrating Last.fm credentials..");
final box = PersistenceCacheBox<ScrobblerState>(
"scrobbler",
fromJson: (json) => ScrobblerState.fromJson(json),
encrypted: true,
);
final data = await box.getData();
if (data == null) return;
await _database.into(_database.scrobblerTable).insert(
ScrobblerTableCompanion.insert(
id: const Value(0),
passwordHash: DecryptedText(data.passwordHash),
username: data.username,
),
mode: InsertMode.replace,
);
AppLogger.log.i("✅ Migrated Last.fm credentials");
}
Future<void> migratePlaybackHistory() async {
AppLogger.log.i("🔵 Migrating playback history..");
final box = PersistenceCacheBox<PlaybackHistoryState>(
"playback_history",
fromJson: (json) => PlaybackHistoryState.fromJson(json),
);
final data = await box.getData();
if (data == null) return;
await _database.batch((batch) {
batch.insertAll(
_database.historyTable,
data.items.map(
(item) => switch (item) {
PlaybackHistoryAlbum() => HistoryTableCompanion.insert(
createdAt: Value(item.date),
itemId: item.album.id!,
data: item.album.toJson(),
type: db.HistoryEntryType.album,
),
PlaybackHistoryPlaylist() => HistoryTableCompanion.insert(
createdAt: Value(item.date),
itemId: item.playlist.id!,
data: item.playlist.toJson(),
type: db.HistoryEntryType.playlist,
),
PlaybackHistoryTrack() => HistoryTableCompanion.insert(
createdAt: Value(item.date),
itemId: item.track.id!,
data: item.track.toJson(),
type: db.HistoryEntryType.track,
),
_ => throw Exception("Unknown history item type"),
},
),
);
});
AppLogger.log.i("✅ Migrated playback history");
}
Future<void> migrateFromHiveToDrift(AppDatabase database) async {
if (KVStoreService.hasMigratedToDrift) return;
await PersistenceCacheBox.initializeBoxes(
path: await getHiveCacheDir(),
);
_database = database;
await migrateAuthenticationInfo();
await migratePreferences();
await migrateSkipSegment();
await migrateSourceMatches();
await migrateBlacklist();
await migratePlaybackHistory();
await migrateLastFmCredentials();
await KVStoreService.setHasMigratedToDrift(true);
AppLogger.log.i("🚀 Migrated all data to Drift");
}

View File

@ -6,6 +6,7 @@
#include "generated_plugin_registrant.h"
#include <audioplayers_linux/audioplayers_linux_plugin.h>
#include <desktop_webview_window/desktop_webview_window_plugin.h>
#include <file_selector_linux/file_selector_plugin.h>
#include <flutter_secure_storage_linux/flutter_secure_storage_linux_plugin.h>
@ -21,6 +22,9 @@
#include <window_manager/window_manager_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) audioplayers_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "AudioplayersLinuxPlugin");
audioplayers_linux_plugin_register_with_registrar(audioplayers_linux_registrar);
g_autoptr(FlPluginRegistrar) desktop_webview_window_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "DesktopWebviewWindowPlugin");
desktop_webview_window_plugin_register_with_registrar(desktop_webview_window_registrar);

View File

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_linux
desktop_webview_window
file_selector_linux
flutter_secure_storage_linux

View File

@ -8,6 +8,7 @@ import Foundation
import app_links
import audio_service
import audio_session
import audioplayers_darwin
import bonsoir_darwin
import desktop_webview_window
import device_info_plus
@ -32,6 +33,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppLinksMacosPlugin.register(with: registry.registrar(forPlugin: "AppLinksMacosPlugin"))
AudioServicePlugin.register(with: registry.registrar(forPlugin: "AudioServicePlugin"))
AudioSessionPlugin.register(with: registry.registrar(forPlugin: "AudioSessionPlugin"))
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
SwiftBonsoirPlugin.register(with: registry.registrar(forPlugin: "SwiftBonsoirPlugin"))
DesktopWebviewWindowPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewWindowPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))

View File

@ -5,6 +5,8 @@ PODS:
- FlutterMacOS
- audio_session (0.0.1):
- FlutterMacOS
- audioplayers_darwin (0.0.1):
- FlutterMacOS
- bonsoir_darwin (0.0.1):
- Flutter
- FlutterMacOS
@ -46,20 +48,21 @@ PODS:
- sqflite_darwin (0.0.4):
- Flutter
- FlutterMacOS
- sqlite3 (3.47.0):
- sqlite3/common (= 3.47.0)
- sqlite3/common (3.47.0)
- sqlite3/dbstatvtab (3.47.0):
- sqlite3 (3.47.2):
- sqlite3/common (= 3.47.2)
- sqlite3/common (3.47.2)
- sqlite3/dbstatvtab (3.47.2):
- sqlite3/common
- sqlite3/fts5 (3.47.0):
- sqlite3/fts5 (3.47.2):
- sqlite3/common
- sqlite3/perf-threadsafe (3.47.0):
- sqlite3/perf-threadsafe (3.47.2):
- sqlite3/common
- sqlite3/rtree (3.47.0):
- sqlite3/rtree (3.47.2):
- sqlite3/common
- sqlite3_flutter_libs (0.0.1):
- Flutter
- FlutterMacOS
- sqlite3 (~> 3.47.0)
- sqlite3 (~> 3.47.1)
- sqlite3/dbstatvtab
- sqlite3/fts5
- sqlite3/perf-threadsafe
@ -77,6 +80,7 @@ DEPENDENCIES:
- app_links (from `Flutter/ephemeral/.symlinks/plugins/app_links/macos`)
- audio_service (from `Flutter/ephemeral/.symlinks/plugins/audio_service/macos`)
- audio_session (from `Flutter/ephemeral/.symlinks/plugins/audio_session/macos`)
- audioplayers_darwin (from `Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos`)
- bonsoir_darwin (from `Flutter/ephemeral/.symlinks/plugins/bonsoir_darwin/darwin`)
- desktop_webview_window (from `Flutter/ephemeral/.symlinks/plugins/desktop_webview_window/macos`)
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
@ -95,7 +99,7 @@ DEPENDENCIES:
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
- shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`)
- sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`)
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos`)
- sqlite3_flutter_libs (from `Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin`)
- system_theme (from `Flutter/ephemeral/.symlinks/plugins/system_theme/macos`)
- tray_manager (from `Flutter/ephemeral/.symlinks/plugins/tray_manager/macos`)
- url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`)
@ -113,6 +117,8 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/audio_service/macos
audio_session:
:path: Flutter/ephemeral/.symlinks/plugins/audio_session/macos
audioplayers_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/audioplayers_darwin/macos
bonsoir_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/bonsoir_darwin/darwin
desktop_webview_window:
@ -150,7 +156,7 @@ EXTERNAL SOURCES:
sqflite_darwin:
:path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin
sqlite3_flutter_libs:
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/macos
:path: Flutter/ephemeral/.symlinks/plugins/sqlite3_flutter_libs/darwin
system_theme:
:path: Flutter/ephemeral/.symlinks/plugins/system_theme/macos
tray_manager:
@ -164,6 +170,7 @@ SPEC CHECKSUMS:
app_links: 10e0a0ab602ffaf34d142cd4862f29d34b303b2a
audio_service: b88ff778e0e3915efd4cd1a5ad6f0beef0c950a9
audio_session: dea1f41890dbf1718f04a56f1d6150fd50039b72
audioplayers_darwin: dcad41de4fbd0099cb3749f7ab3b0cb8f70b810c
bonsoir_darwin: e3b8526c42ca46a885142df84229131dfabea842
desktop_webview_window: 89bb3d691f4c80314a10be312f4cd35db93a9d5a
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
@ -183,8 +190,8 @@ SPEC CHECKSUMS:
screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161
shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: 0aa20658a9b238a3b1ff7175eb7bdd863b0ab4fd
sqlite3_flutter_libs: f0b7a85544d8bac7b8bac12eac7d05bcfdd786d0
sqlite3: 7559e33dae4c78538df563795af3a86fc887ee71
sqlite3_flutter_libs: 1b4e98da20ebd4e9b1240269b78cdcf492dbe9f3
system_theme: c7b9f6659a5caa26c9bc2284da096781e9a6fcbc
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404

View File

@ -142,6 +142,62 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.21"
audioplayers:
dependency: transitive
description:
name: audioplayers
sha256: c346ba5a39dc208f1bab55fc239855f573d69b0e832402114bf0b793622adc4d
url: "https://pub.dev"
source: hosted
version: "6.1.0"
audioplayers_android:
dependency: transitive
description:
name: audioplayers_android
sha256: de576b890befe27175c2f511ba8b742bec83765fa97c3ce4282bba46212f58e4
url: "https://pub.dev"
source: hosted
version: "5.0.0"
audioplayers_darwin:
dependency: transitive
description:
name: audioplayers_darwin
sha256: e507887f3ff18d8e5a10a668d7bedc28206b12e10b98347797257c6ae1019c3b
url: "https://pub.dev"
source: hosted
version: "6.0.0"
audioplayers_linux:
dependency: transitive
description:
name: audioplayers_linux
sha256: "3d3d244c90436115417f170426ce768856d8fe4dfc5ed66a049d2890acfa82f9"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
audioplayers_platform_interface:
dependency: transitive
description:
name: audioplayers_platform_interface
sha256: "6834dd48dfb7bc6c2404998ebdd161f79cd3774a7e6779e1348d54a3bfdcfaa5"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
audioplayers_web:
dependency: transitive
description:
name: audioplayers_web
sha256: "3609bdf0e05e66a3d9750ee40b1e37f2a622c4edb796cc600b53a90a30a2ace4"
url: "https://pub.dev"
source: hosted
version: "5.0.1"
audioplayers_windows:
dependency: transitive
description:
name: audioplayers_windows
sha256: "8605762dddba992138d476f6a0c3afd9df30ac5b96039929063eceed416795c2"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
auto_size_text:
dependency: "direct main"
description:
@ -390,6 +446,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.1.2"
country_flags:
dependency: transitive
description:
name: country_flags
sha256: dad797491167a5b8dee465b969cb756795d842fdfc3fc1ff93f22e9c1884b73d
url: "https://pub.dev"
source: hosted
version: "3.1.0"
coverage:
dependency: transitive
description:
@ -462,6 +526,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.2.0"
data_widget:
dependency: transitive
description:
name: data_widget
sha256: "95388df890189014f702b7e93f9de6bcf7d45143a99f6288f31899f10be441ba"
url: "https://pub.dev"
source: hosted
version: "0.0.2"
dbus:
dependency: transitive
description:
@ -552,6 +624,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.15"
email_validator:
dependency: transitive
description:
name: email_validator
sha256: b19aa5d92fdd76fbc65112060c94d45ba855105a28bb6e462de7ff03b12fa1fb
url: "https://pub.dev"
source: hosted
version: "3.0.0"
encrypt:
dependency: "direct main"
description:
@ -1276,6 +1356,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.0"
jovial_misc:
dependency: transitive
description:
name: jovial_misc
sha256: "4b10a4cac4f492d9692e97699bff775efa84abdba29909124cbccf3126e31cea"
url: "https://pub.dev"
source: hosted
version: "0.9.0"
jovial_svg:
dependency: transitive
description:
name: jovial_svg
sha256: ca14d42956b9949c36333065c9141f100e930c918f57f4bd8dd59d35581bd3fc
url: "https://pub.dev"
source: hosted
version: "1.1.24"
js:
dependency: transitive
description:
@ -1740,6 +1836,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.2"
phonecodes:
dependency: transitive
description:
name: phonecodes
sha256: "094a76b0ba3d8f9c1c83044ae8783d46e6906703c86eb08facd876844c264bf5"
url: "https://pub.dev"
source: hosted
version: "0.0.3"
piped_client:
dependency: "direct main"
description:
@ -1748,6 +1852,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.1"
pixel_snap:
dependency: transitive
description:
name: pixel_snap
sha256: "677410ea37b07cd37ecb6d5e6c0d8d7615a7cf3bd92ba406fd1ac57e937d1fb0"
url: "https://pub.dev"
source: hosted
version: "0.1.5"
platform:
dependency: transitive
description:
@ -1925,6 +2037,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.1"
shadcn_flutter:
dependency: "direct main"
description:
name: shadcn_flutter
sha256: eaf10ec804beddf2059dd55b802188b64277a5e4fc577defbc7c012253caef1a
url: "https://pub.dev"
source: hosted
version: "0.0.23"
shared_preferences:
dependency: "direct main"
description:
@ -2258,6 +2378,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.3.0+3"
syntax_highlight:
dependency: transitive
description:
name: syntax_highlight
sha256: ee33b6aa82cc722bb9b40152a792181dee222353b486c0255fde666a3e3a4997
url: "https://pub.dev"
source: hosted
version: "0.4.0"
system_theme:
dependency: "direct main"
description:

View File

@ -102,6 +102,7 @@ dependencies:
ref: dart-3-support
url: https://github.com/KRTirtho/scrobblenaut.git
scroll_to_index: ^3.0.1
shadcn_flutter: ^0.0.23
shared_preferences: ^2.2.3
shelf: ^1.4.1
shelf_router: ^1.1.4
@ -165,6 +166,53 @@ flutter:
- assets/logos/
- assets/backgrounds/
- LICENSE
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
fonts:
- asset: packages/shadcn_flutter/icons/RadixIcons.otf
- family: BootstrapIcons
fonts:
- asset: packages/shadcn_flutter/icons/BootstrapIcons.otf
flutter_gen:
output: lib/collections

238
web/flutter_bootstrap.js Normal file
View File

@ -0,0 +1,238 @@
const words = [
'Something is happening. Please wait.',
'Please be patient. This may take a while.',
'While you wait, please consider that this is a good time to take a break.',
'Please wait. This is a good time to go grab a cup of coffee.',
'Sometimes the things that are worth waiting for take time.',
'Please wait. This is a good time to stretch your legs.',
'Posture check! Please wait while we load the application.',
];
const loaderWidget = `
<div style="padding-right: 32px; padding-bottom: 32px; font-smooth: always; display: flex; flex-direction: column; align-items: end">
Loading Application...
<div id="words" style="font-size: 16px; opacity: 0.6; font-weight: 300; text-align: right; margin-top: 4px">
${words[Math.floor(Math.random() * words.length)]}
</div>
</div>`
const shadcn_flutter_config = {
loaderWidget: loaderWidget,
backgroundColor: null,
foregroundColor: null,
loaderColor: null,
fontFamily: 'Geist Sans',
fontSize: '24px',
fontWeight: '400',
mainAxisAlignment: 'end',
crossAxisAlignment: 'end',
externalScripts: [
{
src: 'https://cdn.jsdelivr.net/npm/@fontsource/geist-sans@5.0.3/400.min.css',
type: 'stylesheet',
},
{
src: 'https://cdn.jsdelivr.net/npm/@fontsource/geist-sans@5.0.3/300.min.css',
type: 'stylesheet',
},
]
};
{{flutter_js}}
{{flutter_build_config}}
class ShadcnAppConfig {
background;
foreground;
fontFamily;
fontSize;
fontWeight;
mainAxisAlignment;
crossAxisAlignment;
loaderWidget;
loaderColor;
externalScripts;
constructor({ background, foreground, fontFamily, fontSize, fontWeight, mainAxisAlignment, crossAxisAlignment, loaderWidget, loaderColor, externalScripts }) {
this.background = background;
this.foreground = foreground;
this.fontFamily = fontFamily;
this.fontSize = fontSize;
this.fontWeight = fontWeight;
this.mainAxisAlignment = mainAxisAlignment;
this.crossAxisAlignment = crossAxisAlignment;
this.loaderWidget = loaderWidget;
this.loaderColor = loaderColor;
this.externalScripts = externalScripts;
if (this.background == null) {
this.background = localStorage.getItem('shadcn_flutter.background') || '#09090b';
}
if (this.foreground == null) {
this.foreground = localStorage.getItem('shadcn_flutter.foreground') || '#ffffff';
}
if (this.loaderColor == null) {
this.loaderColor = localStorage.getItem('shadcn_flutter.primary') || '#3c83f6';
}
}
}
class ShadcnAppThemeChangedEvent extends CustomEvent {
constructor(theme) {
super('shadcn_flutter_theme_changed', { detail: theme });
}
}
class ShadcnAppTheme {
background;
foreground;
primary;
constructor(background, foreground, primary) {
this.background = background;
this.foreground = foreground;
this.primary = primary;
}
}
class ShadcnApp {
config;
constructor(config) {
this.config = config;
}
loadApp() {
window.addEventListener('shadcn_flutter_app_ready', this.onAppReady);
window.addEventListener('shadcn_flutter_theme_changed', this.onThemeChanged);
this.#initializeDocument();
let externalScriptIndex = 0;
this.#loadExternalScripts(externalScriptIndex, () => {
_flutter.loader.load({
onEntrypointLoaded: async function(engineInitializer) {
const appRunner = await engineInitializer.initializeEngine();
await appRunner.runApp();
}
});
});
}
#loadExternalScripts(index, onDone) {
if (index >= this.config.externalScripts.length) {
onDone();
return;
}
this.#loadScriptDynamically(this.config.externalScripts[index], () => {
this.#loadExternalScripts(index + 1, onDone);
});
}
#createStyleSheet(css) {
const style = document.createElement('style');
style.type = 'text/css';
style.appendChild(document.createTextNode(css));
document.head.appendChild(style);
}
#loadScriptDynamically(src, callback) {
if (typeof src === 'string') {
src = { src: src, type: 'script' };
}
if (src.type === 'script') {
const script = document.createElement('script');
script.src = src.src;
script.onload = callback;
document.body.appendChild(script);
} else if (src.type === 'module') {
const script = document.createElement('script');
script.type = 'module';
script.src = src.src;
script.onload = callback;
document.body.appendChild(script);
} else if (src.type === 'stylesheet') {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.href = src.src;
link.onload = callback;
document.head.appendChild(link);
} else {
throw new Error('Unknown type of file to load: ' + src);
}
}
#initializeDocument() {
const loaderStyle = `
display: flex;
justify-content: ${this.config.mainAxisAlignment};
align-items: ${this.config.crossAxisAlignment};
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: ${this.config.background};
color: ${this.config.foreground};
z-index: 9998;
font-family: ${this.config.fontFamily};
font-size: ${this.config.fontSize};
font-weight: ${this.config.fontWeight};
text-align: center;
transition: opacity 0.5s;
opacity: 1;
pointer-events: initial;
`;
const loaderBarCss = `
.loader {
height: 7px;
background: repeating-linear-gradient(-45deg,${this.config.loaderColor} 0 15px,#000 0 20px) left/200% 100%;
animation: l3 20s infinite linear;
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 9999;
}
@keyframes l3 {
100% {background-position:right}
}`;
const loaderDiv = document.createElement('div');
loaderDiv.style.cssText = loaderStyle;
loaderDiv.innerHTML = this.config.loaderWidget;
document.body.appendChild(loaderDiv);
document.body.style.backgroundColor = this.config.background;
const loaderBarDiv = document.createElement('div');
loaderBarDiv.className = 'loader';
loaderDiv.appendChild(loaderBarDiv);
this.#createStyleSheet(loaderBarCss);
}
onAppReady() {
const loaderDiv = document.querySelector('div');
loaderDiv.style.opacity = 0;
loaderDiv.style.pointerEvents = 'none';
}
onThemeChanged(event) {
let theme = event.detail;
let background = theme['background'];
let foreground = theme['foreground'];
let primary = theme['primary'];
localStorage.setItem('shadcn_flutter.background', background);
localStorage.setItem('shadcn_flutter.foreground', foreground);
localStorage.setItem('shadcn_flutter.primary', primary);
}
}
globalThis.ShadcnApp = ShadcnApp;
globalThis.ShadcnAppConfig = ShadcnAppConfig;
globalThis.ShadcnAppThemeChangedEvent = ShadcnAppThemeChangedEvent;
globalThis.ShadcnAppTheme = ShadcnAppTheme;
const shadcn_flutter = new ShadcnApp(new ShadcnAppConfig(shadcn_flutter_config));
shadcn_flutter.loadApp();

View File

@ -7,6 +7,7 @@
#include "generated_plugin_registrant.h"
#include <app_links/app_links_plugin_c_api.h>
#include <audioplayers_windows/audioplayers_windows_plugin.h>
#include <bonsoir_windows/bonsoir_windows_plugin_c_api.h>
#include <desktop_webview_window/desktop_webview_window_plugin.h>
#include <file_selector_windows/file_selector_windows.h>
@ -25,6 +26,8 @@
void RegisterPlugins(flutter::PluginRegistry* registry) {
AppLinksPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AppLinksPluginCApi"));
AudioplayersWindowsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
BonsoirWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("BonsoirWindowsPluginCApi"));
DesktopWebviewWindowPluginRegisterWithRegistrar(

View File

@ -4,6 +4,7 @@
list(APPEND FLUTTER_PLUGIN_LIST
app_links
audioplayers_windows
bonsoir_windows
desktop_webview_window
file_selector_windows