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_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<ScoopListEvent, ScoopListState> {
|
||||
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';
|
||||
|
||||
@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';
|
||||
|
||||
@immutable
|
||||
abstract class ScoopListState {}
|
||||
abstract class ScoopListState extends Equatable {
|
||||
const ScoopListState();
|
||||
|
||||
@override
|
||||
List<Object> get props => [];
|
||||
}
|
||||
|
||||
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: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(),
|
||||
|
|
|
|||
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: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<AppListFragment> {
|
||||
final scrollController = ScrollController();
|
||||
final openedApps = <ScoopAppModel>{};
|
||||
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<ScoopListBloc>().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<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: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<HomePage> {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return NavigationView(
|
||||
appBar: _buildAppbar(),
|
||||
pane: _buildNavigationPane(),
|
||||
content: _buildContent(),
|
||||
return BlocBuilder<ScoopListBloc, ScoopListState>(
|
||||
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<ScoopListBloc>().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<HomePage> {
|
|||
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<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(
|
||||
index: _fragmentIndex,
|
||||
children: const [
|
||||
AppListFragment(),
|
||||
Center(child: Text("Scoops")),
|
||||
Center(child: Text("Settings")),
|
||||
Center(child: Text("Updates")),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
|
|
|||
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,
|
||||
children: [
|
||||
MinimizeWindowButton(),
|
||||
MaximizeWindowButton(),
|
||||
CloseWindowButton(),
|
||||
],
|
||||
),
|
||||
|
|
|
|||
93
pubspec.lock
93
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"
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -8,10 +8,13 @@
|
|||
|
||||
#include <bitsdojo_window_windows/bitsdojo_window_plugin.h>
|
||||
#include <system_theme/system_theme_plugin.h>
|
||||
#include <url_launcher_windows/url_launcher_windows.h>
|
||||
|
||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||
BitsdojoWindowPluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("BitsdojoWindowPlugin"));
|
||||
SystemThemePluginRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("SystemThemePlugin"));
|
||||
UrlLauncherWindowsRegisterWithRegistrar(
|
||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
bitsdojo_window_windows
|
||||
system_theme
|
||||
url_launcher_windows
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue