diff --git a/frontend/lib/api/aur.dart b/frontend/lib/api/aur.dart new file mode 100644 index 0000000..b3ee770 --- /dev/null +++ b/frontend/lib/api/aur.dart @@ -0,0 +1,15 @@ +import 'package:aurcache/models/aur_package.dart'; + +import 'api_client.dart'; + +extension AURApi on ApiClient { + Future> getAurPackages(String query) async { + final resp = await getRawClient().get("/search?query=$query"); + + final responseObject = resp.data as List; + final List packages = responseObject + .map((e) => AurPackage.fromJson(e)) + .toList(growable: false); + return packages; + } +} diff --git a/frontend/lib/components/api/APIBuilder.dart b/frontend/lib/components/api/APIBuilder.dart index f605122..033549f 100644 --- a/frontend/lib/components/api/APIBuilder.dart +++ b/frontend/lib/components/api/APIBuilder.dart @@ -26,6 +26,15 @@ class _APIBuilderState extends State> { Timer? timer; + @override + void didUpdateWidget(APIBuilder oldWidget) { + if (oldWidget.dto != widget.dto) { + Provider.of(context, listen: false) + .loadFuture(context, dto: widget.dto); + } + super.didUpdateWidget(oldWidget); + } + @override void initState() { super.initState(); diff --git a/frontend/lib/components/aur_search_table.dart b/frontend/lib/components/aur_search_table.dart new file mode 100644 index 0000000..f0b606d --- /dev/null +++ b/frontend/lib/components/aur_search_table.dart @@ -0,0 +1,56 @@ +import 'package:aurcache/api/packages.dart'; +import 'package:aurcache/models/aur_package.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import '../api/API.dart'; +import '../constants/color_constants.dart'; +import 'confirm_popup.dart'; + +class AurSearchTable extends StatelessWidget { + const AurSearchTable({super.key, required this.data}); + final List data; + + @override + Widget build(BuildContext context) { + return DataTable( + horizontalMargin: 0, + columnSpacing: defaultPadding, + columns: const [ + DataColumn( + label: Text("Package Name"), + ), + DataColumn( + label: Text("Version"), + ), + DataColumn( + label: Text("Action"), + ), + ], + rows: + data.map((e) => buildDataRow(e, context)).toList(growable: false)); + } + + DataRow buildDataRow(AurPackage package, BuildContext context) { + return DataRow( + cells: [ + DataCell(Text(package.name)), + DataCell(Text(package.version.toString())), + DataCell( + TextButton( + child: const Text("Install", style: TextStyle(color: greenColor)), + onPressed: () async { + final confirmResult = await showConfirmationDialog( + context, + "Install Package?", + "Are you sure to install Package: ${package.name}", () async { + await API.addPackage(name: package.name); + context.go("/"); + }, null); + if (!confirmResult) return; + }, + ), + ), + ], + ); + } +} diff --git a/frontend/lib/components/confirm_popup.dart b/frontend/lib/components/confirm_popup.dart index 5b2163e..ce22e20 100644 --- a/frontend/lib/components/confirm_popup.dart +++ b/frontend/lib/components/confirm_popup.dart @@ -1,6 +1,12 @@ import 'package:flutter/material.dart'; -Future showDeleteConfirmationDialog(BuildContext context) async { +Future showConfirmationDialog( + BuildContext context, + String title, + String content, + void Function() successCallback, + void Function()? errorCallback, +) async { return (await showDialog( context: context, barrierDismissible: false, @@ -17,20 +23,24 @@ Future showDeleteConfirmationDialog(BuildContext context) async { ), // Delete confirmation dialog AlertDialog( - title: Text('Confirm Delete'), - content: Text('Are you sure you want to delete this item?'), + title: Text(title), + content: Text(content), actions: [ TextButton( onPressed: () { Navigator.of(context).pop(true); + successCallback(); }, - child: Text('Yes, Delete'), + child: const Text('Yes'), ), TextButton( onPressed: () { Navigator.of(context).pop(false); // Dismiss dialog + if (errorCallback != null) { + errorCallback(); + } }, - child: Text('Cancel'), + child: const Text('Cancel'), ), ], ), diff --git a/frontend/lib/components/packages_table.dart b/frontend/lib/components/packages_table.dart index 0e36ebb..29e13da 100644 --- a/frontend/lib/components/packages_table.dart +++ b/frontend/lib/components/packages_table.dart @@ -87,19 +87,18 @@ class PackagesTable extends StatelessWidget { child: const Text("Delete", style: TextStyle(color: Colors.redAccent)), onPressed: () async { - final confirmResult = - await showDeleteConfirmationDialog(context); - if (!confirmResult) return; - - final succ = await API.deletePackage(package.id); - if (succ) { - Provider.of(context, listen: false) - .refresh(context); - Provider.of(context, listen: false) - .refresh(context); - Provider.of(context, listen: false) - .refresh(context); - } + await showConfirmationDialog(context, "Delete Package", + "Are you sure to delete this Package?", () async { + final succ = await API.deletePackage(package.id); + if (succ) { + Provider.of(context, listen: false) + .refresh(context); + Provider.of(context, listen: false) + .refresh(context); + Provider.of(context, listen: false) + .refresh(context); + } + }, null); }, ), ], diff --git a/frontend/lib/components/routing/router.dart b/frontend/lib/components/routing/router.dart index 7fcb72d..17fa7c4 100644 --- a/frontend/lib/components/routing/router.dart +++ b/frontend/lib/components/routing/router.dart @@ -1,3 +1,4 @@ +import 'package:aurcache/screens/aur_screen.dart'; import 'package:aurcache/screens/build_screen.dart'; import 'package:aurcache/screens/builds_screen.dart'; import 'package:aurcache/screens/dashboard_screen.dart'; @@ -40,6 +41,10 @@ final appRouter = GoRouter( path: '/packages', builder: (context, state) => const PackagesScreen(), ), + GoRoute( + path: '/aur', + builder: (context, state) => AurScreen(), + ), GoRoute( path: '/package/:id', builder: (context, state) { diff --git a/frontend/lib/components/routing/side_menu.dart b/frontend/lib/components/routing/side_menu.dart index fbdbb32..6aadb09 100644 --- a/frontend/lib/components/routing/side_menu.dart +++ b/frontend/lib/components/routing/side_menu.dart @@ -43,12 +43,16 @@ class SideMenu extends StatelessWidget { DrawerListTile( title: "Builds", svgSrc: "assets/icons/menu_tran.svg", - press: () {}, + press: () { + context.go("/builds"); + }, ), DrawerListTile( title: "AUR", svgSrc: "assets/icons/menu_task.svg", - press: () {}, + press: () { + context.go("/aur"); + }, ), DrawerListTile( title: "Settings", diff --git a/frontend/lib/models/aur_package.dart b/frontend/lib/models/aur_package.dart new file mode 100644 index 0000000..e5407d7 --- /dev/null +++ b/frontend/lib/models/aur_package.dart @@ -0,0 +1,12 @@ +class AurPackage { + final String name, version; + + AurPackage({required this.name, required this.version}); + + factory AurPackage.fromJson(Map json) { + return AurPackage( + name: json["name"] as String, + version: json["version"] as String, + ); + } +} diff --git a/frontend/lib/providers/aur_search_provider.dart b/frontend/lib/providers/aur_search_provider.dart new file mode 100644 index 0000000..fbf1c81 --- /dev/null +++ b/frontend/lib/providers/aur_search_provider.dart @@ -0,0 +1,18 @@ +import 'package:aurcache/api/aur.dart'; +import 'package:aurcache/models/aur_package.dart'; + +import '../api/API.dart'; +import 'BaseProvider.dart'; + +class AurSearchDTO { + final String query; + + AurSearchDTO({required this.query}); +} + +class AURSearchProvider extends BaseProvider, AurSearchDTO> { + @override + loadFuture(context, {dto}) { + data = API.getAurPackages(dto!.query); + } +} diff --git a/frontend/lib/screens/aur_screen.dart b/frontend/lib/screens/aur_screen.dart new file mode 100644 index 0000000..bcaa6a0 --- /dev/null +++ b/frontend/lib/screens/aur_screen.dart @@ -0,0 +1,93 @@ +import 'dart:async'; + +import 'package:aurcache/components/aur_search_table.dart'; +import 'package:aurcache/models/aur_package.dart'; +import 'package:aurcache/providers/aur_search_provider.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import '../components/api/APIBuilder.dart'; +import '../constants/color_constants.dart'; +import '../providers/packages_provider.dart'; + +class AurScreen extends StatefulWidget { + const AurScreen({super.key}); + + @override + State createState() => _AurScreenState(); +} + +class _AurScreenState extends State { + TextEditingController controller = TextEditingController(); + String query = ""; + Timer? timer; + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar(), + body: MultiProvider( + providers: [ + ChangeNotifierProvider(create: (_) => PackagesProvider()), + ChangeNotifierProvider(create: (_) => AURSearchProvider()) + ], + child: Padding( + padding: const EdgeInsets.all(defaultPadding), + child: Container( + padding: const EdgeInsets.all(defaultPadding), + decoration: const BoxDecoration( + color: secondaryColor, + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "AUR Packages", + style: Theme.of(context).textTheme.subtitle1, + ), + const Text("Search:"), + TextField( + controller: controller, + onChanged: (value) { + // cancel old timer if active + timer?.cancel(); + // schedule new timer + timer = Timer(const Duration(milliseconds: 300), () { + setState(() { + query = value; + }); + }); + }, + decoration: + const InputDecoration(hintText: "Type to search...")), + SizedBox( + width: double.infinity, + child: APIBuilder, + AurSearchDTO>( + dto: AurSearchDTO(query: query), + onLoad: () => Center( + child: Column( + children: [ + const SizedBox( + height: 15, + ), + query.length < 3 + ? const Text( + "Type to search for an AUR package") + : const Text("loading") + ], + ), + ), + onData: (data) => AurSearchTable(data: data)), + ) + ], + ), + ), + ), + ), + ), + ); + } +} diff --git a/frontend/lib/screens/package_screen.dart b/frontend/lib/screens/package_screen.dart index 11877d1..83dd30f 100644 --- a/frontend/lib/screens/package_screen.dart +++ b/frontend/lib/screens/package_screen.dart @@ -61,23 +61,28 @@ class _PackageScreenState extends State { child: ElevatedButton( onPressed: () async { final confirmResult = - await showDeleteConfirmationDialog(context); - if (!confirmResult) return; + await showConfirmationDialog( + context, + "Delete Package", + "Are you sure to delete this Package?", + () async { + final succ = await API.deletePackage(pkg.id); + if (succ) { + context.pop(); - final succ = await API.deletePackage(pkg.id); - if (succ) { - context.pop(); - - Provider.of(context, - listen: false) - .refresh(context); - Provider.of(context, - listen: false) - .refresh(context); - Provider.of(context, - listen: false) - .refresh(context); - } + Provider.of(context, + listen: false) + .refresh(context); + Provider.of(context, + listen: false) + .refresh(context); + Provider.of(context, + listen: false) + .refresh(context); + } + }, + () {}, + ); }, child: const Text( "Delete",