upgrade min sdk; run formatter

This commit is contained in:
Fabian Freund
2025-06-02 22:25:10 +02:00
parent 8fccd767af
commit ad03cc9bac
171 changed files with 4817 additions and 4685 deletions
@@ -10,8 +10,9 @@ class FetchArticlesController extends _$FetchArticlesController {
Future<void> fetchAllArticles() async {
state = const AsyncLoading();
state = await AsyncValue.guard(() async {
final feeds =
await ref.read(feedRepositoryProvider.notifier).getAllFeeds();
final feeds = await ref
.read(feedRepositoryProvider.notifier)
.getAllFeeds();
await Future.wait(
feeds.map((feed) async {
@@ -11,19 +11,19 @@ String _$fetchArticlesControllerHash() =>
/// See also [FetchArticlesController].
@ProviderFor(FetchArticlesController)
final fetchArticlesControllerProvider = AutoDisposeNotifierProvider<
FetchArticlesController,
AsyncValue<void>
>.internal(
FetchArticlesController.new,
name: r'fetchArticlesControllerProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
final fetchArticlesControllerProvider =
AutoDisposeNotifierProvider<
FetchArticlesController,
AsyncValue<void>
>.internal(
FetchArticlesController.new,
name: r'fetchArticlesControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$fetchArticlesControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$FetchArticlesController = AutoDisposeNotifier<AsyncValue<void>>;
// ignore_for_file: type=lint
@@ -91,121 +91,118 @@ class FeedArticleScreen extends HookConsumerWidget {
}, [article]);
return NestedScrollView(
headerSliverBuilder:
(context, innerBoxIsScrolled) => [
SliverAppBar.large(
pinned: false,
flexibleSpace: FlexibleSpaceBar(
centerTitle: false,
titlePadding: EdgeInsetsDirectional.only(
start: 72,
end: 72,
bottom: bottomHeight + 16,
),
title: Text(
article.displayTitle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
background: articleImages?.firstOrNull.mapNotNull(
(img) => Image.network(
img.toString(),
fit: BoxFit.cover,
color: Theme.of(
context,
).colorScheme.surface.withAlpha(200),
colorBlendMode: BlendMode.darken,
),
),
),
bottom: PreferredSize(
preferredSize: Size(double.infinity, bottomHeight),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 16.0,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(),
Text(
'Published: ${hasArticleCreated ? ref.read(formatProvider.notifier).fullDateTimeWithTimezone(article.created!) : 'N/A'}',
style: Theme.of(context).textTheme.bodySmall
?.copyWith(fontStyle: FontStyle.italic),
),
if (hasArticleUpdated)
Text(
'Updated: ${ref.read(formatProvider.notifier).fullDateTimeWithTimezone(article.updated!)}',
style: Theme.of(context).textTheme.bodySmall
?.copyWith(fontStyle: FontStyle.italic),
),
if (hasAuthors)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
const Text('Authors:'),
const SizedBox(width: 8),
Expanded(
child: AuthorsHorizontalList(
authors: article.authors!,
),
),
],
),
),
if (hasTags)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
const Text('Tags:'),
const SizedBox(width: 8),
Expanded(
child: TagsHorizontalList(
tags: article.tags!,
),
),
],
),
),
],
),
),
),
actions: [
if (articleLink != null)
IconButton(
onPressed: () async {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(url: articleLink.uri);
if (context.mounted) {
BrowserRoute().go(context);
}
},
icon: const Icon(Icons.open_in_browser),
),
],
headerSliverBuilder: (context, innerBoxIsScrolled) => [
SliverAppBar.large(
pinned: false,
flexibleSpace: FlexibleSpaceBar(
centerTitle: false,
titlePadding: EdgeInsetsDirectional.only(
start: 72,
end: 72,
bottom: bottomHeight + 16,
),
// SliverToBoxAdapter(
// child: Padding(
// padding: const EdgeInsets.all(8.0),
// child: TabBar(
// controller: tabController,
// tabs: [
// ...tabs.map(
// (tab) => switch (tab) {
// _Pages.summary => const Tab(text: 'Summary'),
// _Pages.content => const Tab(text: 'Article'),
// },
// ),
// ],
// ),
// ),
// ),
title: Text(
article.displayTitle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
background: articleImages?.firstOrNull.mapNotNull(
(img) => Image.network(
img.toString(),
fit: BoxFit.cover,
color: Theme.of(
context,
).colorScheme.surface.withAlpha(200),
colorBlendMode: BlendMode.darken,
),
),
),
bottom: PreferredSize(
preferredSize: Size(double.infinity, bottomHeight),
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Divider(),
Text(
'Published: ${hasArticleCreated ? ref.read(formatProvider.notifier).fullDateTimeWithTimezone(article.created!) : 'N/A'}',
style: Theme.of(context).textTheme.bodySmall
?.copyWith(fontStyle: FontStyle.italic),
),
if (hasArticleUpdated)
Text(
'Updated: ${ref.read(formatProvider.notifier).fullDateTimeWithTimezone(article.updated!)}',
style: Theme.of(context).textTheme.bodySmall
?.copyWith(fontStyle: FontStyle.italic),
),
if (hasAuthors)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
const Text('Authors:'),
const SizedBox(width: 8),
Expanded(
child: AuthorsHorizontalList(
authors: article.authors!,
),
),
],
),
),
if (hasTags)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
const Text('Tags:'),
const SizedBox(width: 8),
Expanded(
child: TagsHorizontalList(
tags: article.tags!,
),
),
],
),
),
],
),
),
),
actions: [
if (articleLink != null)
IconButton(
onPressed: () async {
await ref
.read(tabRepositoryProvider.notifier)
.addTab(url: articleLink.uri);
if (context.mounted) {
BrowserRoute().go(context);
}
},
icon: const Icon(Icons.open_in_browser),
),
],
),
// SliverToBoxAdapter(
// child: Padding(
// padding: const EdgeInsets.all(8.0),
// child: TabBar(
// controller: tabController,
// tabs: [
// ...tabs.map(
// (tab) => switch (tab) {
// _Pages.summary => const Tab(text: 'Summary'),
// _Pages.content => const Tab(text: 'Article'),
// },
// ),
// ],
// ),
// ),
// ),
],
body: TabBarView(
controller: tabController,
children: [
@@ -230,17 +227,17 @@ class FeedArticleScreen extends HookConsumerWidget {
}
}
},
styleSheet: MarkdownStyleSheet.fromTheme(
Theme.of(context),
).copyWith(
blockquoteDecoration: BoxDecoration(
color:
Theme.of(
styleSheet:
MarkdownStyleSheet.fromTheme(
Theme.of(context),
).copyWith(
blockquoteDecoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.secondaryContainer,
borderRadius: BorderRadius.circular(2.0),
),
),
borderRadius: BorderRadius.circular(2.0),
),
),
data: switch (tab) {
_Pages.summary => article.summaryMarkdown!,
_Pages.content => article.contentMarkdown!,
@@ -253,13 +250,12 @@ class FeedArticleScreen extends HookConsumerWidget {
},
);
},
error:
(error, stackTrace) => Center(
child: FailureWidget(
title: 'Failed reading article',
exception: error,
),
),
error: (error, stackTrace) => Center(
child: FailureWidget(
title: 'Failed reading article',
exception: error,
),
),
loading: () => const Center(child: CircularProgressIndicator()),
),
);
@@ -70,21 +70,20 @@ class FeedArticleListScreen extends HookConsumerWidget {
controller: searchTextController,
decoration: InputDecoration(
label: const Text('Search'),
suffixIcon:
hasText
? IconButton(
onPressed: () {
searchTextController.clear();
focusNode.requestFocus();
},
icon: const Icon(Icons.clear),
)
: SpeechToTextButton(
onTextReceived: (data) {
searchTextController.text =
data.toString();
},
),
suffixIcon: hasText
? IconButton(
onPressed: () {
searchTextController.clear();
focusNode.requestFocus();
},
icon: const Icon(Icons.clear),
)
: SpeechToTextButton(
onTextReceived: (data) {
searchTextController.text = data
.toString();
},
),
),
),
const SizedBox(height: 4),
@@ -99,30 +98,29 @@ class FeedArticleListScreen extends HookConsumerWidget {
controller: controller,
shrinkWrap: true,
scrollDirection: Axis.horizontal,
children:
tags
.map(
(tag) => Padding(
padding: const EdgeInsets.only(
right: 8.0,
),
child: FilterChip(
label: Text(tag),
showCheckmark: false,
selected: true,
onSelected: (value) {},
onDeleted: () {
ref
.read(
articleFilterProvider
.notifier,
)
.removeTag(tag);
},
),
),
)
.toList(),
children: tags
.map(
(tag) => Padding(
padding: const EdgeInsets.only(
right: 8.0,
),
child: FilterChip(
label: Text(tag),
showCheckmark: false,
selected: true,
onSelected: (value) {},
onDeleted: () {
ref
.read(
articleFilterProvider
.notifier,
)
.removeTag(tag);
},
),
),
)
.toList(),
);
},
),
@@ -175,13 +173,12 @@ class FeedArticleListScreen extends HookConsumerWidget {
),
);
},
error:
(error, stackTrace) => Center(
child: FailureWidget(
title: 'Failed to load Articles',
exception: error,
),
),
error: (error, stackTrace) => Center(
child: FailureWidget(
title: 'Failed to load Articles',
exception: error,
),
),
loading: () => const SizedBox.shrink(),
);
},
@@ -60,39 +60,34 @@ class FeedEditScreen extends HookConsumerWidget {
return _FeedEditContent(mode: _mode, initialFeed: initialFeed);
},
error:
(error, stackTrace) => Scaffold(
key: const ValueKey('error'),
appBar: AppBar(),
body: Center(
child: FailureWidget(
title: 'Failed to load feed',
exception: error,
error: (error, stackTrace) => Scaffold(
key: const ValueKey('error'),
appBar: AppBar(),
body: Center(
child: FailureWidget(title: 'Failed to load feed', exception: error),
),
),
loading: () => Scaffold(
key: const ValueKey('loading'),
appBar: AppBar(
title: Text(switch (_mode) {
_DialogMode.create => 'New Feed',
_DialogMode.edit => 'Edit Feed',
}),
),
body: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
Padding(
padding: EdgeInsets.only(top: 8.0),
child: Text('Fetching feed...'),
),
),
),
loading:
() => Scaffold(
key: const ValueKey('loading'),
appBar: AppBar(
title: Text(switch (_mode) {
_DialogMode.create => 'New Feed',
_DialogMode.edit => 'Edit Feed',
}),
),
body: const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
CircularProgressIndicator(),
Padding(
padding: EdgeInsets.only(top: 8.0),
child: Text('Fetching feed...'),
),
],
),
),
],
),
),
),
);
}
}
@@ -141,11 +136,10 @@ class _FeedEditContent extends HookConsumerWidget {
onPressed: () async {
if (formKey.currentState?.validate() ?? false) {
final feedData = FeedData(
url:
uri_parser.tryParseUrl(
urlTextController.text,
eagerParsing: true,
)!,
url: uri_parser.tryParseUrl(
urlTextController.text,
eagerParsing: true,
)!,
authors: initialFeed.authors,
description: descriptionTextController.text.whenNotEmpty,
icon: uri_parser.tryParseUrl(
@@ -32,17 +32,16 @@ class FeedListScreen extends HookConsumerWidget {
),
);
},
error:
(error, stackTrace) => Center(
child: FailureWidget(
title: 'Failed to load Feeds',
exception: error,
onRetry: () {
// ignore: unused_result
ref.refresh(feedListProvider);
},
),
),
error: (error, stackTrace) => Center(
child: FailureWidget(
title: 'Failed to load Feeds',
exception: error,
onRetry: () {
// ignore: unused_result
ref.refresh(feedListProvider);
},
),
),
loading: () => const SizedBox.shrink(),
),
floatingActionButton: FloatingActionButton.extended(
@@ -15,51 +15,44 @@ class SelectFeedDialog extends HookConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
return SimpleDialog(
title: const Text('Add Feed'),
children:
feedUris
.map(
(uri) => HookConsumer(
builder: (context, ref, child) {
final feedAsync = ref.watch(fetchWebFeedProvider(uri));
children: feedUris
.map(
(uri) => HookConsumer(
builder: (context, ref, child) {
final feedAsync = ref.watch(fetchWebFeedProvider(uri));
return feedAsync.when(
data: (data) {
return ListTile(
title: Text(
data.feedData.title.whenNotEmpty ?? 'Unnamed Feed',
),
subtitle: Text(uri.toString()),
trailing: const Icon(Icons.add),
onTap: () {
FeedCreateRoute(
feedId: uri,
).pushReplacement(context);
},
);
return feedAsync.when(
data: (data) {
return ListTile(
title: Text(
data.feedData.title.whenNotEmpty ?? 'Unnamed Feed',
),
subtitle: Text(uri.toString()),
trailing: const Icon(Icons.add),
onTap: () {
FeedCreateRoute(feedId: uri).pushReplacement(context);
},
error:
(error, stackTrace) => FailureWidget(
title: 'Failed to fetch Feed',
exception: error,
onRetry: () {
// ignore: unused_result
ref.refresh(fetchWebFeedProvider(uri));
},
),
loading:
() => Skeletonizer(
child: ListTile(
title: Text(BoneMock.title),
subtitle: Skeleton.keep(
child: Text(uri.toString()),
),
),
),
);
},
),
)
.toList(),
error: (error, stackTrace) => FailureWidget(
title: 'Failed to fetch Feed',
exception: error,
onRetry: () {
// ignore: unused_result
ref.refresh(fetchWebFeedProvider(uri));
},
),
loading: () => Skeletonizer(
child: ListTile(
title: Text(BoneMock.title),
subtitle: Skeleton.keep(child: Text(uri.toString())),
),
),
);
},
),
)
.toList(),
);
}
}
@@ -11,26 +11,25 @@ class AuthorsHorizontalList extends StatelessWidget {
Set<String> selectedTags = const {},
void Function(String tagId, bool value)? onTagSelected,
}) {
_authors =
authors.map((author) {
final label = Text(
'${author.name ?? ''} ${author.email.mapNotNull((email) => '($email)') ?? ''}'
.trim(),
);
_authors = authors.map((author) {
final label = Text(
'${author.name ?? ''} ${author.email.mapNotNull((email) => '($email)') ?? ''}'
.trim(),
);
return onTagSelected.mapNotNull(
(onTagSelected) => FilterChip(
label: label,
selected: selectedTags.contains(author.name),
onSelected: (value) {
if (author.name.isNotEmpty) {
onTagSelected(author.name!, value);
}
},
),
) ??
Chip(label: label);
}).toList();
return onTagSelected.mapNotNull(
(onTagSelected) => FilterChip(
label: label,
selected: selectedTags.contains(author.name),
onSelected: (value) {
if (author.name.isNotEmpty) {
onTagSelected(author.name!, value);
}
},
),
) ??
Chip(label: label);
}).toList();
}
@override
@@ -70,11 +70,12 @@ class FeedArticleCard extends HookConsumerWidget {
MarkdownBody(
data: titleHighlight!,
styleSheet: MarkdownStyleSheet(
p: Theme.of(
context,
).textTheme.titleMedium?.copyWith(
color: Theme.of(context).colorScheme.onSurface,
),
p: Theme.of(context).textTheme.titleMedium
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurface,
),
),
),
if (titleHighlight.isEmpty)
@@ -86,23 +87,19 @@ class FeedArticleCard extends HookConsumerWidget {
MarkdownBody(
data: searchSnippet!,
styleSheet: MarkdownStyleSheet(
p: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(
color:
Theme.of(
p: Theme.of(context).textTheme.bodyMedium
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
),
a: Theme.of(
context,
).textTheme.bodyMedium?.copyWith(
color:
Theme.of(
),
a: Theme.of(context).textTheme.bodyMedium
?.copyWith(
color: Theme.of(
context,
).colorScheme.onSurfaceVariant,
decoration: TextDecoration.none,
),
decoration: TextDecoration.none,
),
),
),
if (searchSnippet.isEmpty &&
@@ -108,16 +108,18 @@ class FeedCard extends HookConsumerWidget {
Icon(
MdiIcons.newspaperVariantMultipleOutline,
size: 18,
color:
Theme.of(context).colorScheme.onPrimary,
color: Theme.of(
context,
).colorScheme.onPrimary,
),
const SizedBox(width: 4),
Text(
count.toString(),
textAlign: TextAlign.center,
style: TextStyle(
color:
Theme.of(context).colorScheme.onPrimary,
color: Theme.of(
context,
).colorScheme.onPrimary,
),
),
],
@@ -31,17 +31,16 @@ class TagField extends HookWidget {
const SizedBox(height: 4),
Wrap(
spacing: 8.0,
children:
tags.value
.map(
(tag) => InputChip(
label: Text(tag),
onDeleted: () {
tags.value = {...tags.value}..remove(tag);
},
),
)
.toList(),
children: tags.value
.map(
(tag) => InputChip(
label: Text(tag),
onDeleted: () {
tags.value = {...tags.value}..remove(tag);
},
),
)
.toList(),
),
TextField(
controller: textController,
@@ -68,12 +67,11 @@ class TagField extends HookWidget {
},
onSubmitted: (value) {
if (value.isNotEmpty) {
final values =
value
.split(_tagSplitPatter)
.map((str) => str.trim())
.where((str) => str.isNotEmpty)
.toList();
final values = value
.split(_tagSplitPatter)
.map((str) => str.trim())
.where((str) => str.isNotEmpty)
.toList();
if (values.isNotEmpty) {
textController.clear();
@@ -11,28 +11,26 @@ class TagsHorizontalList extends StatelessWidget {
Set<String> selectedTags = const {},
void Function(String tagId, bool value)? onTagSelected,
}) {
_tags =
tags.map((tag) {
final label = Text(
'${tag.id} ${tag.title.mapNotNull((title) => '($title)') ?? ''}'
.trim(),
);
_tags = tags.map((tag) {
final label = Text(
'${tag.id} ${tag.title.mapNotNull((title) => '($title)') ?? ''}'.trim(),
);
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child:
onTagSelected.mapNotNull(
(onTagSelected) => FilterChip(
label: label,
selected: selectedTags.contains(tag.id),
onSelected: (value) {
onTagSelected(tag.id, value);
},
),
) ??
Chip(label: label),
);
}).toList();
return Padding(
padding: const EdgeInsets.only(right: 8.0),
child:
onTagSelected.mapNotNull(
(onTagSelected) => FilterChip(
label: label,
selected: selectedTags.contains(tag.id),
onSelected: (value) {
onTagSelected(tag.id, value);
},
),
) ??
Chip(label: label),
);
}).toList();
}
@override