diff --git a/dist/images/app-view.png b/dist/images/app-view.png index 8244311..4a11d35 100644 Binary files a/dist/images/app-view.png and b/dist/images/app-view.png differ diff --git a/lib/views/app_view.dart b/lib/views/app_view.dart index 59f05fa..8b3ad16 100644 --- a/lib/views/app_view.dart +++ b/lib/views/app_view.dart @@ -1,9 +1,9 @@ import 'package:flutter/material.dart'; import 'package:outlet/core/application.dart'; -import 'package:outlet/views/components/app_actions.dart'; import 'package:outlet/views/components/app_description.dart'; import 'package:outlet/views/components/app_info.dart'; import 'package:outlet/views/components/app_links.dart'; +import 'package:outlet/views/components/app_update.dart'; import 'package:outlet/views/components/screenshots.dart'; class AppView extends StatelessWidget { @@ -42,8 +42,8 @@ class AppView extends StatelessWidget { Expanded( flex: 1, child: Column(spacing: 16.0, children: [ - AppInfo(app: app!), - AppActions(id: app!.id), + AppInfo(id: app!.id), + if (!app!.current) AppUpdate(id: app!.id), AppDescription(app: app!), AppLinks(app: app!), ])), @@ -58,8 +58,8 @@ class AppView extends StatelessWidget { BoxConstraints(minHeight: viewportHeight), padding: const EdgeInsets.fromLTRB(20, 15, 20, 20), child: Column(spacing: 16.0, children: [ - AppInfo(app: app!), - AppActions(id: app!.id), + AppInfo(id: app!.id), + if (!app!.current) AppUpdate(id: app!.id), AppDescription(app: app!), Screenshots(screenshots: app!.screenshots), AppLinks(app: app!), diff --git a/lib/views/components/app_actions.dart b/lib/views/components/app_actions.dart deleted file mode 100644 index fdb6c99..0000000 --- a/lib/views/components/app_actions.dart +++ /dev/null @@ -1,138 +0,0 @@ -import 'dart:io'; -import 'dart:isolate'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:outlet/backends/backend.dart'; -import 'package:outlet/providers/action_queue.dart'; -import 'package:outlet/providers/application_provider.dart'; -import 'package:outlet/views/components/theme.dart'; - -class AppActions extends ConsumerWidget { - final String id; - - const AppActions({ - super.key, - required this.id, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final app = ref.watch(liveApplicationProvider(id)); - final actionQueue = ref.watch(actionQueueProvider.notifier); - bool isQueued = false; - - if (app != null) { - isQueued = ref.watch(isAppActionQueuedProvider(app.id)); - } - - String getInstallButtonText() { - if (app != null) { - if (app.installed) { - return 'Uninstall'; - } else if (isQueued) { - return 'Cancel'; - } - } - return 'Install'; - } - - final Color color = fgColor(context); - - return (app != null) - ? Container( - height: 70, - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(25), - boxShadow: const [ - BoxShadow(color: Colors.black12, blurRadius: 20.0) - ]), - child: Padding( - padding: const EdgeInsets.fromLTRB(20, 10, 20, 10), - child: Row(spacing: 10, children: [ - TextButton( - onPressed: () async { - if (app.installed) { - final data = { - "uninstallTarget": app.getUninstallTarget() - }; - await _uninstallWorker(data); - ref.read(installedAppListProvider.notifier).refresh(); - } else if (isQueued) { - actionQueue.removeAction(app.id); - } else { - final data = { - "installTarget": app.getInstallTarget(), - "remote": app.remote! - }; - actionQueue.add("Installing ${app.getLocalizedName()}", - app.id, _installWorker, data); - } - }, - style: const ButtonStyle( - backgroundColor: - WidgetStatePropertyAll(Colors.black), - ), - child: Text(getInstallButtonText(), - style: const TextStyle(color: Colors.white)), - ), - if (app.installed) - TextButton( - onPressed: () async { - var launch = app.launchCommand(); - await Process.start( - launch.split(' ').first, - launch.split(' ').sublist(1), - mode: ProcessStartMode.detached, - ); - }, - style: const ButtonStyle( - backgroundColor: - WidgetStatePropertyAll(Colors.black), - ), - child: const Text("Open", - style: TextStyle(color: Colors.white)), - ), - if (app.installed && app.current != true) - TextButton( - onPressed: () async { - final data = {"updateTarget": app.getUpdateTarget()}; - actionQueue.add("Updating ${app.getLocalizedName()}", - app.id, _updateWorker, data); - }, - style: const ButtonStyle( - backgroundColor: - WidgetStatePropertyAll(Colors.black), - ), - child: const Text("Update", - style: TextStyle(color: Colors.white)), - ), - Expanded(child: Container()), - ]), - ), - ) - : Container(); - } -} - -Future _installWorker(Map data) async { - Backend backend = getBackend(); - await Isolate.run(() { - backend.installApplication( - data["installTarget"] as String, data["remote"] as String); - }); -} - -Future _uninstallWorker(Map data) async { - Backend backend = getBackend(); - await Isolate.run(() { - backend.uninstallApplication(data["uninstallTarget"] as String); - }); -} - -Future _updateWorker(Map data) async { - Backend backend = getBackend(); - await Isolate.run(() { - backend.updateApplication(data["updateTarget"] as String); - }); -} diff --git a/lib/views/components/app_description.dart b/lib/views/components/app_description.dart index b461728..fb4e05c 100644 --- a/lib/views/components/app_description.dart +++ b/lib/views/components/app_description.dart @@ -1,6 +1,7 @@ import 'package:flutter_html/flutter_html.dart'; import 'package:flutter/material.dart'; import 'package:outlet/core/application.dart'; +import 'package:outlet/views/components/expandable_container.dart'; import 'package:outlet/views/components/theme.dart'; import 'package:outlet/views/components/badges.dart'; @@ -32,12 +33,13 @@ class AppDescription extends StatelessWidget { const SizedBox(height: 8), Row( children: [ - const Expanded( - child: Text('Description', - style: TextStyle( + Expanded( + child: Text(app.getLocalizedSummary(), + style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, ))), + const SizedBox(width: 10), CategoryList(categories: app.categories, size: 30), ], ), @@ -65,83 +67,3 @@ class AppDescription extends StatelessWidget { ); } } - -class ExpandableContainer extends StatefulWidget { - final Widget child; - final double maxHeight; - - const ExpandableContainer({ - super.key, - required this.child, - this.maxHeight = 150.0, - }); - - @override - State createState() => _ExpandableContainerState(); -} - -class _ExpandableContainerState extends State { - bool _isExpanded = false; - bool _showExpandButton = false; - - final GlobalKey _contentKey = GlobalKey(); - - @override - void initState() { - super.initState(); - WidgetsBinding.instance - .addPostFrameCallback((_) => _measureContentHeight()); - } - - void _measureContentHeight() { - final RenderBox? renderBox = - _contentKey.currentContext?.findRenderObject() as RenderBox?; - - if (renderBox != null && renderBox.hasSize) { - final double actualHeight = renderBox.size.height; - - if (actualHeight - widget.maxHeight > 20 && !_showExpandButton) { - setState(() { - _showExpandButton = true; - }); - } - } - } - - @override - Widget build(BuildContext context) { - final double maxConstraint = - _isExpanded || !_showExpandButton ? double.infinity : widget.maxHeight; - - return Column( - children: [ - ConstrainedBox( - constraints: BoxConstraints( - maxHeight: maxConstraint, - ), - child: KeyedSubtree( - key: _contentKey, - child: widget.child, - ), - ), - if (_showExpandButton) - Container( - alignment: Alignment.centerRight, - child: TextButton( - onPressed: () { - setState(() { - _isExpanded = !_isExpanded; - }); - }, - style: const ButtonStyle( - backgroundColor: WidgetStatePropertyAll(Colors.black), - ), - child: Text( - _isExpanded ? 'Show Less' : 'Show More', - style: const TextStyle(color: Colors.white), - ), - )), - ], - ); - } -} diff --git a/lib/views/components/app_info.dart b/lib/views/components/app_info.dart index da81549..cfa7739 100644 --- a/lib/views/components/app_info.dart +++ b/lib/views/components/app_info.dart @@ -1,74 +1,160 @@ +import 'dart:io'; +import 'dart:isolate'; import 'package:flutter/material.dart'; -import 'package:outlet/core/application.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:outlet/backends/backend.dart'; +import 'package:outlet/providers/action_queue.dart'; +import 'package:outlet/providers/application_provider.dart'; import 'package:outlet/views/components/theme.dart'; import 'package:outlet/views/components/app_icon_loader.dart'; import 'package:outlet/views/components/badges.dart'; -class AppInfo extends StatelessWidget { - final Application app; +class AppInfo extends ConsumerWidget { + final String id; const AppInfo({ super.key, - required this.app, + required this.id, }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final app = ref.watch(liveApplicationProvider(id)); + final actionQueue = ref.watch(actionQueueProvider.notifier); + final Color color = fgColor(context); + bool isQueued = false; - return Container( - height: 150, - decoration: BoxDecoration( - color: color, - borderRadius: BorderRadius.circular(25), - boxShadow: const [ - BoxShadow(color: Colors.black12, blurRadius: 20.0) - ]), - child: Padding( - padding: const EdgeInsets.all(20.0), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - spacing: 10.0, - children: [ - AppIconLoader( - icon: app.icon, - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row(spacing: 10, children: [ - Flexible( - child: Text( - app.getLocalizedName(), - softWrap: false, - overflow: TextOverflow.fade, - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - ), - )), - ]), - Expanded( - child: Text( - app.getLocalizedSummary(), - style: const TextStyle(fontSize: 16), - softWrap: true, - maxLines: 2, - overflow: TextOverflow.ellipsis, - )), - Row(spacing: 5, children: [ - Text((app.getLocalizedDeveloperName() != "" - ? "by ${app.getLocalizedDeveloperName()}" - : "")), - app.verified ? const VerifiedBadge(size: 20) : Container(), + if (app != null) { + isQueued = ref.watch(isAppActionQueuedProvider(app.id)); + } + + String getInstallButtonText() { + if (app != null) { + if (app.installed) { + return 'Uninstall'; + } else if (isQueued) { + return 'Cancel'; + } + } + return 'Install'; + } + + return (app != null) + ? Container( + height: 150, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(25), + boxShadow: const [ + BoxShadow(color: Colors.black12, blurRadius: 20.0) ]), - ], - )), - ], - ), - ), - ); + child: Padding( + padding: const EdgeInsets.all(20.0), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 10.0, + children: [ + AppIconLoader( + icon: app.icon, + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(spacing: 10, children: [ + Flexible( + child: Text( + app.getLocalizedName(), + softWrap: false, + overflow: TextOverflow.fade, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + ), + )), + ]), + Row(spacing: 5, children: [ + Text((app.getLocalizedDeveloperName() != "" + ? "by ${app.getLocalizedDeveloperName()}" + : "")), + app.verified + ? const VerifiedBadge(size: 20) + : Container(), + ]), + Expanded(child: Container()), + Row(spacing: 10, children: [ + if (app.installed) + TextButton( + onPressed: () async { + var launch = app.launchCommand(); + await Process.start( + launch.split(' ').first, + launch.split(' ').sublist(1), + mode: ProcessStartMode.detached, + ); + }, + style: const ButtonStyle( + backgroundColor: + WidgetStatePropertyAll(Colors.black), + ), + child: const Text("Open", + style: TextStyle(color: Colors.white)), + ), + TextButton( + onPressed: () async { + if (app.installed) { + final data = { + "uninstallTarget": app.getUninstallTarget() + }; + await _uninstallWorker(data); + ref + .read(installedAppListProvider.notifier) + .refresh(); + } else if (isQueued) { + actionQueue.removeAction(app.id); + } else { + final data = { + "installTarget": app.getInstallTarget(), + "remote": app.remote! + }; + actionQueue.add( + "Installing ${app.getLocalizedName()}", + app.id, + _installWorker, + data); + } + }, + style: const ButtonStyle( + backgroundColor: + WidgetStatePropertyAll(Colors.black), + ), + child: Text(getInstallButtonText(), + style: const TextStyle(color: Colors.white)), + ) + ]), + ], + )), + ], + ), + ), + ) + : Container(); } } + +Future _installWorker(Map data) async { + Backend backend = getBackend(); + await Isolate.run(() { + backend.installApplication( + data["installTarget"] as String, data["remote"] as String); + }); +} + +Future _uninstallWorker(Map data) async { + Backend backend = getBackend(); + await Isolate.run(() { + backend.uninstallApplication(data["uninstallTarget"] as String); + }); +} diff --git a/lib/views/components/app_update.dart b/lib/views/components/app_update.dart new file mode 100644 index 0000000..fbc0775 --- /dev/null +++ b/lib/views/components/app_update.dart @@ -0,0 +1,87 @@ +import 'dart:isolate'; +import 'package:flutter_html/flutter_html.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:outlet/backends/backend.dart'; +import 'package:outlet/providers/action_queue.dart'; +import 'package:outlet/providers/application_provider.dart'; +import 'package:outlet/views/components/expandable_container.dart'; +import 'package:outlet/views/components/theme.dart'; + +class AppUpdate extends ConsumerWidget { + final String id; + + const AppUpdate({ + super.key, + required this.id, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final app = ref.watch(liveApplicationProvider(id)); + final actionQueue = ref.watch(actionQueueProvider.notifier); + + final Color color = fgColor(context); + + return (app != null) + ? Container( + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(25), + boxShadow: const [ + BoxShadow(color: Colors.black12, blurRadius: 20.0) + ]), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column(children: [ + Row(spacing: 10, children: [ + Text("Version ${app.releases.first.version ?? 'unknown'}"), + if (app.installed && app.current != true) + TextButton( + onPressed: () async { + final data = {"updateTarget": app.getUpdateTarget()}; + actionQueue.add("Updating ${app.getLocalizedName()}", + app.id, _updateWorker, data); + }, + style: const ButtonStyle( + backgroundColor: + WidgetStatePropertyAll(Colors.black), + ), + child: const Text("Update", + style: TextStyle(color: Colors.white)), + ), + Expanded(child: Container()), + ]), + ExpandableContainer( + maxHeight: 100, + child: Html( + data: app.releases.first.description['C'] ?? + 'No release notes available.', + style: { + "h1": Style( + fontSize: FontSize.xxLarge, + textAlign: TextAlign.center, + ), + "p": Style( + margin: Margins.zero, + padding: HtmlPaddings.zero, + lineHeight: const LineHeight(1.5), + ), + "ul": Style( + padding: HtmlPaddings.only(left: 20), + margin: Margins.only(top: 8, bottom: 8), + ), + })), + ]), + ), + ) + : Container(); + } +} + +Future _updateWorker(Map data) async { + Backend backend = getBackend(); + await Isolate.run(() { + backend.updateApplication(data["updateTarget"] as String); + }); +} diff --git a/lib/views/components/expandable_container.dart b/lib/views/components/expandable_container.dart new file mode 100644 index 0000000..d5c4eec --- /dev/null +++ b/lib/views/components/expandable_container.dart @@ -0,0 +1,81 @@ +import 'package:flutter/material.dart'; + +class ExpandableContainer extends StatefulWidget { + final Widget child; + final double maxHeight; + + const ExpandableContainer({ + super.key, + required this.child, + this.maxHeight = 150.0, + }); + + @override + State createState() => _ExpandableContainerState(); +} + +class _ExpandableContainerState extends State { + bool _isExpanded = false; + bool _showExpandButton = false; + + final GlobalKey _contentKey = GlobalKey(); + + @override + void initState() { + super.initState(); + WidgetsBinding.instance + .addPostFrameCallback((_) => _measureContentHeight()); + } + + void _measureContentHeight() { + final RenderBox? renderBox = + _contentKey.currentContext?.findRenderObject() as RenderBox?; + + if (renderBox != null && renderBox.hasSize) { + final double actualHeight = renderBox.size.height; + + if (actualHeight - widget.maxHeight > 20 && !_showExpandButton) { + setState(() { + _showExpandButton = true; + }); + } + } + } + + @override + Widget build(BuildContext context) { + final double maxConstraint = + _isExpanded || !_showExpandButton ? double.infinity : widget.maxHeight; + + return Column( + children: [ + ConstrainedBox( + constraints: BoxConstraints( + maxHeight: maxConstraint, + ), + child: KeyedSubtree( + key: _contentKey, + child: widget.child, + ), + ), + if (_showExpandButton) + Container( + alignment: Alignment.centerRight, + child: TextButton( + onPressed: () { + setState(() { + _isExpanded = !_isExpanded; + }); + }, + style: const ButtonStyle( + backgroundColor: WidgetStatePropertyAll(Colors.black), + ), + child: Text( + _isExpanded ? 'Show Less' : 'Show More', + style: const TextStyle(color: Colors.white), + ), + )), + ], + ); + } +}