Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor: introduce SnapModel #1290

Merged
merged 3 commits into from
Jul 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/snapd.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export 'src/snapd/snap_launcher.dart';
export 'src/snapd/snap_provider.dart';
export 'src/snapd/snap_model.dart';
export 'src/snapd/snapd_service.dart';
export 'src/snapd/snapx.dart';
122 changes: 45 additions & 77 deletions lib/src/detail/detail_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -23,78 +23,60 @@ class DetailPage extends ConsumerWidget {

@override
Widget build(BuildContext context, WidgetRef ref) {
final storeState = ref.watch(storeSnapProvider(snapName));
return storeState.when(
data: (storeSnap) {
final localState = ref.watch(localSnapProvider(snapName));
return localState.when(
data: (localSnap) => _SnapView(
storeSnap: storeSnap,
localSnap: localSnap,
),
error: (error, __) => _SnapView(
storeSnap: storeSnap,
),
loading: () => _SnapView(storeSnap: storeSnap, busy: true),
);
},
final model = ref.watch(snapModelProvider(snapName));
return model.state.when(
data: (_) => _SnapView(model: model),
error: (error, stackTrace) => ErrorWidget(error),
loading: () => const Center(child: YaruCircularProgressIndicator()),
);
}
}

class _SnapView extends ConsumerWidget {
const _SnapView({
this.storeSnap,
this.localSnap,
this.busy = false,
});
const _SnapView({required this.model});

final Snap? storeSnap;
final Snap? localSnap;
final bool busy;
final SnapModel model;

@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);

final channels = storeSnap?.channels.keys;
final selectedChannel = storeSnap != null
? ref.watch(selectedChannelProvider(storeSnap!.name))
: null;
final channels = model.availableChannels;
final selectedChannel = model.selectedChannel;

final channelInfo =
selectedChannel != null ? storeSnap?.channels[selectedChannel] : null;
final channelInfo = selectedChannel != null
? model.storeSnap?.channels[selectedChannel]
: null;

// TODO: move logic into view model, once app page UI is settled
final snapInfos = <SnapInfo>[
(
label: l10n.detailPageVersionLabel,
value: channelInfo?.version ??
storeSnap?.version ??
localSnap?.version ??
model.storeSnap?.version ??
model.localSnap?.version ??
'',
),
(
label: l10n.detailPageConfinementLabel,
value: channelInfo?.confinement.name ??
storeSnap?.confinement.name ??
localSnap?.confinement.name ??
model.storeSnap?.confinement.name ??
model.localSnap?.confinement.name ??
'',
),
if (storeSnap?.downloadSize != null)
if (model.storeSnap?.downloadSize != null)
(
label: l10n.detailPageDownloadSizeLabel,
value: context
.formatByteSize(channelInfo?.size ?? storeSnap!.downloadSize!)
value: context.formatByteSize(
channelInfo?.size ?? model.storeSnap!.downloadSize!)
),
(
label: l10n.detailPageLicenseLabel,
value: storeSnap?.license ?? localSnap?.license ?? '',
value: model.storeSnap?.license ?? model.localSnap?.license ?? '',
),
(
label: l10n.detailPageWebsiteLabel,
value: storeSnap?.website ?? localSnap?.website ?? '',
value: model.storeSnap?.website ?? model.localSnap?.website ?? '',
),
];

Expand All @@ -104,7 +86,7 @@ class _SnapView extends ConsumerWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const YaruBackButton(),
_Header(snap: storeSnap ?? localSnap),
_Header(snap: model.storeSnap ?? model.localSnap),
const SizedBox(height: kPagePadding),
Row(
children: [
Expand All @@ -118,19 +100,13 @@ class _SnapView extends ConsumerWidget {
itemBuilder: (_) => channels
.map((e) => PopupMenuItem(value: e, child: Text(e)))
.toList(),
onSelected: (value) => ref
.read(selectedChannelProvider(storeSnap!.name).notifier)
.state = value,
enabled: !busy,
onSelected: (value) => model.selectedChannel = value,
enabled: model.activeChanges.isEmpty,
child: Text(selectedChannel),
),
const SizedBox(width: 16),
Flexible(
child: _SnapActionButtons(
busy: busy,
localSnap: localSnap,
storeSnap: storeSnap,
),
child: _SnapActionButtons(model: model),
)
],
),
Expand All @@ -155,14 +131,16 @@ class _SnapView extends ConsumerWidget {
child: SizedBox(
width: double.infinity,
child: MarkdownBody(
data: storeSnap?.description ?? localSnap?.description ?? '',
data: model.storeSnap?.description ??
model.localSnap?.description ??
'',
),
),
),
if (storeSnap != null)
if (model.storeSnap != null)
_Section(
header: const Text('Gallery'),
child: SnapScreenshotGallery(snap: storeSnap!),
child: SnapScreenshotGallery(snap: model.storeSnap!),
),
],
),
Expand All @@ -172,42 +150,33 @@ class _SnapView extends ConsumerWidget {

class _SnapActionButtons extends ConsumerWidget {
const _SnapActionButtons({
required this.busy,
this.localSnap,
this.storeSnap,
}) : assert(localSnap != null || storeSnap != null);
required this.model,
});

final bool busy;
final Snap? localSnap;
final Snap? storeSnap;
final SnapModel model;

@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);

final localSnapNotifier = ref
.read(localSnapProvider(storeSnap?.name ?? localSnap!.name).notifier);
final snapLauncher =
localSnap != null ? ref.watch(launchProvider(localSnap!)) : null;
final refreshableSnaps = ref.watch(refreshProvider);
final selectedChannel = storeSnap != null
? ref.watch(selectedChannelProvider(storeSnap!.name))
final snapLauncher = model.localSnap != null
? ref.watch(launchProvider(model.localSnap!))
: null;
final canRefresh = localSnap == null
final refreshableSnaps = ref.watch(refreshProvider);
final canRefresh = model.localSnap == null
? false
: refreshableSnaps.whenOrNull(
data: (snaps) => snaps.singleWhereOrNull(
(snap) => snap.name == localSnap!.name)) !=
(snap) => snap.name == model.localSnap!.name)) !=
null ||
selectedChannel != localSnap!.trackingChannel;
model.selectedChannel != model.localSnap!.trackingChannel;

final installRemoveButton = PushButton.elevated(
onPressed: busy
onPressed: model.activeChanges.isNotEmpty
? null
: localSnap != null
? localSnapNotifier.remove
: () => localSnapNotifier.install(channel: selectedChannel),
child: busy
: model.localSnap != null
? model.remove
: () => model.install(),
child: model.activeChanges.isNotEmpty
? Center(
child: SizedBox.square(
dimension: IconTheme.of(context).size,
Expand All @@ -217,15 +186,14 @@ class _SnapActionButtons extends ConsumerWidget {
),
)
: Text(
localSnap != null
model.localSnap != null
? l10n.detailPageRemoveLabel
: l10n.detailPageInstallLabel,
),
);
final refreshButton = canRefresh
? PushButton.elevated(
onPressed: () =>
localSnapNotifier.refresh(channel: selectedChannel),
onPressed: () => model.refresh(),
child: Text(l10n.detailPageUpdateLabel),
)
: null;
Expand Down
104 changes: 104 additions & 0 deletions lib/src/snapd/snap_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import 'dart:async';

import 'package:collection/collection.dart';
import 'package:flutter/widgets.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:snapd/snapd.dart';
import 'package:ubuntu_service/ubuntu_service.dart';

import '/snapd.dart';

final snapModelProvider = ChangeNotifierProvider.family<SnapModel, String>(
(ref, snapName) => SnapModel(getService<SnapdService>(), snapName)..init());

class SnapModel extends ChangeNotifier {
SnapModel(this.snapd, this.snapName) : _state = const AsyncValue.loading();
final SnapdService snapd;
final String snapName;

final List<String> activeChanges = [];

AsyncValue<void> get state => _state;
AsyncValue<void> _state;

Snap? localSnap;
Snap? storeSnap;

String? get selectedChannel => _selectedChannel;
String? _selectedChannel;
set selectedChannel(String? channel) {
if (channel == _selectedChannel) return;
_selectedChannel = channel;
notifyListeners();
}

List<String>? get availableChannels => storeSnap?.channels.keys.toList();

StreamSubscription? storeSnapSubscription;

Future<void> init() async {
storeSnapSubscription = snapd.getStoreSnap(snapName).listen((snap) {
_setStoreSnap(snap);
_setDefaultSelectedChannel();
notifyListeners();
});
_state = await AsyncValue.guard(() async {
await _getLocalSnap();
_setDefaultSelectedChannel();
notifyListeners();
});
}

@override
Future<void> dispose() async {
await storeSnapSubscription?.cancel();
storeSnapSubscription = null;
super.dispose();
}

void _setStoreSnap(Snap? newStoreSnap) {
if (newStoreSnap == storeSnap) return;
storeSnap = newStoreSnap;
}

Future<void> _getLocalSnap() async {
try {
localSnap = await snapd.getSnap(snapName);
} on SnapdException catch (e) {
if (e.kind != 'snap-not-found') rethrow;
localSnap = null;
}
}

void _setDefaultSelectedChannel() {
final channels = storeSnap?.channels.keys;
final localChannel = localSnap?.trackingChannel;
if (localChannel != null && (channels?.contains(localChannel) ?? false)) {
_selectedChannel = localChannel;
} else if (channels?.contains('latest/stable') ?? false) {
_selectedChannel = 'latest/stable';
} else {
_selectedChannel =
channels?.firstWhereOrNull((c) => c.contains('stable')) ??
channels?.firstOrNull;
}
}

Future<void> _snapAction(Future<String> Function() action) async {
final changeId = await action.call();
activeChanges.add(changeId);
notifyListeners();
await snapd.waitChange(changeId);
activeChanges.removeWhere((id) => id == changeId);
await _getLocalSnap();
notifyListeners();
}

Future<void> install() =>
_snapAction(() => snapd.install(snapName, channel: selectedChannel));

Future<void> refresh() =>
_snapAction(() => snapd.refresh(snapName, channel: selectedChannel));

Future<void> remove() => _snapAction(() => snapd.remove(snapName));
}
Loading