completed ui for current user apps
This commit is contained in:
parent
24f458b06f
commit
41b7d22fd9
14 changed files with 718 additions and 37 deletions
|
|
@ -1,11 +1,56 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
|
import 'package:bloc_concurrency/bloc_concurrency.dart';
|
||||||
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:meta/meta.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_event.dart';
|
||||||
part 'scoop_list_state.dart';
|
part 'scoop_list_state.dart';
|
||||||
|
|
||||||
class ScoopListBloc extends Bloc<ScoopListEvent, ScoopListState> {
|
class ScoopListBloc extends Bloc<ScoopListEvent, ScoopListState> {
|
||||||
ScoopListBloc() : super(ScoopListInitial()) {
|
ScoopListBloc() : super(ScoopListInitial()) {
|
||||||
on<ScoopListEvent>((event, emit) {});
|
on<ScoopLocate>(
|
||||||
|
(event, emit) {
|
||||||
|
if (scoopInstalled()) {
|
||||||
|
add(ScoopListRequested());
|
||||||
|
} else {
|
||||||
|
emit(ScoopNotFound());
|
||||||
|
}
|
||||||
|
},
|
||||||
|
transformer: droppable(),
|
||||||
|
);
|
||||||
|
on<ScoopUpdateRequested>(
|
||||||
|
(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<ScoopListRequested>(
|
||||||
|
(event, emit) async {
|
||||||
|
emit(ScoopListLoading());
|
||||||
|
final apps = await getInstalledScoopApps();
|
||||||
|
emit(ScoopLocalAppList(apps));
|
||||||
|
},
|
||||||
|
transformer: droppable(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,24 @@
|
||||||
part of 'scoop_list_bloc.dart';
|
part of 'scoop_list_bloc.dart';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
abstract class ScoopListEvent {}
|
abstract class ScoopListEvent extends Equatable {
|
||||||
|
const ScoopListEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> 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<Object> get props => [query];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,42 @@
|
||||||
part of 'scoop_list_bloc.dart';
|
part of 'scoop_list_bloc.dart';
|
||||||
|
|
||||||
@immutable
|
@immutable
|
||||||
abstract class ScoopListState {}
|
abstract class ScoopListState extends Equatable {
|
||||||
|
const ScoopListState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
class ScoopListInitial extends ScoopListState {}
|
class ScoopListInitial extends ScoopListState {}
|
||||||
|
|
||||||
|
class ScoopNotFound extends ScoopListState {}
|
||||||
|
|
||||||
|
class ScoopListLoading extends ScoopListState {}
|
||||||
|
|
||||||
|
class ScoopLocalAppList extends ScoopListState {
|
||||||
|
final List<ScoopAppModel> apps;
|
||||||
|
|
||||||
|
const ScoopLocalAppList(this.apps);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [apps];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScoopUpdateInProgress extends ScoopListState {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
const ScoopUpdateInProgress(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [message];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScoopUpdateError extends ScoopListState {
|
||||||
|
final String message;
|
||||||
|
|
||||||
|
const ScoopUpdateError(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object> get props => [message];
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
import 'package:ladle/bloc/scoop_list_bloc.dart';
|
import 'package:ladle/bloc/scoop_list_bloc.dart';
|
||||||
import 'package:system_theme/system_theme.dart';
|
import 'package:system_theme/system_theme.dart';
|
||||||
|
|
||||||
|
|
@ -12,8 +13,10 @@ void main() async {
|
||||||
|
|
||||||
await SystemTheme.accentColor.load();
|
await SystemTheme.accentColor.load();
|
||||||
|
|
||||||
|
Intl.defaultLocale = 'en_US';
|
||||||
|
|
||||||
doWhenWindowReady(() {
|
doWhenWindowReady(() {
|
||||||
const initialSize = Size(600, 450);
|
const initialSize = Size(800, 600);
|
||||||
appWindow.minSize = initialSize;
|
appWindow.minSize = initialSize;
|
||||||
appWindow.size = initialSize;
|
appWindow.size = initialSize;
|
||||||
appWindow.alignment = Alignment.center;
|
appWindow.alignment = Alignment.center;
|
||||||
|
|
@ -24,7 +27,7 @@ void main() async {
|
||||||
runApp(MultiBlocProvider(
|
runApp(MultiBlocProvider(
|
||||||
providers: [
|
providers: [
|
||||||
BlocProvider(
|
BlocProvider(
|
||||||
create: (context) => ScoopListBloc(),
|
create: (context) => ScoopListBloc()..add(ScoopLocate()),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
child: const LadleApp(),
|
child: const LadleApp(),
|
||||||
|
|
|
||||||
18
lib/models/scoop_app_model.dart
Normal file
18
lib/models/scoop_app_model.dart
Normal file
|
|
@ -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<Object?> get props => [name, description, bucket, updatedAt];
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,14 @@
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:fluent_ui/fluent_ui.dart';
|
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 {
|
class AppListFragment extends StatefulWidget {
|
||||||
const AppListFragment({Key? key}) : super(key: key);
|
const AppListFragment({Key? key}) : super(key: key);
|
||||||
|
|
@ -8,32 +18,272 @@ class AppListFragment extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _AppListFragmentState extends State<AppListFragment> {
|
class _AppListFragmentState extends State<AppListFragment> {
|
||||||
|
final scrollController = ScrollController();
|
||||||
|
final openedApps = <ScoopAppModel>{};
|
||||||
int currentIndex = 0;
|
int currentIndex = 0;
|
||||||
int tabs = 0;
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
final state = context.read<ScoopListBloc>().state;
|
||||||
height: 600,
|
if (state is! ScoopLocalAppList) {
|
||||||
child: TabView(
|
return const Center(child: Text("No apps installed"));
|
||||||
currentIndex: currentIndex,
|
}
|
||||||
onChanged: (index) => setState(() => currentIndex = index),
|
|
||||||
onNewPressed: () {
|
return ScaffoldPage(
|
||||||
setState(() => tabs++);
|
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(
|
content: Flex(
|
||||||
text: Text('Tab $index'),
|
direction: Axis.horizontal,
|
||||||
closeIcon: FluentIcons.chrome_close,
|
children: [
|
||||||
);
|
Expanded(
|
||||||
}),
|
flex: 1,
|
||||||
bodies: List.generate(
|
child: ListView.builder(
|
||||||
tabs,
|
key: PageStorageKey(state.apps.length),
|
||||||
(index) => Container(
|
controller: scrollController,
|
||||||
color: index.isEven ? Colors.red : Colors.yellow,
|
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<ScoopListBloc>().add(ScoopListRequested())),
|
||||||
|
).padding(right: 8),
|
||||||
|
OutlinedButton(
|
||||||
|
child: const Text("Uninstall"),
|
||||||
|
onPressed: () => _runScoopRoutine(
|
||||||
|
title: "Uninstalling ${appModel.name}",
|
||||||
|
params: ["uninstall", appModel.name],
|
||||||
|
).then((_) =>
|
||||||
|
context.read<ScoopListBloc>().add(ScoopListRequested())),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).paddingDirectional(bottom: 16),
|
||||||
|
Text(
|
||||||
|
appModel.description,
|
||||||
|
style: FluentTheme.of(context).typography.body,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
).padding(all: 16);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _runScoopRoutine({
|
||||||
|
String executable = "scoop",
|
||||||
|
required String title,
|
||||||
|
required List<String> 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<int>(
|
||||||
|
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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,10 @@
|
||||||
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
import 'package:bitsdojo_window/bitsdojo_window.dart';
|
||||||
import 'package:fluent_ui/fluent_ui.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 '../widgets/windows_buttons_widget.dart';
|
||||||
import 'app_list_fragment.dart';
|
import 'app_list_fragment.dart';
|
||||||
|
|
||||||
|
|
@ -16,23 +20,89 @@ class _HomePageState extends State<HomePage> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return NavigationView(
|
return BlocBuilder<ScoopListBloc, ScoopListState>(
|
||||||
appBar: _buildAppbar(),
|
builder: (context, state) {
|
||||||
pane: _buildNavigationPane(),
|
if (state is ScoopNotFound) {
|
||||||
content: _buildContent(),
|
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(
|
return NavigationAppBar(
|
||||||
title: MoveWindow(
|
title: MoveWindow(
|
||||||
child: const Align(
|
child: const Align(
|
||||||
alignment: Alignment.centerLeft,
|
alignment: Alignment.center,
|
||||||
child: Text("Ladle"),
|
child: Text("Ladle"),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
actions: MoveWindow(
|
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<ScoopListBloc>().add(ScoopUpdateRequested()),
|
||||||
|
),
|
||||||
|
WindowButton(
|
||||||
|
iconBuilder: (buttonContext) => const Icon(
|
||||||
|
FluentIcons.settings,
|
||||||
|
size: 12,
|
||||||
|
),
|
||||||
|
onPressed: () => Navigator.pushNamed(context, "/settings"),
|
||||||
|
),
|
||||||
|
const WindowButtons(),
|
||||||
|
]),
|
||||||
),
|
),
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
);
|
);
|
||||||
|
|
@ -41,17 +111,20 @@ class _HomePageState extends State<HomePage> {
|
||||||
NavigationPane _buildNavigationPane() {
|
NavigationPane _buildNavigationPane() {
|
||||||
return NavigationPane(
|
return NavigationPane(
|
||||||
selected: _fragmentIndex,
|
selected: _fragmentIndex,
|
||||||
displayMode: PaneDisplayMode.top,
|
displayMode: PaneDisplayMode.compact,
|
||||||
onChanged: (i) => setState(() => _fragmentIndex = i),
|
onChanged: (i) => setState(() => _fragmentIndex = i),
|
||||||
|
menuButton: const SizedBox(),
|
||||||
items: [
|
items: [
|
||||||
PaneItem(
|
PaneItem(
|
||||||
title: const Text("Home"),
|
title: const Text("Home"),
|
||||||
icon: const Icon(FluentIcons.home),
|
icon: const Icon(FluentIcons.home),
|
||||||
),
|
),
|
||||||
PaneItem(
|
PaneItem(
|
||||||
title: const Text("Updates"),
|
title: const Text("Apps"),
|
||||||
icon: const Icon(FluentIcons.update_restore),
|
icon: const Icon(FluentIcons.apps_content),
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
footerItems: [
|
||||||
PaneItemSeparator(),
|
PaneItemSeparator(),
|
||||||
PaneItem(
|
PaneItem(
|
||||||
title: const Text("Settings"),
|
title: const Text("Settings"),
|
||||||
|
|
@ -61,13 +134,34 @@ class _HomePageState extends State<HomePage> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
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(
|
return NavigationBody(
|
||||||
index: _fragmentIndex,
|
index: _fragmentIndex,
|
||||||
children: const [
|
children: const [
|
||||||
AppListFragment(),
|
AppListFragment(),
|
||||||
Center(child: Text("Scoops")),
|
Center(child: Text("Updates")),
|
||||||
Center(child: Text("Settings")),
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
100
lib/utils/scoop_utils.dart
Normal file
100
lib/utils/scoop_utils.dart
Normal file
|
|
@ -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<List<String>> getScoopBuckets() async {
|
||||||
|
final home = Platform.environment["UserProfile"];
|
||||||
|
final scoopBucketsDir = Directory("$home/scoop/buckets");
|
||||||
|
final buckets = <String>[];
|
||||||
|
|
||||||
|
if (await scoopBucketsDir.exists()) {
|
||||||
|
await scoopBucketsDir.list().forEach((element) {
|
||||||
|
if (element is Directory) {
|
||||||
|
buckets.add(element.path.split("\\").last);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return buckets;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<List<ScoopAppModel>> searchScoopApps(String query) async {
|
||||||
|
final home = Platform.environment["UserProfile"];
|
||||||
|
final buckets = await getScoopBuckets();
|
||||||
|
final apps = <ScoopAppModel>[];
|
||||||
|
|
||||||
|
for (final bucket in buckets) {
|
||||||
|
final bucketDir = Directory("$home/scoop/buckets/$bucket/bucket");
|
||||||
|
final files = bucketDir.listSync().whereType<File>().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<List<ScoopAppModel>> getInstalledScoopApps() async {
|
||||||
|
final home = Platform.environment["UserProfile"];
|
||||||
|
final scoopAppsDir = Directory("$home/scoop/apps");
|
||||||
|
final apps = <ScoopAppModel>[];
|
||||||
|
|
||||||
|
if (await scoopAppsDir.exists()) {
|
||||||
|
final elementsInDir = scoopAppsDir.listSync().whereType<Directory>();
|
||||||
|
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;
|
||||||
|
}
|
||||||
14
lib/utils/set_extension.dart
Normal file
14
lib/utils/set_extension.dart
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
extension SetExtension<T> on Set<T> {
|
||||||
|
int indexOf(T el) {
|
||||||
|
if (contains(el)) {
|
||||||
|
int idx = 0;
|
||||||
|
for (final e in this) {
|
||||||
|
if (e == el) {
|
||||||
|
return idx;
|
||||||
|
}
|
||||||
|
idx++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ class WindowButtons extends StatelessWidget {
|
||||||
mainAxisAlignment: MainAxisAlignment.end,
|
mainAxisAlignment: MainAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
MinimizeWindowButton(),
|
MinimizeWindowButton(),
|
||||||
|
MaximizeWindowButton(),
|
||||||
CloseWindowButton(),
|
CloseWindowButton(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
93
pubspec.lock
93
pubspec.lock
|
|
@ -50,6 +50,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "8.1.0"
|
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:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -148,7 +155,7 @@ packages:
|
||||||
source: sdk
|
source: sdk
|
||||||
version: "0.0.0"
|
version: "0.0.0"
|
||||||
intl:
|
intl:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: intl
|
name: intl
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
|
|
@ -161,6 +168,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.6.4"
|
version: "0.6.4"
|
||||||
|
lint:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: lint
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.0"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -257,6 +271,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
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:
|
string_scanner:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -264,6 +285,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.0"
|
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:
|
system_theme:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|
@ -292,6 +320,62 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.4.9"
|
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:
|
vector_math:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -306,6 +390,13 @@ packages:
|
||||||
url: "https://pub.dartlang.org"
|
url: "https://pub.dartlang.org"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.0"
|
version: "3.0.0"
|
||||||
|
yaml:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: yaml
|
||||||
|
url: "https://pub.dartlang.org"
|
||||||
|
source: hosted
|
||||||
|
version: "3.1.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=2.17.6 <3.0.0"
|
dart: ">=2.17.6 <3.0.0"
|
||||||
flutter: ">=3.0.0"
|
flutter: ">=3.0.0"
|
||||||
|
|
|
||||||
|
|
@ -9,13 +9,18 @@ environment:
|
||||||
dependencies:
|
dependencies:
|
||||||
bitsdojo_window: ^0.1.4
|
bitsdojo_window: ^0.1.4
|
||||||
bloc: ^8.1.0
|
bloc: ^8.1.0
|
||||||
|
bloc_concurrency: ^0.2.0
|
||||||
equatable: ^2.0.5
|
equatable: ^2.0.5
|
||||||
fluent_ui: ^3.12.0
|
fluent_ui: ^3.12.0
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_bloc: ^8.1.1
|
flutter_bloc: ^8.1.1
|
||||||
|
intl: ^0.17.0
|
||||||
meta: ^1.7.0
|
meta: ^1.7.0
|
||||||
|
styled_widget: ^0.4.0+3
|
||||||
system_theme: ^2.0.0
|
system_theme: ^2.0.0
|
||||||
|
url_launcher: ^6.1.5
|
||||||
|
yaml: ^3.1.1
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,13 @@
|
||||||
|
|
||||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||||
#include <system_theme/system_theme_plugin.h>
|
#include <system_theme/system_theme_plugin.h>
|
||||||
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
BitsdojoWindowPluginRegisterWithRegistrar(
|
BitsdojoWindowPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
||||||
SystemThemePluginRegisterWithRegistrar(
|
SystemThemePluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("SystemThemePlugin"));
|
registry->GetRegistrarForPlugin("SystemThemePlugin"));
|
||||||
|
UrlLauncherWindowsRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
bitsdojo_window_windows
|
bitsdojo_window_windows
|
||||||
system_theme
|
system_theme
|
||||||
|
url_launcher_windows
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue