diff --git a/lib/bloc/scoop_list_bloc.dart b/lib/bloc/scoop_list_bloc.dart index cf0d87e..d2bf6af 100644 --- a/lib/bloc/scoop_list_bloc.dart +++ b/lib/bloc/scoop_list_bloc.dart @@ -1,11 +1,56 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:bloc/bloc.dart'; +import 'package:bloc_concurrency/bloc_concurrency.dart'; +import 'package:equatable/equatable.dart'; import 'package:meta/meta.dart'; +import '../models/scoop_app_model.dart'; +import '../utils/scoop_utils.dart'; + part 'scoop_list_event.dart'; part 'scoop_list_state.dart'; class ScoopListBloc extends Bloc { ScoopListBloc() : super(ScoopListInitial()) { - on((event, emit) {}); + on( + (event, emit) { + if (scoopInstalled()) { + add(ScoopListRequested()); + } else { + emit(ScoopNotFound()); + } + }, + transformer: droppable(), + ); + on( + (event, emit) async { + try { + final process = + await Process.start('scoop', ["update"], runInShell: true); + await process.stdout.transform(utf8.decoder).forEach((e) { + emit(ScoopUpdateInProgress(e)); + }); + int exitCode = await process.exitCode; + if (exitCode != 0) { + throw Exception( + "Command `scoop update` failed with code $exitCode"); + } + add(ScoopListRequested()); + } catch (e) { + emit(ScoopUpdateError(e.toString())); + } + }, + transformer: droppable(), + ); + on( + (event, emit) async { + emit(ScoopListLoading()); + final apps = await getInstalledScoopApps(); + emit(ScoopLocalAppList(apps)); + }, + transformer: droppable(), + ); } } diff --git a/lib/bloc/scoop_list_event.dart b/lib/bloc/scoop_list_event.dart index 97f923f..7dbdd6c 100644 --- a/lib/bloc/scoop_list_event.dart +++ b/lib/bloc/scoop_list_event.dart @@ -1,4 +1,24 @@ part of 'scoop_list_bloc.dart'; @immutable -abstract class ScoopListEvent {} +abstract class ScoopListEvent extends Equatable { + const ScoopListEvent(); + + @override + List get props => []; +} + +class ScoopLocate extends ScoopListEvent {} + +class ScoopUpdateRequested extends ScoopListEvent {} + +class ScoopListRequested extends ScoopListEvent {} + +class ScoopSearchRequested extends ScoopListEvent { + final String query; + + const ScoopSearchRequested(this.query); + + @override + List get props => [query]; +} diff --git a/lib/bloc/scoop_list_state.dart b/lib/bloc/scoop_list_state.dart index dfbd4c9..909bd41 100644 --- a/lib/bloc/scoop_list_state.dart +++ b/lib/bloc/scoop_list_state.dart @@ -1,6 +1,42 @@ part of 'scoop_list_bloc.dart'; @immutable -abstract class ScoopListState {} +abstract class ScoopListState extends Equatable { + const ScoopListState(); + + @override + List get props => []; +} class ScoopListInitial extends ScoopListState {} + +class ScoopNotFound extends ScoopListState {} + +class ScoopListLoading extends ScoopListState {} + +class ScoopLocalAppList extends ScoopListState { + final List apps; + + const ScoopLocalAppList(this.apps); + + @override + List get props => [apps]; +} + +class ScoopUpdateInProgress extends ScoopListState { + final String message; + + const ScoopUpdateInProgress(this.message); + + @override + List get props => [message]; +} + +class ScoopUpdateError extends ScoopListState { + final String message; + + const ScoopUpdateError(this.message); + + @override + List get props => [message]; +} diff --git a/lib/main.dart b/lib/main.dart index a238b51..2c9c292 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,6 +1,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; import 'package:ladle/bloc/scoop_list_bloc.dart'; import 'package:system_theme/system_theme.dart'; @@ -12,8 +13,10 @@ void main() async { await SystemTheme.accentColor.load(); + Intl.defaultLocale = 'en_US'; + doWhenWindowReady(() { - const initialSize = Size(600, 450); + const initialSize = Size(800, 600); appWindow.minSize = initialSize; appWindow.size = initialSize; appWindow.alignment = Alignment.center; @@ -24,7 +27,7 @@ void main() async { runApp(MultiBlocProvider( providers: [ BlocProvider( - create: (context) => ScoopListBloc(), + create: (context) => ScoopListBloc()..add(ScoopLocate()), ), ], child: const LadleApp(), diff --git a/lib/models/scoop_app_model.dart b/lib/models/scoop_app_model.dart new file mode 100644 index 0000000..9be26c6 --- /dev/null +++ b/lib/models/scoop_app_model.dart @@ -0,0 +1,18 @@ +import 'package:equatable/equatable.dart'; + +class ScoopAppModel extends Equatable { + final String name; + final String description; + final String bucket; + final DateTime updatedAt; + + const ScoopAppModel({ + required this.name, + required this.description, + required this.bucket, + required this.updatedAt, + }); + + @override + List get props => [name, description, bucket, updatedAt]; +} diff --git a/lib/pages/app_list_fragment.dart b/lib/pages/app_list_fragment.dart index a6c00a5..ffd1c2b 100644 --- a/lib/pages/app_list_fragment.dart +++ b/lib/pages/app_list_fragment.dart @@ -1,4 +1,14 @@ +import 'dart:convert'; +import 'dart:io'; + import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:intl/intl.dart'; +import 'package:ladle/models/scoop_app_model.dart'; +import 'package:ladle/utils/set_extension.dart'; +import 'package:styled_widget/styled_widget.dart'; + +import '../bloc/scoop_list_bloc.dart'; class AppListFragment extends StatefulWidget { const AppListFragment({Key? key}) : super(key: key); @@ -8,32 +18,272 @@ class AppListFragment extends StatefulWidget { } class _AppListFragmentState extends State { + final scrollController = ScrollController(); + final openedApps = {}; int currentIndex = 0; - int tabs = 0; @override Widget build(BuildContext context) { - return SizedBox( - height: 600, - child: TabView( - currentIndex: currentIndex, - onChanged: (index) => setState(() => currentIndex = index), - onNewPressed: () { - setState(() => tabs++); + final state = context.read().state; + if (state is! ScoopLocalAppList) { + return const Center(child: Text("No apps installed")); + } + + return ScaffoldPage( + padding: EdgeInsets.zero, + header: AutoSuggestBox( + items: state.apps.map((e) => e.name).toList(), + leadingIcon: const Icon(FluentIcons.search).padding(all: 16), + placeholder: "Search for apps", + onSelected: (value) { + final app = state.apps.firstWhere((e) => e.name == value); + setState(() { + openedApps.add(app); + }); }, - tabs: List.generate(tabs, (index) { - return Tab( - text: Text('Tab $index'), - closeIcon: FluentIcons.chrome_close, - ); - }), - bodies: List.generate( - tabs, - (index) => Container( - color: index.isEven ? Colors.red : Colors.yellow, + ), + content: Flex( + direction: Axis.horizontal, + children: [ + Expanded( + flex: 1, + child: ListView.builder( + key: PageStorageKey(state.apps.length), + controller: scrollController, + itemCount: state.apps.length, + itemBuilder: (context, index) => + _createAppWidget(state.apps[index]), + ).backgroundColor(Colors.black.withAlpha(50)), ), + Expanded( + flex: 3, + child: TabView( + wheelScroll: true, + currentIndex: currentIndex, + onChanged: (index) => setState(() => currentIndex = index), + tabs: openedApps + .map((e) => Tab( + text: Text(e.name), + closeIcon: FluentIcons.chrome_close, + onClosed: () => setState( + () { + final appIdx = openedApps.indexOf(e); + if (appIdx < currentIndex) { + currentIndex--; + } else { + currentIndex = 0; + } + openedApps.remove(e); + }, + ), + )) + .toList(growable: false), + bodies: openedApps.map(_createAppPage).toList(growable: false), + ), + ), + ], + ), + ); + } + + Widget _createAppWidget(ScoopAppModel appModel) { + return GestureDetector( + onTap: () => setState(() { + if (openedApps.contains(appModel)) { + currentIndex = openedApps.indexOf(appModel); + } else { + openedApps.add(appModel); + currentIndex = openedApps.length - 1; + } + }), + behavior: HitTestBehavior.opaque, + child: Container( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Expanded( + child: Text( + appModel.name, + style: const TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + ), + overflow: TextOverflow.ellipsis, + ), + ), + Text( + DateFormat("dd.MM.yyyy hh:mm") + .format(appModel.updatedAt), + style: const TextStyle( + fontSize: 10, + ), + ), + ], + ), + Text( + appModel.description, + style: const TextStyle( + fontSize: 12, + ), + overflow: TextOverflow.ellipsis, + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Icon( + FluentIcons.repo_solid, + size: 12, + ), + const SizedBox(width: 5), + Text( + appModel.bucket, + style: const TextStyle( + fontSize: 12, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ], + ), + ), + ], ), ), ); } + + Widget _createAppPage(ScoopAppModel appModel) { + return Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(appModel.name).fontSize(32).bold().paddingDirectional(bottom: 16), + Row( + children: [ + Chip( + image: const Icon(FluentIcons.repo_solid), + text: Text(appModel.bucket), + ).padding(right: 8), + Chip.selected( + image: const Icon(FluentIcons.calendar), + text: Text( + DateFormat("dd.MM.yyyy hh:mm").format(appModel.updatedAt)), + ).padding(right: 8), + OutlinedButton( + child: const Text("Update"), + onPressed: () => _runScoopRoutine( + title: "Updating ${appModel.name}", + params: ["update", appModel.name], + ), + ).padding(right: 8), + OutlinedButton( + child: const Text("Reinstall"), + onPressed: () => _runScoopRoutine( + title: "Reinstalling ${appModel.name}", + executable: "powershell", + params: [ + "-c", + "scoop", + "uninstall", + appModel.name, + "&&", + "scoop", + "install", + appModel.name + ], + ).then((_) => + context.read().add(ScoopListRequested())), + ).padding(right: 8), + OutlinedButton( + child: const Text("Uninstall"), + onPressed: () => _runScoopRoutine( + title: "Uninstalling ${appModel.name}", + params: ["uninstall", appModel.name], + ).then((_) => + context.read().add(ScoopListRequested())), + ), + ], + ).paddingDirectional(bottom: 16), + Text( + appModel.description, + style: FluentTheme.of(context).typography.body, + ), + ], + ).padding(all: 16); + } + + Future _runScoopRoutine({ + String executable = "scoop", + required String title, + required List params, + }) async { + final textController = TextEditingController(); + final textScroller = ScrollController(); + final process = await Process.start(executable, params, runInShell: true); + await showDialog( + context: context, + barrierDismissible: false, + builder: (context) { + textController.text = "Waiting for process to start...\n"; + process.stdout.transform(utf8.decoder).forEach((element) { + element = element.trim(); + if (element.isEmpty) return; + textController.text += + "${DateFormat("hh:mm:ss").format(DateTime.now())} $element\n"; + textScroller.jumpTo(textScroller.position.maxScrollExtent); + }); + process.stderr.transform(utf8.decoder).forEach((element) { + element = element.trim(); + if (element.isEmpty) return; + textController.text += + "${DateFormat("hh:mm:ss").format(DateTime.now())} stderr: $element\n"; + textScroller.jumpTo(textScroller.position.maxScrollExtent); + }); + return FutureBuilder( + future: process.exitCode, + builder: (context, snapshot) { + return ContentDialog( + constraints: const BoxConstraints(minWidth: 1000), + title: Text(title), + content: TextBox( + enabled: false, + maxLines: 5, + minLines: 5, + controller: textController, + scrollController: textScroller, + scrollPhysics: snapshot.hasData + ? null + : const NeverScrollableScrollPhysics(), + ), + actions: snapshot.hasData + ? [ + Button( + child: const Text("Close"), + onPressed: () => Navigator.of(context).pop(), + ), + ] + : [ + Button( + child: const Text("Cancel"), + onPressed: () { + process.kill(); + Navigator.pop(context); + }, + ) + ], + ); + }, + ); + }, + ); + process.kill(); + } } diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 29c550a..a3e041f 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -1,6 +1,10 @@ import 'package:bitsdojo_window/bitsdojo_window.dart'; import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:url_launcher/url_launcher.dart'; +import '../bloc/scoop_list_bloc.dart'; import '../widgets/windows_buttons_widget.dart'; import 'app_list_fragment.dart'; @@ -16,23 +20,89 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { - return NavigationView( - appBar: _buildAppbar(), - pane: _buildNavigationPane(), - content: _buildContent(), + return BlocBuilder( + builder: (context, state) { + if (state is ScoopNotFound) { + return _appWithoutScoopInstalled(); + } + return NavigationView( + appBar: _buildAppbar(state), + pane: _buildNavigationPane(), + content: _buildContent(state), + ); + }, ); } - NavigationAppBar _buildAppbar() { + Widget _appWithoutScoopInstalled() { + return NavigationView( + appBar: NavigationAppBar( + title: MoveWindow( + child: const Align( + alignment: Alignment.center, + child: Text("Ladle"), + ), + ), + actions: MoveWindow( + child: Row(children: const [ + Spacer(), + WindowButtons(), + ]), + ), + automaticallyImplyLeading: false, + ), + content: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + "Scoop not found!\nYou can find installation instructions here:", + textAlign: TextAlign.center, + ), + Button( + onPressed: () { + launchUrl(Uri.parse("https://scoop.sh/")); + }, + child: const Text("Scoop website"), + ).padding(all: 10), + ], + ), + ); + } + + NavigationAppBar _buildAppbar(ScoopListState state) { return NavigationAppBar( title: MoveWindow( child: const Align( - alignment: Alignment.centerLeft, + alignment: Alignment.center, child: Text("Ladle"), ), ), actions: MoveWindow( - child: Row(children: const [Spacer(), WindowButtons()]), + child: Row(children: [ + const Spacer(), + WindowButton( + iconBuilder: (buttonContext) => Center( + child: state is ScoopListLoading + ? const ProgressBar() + : const Icon( + FluentIcons.refresh, + size: 12, + ), + ), + onPressed: state is ScoopListLoading + ? null + : () => + context.read().add(ScoopUpdateRequested()), + ), + WindowButton( + iconBuilder: (buttonContext) => const Icon( + FluentIcons.settings, + size: 12, + ), + onPressed: () => Navigator.pushNamed(context, "/settings"), + ), + const WindowButtons(), + ]), ), automaticallyImplyLeading: false, ); @@ -41,17 +111,20 @@ class _HomePageState extends State { NavigationPane _buildNavigationPane() { return NavigationPane( selected: _fragmentIndex, - displayMode: PaneDisplayMode.top, + displayMode: PaneDisplayMode.compact, onChanged: (i) => setState(() => _fragmentIndex = i), + menuButton: const SizedBox(), items: [ PaneItem( title: const Text("Home"), icon: const Icon(FluentIcons.home), ), PaneItem( - title: const Text("Updates"), - icon: const Icon(FluentIcons.update_restore), + title: const Text("Apps"), + icon: const Icon(FluentIcons.apps_content), ), + ], + footerItems: [ PaneItemSeparator(), PaneItem( title: const Text("Settings"), @@ -61,13 +134,34 @@ class _HomePageState extends State { ); } - NavigationBody _buildContent() { + Widget _buildContent(ScoopListState state) { + if (state is ScoopListLoading) { + return const Center(child: ProgressBar()); + } else if (state is ScoopUpdateInProgress) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("Updating Scoop").fontSize(24), + Text(state.message).textColor(Colors.orange).italic(), + const ProgressBar(), + ], + ); + } else if (state is ScoopUpdateError) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text("Scoop update error").fontSize(24), + Text(state.message).textColor(Colors.red).italic(), + ], + ); + } return NavigationBody( index: _fragmentIndex, children: const [ AppListFragment(), - Center(child: Text("Scoops")), - Center(child: Text("Settings")), + Center(child: Text("Updates")), ], ); } diff --git a/lib/utils/scoop_utils.dart b/lib/utils/scoop_utils.dart new file mode 100644 index 0000000..84a065a --- /dev/null +++ b/lib/utils/scoop_utils.dart @@ -0,0 +1,100 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:yaml/yaml.dart'; + +import '../models/scoop_app_model.dart'; + +bool scoopInstalled() { + final home = Platform.environment["UserProfile"]; + return Directory("$home/scoop").existsSync(); +} + +Future> getScoopBuckets() async { + final home = Platform.environment["UserProfile"]; + final scoopBucketsDir = Directory("$home/scoop/buckets"); + final buckets = []; + + if (await scoopBucketsDir.exists()) { + await scoopBucketsDir.list().forEach((element) { + if (element is Directory) { + buckets.add(element.path.split("\\").last); + } + }); + } + + return buckets; +} + +Future> searchScoopApps(String query) async { + final home = Platform.environment["UserProfile"]; + final buckets = await getScoopBuckets(); + final apps = []; + + for (final bucket in buckets) { + final bucketDir = Directory("$home/scoop/buckets/$bucket/bucket"); + final files = bucketDir.listSync().whereType().where( + (element) => + element.path.endsWith(".json") || + element.path.endsWith(".yml") || + element.path.endsWith(".yaml"), + ); + for (final file in files) { + final content = await file.readAsString(); + final data = + file.path.endsWith(".json") ? jsonDecode(content) : loadYaml(content); + + String appName = + file.path.split("\\").last.replaceAll(RegExp(r"\.(json|ya?ml)"), ""); + String appDescription = data["description"] ?? "No description"; + DateTime appUpdatedAt = await file.lastModified(); + + apps.add(ScoopAppModel( + name: appName, + description: appDescription, + bucket: bucket, + updatedAt: appUpdatedAt, + )); + } + } + + return apps; +} + +Future> getInstalledScoopApps() async { + final home = Platform.environment["UserProfile"]; + final scoopAppsDir = Directory("$home/scoop/apps"); + final apps = []; + + if (await scoopAppsDir.exists()) { + final elementsInDir = scoopAppsDir.listSync().whereType(); + for (final element in elementsInDir) { + String appName = element.path.split("\\").last; + String appDescription = "No description"; + String appBucket = "UNKNOWN"; + DateTime appUpdatedAt = DateTime.fromMicrosecondsSinceEpoch(0); + + final manifestFile = File("${element.path}/current/manifest.json"); + if (await manifestFile.exists()) { + final manifestData = jsonDecode(await manifestFile.readAsString()); + appDescription = manifestData["description"] ?? appDescription; + appUpdatedAt = await manifestFile.lastModified(); + } + + final installFile = File("${element.path}/current/install.json"); + if (await installFile.exists()) { + final installData = jsonDecode(await installFile.readAsString()); + appBucket = installData["bucket"] ?? appBucket; + } + + apps.add(ScoopAppModel( + name: appName, + description: appDescription, + bucket: appBucket, + updatedAt: appUpdatedAt, + )); + } + } + + return apps; +} diff --git a/lib/utils/set_extension.dart b/lib/utils/set_extension.dart new file mode 100644 index 0000000..d123c32 --- /dev/null +++ b/lib/utils/set_extension.dart @@ -0,0 +1,14 @@ +extension SetExtension on Set { + int indexOf(T el) { + if (contains(el)) { + int idx = 0; + for (final e in this) { + if (e == el) { + return idx; + } + idx++; + } + } + return -1; + } +} diff --git a/lib/widgets/windows_buttons_widget.dart b/lib/widgets/windows_buttons_widget.dart index 4831d4f..4195fda 100644 --- a/lib/widgets/windows_buttons_widget.dart +++ b/lib/widgets/windows_buttons_widget.dart @@ -16,6 +16,7 @@ class WindowButtons extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ MinimizeWindowButton(), + MaximizeWindowButton(), CloseWindowButton(), ], ), diff --git a/pubspec.lock b/pubspec.lock index 7773790..b6a5285 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -50,6 +50,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "8.1.0" + bloc_concurrency: + dependency: "direct main" + description: + name: bloc_concurrency + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" boolean_selector: dependency: transitive description: @@ -148,7 +155,7 @@ packages: source: sdk version: "0.0.0" intl: - dependency: transitive + dependency: "direct main" description: name: intl url: "https://pub.dartlang.org" @@ -161,6 +168,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.4" + lint: + dependency: transitive + description: + name: lint + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" lints: dependency: transitive description: @@ -257,6 +271,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + stream_transform: + dependency: transitive + description: + name: stream_transform + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" string_scanner: dependency: transitive description: @@ -264,6 +285,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + styled_widget: + dependency: "direct main" + description: + name: styled_widget + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.0+3" system_theme: dependency: "direct main" description: @@ -292,6 +320,62 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.9" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.5" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.18" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.17" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.13" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" vector_math: dependency: transitive description: @@ -306,6 +390,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0" + yaml: + dependency: "direct main" + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" sdks: dart: ">=2.17.6 <3.0.0" flutter: ">=3.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index 0ac10c8..21c780a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,13 +9,18 @@ environment: dependencies: bitsdojo_window: ^0.1.4 bloc: ^8.1.0 + bloc_concurrency: ^0.2.0 equatable: ^2.0.5 fluent_ui: ^3.12.0 flutter: sdk: flutter flutter_bloc: ^8.1.1 + intl: ^0.17.0 meta: ^1.7.0 + styled_widget: ^0.4.0+3 system_theme: ^2.0.0 + url_launcher: ^6.1.5 + yaml: ^3.1.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 4220b46..9b018e7 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,13 @@ #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { BitsdojoWindowPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("BitsdojoWindowPlugin")); SystemThemePluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("SystemThemePlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 017d938..65fa24a 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,6 +5,7 @@ list(APPEND FLUTTER_PLUGIN_LIST bitsdojo_window_windows system_theme + url_launcher_windows ) list(APPEND FLUTTER_FFI_PLUGIN_LIST