🎨 Use feature based folder structure
This commit is contained in:
108
lib/discovery/discovery/article_detail.dart
Normal file
108
lib/discovery/discovery/article_detail.dart
Normal file
@@ -0,0 +1,108 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/core/widgets/content/markdown.dart';
|
||||
import 'package:island/discovery/discovery/article_pod.dart';
|
||||
import 'package:url_launcher/url_launcher_string.dart';
|
||||
import 'package:island/discovery/discovery_models/webfeed.dart';
|
||||
import 'package:island/shared/widgets/app_scaffold.dart';
|
||||
import 'package:island/shared/widgets/loading_indicator.dart';
|
||||
import 'package:html2md/html2md.dart' as html2md;
|
||||
|
||||
class ArticleDetailScreen extends ConsumerWidget {
|
||||
final String articleId;
|
||||
|
||||
const ArticleDetailScreen({super.key, required this.articleId});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final articleAsync = ref.watch(articleDetailProvider(articleId));
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
body: articleAsync.when(
|
||||
data: (article) => AppScaffold(
|
||||
appBar: AppBar(
|
||||
leading: const BackButton(),
|
||||
title: Text(article.title),
|
||||
),
|
||||
body: _ArticleDetailContent(article: article),
|
||||
),
|
||||
loading: () => const Center(child: LoadingIndicator()),
|
||||
error: (error, stackTrace) =>
|
||||
Center(child: Text('Failed to load article: $error')),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ArticleDetailContent extends HookConsumerWidget {
|
||||
final SnWebArticle article;
|
||||
|
||||
const _ArticleDetailContent({required this.article});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final markdownContent = useMemoized(
|
||||
() => html2md.convert(article.content ?? ''),
|
||||
[article],
|
||||
);
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
if (article.preview?.imageUrl != null)
|
||||
Image.network(
|
||||
article.preview!.imageUrl!,
|
||||
width: double.infinity,
|
||||
height: 200,
|
||||
fit: BoxFit.cover,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
article.title,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
if (article.feed?.title != null)
|
||||
Text(
|
||||
article.feed!.title,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
const Divider(height: 32),
|
||||
if (article.content != null)
|
||||
...MarkdownTextContent.buildGenerator(
|
||||
isDark: Theme.of(context).brightness == Brightness.dark,
|
||||
).buildWidgets(markdownContent)
|
||||
else if (article.preview?.description != null)
|
||||
Text(article.preview!.description!),
|
||||
const Gap(24),
|
||||
FilledButton(
|
||||
onPressed: () => launchUrlString(
|
||||
article.url,
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
child: const Text('Read Full Article'),
|
||||
),
|
||||
Gap(MediaQuery.of(context).padding.bottom),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
30
lib/discovery/discovery/article_pod.dart
Normal file
30
lib/discovery/discovery/article_pod.dart
Normal file
@@ -0,0 +1,30 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/discovery/discovery_models/webfeed.dart';
|
||||
|
||||
/// Provider that fetches a single article by its ID
|
||||
final articleDetailProvider = FutureProvider.autoDispose
|
||||
.family<SnWebArticle, String>((ref, articleId) async {
|
||||
final dio = ref.watch(apiClientProvider);
|
||||
|
||||
try {
|
||||
final response = await dio.get<Map<String, dynamic>>(
|
||||
'/insight/feeds/articles/$articleId',
|
||||
);
|
||||
|
||||
if (response.statusCode == 200 && response.data != null) {
|
||||
return SnWebArticle.fromJson(response.data!);
|
||||
} else {
|
||||
throw Exception('Failed to load article');
|
||||
}
|
||||
} on DioException catch (e) {
|
||||
if (e.response?.statusCode == 404) {
|
||||
throw Exception('Article not found');
|
||||
} else {
|
||||
throw Exception('Failed to load article: ${e.message}');
|
||||
}
|
||||
} catch (e) {
|
||||
throw Exception('Failed to load article: $e');
|
||||
}
|
||||
});
|
||||
202
lib/discovery/discovery/articles.dart
Normal file
202
lib/discovery/discovery/articles.dart
Normal file
@@ -0,0 +1,202 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/discovery/discovery_models/webfeed.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/pagination/pagination.dart';
|
||||
import 'package:island/shared/widgets/app_scaffold.dart';
|
||||
import 'package:island/shared/widgets/pagination_list.dart';
|
||||
import 'package:island/discovery/web_article_card.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
|
||||
part 'articles.g.dart';
|
||||
part 'articles.freezed.dart';
|
||||
|
||||
@freezed
|
||||
sealed class ArticleListQuery with _$ArticleListQuery {
|
||||
const factory ArticleListQuery({String? feedId, String? publisherId}) =
|
||||
_ArticleListQuery;
|
||||
}
|
||||
|
||||
final articlesListNotifierProvider = AsyncNotifierProvider.family.autoDispose(
|
||||
ArticlesListNotifier.new,
|
||||
);
|
||||
|
||||
class ArticlesListNotifier extends AsyncNotifier<PaginationState<SnWebArticle>>
|
||||
with AsyncPaginationController<SnWebArticle> {
|
||||
static const int pageSize = 20;
|
||||
|
||||
final ArticleListQuery arg;
|
||||
ArticlesListNotifier(this.arg);
|
||||
|
||||
@override
|
||||
Future<List<SnWebArticle>> fetch() async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
final queryParams = {
|
||||
'limit': pageSize,
|
||||
'offset': fetchedCount.toString(),
|
||||
'feedId': arg.feedId,
|
||||
'publisherId': arg.publisherId,
|
||||
}..removeWhere((key, value) => value == null);
|
||||
|
||||
try {
|
||||
final response = await client.get(
|
||||
'/insight/feeds/articles',
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
|
||||
final articles = response.data
|
||||
.map((json) => SnWebArticle.fromJson(json as Map<String, dynamic>))
|
||||
.cast<SnWebArticle>()
|
||||
.toList();
|
||||
|
||||
totalCount = int.tryParse(response.headers.value('X-Total') ?? '0') ?? 0;
|
||||
|
||||
return articles;
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching articles: $e');
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class SliverArticlesList extends ConsumerWidget {
|
||||
final String? feedId;
|
||||
final String? publisherId;
|
||||
final Color? backgroundColor;
|
||||
final EdgeInsets? padding;
|
||||
final Function? onRefresh;
|
||||
|
||||
const SliverArticlesList({
|
||||
super.key,
|
||||
this.feedId,
|
||||
this.publisherId,
|
||||
this.backgroundColor,
|
||||
this.padding,
|
||||
this.onRefresh,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final provider = articlesListNotifierProvider(
|
||||
ArticleListQuery(feedId: feedId, publisherId: publisherId),
|
||||
);
|
||||
return PaginationList(
|
||||
spacing: 12,
|
||||
provider: provider,
|
||||
notifier: provider.notifier,
|
||||
isRefreshable: false,
|
||||
isSliver: true,
|
||||
itemBuilder: (context, index, article) {
|
||||
return WebArticleCard(article: article, showDetails: true);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
Future<List<SnWebFeed>> subscribedFeeds(Ref ref) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final response = await client.get('/insight/feeds/subscribed');
|
||||
final data = response.data as List<dynamic>;
|
||||
return data.map((json) => SnWebFeed.fromJson(json)).toList();
|
||||
}
|
||||
|
||||
class ArticlesScreen extends ConsumerWidget {
|
||||
const ArticlesScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final subscribedFeedsAsync = ref.watch(subscribedFeedsProvider);
|
||||
|
||||
return subscribedFeedsAsync.when(
|
||||
data: (feeds) {
|
||||
return DefaultTabController(
|
||||
length: feeds.length + 1,
|
||||
child: AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: const Text('Articles'),
|
||||
bottom: TabBar(
|
||||
isScrollable: true,
|
||||
tabs: [
|
||||
Tab(
|
||||
child: Text(
|
||||
'All',
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
...feeds.map(
|
||||
(feed) => Tab(
|
||||
child: Text(
|
||||
feed.title,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).appBarTheme.foregroundColor!,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
body: TabBarView(
|
||||
children: [
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 12,
|
||||
left: 8,
|
||||
right: 8,
|
||||
),
|
||||
sliver: SliverArticlesList(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
...feeds.map((feed) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverPadding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 8,
|
||||
left: 8,
|
||||
right: 8,
|
||||
),
|
||||
sliver: SliverArticlesList(feedId: feed.id),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
loading: () => AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(title: const Text('Articles')),
|
||||
body: const Center(child: CircularProgressIndicator()),
|
||||
),
|
||||
error: (err, stack) => AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(title: const Text('Articles')),
|
||||
body: Center(child: Text('Error: $err')),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
268
lib/discovery/discovery/articles.freezed.dart
Normal file
268
lib/discovery/discovery/articles.freezed.dart
Normal file
@@ -0,0 +1,268 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'articles.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
/// @nodoc
|
||||
mixin _$ArticleListQuery {
|
||||
|
||||
String? get feedId; String? get publisherId;
|
||||
/// Create a copy of ArticleListQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$ArticleListQueryCopyWith<ArticleListQuery> get copyWith => _$ArticleListQueryCopyWithImpl<ArticleListQuery>(this as ArticleListQuery, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is ArticleListQuery&&(identical(other.feedId, feedId) || other.feedId == feedId)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,feedId,publisherId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ArticleListQuery(feedId: $feedId, publisherId: $publisherId)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $ArticleListQueryCopyWith<$Res> {
|
||||
factory $ArticleListQueryCopyWith(ArticleListQuery value, $Res Function(ArticleListQuery) _then) = _$ArticleListQueryCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String? feedId, String? publisherId
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$ArticleListQueryCopyWithImpl<$Res>
|
||||
implements $ArticleListQueryCopyWith<$Res> {
|
||||
_$ArticleListQueryCopyWithImpl(this._self, this._then);
|
||||
|
||||
final ArticleListQuery _self;
|
||||
final $Res Function(ArticleListQuery) _then;
|
||||
|
||||
/// Create a copy of ArticleListQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? feedId = freezed,Object? publisherId = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
feedId: freezed == feedId ? _self.feedId : feedId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,publisherId: freezed == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [ArticleListQuery].
|
||||
extension ArticleListQueryPatterns on ArticleListQuery {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _ArticleListQuery value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ArticleListQuery() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _ArticleListQuery value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ArticleListQuery():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _ArticleListQuery value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _ArticleListQuery() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String? feedId, String? publisherId)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ArticleListQuery() when $default != null:
|
||||
return $default(_that.feedId,_that.publisherId);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String? feedId, String? publisherId) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ArticleListQuery():
|
||||
return $default(_that.feedId,_that.publisherId);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String? feedId, String? publisherId)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _ArticleListQuery() when $default != null:
|
||||
return $default(_that.feedId,_that.publisherId);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
|
||||
|
||||
class _ArticleListQuery implements ArticleListQuery {
|
||||
const _ArticleListQuery({this.feedId, this.publisherId});
|
||||
|
||||
|
||||
@override final String? feedId;
|
||||
@override final String? publisherId;
|
||||
|
||||
/// Create a copy of ArticleListQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$ArticleListQueryCopyWith<_ArticleListQuery> get copyWith => __$ArticleListQueryCopyWithImpl<_ArticleListQuery>(this, _$identity);
|
||||
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _ArticleListQuery&&(identical(other.feedId, feedId) || other.feedId == feedId)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId));
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,feedId,publisherId);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'ArticleListQuery(feedId: $feedId, publisherId: $publisherId)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$ArticleListQueryCopyWith<$Res> implements $ArticleListQueryCopyWith<$Res> {
|
||||
factory _$ArticleListQueryCopyWith(_ArticleListQuery value, $Res Function(_ArticleListQuery) _then) = __$ArticleListQueryCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String? feedId, String? publisherId
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$ArticleListQueryCopyWithImpl<$Res>
|
||||
implements _$ArticleListQueryCopyWith<$Res> {
|
||||
__$ArticleListQueryCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _ArticleListQuery _self;
|
||||
final $Res Function(_ArticleListQuery) _then;
|
||||
|
||||
/// Create a copy of ArticleListQuery
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? feedId = freezed,Object? publisherId = freezed,}) {
|
||||
return _then(_ArticleListQuery(
|
||||
feedId: freezed == feedId ? _self.feedId : feedId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,publisherId: freezed == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
|
||||
as String?,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
49
lib/discovery/discovery/articles.g.dart
Normal file
49
lib/discovery/discovery/articles.g.dart
Normal file
@@ -0,0 +1,49 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'articles.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(subscribedFeeds)
|
||||
final subscribedFeedsProvider = SubscribedFeedsProvider._();
|
||||
|
||||
final class SubscribedFeedsProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<List<SnWebFeed>>,
|
||||
List<SnWebFeed>,
|
||||
FutureOr<List<SnWebFeed>>
|
||||
>
|
||||
with $FutureModifier<List<SnWebFeed>>, $FutureProvider<List<SnWebFeed>> {
|
||||
SubscribedFeedsProvider._()
|
||||
: super(
|
||||
from: null,
|
||||
argument: null,
|
||||
retry: null,
|
||||
name: r'subscribedFeedsProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$subscribedFeedsHash();
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<List<SnWebFeed>> $createElement(
|
||||
$ProviderPointer pointer,
|
||||
) => $FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<List<SnWebFeed>> create(Ref ref) {
|
||||
return subscribedFeeds(ref);
|
||||
}
|
||||
}
|
||||
|
||||
String _$subscribedFeedsHash() => r'91d6f909a3d2c9f68028550223f7c7b2128727d2';
|
||||
193
lib/discovery/discovery/feeds/feed_detail.dart
Normal file
193
lib/discovery/discovery/feeds/feed_detail.dart
Normal file
@@ -0,0 +1,193 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/discovery/discovery_models/webfeed.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/pagination/pagination.dart';
|
||||
import 'package:island/shared/widgets/alert.dart';
|
||||
import 'package:island/shared/widgets/app_scaffold.dart';
|
||||
import 'package:island/shared/widgets/pagination_list.dart';
|
||||
import 'package:island/discovery/web_article_card.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
part 'feed_detail.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<SnWebFeed> marketplaceWebFeed(Ref ref, String feedId) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final resp = await apiClient.get('/insight/feeds/$feedId');
|
||||
return SnWebFeed.fromJson(resp.data);
|
||||
}
|
||||
|
||||
final marketplaceWebFeedContentNotifierProvider = AsyncNotifierProvider.family
|
||||
.autoDispose(MarketplaceWebFeedContentNotifier.new);
|
||||
|
||||
class MarketplaceWebFeedContentNotifier
|
||||
extends AsyncNotifier<PaginationState<SnWebArticle>>
|
||||
with AsyncPaginationController<SnWebArticle> {
|
||||
static const int pageSize = 20;
|
||||
|
||||
final String arg;
|
||||
MarketplaceWebFeedContentNotifier(this.arg);
|
||||
|
||||
@override
|
||||
Future<List<SnWebArticle>> fetch() async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
final queryParams = {'offset': fetchedCount.toString(), 'take': pageSize};
|
||||
|
||||
final response = await client.get(
|
||||
'/insight/feeds/$arg/articles',
|
||||
queryParameters: queryParams,
|
||||
);
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final articles = response.data
|
||||
.map((json) => SnWebArticle.fromJson(json))
|
||||
.cast<SnWebArticle>()
|
||||
.toList();
|
||||
|
||||
return articles;
|
||||
}
|
||||
}
|
||||
|
||||
/// Provider for web feed subscription status
|
||||
@riverpod
|
||||
Future<bool> marketplaceWebFeedSubscription(
|
||||
Ref ref, {
|
||||
required String feedId,
|
||||
}) async {
|
||||
final api = ref.watch(apiClientProvider);
|
||||
try {
|
||||
await api.get('/insight/feeds/$feedId/subscription');
|
||||
// If not 404, consider subscribed
|
||||
return true;
|
||||
} on Object catch (e) {
|
||||
// Dio error handling agnostic: treat 404 as not-subscribed, rethrow others
|
||||
final msg = e.toString();
|
||||
if (msg.contains('404')) return false;
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
class MarketplaceWebFeedDetailScreen extends HookConsumerWidget {
|
||||
final String id;
|
||||
const MarketplaceWebFeedDetailScreen({super.key, required this.id});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final feed = ref.watch(marketplaceWebFeedProvider(id));
|
||||
final subscribed = ref.watch(
|
||||
marketplaceWebFeedSubscriptionProvider(feedId: id),
|
||||
);
|
||||
|
||||
// Subscribe to web feed
|
||||
Future<void> subscribeToFeed() async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
await apiClient.post('/insight/feeds/$id/subscribe');
|
||||
HapticFeedback.selectionClick();
|
||||
ref.invalidate(marketplaceWebFeedSubscriptionProvider(feedId: id));
|
||||
if (!context.mounted) return;
|
||||
showSnackBar('webFeedSubscribed'.tr());
|
||||
}
|
||||
|
||||
// Unsubscribe from web feed
|
||||
Future<void> unsubscribeFromFeed() async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
await apiClient.delete('/insight/feeds/$id/subscribe');
|
||||
HapticFeedback.selectionClick();
|
||||
ref.invalidate(marketplaceWebFeedSubscriptionProvider(feedId: id));
|
||||
if (!context.mounted) return;
|
||||
showSnackBar('webFeedUnsubscribed'.tr());
|
||||
}
|
||||
|
||||
final feedNotifier = ref.watch(
|
||||
marketplaceWebFeedContentNotifierProvider(id).notifier,
|
||||
);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(title: Text(feed.value?.title ?? 'loading'.tr())),
|
||||
body: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Feed meta
|
||||
feed
|
||||
.when(
|
||||
data: (data) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(data.description ?? 'descriptionNone'.tr()),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const Icon(Symbols.rss_feed, size: 16),
|
||||
Text(
|
||||
'webFeedArticleCount'.plural(
|
||||
feedNotifier.totalCount ?? 0,
|
||||
),
|
||||
),
|
||||
],
|
||||
).opacity(0.85),
|
||||
Row(
|
||||
spacing: 4,
|
||||
children: [
|
||||
const Icon(Symbols.link, size: 16),
|
||||
SelectableText(data.url),
|
||||
],
|
||||
).opacity(0.85),
|
||||
],
|
||||
),
|
||||
error: (err, _) => Text(err.toString()),
|
||||
loading: () => CircularProgressIndicator().center(),
|
||||
)
|
||||
.padding(horizontal: 24, vertical: 24),
|
||||
const Divider(height: 1),
|
||||
// Articles list
|
||||
Expanded(
|
||||
child: PaginationList(
|
||||
spacing: 8,
|
||||
padding: EdgeInsets.symmetric(vertical: 8),
|
||||
provider: marketplaceWebFeedContentNotifierProvider(id),
|
||||
notifier: marketplaceWebFeedContentNotifierProvider(id).notifier,
|
||||
itemBuilder: (context, index, article) {
|
||||
return WebArticleCard(article: article).padding(horizontal: 12);
|
||||
},
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: 16 + MediaQuery.of(context).padding.bottom,
|
||||
left: 24,
|
||||
right: 24,
|
||||
top: 16,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
child: subscribed.when(
|
||||
data: (isSubscribed) => FilledButton.icon(
|
||||
onPressed: isSubscribed ? unsubscribeFromFeed : subscribeToFeed,
|
||||
icon: Icon(
|
||||
isSubscribed ? Symbols.remove_circle : Symbols.add_circle,
|
||||
),
|
||||
label: Text(
|
||||
isSubscribed ? 'unsubscribe'.tr() : 'subscribe'.tr(),
|
||||
),
|
||||
),
|
||||
loading: () => const SizedBox(
|
||||
height: 32,
|
||||
width: 32,
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
).center(),
|
||||
error: (_, _) => OutlinedButton.icon(
|
||||
onPressed: subscribeToFeed,
|
||||
icon: const Icon(Symbols.add_circle),
|
||||
label: Text('subscribe').tr(),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
166
lib/discovery/discovery/feeds/feed_detail.g.dart
Normal file
166
lib/discovery/discovery/feeds/feed_detail.g.dart
Normal file
@@ -0,0 +1,166 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'feed_detail.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// ignore_for_file: type=lint, type=warning
|
||||
|
||||
@ProviderFor(marketplaceWebFeed)
|
||||
final marketplaceWebFeedProvider = MarketplaceWebFeedFamily._();
|
||||
|
||||
final class MarketplaceWebFeedProvider
|
||||
extends
|
||||
$FunctionalProvider<
|
||||
AsyncValue<SnWebFeed>,
|
||||
SnWebFeed,
|
||||
FutureOr<SnWebFeed>
|
||||
>
|
||||
with $FutureModifier<SnWebFeed>, $FutureProvider<SnWebFeed> {
|
||||
MarketplaceWebFeedProvider._({
|
||||
required MarketplaceWebFeedFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'marketplaceWebFeedProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$marketplaceWebFeedHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'marketplaceWebFeedProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<SnWebFeed> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<SnWebFeed> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return marketplaceWebFeed(ref, argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is MarketplaceWebFeedProvider && other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$marketplaceWebFeedHash() =>
|
||||
r'36f3235ba346b0d416ce5e66dca8d6cecbafb608';
|
||||
|
||||
final class MarketplaceWebFeedFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<SnWebFeed>, String> {
|
||||
MarketplaceWebFeedFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'marketplaceWebFeedProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
MarketplaceWebFeedProvider call(String feedId) =>
|
||||
MarketplaceWebFeedProvider._(argument: feedId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'marketplaceWebFeedProvider';
|
||||
}
|
||||
|
||||
/// Provider for web feed subscription status
|
||||
|
||||
@ProviderFor(marketplaceWebFeedSubscription)
|
||||
final marketplaceWebFeedSubscriptionProvider =
|
||||
MarketplaceWebFeedSubscriptionFamily._();
|
||||
|
||||
/// Provider for web feed subscription status
|
||||
|
||||
final class MarketplaceWebFeedSubscriptionProvider
|
||||
extends $FunctionalProvider<AsyncValue<bool>, bool, FutureOr<bool>>
|
||||
with $FutureModifier<bool>, $FutureProvider<bool> {
|
||||
/// Provider for web feed subscription status
|
||||
MarketplaceWebFeedSubscriptionProvider._({
|
||||
required MarketplaceWebFeedSubscriptionFamily super.from,
|
||||
required String super.argument,
|
||||
}) : super(
|
||||
retry: null,
|
||||
name: r'marketplaceWebFeedSubscriptionProvider',
|
||||
isAutoDispose: true,
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
);
|
||||
|
||||
@override
|
||||
String debugGetCreateSourceHash() => _$marketplaceWebFeedSubscriptionHash();
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return r'marketplaceWebFeedSubscriptionProvider'
|
||||
''
|
||||
'($argument)';
|
||||
}
|
||||
|
||||
@$internal
|
||||
@override
|
||||
$FutureProviderElement<bool> $createElement($ProviderPointer pointer) =>
|
||||
$FutureProviderElement(pointer);
|
||||
|
||||
@override
|
||||
FutureOr<bool> create(Ref ref) {
|
||||
final argument = this.argument as String;
|
||||
return marketplaceWebFeedSubscription(ref, feedId: argument);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is MarketplaceWebFeedSubscriptionProvider &&
|
||||
other.argument == argument;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
return argument.hashCode;
|
||||
}
|
||||
}
|
||||
|
||||
String _$marketplaceWebFeedSubscriptionHash() =>
|
||||
r'6efa43b4d7e2ab62a721a67e035038dcf63be524';
|
||||
|
||||
/// Provider for web feed subscription status
|
||||
|
||||
final class MarketplaceWebFeedSubscriptionFamily extends $Family
|
||||
with $FunctionalFamilyOverride<FutureOr<bool>, String> {
|
||||
MarketplaceWebFeedSubscriptionFamily._()
|
||||
: super(
|
||||
retry: null,
|
||||
name: r'marketplaceWebFeedSubscriptionProvider',
|
||||
dependencies: null,
|
||||
$allTransitiveDependencies: null,
|
||||
isAutoDispose: true,
|
||||
);
|
||||
|
||||
/// Provider for web feed subscription status
|
||||
|
||||
MarketplaceWebFeedSubscriptionProvider call({required String feedId}) =>
|
||||
MarketplaceWebFeedSubscriptionProvider._(argument: feedId, from: this);
|
||||
|
||||
@override
|
||||
String toString() => r'marketplaceWebFeedSubscriptionProvider';
|
||||
}
|
||||
150
lib/discovery/discovery/feeds/feed_marketplace.dart
Normal file
150
lib/discovery/discovery/feeds/feed_marketplace.dart
Normal file
@@ -0,0 +1,150 @@
|
||||
import 'dart:async';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/discovery/discovery_models/webfeed.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
import 'package:island/pagination/pagination.dart';
|
||||
import 'package:island/shared/widgets/app_scaffold.dart';
|
||||
import 'package:island/shared/widgets/pagination_list.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
final marketplaceWebFeedsNotifierProvider = AsyncNotifierProvider.autoDispose(
|
||||
MarketplaceWebFeedsNotifier.new,
|
||||
);
|
||||
|
||||
class MarketplaceWebFeedsNotifier
|
||||
extends AsyncNotifier<PaginationState<SnWebFeed>>
|
||||
with
|
||||
AsyncPaginationController<SnWebFeed>,
|
||||
AsyncPaginationFilter<String?, SnWebFeed> {
|
||||
@override
|
||||
String? currentFilter;
|
||||
|
||||
@override
|
||||
Future<List<SnWebFeed>> fetch() async {
|
||||
final client = ref.read(apiClientProvider);
|
||||
|
||||
final response = await client.get(
|
||||
'/insight/feeds/explore',
|
||||
queryParameters: {
|
||||
'offset': fetchedCount.toString(),
|
||||
'take': 20,
|
||||
if (currentFilter != null && currentFilter!.isNotEmpty)
|
||||
'query': currentFilter,
|
||||
},
|
||||
);
|
||||
|
||||
totalCount = int.parse(response.headers.value('X-Total') ?? '0');
|
||||
final feeds = response.data
|
||||
.map((e) => SnWebFeed.fromJson(e))
|
||||
.cast<SnWebFeed>()
|
||||
.toList();
|
||||
|
||||
return feeds;
|
||||
}
|
||||
}
|
||||
|
||||
/// Marketplace screen for browsing web feeds.
|
||||
class MarketplaceWebFeedsScreen extends HookConsumerWidget {
|
||||
const MarketplaceWebFeedsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final query = useState<String?>(null);
|
||||
final searchController = useTextEditingController();
|
||||
final focusNode = useFocusNode();
|
||||
final debounceTimer = useState<Timer?>(null);
|
||||
|
||||
// Clear search when query is cleared
|
||||
useEffect(() {
|
||||
if (query.value == null || query.value!.isEmpty) {
|
||||
searchController.clear();
|
||||
}
|
||||
return null;
|
||||
}, [query]);
|
||||
|
||||
// Clean up timer on dispose
|
||||
useEffect(() {
|
||||
return () {
|
||||
debounceTimer.value?.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return AppScaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('webFeeds').tr(),
|
||||
actions: const [Gap(8)],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SearchBar(
|
||||
elevation: WidgetStateProperty.all(4),
|
||||
controller: searchController,
|
||||
focusNode: focusNode,
|
||||
hintText: 'search'.tr(),
|
||||
leading: const Icon(Symbols.search),
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 24),
|
||||
),
|
||||
onTapOutside: (_) =>
|
||||
FocusManager.instance.primaryFocus?.unfocus(),
|
||||
trailing: [
|
||||
if (query.value != null && query.value!.isNotEmpty)
|
||||
IconButton(
|
||||
icon: const Icon(Symbols.close),
|
||||
onPressed: () {
|
||||
query.value = null;
|
||||
searchController.clear();
|
||||
focusNode.unfocus();
|
||||
},
|
||||
),
|
||||
],
|
||||
onChanged: (value) {
|
||||
// Debounce search to avoid excessive API calls
|
||||
debounceTimer.value?.cancel();
|
||||
debounceTimer.value = Timer(
|
||||
const Duration(milliseconds: 500),
|
||||
() {
|
||||
query.value = value.isEmpty ? null : value;
|
||||
},
|
||||
);
|
||||
},
|
||||
onSubmitted: (value) {
|
||||
query.value = value.isEmpty ? null : value;
|
||||
focusNode.unfocus();
|
||||
},
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: PaginationList(
|
||||
provider: marketplaceWebFeedsNotifierProvider,
|
||||
notifier: marketplaceWebFeedsNotifierProvider.notifier,
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index, feed) {
|
||||
return ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(horizontal: 24),
|
||||
title: Text(feed.title),
|
||||
subtitle: Text(feed.description ?? ''),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
onTap: () {
|
||||
// Navigate to web feed detail page
|
||||
context.pushNamed(
|
||||
'webFeedDetail',
|
||||
pathParameters: {'feedId': feed.id},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
65
lib/discovery/discovery/realms.dart
Normal file
65
lib/discovery/discovery/realms.dart
Normal file
@@ -0,0 +1,65 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/realms/realms_widgets/realm/realm_list.dart';
|
||||
import 'package:island/shared/widgets/app_scaffold.dart';
|
||||
import 'dart:async';
|
||||
|
||||
class DiscoveryRealmsScreen extends HookConsumerWidget {
|
||||
const DiscoveryRealmsScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
Timer? debounceTimer;
|
||||
final searchController = useTextEditingController();
|
||||
final currentQuery = useState<String?>(null);
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(title: Text('discoverRealms'.tr())),
|
||||
body: Stack(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
SliverGap(88),
|
||||
SliverRealmList(
|
||||
query: currentQuery.value,
|
||||
key: ValueKey(currentQuery.value),
|
||||
),
|
||||
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: SearchBar(
|
||||
elevation: WidgetStateProperty.all(4),
|
||||
controller: searchController,
|
||||
hintText: 'search'.tr(),
|
||||
leading: const Icon(Icons.search),
|
||||
padding: WidgetStateProperty.all(
|
||||
const EdgeInsets.symmetric(horizontal: 24),
|
||||
),
|
||||
onChanged: (value) {
|
||||
if (debounceTimer?.isActive ?? false) {
|
||||
debounceTimer?.cancel();
|
||||
}
|
||||
debounceTimer = Timer(const Duration(milliseconds: 300), () {
|
||||
if (currentQuery.value != value) {
|
||||
currentQuery.value = value;
|
||||
}
|
||||
});
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
34
lib/discovery/discovery_models/auto_completion.dart
Normal file
34
lib/discovery/discovery_models/auto_completion.dart
Normal file
@@ -0,0 +1,34 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'auto_completion.freezed.dart';
|
||||
part 'auto_completion.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class AutoCompletionResponse with _$AutoCompletionResponse {
|
||||
const factory AutoCompletionResponse.account({
|
||||
required String type,
|
||||
required List<AutoCompletionItem> items,
|
||||
}) = AutoCompletionAccountResponse;
|
||||
|
||||
const factory AutoCompletionResponse.sticker({
|
||||
required String type,
|
||||
required List<AutoCompletionItem> items,
|
||||
}) = AutoCompletionStickerResponse;
|
||||
|
||||
factory AutoCompletionResponse.fromJson(Map<String, dynamic> json) =>
|
||||
_$AutoCompletionResponseFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class AutoCompletionItem with _$AutoCompletionItem {
|
||||
const factory AutoCompletionItem({
|
||||
required String id,
|
||||
required String displayName,
|
||||
required String? secondaryText,
|
||||
required String type,
|
||||
required dynamic data,
|
||||
}) = _AutoCompletionItem;
|
||||
|
||||
factory AutoCompletionItem.fromJson(Map<String, dynamic> json) =>
|
||||
_$AutoCompletionItemFromJson(json);
|
||||
}
|
||||
663
lib/discovery/discovery_models/auto_completion.freezed.dart
Normal file
663
lib/discovery/discovery_models/auto_completion.freezed.dart
Normal file
@@ -0,0 +1,663 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'auto_completion.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
AutoCompletionResponse _$AutoCompletionResponseFromJson(
|
||||
Map<String, dynamic> json
|
||||
) {
|
||||
switch (json['runtimeType']) {
|
||||
case 'account':
|
||||
return AutoCompletionAccountResponse.fromJson(
|
||||
json
|
||||
);
|
||||
case 'sticker':
|
||||
return AutoCompletionStickerResponse.fromJson(
|
||||
json
|
||||
);
|
||||
|
||||
default:
|
||||
throw CheckedFromJsonException(
|
||||
json,
|
||||
'runtimeType',
|
||||
'AutoCompletionResponse',
|
||||
'Invalid union type "${json['runtimeType']}"!'
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
mixin _$AutoCompletionResponse {
|
||||
|
||||
String get type; List<AutoCompletionItem> get items;
|
||||
/// Create a copy of AutoCompletionResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$AutoCompletionResponseCopyWith<AutoCompletionResponse> get copyWith => _$AutoCompletionResponseCopyWithImpl<AutoCompletionResponse>(this as AutoCompletionResponse, _$identity);
|
||||
|
||||
/// Serializes this AutoCompletionResponse to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoCompletionResponse&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.items, items));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(items));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AutoCompletionResponse(type: $type, items: $items)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $AutoCompletionResponseCopyWith<$Res> {
|
||||
factory $AutoCompletionResponseCopyWith(AutoCompletionResponse value, $Res Function(AutoCompletionResponse) _then) = _$AutoCompletionResponseCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String type, List<AutoCompletionItem> items
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$AutoCompletionResponseCopyWithImpl<$Res>
|
||||
implements $AutoCompletionResponseCopyWith<$Res> {
|
||||
_$AutoCompletionResponseCopyWithImpl(this._self, this._then);
|
||||
|
||||
final AutoCompletionResponse _self;
|
||||
final $Res Function(AutoCompletionResponse) _then;
|
||||
|
||||
/// Create a copy of AutoCompletionResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? items = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,items: null == items ? _self.items : items // ignore: cast_nullable_to_non_nullable
|
||||
as List<AutoCompletionItem>,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [AutoCompletionResponse].
|
||||
extension AutoCompletionResponsePatterns on AutoCompletionResponse {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>({TResult Function( AutoCompletionAccountResponse value)? account,TResult Function( AutoCompletionStickerResponse value)? sticker,required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case AutoCompletionAccountResponse() when account != null:
|
||||
return account(_that);case AutoCompletionStickerResponse() when sticker != null:
|
||||
return sticker(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>({required TResult Function( AutoCompletionAccountResponse value) account,required TResult Function( AutoCompletionStickerResponse value) sticker,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case AutoCompletionAccountResponse():
|
||||
return account(_that);case AutoCompletionStickerResponse():
|
||||
return sticker(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>({TResult? Function( AutoCompletionAccountResponse value)? account,TResult? Function( AutoCompletionStickerResponse value)? sticker,}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case AutoCompletionAccountResponse() when account != null:
|
||||
return account(_that);case AutoCompletionStickerResponse() when sticker != null:
|
||||
return sticker(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>({TResult Function( String type, List<AutoCompletionItem> items)? account,TResult Function( String type, List<AutoCompletionItem> items)? sticker,required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case AutoCompletionAccountResponse() when account != null:
|
||||
return account(_that.type,_that.items);case AutoCompletionStickerResponse() when sticker != null:
|
||||
return sticker(_that.type,_that.items);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>({required TResult Function( String type, List<AutoCompletionItem> items) account,required TResult Function( String type, List<AutoCompletionItem> items) sticker,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case AutoCompletionAccountResponse():
|
||||
return account(_that.type,_that.items);case AutoCompletionStickerResponse():
|
||||
return sticker(_that.type,_that.items);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>({TResult? Function( String type, List<AutoCompletionItem> items)? account,TResult? Function( String type, List<AutoCompletionItem> items)? sticker,}) {final _that = this;
|
||||
switch (_that) {
|
||||
case AutoCompletionAccountResponse() when account != null:
|
||||
return account(_that.type,_that.items);case AutoCompletionStickerResponse() when sticker != null:
|
||||
return sticker(_that.type,_that.items);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class AutoCompletionAccountResponse implements AutoCompletionResponse {
|
||||
const AutoCompletionAccountResponse({required this.type, required final List<AutoCompletionItem> items, final String? $type}): _items = items,$type = $type ?? 'account';
|
||||
factory AutoCompletionAccountResponse.fromJson(Map<String, dynamic> json) => _$AutoCompletionAccountResponseFromJson(json);
|
||||
|
||||
@override final String type;
|
||||
final List<AutoCompletionItem> _items;
|
||||
@override List<AutoCompletionItem> get items {
|
||||
if (_items is EqualUnmodifiableListView) return _items;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_items);
|
||||
}
|
||||
|
||||
|
||||
@JsonKey(name: 'runtimeType')
|
||||
final String $type;
|
||||
|
||||
|
||||
/// Create a copy of AutoCompletionResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$AutoCompletionAccountResponseCopyWith<AutoCompletionAccountResponse> get copyWith => _$AutoCompletionAccountResponseCopyWithImpl<AutoCompletionAccountResponse>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$AutoCompletionAccountResponseToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoCompletionAccountResponse&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._items, _items));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(_items));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AutoCompletionResponse.account(type: $type, items: $items)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $AutoCompletionAccountResponseCopyWith<$Res> implements $AutoCompletionResponseCopyWith<$Res> {
|
||||
factory $AutoCompletionAccountResponseCopyWith(AutoCompletionAccountResponse value, $Res Function(AutoCompletionAccountResponse) _then) = _$AutoCompletionAccountResponseCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String type, List<AutoCompletionItem> items
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$AutoCompletionAccountResponseCopyWithImpl<$Res>
|
||||
implements $AutoCompletionAccountResponseCopyWith<$Res> {
|
||||
_$AutoCompletionAccountResponseCopyWithImpl(this._self, this._then);
|
||||
|
||||
final AutoCompletionAccountResponse _self;
|
||||
final $Res Function(AutoCompletionAccountResponse) _then;
|
||||
|
||||
/// Create a copy of AutoCompletionResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? items = null,}) {
|
||||
return _then(AutoCompletionAccountResponse(
|
||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable
|
||||
as List<AutoCompletionItem>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class AutoCompletionStickerResponse implements AutoCompletionResponse {
|
||||
const AutoCompletionStickerResponse({required this.type, required final List<AutoCompletionItem> items, final String? $type}): _items = items,$type = $type ?? 'sticker';
|
||||
factory AutoCompletionStickerResponse.fromJson(Map<String, dynamic> json) => _$AutoCompletionStickerResponseFromJson(json);
|
||||
|
||||
@override final String type;
|
||||
final List<AutoCompletionItem> _items;
|
||||
@override List<AutoCompletionItem> get items {
|
||||
if (_items is EqualUnmodifiableListView) return _items;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_items);
|
||||
}
|
||||
|
||||
|
||||
@JsonKey(name: 'runtimeType')
|
||||
final String $type;
|
||||
|
||||
|
||||
/// Create a copy of AutoCompletionResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$AutoCompletionStickerResponseCopyWith<AutoCompletionStickerResponse> get copyWith => _$AutoCompletionStickerResponseCopyWithImpl<AutoCompletionStickerResponse>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$AutoCompletionStickerResponseToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoCompletionStickerResponse&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._items, _items));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(_items));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AutoCompletionResponse.sticker(type: $type, items: $items)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $AutoCompletionStickerResponseCopyWith<$Res> implements $AutoCompletionResponseCopyWith<$Res> {
|
||||
factory $AutoCompletionStickerResponseCopyWith(AutoCompletionStickerResponse value, $Res Function(AutoCompletionStickerResponse) _then) = _$AutoCompletionStickerResponseCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String type, List<AutoCompletionItem> items
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$AutoCompletionStickerResponseCopyWithImpl<$Res>
|
||||
implements $AutoCompletionStickerResponseCopyWith<$Res> {
|
||||
_$AutoCompletionStickerResponseCopyWithImpl(this._self, this._then);
|
||||
|
||||
final AutoCompletionStickerResponse _self;
|
||||
final $Res Function(AutoCompletionStickerResponse) _then;
|
||||
|
||||
/// Create a copy of AutoCompletionResponse
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? items = null,}) {
|
||||
return _then(AutoCompletionStickerResponse(
|
||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,items: null == items ? _self._items : items // ignore: cast_nullable_to_non_nullable
|
||||
as List<AutoCompletionItem>,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$AutoCompletionItem {
|
||||
|
||||
String get id; String get displayName; String? get secondaryText; String get type; dynamic get data;
|
||||
/// Create a copy of AutoCompletionItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$AutoCompletionItemCopyWith<AutoCompletionItem> get copyWith => _$AutoCompletionItemCopyWithImpl<AutoCompletionItem>(this as AutoCompletionItem, _$identity);
|
||||
|
||||
/// Serializes this AutoCompletionItem to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AutoCompletionItem&&(identical(other.id, id) || other.id == id)&&(identical(other.displayName, displayName) || other.displayName == displayName)&&(identical(other.secondaryText, secondaryText) || other.secondaryText == secondaryText)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.data, data));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,displayName,secondaryText,type,const DeepCollectionEquality().hash(data));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AutoCompletionItem(id: $id, displayName: $displayName, secondaryText: $secondaryText, type: $type, data: $data)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $AutoCompletionItemCopyWith<$Res> {
|
||||
factory $AutoCompletionItemCopyWith(AutoCompletionItem value, $Res Function(AutoCompletionItem) _then) = _$AutoCompletionItemCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String displayName, String? secondaryText, String type, dynamic data
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$AutoCompletionItemCopyWithImpl<$Res>
|
||||
implements $AutoCompletionItemCopyWith<$Res> {
|
||||
_$AutoCompletionItemCopyWithImpl(this._self, this._then);
|
||||
|
||||
final AutoCompletionItem _self;
|
||||
final $Res Function(AutoCompletionItem) _then;
|
||||
|
||||
/// Create a copy of AutoCompletionItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? displayName = null,Object? secondaryText = freezed,Object? type = null,Object? data = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,displayName: null == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable
|
||||
as String,secondaryText: freezed == secondaryText ? _self.secondaryText : secondaryText // ignore: cast_nullable_to_non_nullable
|
||||
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [AutoCompletionItem].
|
||||
extension AutoCompletionItemPatterns on AutoCompletionItem {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _AutoCompletionItem value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AutoCompletionItem() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _AutoCompletionItem value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AutoCompletionItem():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _AutoCompletionItem value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AutoCompletionItem() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String displayName, String? secondaryText, String type, dynamic data)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AutoCompletionItem() when $default != null:
|
||||
return $default(_that.id,_that.displayName,_that.secondaryText,_that.type,_that.data);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String displayName, String? secondaryText, String type, dynamic data) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AutoCompletionItem():
|
||||
return $default(_that.id,_that.displayName,_that.secondaryText,_that.type,_that.data);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String displayName, String? secondaryText, String type, dynamic data)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AutoCompletionItem() when $default != null:
|
||||
return $default(_that.id,_that.displayName,_that.secondaryText,_that.type,_that.data);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _AutoCompletionItem implements AutoCompletionItem {
|
||||
const _AutoCompletionItem({required this.id, required this.displayName, required this.secondaryText, required this.type, required this.data});
|
||||
factory _AutoCompletionItem.fromJson(Map<String, dynamic> json) => _$AutoCompletionItemFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String displayName;
|
||||
@override final String? secondaryText;
|
||||
@override final String type;
|
||||
@override final dynamic data;
|
||||
|
||||
/// Create a copy of AutoCompletionItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$AutoCompletionItemCopyWith<_AutoCompletionItem> get copyWith => __$AutoCompletionItemCopyWithImpl<_AutoCompletionItem>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$AutoCompletionItemToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AutoCompletionItem&&(identical(other.id, id) || other.id == id)&&(identical(other.displayName, displayName) || other.displayName == displayName)&&(identical(other.secondaryText, secondaryText) || other.secondaryText == secondaryText)&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.data, data));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,displayName,secondaryText,type,const DeepCollectionEquality().hash(data));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AutoCompletionItem(id: $id, displayName: $displayName, secondaryText: $secondaryText, type: $type, data: $data)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$AutoCompletionItemCopyWith<$Res> implements $AutoCompletionItemCopyWith<$Res> {
|
||||
factory _$AutoCompletionItemCopyWith(_AutoCompletionItem value, $Res Function(_AutoCompletionItem) _then) = __$AutoCompletionItemCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String displayName, String? secondaryText, String type, dynamic data
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$AutoCompletionItemCopyWithImpl<$Res>
|
||||
implements _$AutoCompletionItemCopyWith<$Res> {
|
||||
__$AutoCompletionItemCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _AutoCompletionItem _self;
|
||||
final $Res Function(_AutoCompletionItem) _then;
|
||||
|
||||
/// Create a copy of AutoCompletionItem
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? displayName = null,Object? secondaryText = freezed,Object? type = null,Object? data = freezed,}) {
|
||||
return _then(_AutoCompletionItem(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,displayName: null == displayName ? _self.displayName : displayName // ignore: cast_nullable_to_non_nullable
|
||||
as String,secondaryText: freezed == secondaryText ? _self.secondaryText : secondaryText // ignore: cast_nullable_to_non_nullable
|
||||
as String?,type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
61
lib/discovery/discovery_models/auto_completion.g.dart
Normal file
61
lib/discovery/discovery_models/auto_completion.g.dart
Normal file
@@ -0,0 +1,61 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'auto_completion.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
AutoCompletionAccountResponse _$AutoCompletionAccountResponseFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => AutoCompletionAccountResponse(
|
||||
type: json['type'] as String,
|
||||
items: (json['items'] as List<dynamic>)
|
||||
.map((e) => AutoCompletionItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AutoCompletionAccountResponseToJson(
|
||||
AutoCompletionAccountResponse instance,
|
||||
) => <String, dynamic>{
|
||||
'type': instance.type,
|
||||
'items': instance.items.map((e) => e.toJson()).toList(),
|
||||
'runtimeType': instance.$type,
|
||||
};
|
||||
|
||||
AutoCompletionStickerResponse _$AutoCompletionStickerResponseFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => AutoCompletionStickerResponse(
|
||||
type: json['type'] as String,
|
||||
items: (json['items'] as List<dynamic>)
|
||||
.map((e) => AutoCompletionItem.fromJson(e as Map<String, dynamic>))
|
||||
.toList(),
|
||||
$type: json['runtimeType'] as String?,
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AutoCompletionStickerResponseToJson(
|
||||
AutoCompletionStickerResponse instance,
|
||||
) => <String, dynamic>{
|
||||
'type': instance.type,
|
||||
'items': instance.items.map((e) => e.toJson()).toList(),
|
||||
'runtimeType': instance.$type,
|
||||
};
|
||||
|
||||
_AutoCompletionItem _$AutoCompletionItemFromJson(Map<String, dynamic> json) =>
|
||||
_AutoCompletionItem(
|
||||
id: json['id'] as String,
|
||||
displayName: json['display_name'] as String,
|
||||
secondaryText: json['secondary_text'] as String?,
|
||||
type: json['type'] as String,
|
||||
data: json['data'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AutoCompletionItemToJson(_AutoCompletionItem instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'display_name': instance.displayName,
|
||||
'secondary_text': instance.secondaryText,
|
||||
'type': instance.type,
|
||||
'data': instance.data,
|
||||
};
|
||||
16
lib/discovery/discovery_models/autocomplete_response.dart
Normal file
16
lib/discovery/discovery_models/autocomplete_response.dart
Normal file
@@ -0,0 +1,16 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'autocomplete_response.freezed.dart';
|
||||
part 'autocomplete_response.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class AutocompleteSuggestion with _$AutocompleteSuggestion {
|
||||
const factory AutocompleteSuggestion({
|
||||
required String type,
|
||||
required String keyword,
|
||||
required dynamic data,
|
||||
}) = _AutocompleteSuggestion;
|
||||
|
||||
factory AutocompleteSuggestion.fromJson(Map<String, dynamic> json) =>
|
||||
_$AutocompleteSuggestionFromJson(json);
|
||||
}
|
||||
@@ -0,0 +1,277 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'autocomplete_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$AutocompleteSuggestion {
|
||||
|
||||
String get type; String get keyword; dynamic get data;
|
||||
/// Create a copy of AutocompleteSuggestion
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$AutocompleteSuggestionCopyWith<AutocompleteSuggestion> get copyWith => _$AutocompleteSuggestionCopyWithImpl<AutocompleteSuggestion>(this as AutocompleteSuggestion, _$identity);
|
||||
|
||||
/// Serializes this AutocompleteSuggestion to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is AutocompleteSuggestion&&(identical(other.type, type) || other.type == type)&&(identical(other.keyword, keyword) || other.keyword == keyword)&&const DeepCollectionEquality().equals(other.data, data));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type,keyword,const DeepCollectionEquality().hash(data));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AutocompleteSuggestion(type: $type, keyword: $keyword, data: $data)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $AutocompleteSuggestionCopyWith<$Res> {
|
||||
factory $AutocompleteSuggestionCopyWith(AutocompleteSuggestion value, $Res Function(AutocompleteSuggestion) _then) = _$AutocompleteSuggestionCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String type, String keyword, dynamic data
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$AutocompleteSuggestionCopyWithImpl<$Res>
|
||||
implements $AutocompleteSuggestionCopyWith<$Res> {
|
||||
_$AutocompleteSuggestionCopyWithImpl(this._self, this._then);
|
||||
|
||||
final AutocompleteSuggestion _self;
|
||||
final $Res Function(AutocompleteSuggestion) _then;
|
||||
|
||||
/// Create a copy of AutocompleteSuggestion
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? keyword = null,Object? data = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,keyword: null == keyword ? _self.keyword : keyword // ignore: cast_nullable_to_non_nullable
|
||||
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [AutocompleteSuggestion].
|
||||
extension AutocompleteSuggestionPatterns on AutocompleteSuggestion {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _AutocompleteSuggestion value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AutocompleteSuggestion() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _AutocompleteSuggestion value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AutocompleteSuggestion():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _AutocompleteSuggestion value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _AutocompleteSuggestion() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String type, String keyword, dynamic data)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AutocompleteSuggestion() when $default != null:
|
||||
return $default(_that.type,_that.keyword,_that.data);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String type, String keyword, dynamic data) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AutocompleteSuggestion():
|
||||
return $default(_that.type,_that.keyword,_that.data);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String type, String keyword, dynamic data)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _AutocompleteSuggestion() when $default != null:
|
||||
return $default(_that.type,_that.keyword,_that.data);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _AutocompleteSuggestion implements AutocompleteSuggestion {
|
||||
const _AutocompleteSuggestion({required this.type, required this.keyword, required this.data});
|
||||
factory _AutocompleteSuggestion.fromJson(Map<String, dynamic> json) => _$AutocompleteSuggestionFromJson(json);
|
||||
|
||||
@override final String type;
|
||||
@override final String keyword;
|
||||
@override final dynamic data;
|
||||
|
||||
/// Create a copy of AutocompleteSuggestion
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$AutocompleteSuggestionCopyWith<_AutocompleteSuggestion> get copyWith => __$AutocompleteSuggestionCopyWithImpl<_AutocompleteSuggestion>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$AutocompleteSuggestionToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _AutocompleteSuggestion&&(identical(other.type, type) || other.type == type)&&(identical(other.keyword, keyword) || other.keyword == keyword)&&const DeepCollectionEquality().equals(other.data, data));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,type,keyword,const DeepCollectionEquality().hash(data));
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'AutocompleteSuggestion(type: $type, keyword: $keyword, data: $data)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$AutocompleteSuggestionCopyWith<$Res> implements $AutocompleteSuggestionCopyWith<$Res> {
|
||||
factory _$AutocompleteSuggestionCopyWith(_AutocompleteSuggestion value, $Res Function(_AutocompleteSuggestion) _then) = __$AutocompleteSuggestionCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String type, String keyword, dynamic data
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$AutocompleteSuggestionCopyWithImpl<$Res>
|
||||
implements _$AutocompleteSuggestionCopyWith<$Res> {
|
||||
__$AutocompleteSuggestionCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _AutocompleteSuggestion _self;
|
||||
final $Res Function(_AutocompleteSuggestion) _then;
|
||||
|
||||
/// Create a copy of AutocompleteSuggestion
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? keyword = null,Object? data = freezed,}) {
|
||||
return _then(_AutocompleteSuggestion(
|
||||
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
|
||||
as String,keyword: null == keyword ? _self.keyword : keyword // ignore: cast_nullable_to_non_nullable
|
||||
as String,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
|
||||
as dynamic,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
23
lib/discovery/discovery_models/autocomplete_response.g.dart
Normal file
23
lib/discovery/discovery_models/autocomplete_response.g.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'autocomplete_response.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_AutocompleteSuggestion _$AutocompleteSuggestionFromJson(
|
||||
Map<String, dynamic> json,
|
||||
) => _AutocompleteSuggestion(
|
||||
type: json['type'] as String,
|
||||
keyword: json['keyword'] as String,
|
||||
data: json['data'],
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$AutocompleteSuggestionToJson(
|
||||
_AutocompleteSuggestion instance,
|
||||
) => <String, dynamic>{
|
||||
'type': instance.type,
|
||||
'keyword': instance.keyword,
|
||||
'data': instance.data,
|
||||
};
|
||||
25
lib/discovery/discovery_models/site_file.dart
Normal file
25
lib/discovery/discovery_models/site_file.dart
Normal file
@@ -0,0 +1,25 @@
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
|
||||
part 'site_file.freezed.dart';
|
||||
part 'site_file.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class SnSiteFileEntry with _$SnSiteFileEntry {
|
||||
const factory SnSiteFileEntry({
|
||||
required bool isDirectory,
|
||||
required String relativePath,
|
||||
required int size, // Size in bytes (0 for directories)
|
||||
required DateTime modified, // ISO 8601 timestamp
|
||||
}) = _SnSiteFileEntry;
|
||||
|
||||
factory SnSiteFileEntry.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnSiteFileEntryFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class SnFileContent with _$SnFileContent {
|
||||
const factory SnFileContent({required String content}) = _SnFileContent;
|
||||
|
||||
factory SnFileContent.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnFileContentFromJson(json);
|
||||
}
|
||||
539
lib/discovery/discovery_models/site_file.freezed.dart
Normal file
539
lib/discovery/discovery_models/site_file.freezed.dart
Normal file
@@ -0,0 +1,539 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'site_file.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnSiteFileEntry {
|
||||
|
||||
bool get isDirectory; String get relativePath; int get size;// Size in bytes (0 for directories)
|
||||
DateTime get modified;
|
||||
/// Create a copy of SnSiteFileEntry
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnSiteFileEntryCopyWith<SnSiteFileEntry> get copyWith => _$SnSiteFileEntryCopyWithImpl<SnSiteFileEntry>(this as SnSiteFileEntry, _$identity);
|
||||
|
||||
/// Serializes this SnSiteFileEntry to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnSiteFileEntry&&(identical(other.isDirectory, isDirectory) || other.isDirectory == isDirectory)&&(identical(other.relativePath, relativePath) || other.relativePath == relativePath)&&(identical(other.size, size) || other.size == size)&&(identical(other.modified, modified) || other.modified == modified));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,isDirectory,relativePath,size,modified);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnSiteFileEntry(isDirectory: $isDirectory, relativePath: $relativePath, size: $size, modified: $modified)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnSiteFileEntryCopyWith<$Res> {
|
||||
factory $SnSiteFileEntryCopyWith(SnSiteFileEntry value, $Res Function(SnSiteFileEntry) _then) = _$SnSiteFileEntryCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool isDirectory, String relativePath, int size, DateTime modified
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnSiteFileEntryCopyWithImpl<$Res>
|
||||
implements $SnSiteFileEntryCopyWith<$Res> {
|
||||
_$SnSiteFileEntryCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnSiteFileEntry _self;
|
||||
final $Res Function(SnSiteFileEntry) _then;
|
||||
|
||||
/// Create a copy of SnSiteFileEntry
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? isDirectory = null,Object? relativePath = null,Object? size = null,Object? modified = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
isDirectory: null == isDirectory ? _self.isDirectory : isDirectory // ignore: cast_nullable_to_non_nullable
|
||||
as bool,relativePath: null == relativePath ? _self.relativePath : relativePath // ignore: cast_nullable_to_non_nullable
|
||||
as String,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
|
||||
as int,modified: null == modified ? _self.modified : modified // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SnSiteFileEntry].
|
||||
extension SnSiteFileEntryPatterns on SnSiteFileEntry {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnSiteFileEntry value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnSiteFileEntry() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnSiteFileEntry value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnSiteFileEntry():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnSiteFileEntry value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnSiteFileEntry() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool isDirectory, String relativePath, int size, DateTime modified)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnSiteFileEntry() when $default != null:
|
||||
return $default(_that.isDirectory,_that.relativePath,_that.size,_that.modified);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool isDirectory, String relativePath, int size, DateTime modified) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnSiteFileEntry():
|
||||
return $default(_that.isDirectory,_that.relativePath,_that.size,_that.modified);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool isDirectory, String relativePath, int size, DateTime modified)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnSiteFileEntry() when $default != null:
|
||||
return $default(_that.isDirectory,_that.relativePath,_that.size,_that.modified);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnSiteFileEntry implements SnSiteFileEntry {
|
||||
const _SnSiteFileEntry({required this.isDirectory, required this.relativePath, required this.size, required this.modified});
|
||||
factory _SnSiteFileEntry.fromJson(Map<String, dynamic> json) => _$SnSiteFileEntryFromJson(json);
|
||||
|
||||
@override final bool isDirectory;
|
||||
@override final String relativePath;
|
||||
@override final int size;
|
||||
// Size in bytes (0 for directories)
|
||||
@override final DateTime modified;
|
||||
|
||||
/// Create a copy of SnSiteFileEntry
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnSiteFileEntryCopyWith<_SnSiteFileEntry> get copyWith => __$SnSiteFileEntryCopyWithImpl<_SnSiteFileEntry>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnSiteFileEntryToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnSiteFileEntry&&(identical(other.isDirectory, isDirectory) || other.isDirectory == isDirectory)&&(identical(other.relativePath, relativePath) || other.relativePath == relativePath)&&(identical(other.size, size) || other.size == size)&&(identical(other.modified, modified) || other.modified == modified));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,isDirectory,relativePath,size,modified);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnSiteFileEntry(isDirectory: $isDirectory, relativePath: $relativePath, size: $size, modified: $modified)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnSiteFileEntryCopyWith<$Res> implements $SnSiteFileEntryCopyWith<$Res> {
|
||||
factory _$SnSiteFileEntryCopyWith(_SnSiteFileEntry value, $Res Function(_SnSiteFileEntry) _then) = __$SnSiteFileEntryCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
bool isDirectory, String relativePath, int size, DateTime modified
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnSiteFileEntryCopyWithImpl<$Res>
|
||||
implements _$SnSiteFileEntryCopyWith<$Res> {
|
||||
__$SnSiteFileEntryCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnSiteFileEntry _self;
|
||||
final $Res Function(_SnSiteFileEntry) _then;
|
||||
|
||||
/// Create a copy of SnSiteFileEntry
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? isDirectory = null,Object? relativePath = null,Object? size = null,Object? modified = null,}) {
|
||||
return _then(_SnSiteFileEntry(
|
||||
isDirectory: null == isDirectory ? _self.isDirectory : isDirectory // ignore: cast_nullable_to_non_nullable
|
||||
as bool,relativePath: null == relativePath ? _self.relativePath : relativePath // ignore: cast_nullable_to_non_nullable
|
||||
as String,size: null == size ? _self.size : size // ignore: cast_nullable_to_non_nullable
|
||||
as int,modified: null == modified ? _self.modified : modified // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnFileContent {
|
||||
|
||||
String get content;
|
||||
/// Create a copy of SnFileContent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnFileContentCopyWith<SnFileContent> get copyWith => _$SnFileContentCopyWithImpl<SnFileContent>(this as SnFileContent, _$identity);
|
||||
|
||||
/// Serializes this SnFileContent to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnFileContent&&(identical(other.content, content) || other.content == content));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,content);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnFileContent(content: $content)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnFileContentCopyWith<$Res> {
|
||||
factory $SnFileContentCopyWith(SnFileContent value, $Res Function(SnFileContent) _then) = _$SnFileContentCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String content
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnFileContentCopyWithImpl<$Res>
|
||||
implements $SnFileContentCopyWith<$Res> {
|
||||
_$SnFileContentCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnFileContent _self;
|
||||
final $Res Function(SnFileContent) _then;
|
||||
|
||||
/// Create a copy of SnFileContent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? content = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SnFileContent].
|
||||
extension SnFileContentPatterns on SnFileContent {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnFileContent value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFileContent() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnFileContent value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFileContent():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnFileContent value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFileContent() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String content)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFileContent() when $default != null:
|
||||
return $default(_that.content);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String content) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFileContent():
|
||||
return $default(_that.content);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String content)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnFileContent() when $default != null:
|
||||
return $default(_that.content);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnFileContent implements SnFileContent {
|
||||
const _SnFileContent({required this.content});
|
||||
factory _SnFileContent.fromJson(Map<String, dynamic> json) => _$SnFileContentFromJson(json);
|
||||
|
||||
@override final String content;
|
||||
|
||||
/// Create a copy of SnFileContent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnFileContentCopyWith<_SnFileContent> get copyWith => __$SnFileContentCopyWithImpl<_SnFileContent>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnFileContentToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnFileContent&&(identical(other.content, content) || other.content == content));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,content);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnFileContent(content: $content)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnFileContentCopyWith<$Res> implements $SnFileContentCopyWith<$Res> {
|
||||
factory _$SnFileContentCopyWith(_SnFileContent value, $Res Function(_SnFileContent) _then) = __$SnFileContentCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String content
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnFileContentCopyWithImpl<$Res>
|
||||
implements _$SnFileContentCopyWith<$Res> {
|
||||
__$SnFileContentCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnFileContent _self;
|
||||
final $Res Function(_SnFileContent) _then;
|
||||
|
||||
/// Create a copy of SnFileContent
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? content = null,}) {
|
||||
return _then(_SnFileContent(
|
||||
content: null == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||
as String,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// dart format on
|
||||
29
lib/discovery/discovery_models/site_file.g.dart
Normal file
29
lib/discovery/discovery_models/site_file.g.dart
Normal file
@@ -0,0 +1,29 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'site_file.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_SnSiteFileEntry _$SnSiteFileEntryFromJson(Map<String, dynamic> json) =>
|
||||
_SnSiteFileEntry(
|
||||
isDirectory: json['is_directory'] as bool,
|
||||
relativePath: json['relative_path'] as String,
|
||||
size: (json['size'] as num).toInt(),
|
||||
modified: DateTime.parse(json['modified'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnSiteFileEntryToJson(_SnSiteFileEntry instance) =>
|
||||
<String, dynamic>{
|
||||
'is_directory': instance.isDirectory,
|
||||
'relative_path': instance.relativePath,
|
||||
'size': instance.size,
|
||||
'modified': instance.modified.toIso8601String(),
|
||||
};
|
||||
|
||||
_SnFileContent _$SnFileContentFromJson(Map<String, dynamic> json) =>
|
||||
_SnFileContent(content: json['content'] as String);
|
||||
|
||||
Map<String, dynamic> _$SnFileContentToJson(_SnFileContent instance) =>
|
||||
<String, dynamic>{'content': instance.content};
|
||||
64
lib/discovery/discovery_models/webfeed.dart
Normal file
64
lib/discovery/discovery_models/webfeed.dart
Normal file
@@ -0,0 +1,64 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:island/posts/posts_models/embed.dart';
|
||||
|
||||
part 'webfeed.freezed.dart';
|
||||
part 'webfeed.g.dart';
|
||||
|
||||
@freezed
|
||||
sealed class SnWebFeedConfig with _$SnWebFeedConfig {
|
||||
const factory SnWebFeedConfig({@Default(false) bool scrapPage}) =
|
||||
_SnWebFeedConfig;
|
||||
|
||||
factory SnWebFeedConfig.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnWebFeedConfigFromJson(json);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class SnWebFeed with _$SnWebFeed {
|
||||
const factory SnWebFeed({
|
||||
required String id,
|
||||
required String url,
|
||||
required String title,
|
||||
String? description,
|
||||
SnScrappedLink? preview,
|
||||
@Default(SnWebFeedConfig()) SnWebFeedConfig config,
|
||||
required String publisherId,
|
||||
@Default([]) List<SnWebArticle> articles,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
}) = _SnWebFeed;
|
||||
|
||||
factory SnWebFeed.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnWebFeedFromJson(json);
|
||||
|
||||
factory SnWebFeed.fromJsonString(String jsonString) =>
|
||||
SnWebFeed.fromJson(jsonDecode(jsonString) as Map<String, dynamic>);
|
||||
}
|
||||
|
||||
@freezed
|
||||
sealed class SnWebArticle with _$SnWebArticle {
|
||||
const factory SnWebArticle({
|
||||
required String id,
|
||||
required String title,
|
||||
required String url,
|
||||
String? author,
|
||||
Map<String, dynamic>? meta,
|
||||
SnScrappedLink? preview,
|
||||
SnWebFeed? feed,
|
||||
String? content,
|
||||
DateTime? publishedAt,
|
||||
required String feedId,
|
||||
required DateTime createdAt,
|
||||
required DateTime updatedAt,
|
||||
DateTime? deletedAt,
|
||||
}) = _SnWebArticle;
|
||||
|
||||
factory SnWebArticle.fromJson(Map<String, dynamic> json) =>
|
||||
_$SnWebArticleFromJson(json);
|
||||
|
||||
factory SnWebArticle.fromJsonString(String jsonString) =>
|
||||
SnWebArticle.fromJson(jsonDecode(jsonString) as Map<String, dynamic>);
|
||||
}
|
||||
955
lib/discovery/discovery_models/webfeed.freezed.dart
Normal file
955
lib/discovery/discovery_models/webfeed.freezed.dart
Normal file
@@ -0,0 +1,955 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
// coverage:ignore-file
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
|
||||
|
||||
part of 'webfeed.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// FreezedGenerator
|
||||
// **************************************************************************
|
||||
|
||||
// dart format off
|
||||
T _$identity<T>(T value) => value;
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnWebFeedConfig {
|
||||
|
||||
bool get scrapPage;
|
||||
/// Create a copy of SnWebFeedConfig
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnWebFeedConfigCopyWith<SnWebFeedConfig> get copyWith => _$SnWebFeedConfigCopyWithImpl<SnWebFeedConfig>(this as SnWebFeedConfig, _$identity);
|
||||
|
||||
/// Serializes this SnWebFeedConfig to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWebFeedConfig&&(identical(other.scrapPage, scrapPage) || other.scrapPage == scrapPage));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,scrapPage);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnWebFeedConfig(scrapPage: $scrapPage)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnWebFeedConfigCopyWith<$Res> {
|
||||
factory $SnWebFeedConfigCopyWith(SnWebFeedConfig value, $Res Function(SnWebFeedConfig) _then) = _$SnWebFeedConfigCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
bool scrapPage
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnWebFeedConfigCopyWithImpl<$Res>
|
||||
implements $SnWebFeedConfigCopyWith<$Res> {
|
||||
_$SnWebFeedConfigCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnWebFeedConfig _self;
|
||||
final $Res Function(SnWebFeedConfig) _then;
|
||||
|
||||
/// Create a copy of SnWebFeedConfig
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? scrapPage = null,}) {
|
||||
return _then(_self.copyWith(
|
||||
scrapPage: null == scrapPage ? _self.scrapPage : scrapPage // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SnWebFeedConfig].
|
||||
extension SnWebFeedConfigPatterns on SnWebFeedConfig {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnWebFeedConfig value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebFeedConfig() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnWebFeedConfig value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebFeedConfig():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnWebFeedConfig value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebFeedConfig() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( bool scrapPage)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebFeedConfig() when $default != null:
|
||||
return $default(_that.scrapPage);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( bool scrapPage) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebFeedConfig():
|
||||
return $default(_that.scrapPage);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( bool scrapPage)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebFeedConfig() when $default != null:
|
||||
return $default(_that.scrapPage);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnWebFeedConfig implements SnWebFeedConfig {
|
||||
const _SnWebFeedConfig({this.scrapPage = false});
|
||||
factory _SnWebFeedConfig.fromJson(Map<String, dynamic> json) => _$SnWebFeedConfigFromJson(json);
|
||||
|
||||
@override@JsonKey() final bool scrapPage;
|
||||
|
||||
/// Create a copy of SnWebFeedConfig
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnWebFeedConfigCopyWith<_SnWebFeedConfig> get copyWith => __$SnWebFeedConfigCopyWithImpl<_SnWebFeedConfig>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnWebFeedConfigToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWebFeedConfig&&(identical(other.scrapPage, scrapPage) || other.scrapPage == scrapPage));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,scrapPage);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnWebFeedConfig(scrapPage: $scrapPage)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnWebFeedConfigCopyWith<$Res> implements $SnWebFeedConfigCopyWith<$Res> {
|
||||
factory _$SnWebFeedConfigCopyWith(_SnWebFeedConfig value, $Res Function(_SnWebFeedConfig) _then) = __$SnWebFeedConfigCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
bool scrapPage
|
||||
});
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnWebFeedConfigCopyWithImpl<$Res>
|
||||
implements _$SnWebFeedConfigCopyWith<$Res> {
|
||||
__$SnWebFeedConfigCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnWebFeedConfig _self;
|
||||
final $Res Function(_SnWebFeedConfig) _then;
|
||||
|
||||
/// Create a copy of SnWebFeedConfig
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? scrapPage = null,}) {
|
||||
return _then(_SnWebFeedConfig(
|
||||
scrapPage: null == scrapPage ? _self.scrapPage : scrapPage // ignore: cast_nullable_to_non_nullable
|
||||
as bool,
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnWebFeed {
|
||||
|
||||
String get id; String get url; String get title; String? get description; SnScrappedLink? get preview; SnWebFeedConfig get config; String get publisherId; List<SnWebArticle> get articles; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnWebFeed
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnWebFeedCopyWith<SnWebFeed> get copyWith => _$SnWebFeedCopyWithImpl<SnWebFeed>(this as SnWebFeed, _$identity);
|
||||
|
||||
/// Serializes this SnWebFeed to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWebFeed&&(identical(other.id, id) || other.id == id)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.config, config) || other.config == config)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&const DeepCollectionEquality().equals(other.articles, articles)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,url,title,description,preview,config,publisherId,const DeepCollectionEquality().hash(articles),createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnWebFeed(id: $id, url: $url, title: $title, description: $description, preview: $preview, config: $config, publisherId: $publisherId, articles: $articles, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnWebFeedCopyWith<$Res> {
|
||||
factory $SnWebFeedCopyWith(SnWebFeed value, $Res Function(SnWebFeed) _then) = _$SnWebFeedCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String url, String title, String? description, SnScrappedLink? preview, SnWebFeedConfig config, String publisherId, List<SnWebArticle> articles, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
$SnScrappedLinkCopyWith<$Res>? get preview;$SnWebFeedConfigCopyWith<$Res> get config;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnWebFeedCopyWithImpl<$Res>
|
||||
implements $SnWebFeedCopyWith<$Res> {
|
||||
_$SnWebFeedCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnWebFeed _self;
|
||||
final $Res Function(SnWebFeed) _then;
|
||||
|
||||
/// Create a copy of SnWebFeed
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? url = null,Object? title = null,Object? description = freezed,Object? preview = freezed,Object? config = null,Object? publisherId = null,Object? articles = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
|
||||
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,preview: freezed == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable
|
||||
as SnScrappedLink?,config: null == config ? _self.config : config // ignore: cast_nullable_to_non_nullable
|
||||
as SnWebFeedConfig,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
|
||||
as String,articles: null == articles ? _self.articles : articles // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnWebArticle>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of SnWebFeed
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnScrappedLinkCopyWith<$Res>? get preview {
|
||||
if (_self.preview == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnScrappedLinkCopyWith<$Res>(_self.preview!, (value) {
|
||||
return _then(_self.copyWith(preview: value));
|
||||
});
|
||||
}/// Create a copy of SnWebFeed
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnWebFeedConfigCopyWith<$Res> get config {
|
||||
|
||||
return $SnWebFeedConfigCopyWith<$Res>(_self.config, (value) {
|
||||
return _then(_self.copyWith(config: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SnWebFeed].
|
||||
extension SnWebFeedPatterns on SnWebFeed {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnWebFeed value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebFeed() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnWebFeed value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebFeed():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnWebFeed value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebFeed() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String url, String title, String? description, SnScrappedLink? preview, SnWebFeedConfig config, String publisherId, List<SnWebArticle> articles, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebFeed() when $default != null:
|
||||
return $default(_that.id,_that.url,_that.title,_that.description,_that.preview,_that.config,_that.publisherId,_that.articles,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String url, String title, String? description, SnScrappedLink? preview, SnWebFeedConfig config, String publisherId, List<SnWebArticle> articles, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebFeed():
|
||||
return $default(_that.id,_that.url,_that.title,_that.description,_that.preview,_that.config,_that.publisherId,_that.articles,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String url, String title, String? description, SnScrappedLink? preview, SnWebFeedConfig config, String publisherId, List<SnWebArticle> articles, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebFeed() when $default != null:
|
||||
return $default(_that.id,_that.url,_that.title,_that.description,_that.preview,_that.config,_that.publisherId,_that.articles,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnWebFeed implements SnWebFeed {
|
||||
const _SnWebFeed({required this.id, required this.url, required this.title, this.description, this.preview, this.config = const SnWebFeedConfig(), required this.publisherId, final List<SnWebArticle> articles = const [], required this.createdAt, required this.updatedAt, this.deletedAt}): _articles = articles;
|
||||
factory _SnWebFeed.fromJson(Map<String, dynamic> json) => _$SnWebFeedFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String url;
|
||||
@override final String title;
|
||||
@override final String? description;
|
||||
@override final SnScrappedLink? preview;
|
||||
@override@JsonKey() final SnWebFeedConfig config;
|
||||
@override final String publisherId;
|
||||
final List<SnWebArticle> _articles;
|
||||
@override@JsonKey() List<SnWebArticle> get articles {
|
||||
if (_articles is EqualUnmodifiableListView) return _articles;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableListView(_articles);
|
||||
}
|
||||
|
||||
@override final DateTime createdAt;
|
||||
@override final DateTime updatedAt;
|
||||
@override final DateTime? deletedAt;
|
||||
|
||||
/// Create a copy of SnWebFeed
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnWebFeedCopyWith<_SnWebFeed> get copyWith => __$SnWebFeedCopyWithImpl<_SnWebFeed>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnWebFeedToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWebFeed&&(identical(other.id, id) || other.id == id)&&(identical(other.url, url) || other.url == url)&&(identical(other.title, title) || other.title == title)&&(identical(other.description, description) || other.description == description)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.config, config) || other.config == config)&&(identical(other.publisherId, publisherId) || other.publisherId == publisherId)&&const DeepCollectionEquality().equals(other._articles, _articles)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,url,title,description,preview,config,publisherId,const DeepCollectionEquality().hash(_articles),createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnWebFeed(id: $id, url: $url, title: $title, description: $description, preview: $preview, config: $config, publisherId: $publisherId, articles: $articles, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnWebFeedCopyWith<$Res> implements $SnWebFeedCopyWith<$Res> {
|
||||
factory _$SnWebFeedCopyWith(_SnWebFeed value, $Res Function(_SnWebFeed) _then) = __$SnWebFeedCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String url, String title, String? description, SnScrappedLink? preview, SnWebFeedConfig config, String publisherId, List<SnWebArticle> articles, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@override $SnScrappedLinkCopyWith<$Res>? get preview;@override $SnWebFeedConfigCopyWith<$Res> get config;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnWebFeedCopyWithImpl<$Res>
|
||||
implements _$SnWebFeedCopyWith<$Res> {
|
||||
__$SnWebFeedCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnWebFeed _self;
|
||||
final $Res Function(_SnWebFeed) _then;
|
||||
|
||||
/// Create a copy of SnWebFeed
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? url = null,Object? title = null,Object? description = freezed,Object? preview = freezed,Object? config = null,Object? publisherId = null,Object? articles = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnWebFeed(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
|
||||
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
as String,description: freezed == description ? _self.description : description // ignore: cast_nullable_to_non_nullable
|
||||
as String?,preview: freezed == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable
|
||||
as SnScrappedLink?,config: null == config ? _self.config : config // ignore: cast_nullable_to_non_nullable
|
||||
as SnWebFeedConfig,publisherId: null == publisherId ? _self.publisherId : publisherId // ignore: cast_nullable_to_non_nullable
|
||||
as String,articles: null == articles ? _self._articles : articles // ignore: cast_nullable_to_non_nullable
|
||||
as List<SnWebArticle>,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnWebFeed
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnScrappedLinkCopyWith<$Res>? get preview {
|
||||
if (_self.preview == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnScrappedLinkCopyWith<$Res>(_self.preview!, (value) {
|
||||
return _then(_self.copyWith(preview: value));
|
||||
});
|
||||
}/// Create a copy of SnWebFeed
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnWebFeedConfigCopyWith<$Res> get config {
|
||||
|
||||
return $SnWebFeedConfigCopyWith<$Res>(_self.config, (value) {
|
||||
return _then(_self.copyWith(config: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// @nodoc
|
||||
mixin _$SnWebArticle {
|
||||
|
||||
String get id; String get title; String get url; String? get author; Map<String, dynamic>? get meta; SnScrappedLink? get preview; SnWebFeed? get feed; String? get content; DateTime? get publishedAt; String get feedId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
|
||||
/// Create a copy of SnWebArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnWebArticleCopyWith<SnWebArticle> get copyWith => _$SnWebArticleCopyWithImpl<SnWebArticle>(this as SnWebArticle, _$identity);
|
||||
|
||||
/// Serializes this SnWebArticle to a JSON map.
|
||||
Map<String, dynamic> toJson();
|
||||
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnWebArticle&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.url, url) || other.url == url)&&(identical(other.author, author) || other.author == author)&&const DeepCollectionEquality().equals(other.meta, meta)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.feed, feed) || other.feed == feed)&&(identical(other.content, content) || other.content == content)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.feedId, feedId) || other.feedId == feedId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,title,url,author,const DeepCollectionEquality().hash(meta),preview,feed,content,publishedAt,feedId,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnWebArticle(id: $id, title: $title, url: $url, author: $author, meta: $meta, preview: $preview, feed: $feed, content: $content, publishedAt: $publishedAt, feedId: $feedId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class $SnWebArticleCopyWith<$Res> {
|
||||
factory $SnWebArticleCopyWith(SnWebArticle value, $Res Function(SnWebArticle) _then) = _$SnWebArticleCopyWithImpl;
|
||||
@useResult
|
||||
$Res call({
|
||||
String id, String title, String url, String? author, Map<String, dynamic>? meta, SnScrappedLink? preview, SnWebFeed? feed, String? content, DateTime? publishedAt, String feedId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
$SnScrappedLinkCopyWith<$Res>? get preview;$SnWebFeedCopyWith<$Res>? get feed;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class _$SnWebArticleCopyWithImpl<$Res>
|
||||
implements $SnWebArticleCopyWith<$Res> {
|
||||
_$SnWebArticleCopyWithImpl(this._self, this._then);
|
||||
|
||||
final SnWebArticle _self;
|
||||
final $Res Function(SnWebArticle) _then;
|
||||
|
||||
/// Create a copy of SnWebArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? title = null,Object? url = null,Object? author = freezed,Object? meta = freezed,Object? preview = freezed,Object? feed = freezed,Object? content = freezed,Object? publishedAt = freezed,Object? feedId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_self.copyWith(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
|
||||
as String,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
|
||||
as String?,meta: freezed == meta ? _self.meta : meta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,preview: freezed == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable
|
||||
as SnScrappedLink?,feed: freezed == feed ? _self.feed : feed // ignore: cast_nullable_to_non_nullable
|
||||
as SnWebFeed?,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||
as String?,publishedAt: freezed == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,feedId: null == feedId ? _self.feedId : feedId // ignore: cast_nullable_to_non_nullable
|
||||
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
/// Create a copy of SnWebArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnScrappedLinkCopyWith<$Res>? get preview {
|
||||
if (_self.preview == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnScrappedLinkCopyWith<$Res>(_self.preview!, (value) {
|
||||
return _then(_self.copyWith(preview: value));
|
||||
});
|
||||
}/// Create a copy of SnWebArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnWebFeedCopyWith<$Res>? get feed {
|
||||
if (_self.feed == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnWebFeedCopyWith<$Res>(_self.feed!, (value) {
|
||||
return _then(_self.copyWith(feed: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Adds pattern-matching-related methods to [SnWebArticle].
|
||||
extension SnWebArticlePatterns on SnWebArticle {
|
||||
/// A variant of `map` that fallback to returning `orElse`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnWebArticle value)? $default,{required TResult orElse(),}){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebArticle() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// Callbacks receives the raw object, upcasted.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case final Subclass2 value:
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnWebArticle value) $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebArticle():
|
||||
return $default(_that);}
|
||||
}
|
||||
/// A variant of `map` that fallback to returning `null`.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case final Subclass value:
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnWebArticle value)? $default,){
|
||||
final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebArticle() when $default != null:
|
||||
return $default(_that);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
/// A variant of `when` that fallback to an `orElse` callback.
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return orElse();
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String title, String url, String? author, Map<String, dynamic>? meta, SnScrappedLink? preview, SnWebFeed? feed, String? content, DateTime? publishedAt, String feedId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebArticle() when $default != null:
|
||||
return $default(_that.id,_that.title,_that.url,_that.author,_that.meta,_that.preview,_that.feed,_that.content,_that.publishedAt,_that.feedId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return orElse();
|
||||
|
||||
}
|
||||
}
|
||||
/// A `switch`-like method, using callbacks.
|
||||
///
|
||||
/// As opposed to `map`, this offers destructuring.
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case Subclass2(:final field2):
|
||||
/// return ...;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String title, String url, String? author, Map<String, dynamic>? meta, SnScrappedLink? preview, SnWebFeed? feed, String? content, DateTime? publishedAt, String feedId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebArticle():
|
||||
return $default(_that.id,_that.title,_that.url,_that.author,_that.meta,_that.preview,_that.feed,_that.content,_that.publishedAt,_that.feedId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
|
||||
}
|
||||
/// A variant of `when` that fallback to returning `null`
|
||||
///
|
||||
/// It is equivalent to doing:
|
||||
/// ```dart
|
||||
/// switch (sealedClass) {
|
||||
/// case Subclass(:final field):
|
||||
/// return ...;
|
||||
/// case _:
|
||||
/// return null;
|
||||
/// }
|
||||
/// ```
|
||||
|
||||
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String title, String url, String? author, Map<String, dynamic>? meta, SnScrappedLink? preview, SnWebFeed? feed, String? content, DateTime? publishedAt, String feedId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
|
||||
switch (_that) {
|
||||
case _SnWebArticle() when $default != null:
|
||||
return $default(_that.id,_that.title,_that.url,_that.author,_that.meta,_that.preview,_that.feed,_that.content,_that.publishedAt,_that.feedId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
|
||||
return null;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
@JsonSerializable()
|
||||
|
||||
class _SnWebArticle implements SnWebArticle {
|
||||
const _SnWebArticle({required this.id, required this.title, required this.url, this.author, final Map<String, dynamic>? meta, this.preview, this.feed, this.content, this.publishedAt, required this.feedId, required this.createdAt, required this.updatedAt, this.deletedAt}): _meta = meta;
|
||||
factory _SnWebArticle.fromJson(Map<String, dynamic> json) => _$SnWebArticleFromJson(json);
|
||||
|
||||
@override final String id;
|
||||
@override final String title;
|
||||
@override final String url;
|
||||
@override final String? author;
|
||||
final Map<String, dynamic>? _meta;
|
||||
@override Map<String, dynamic>? get meta {
|
||||
final value = _meta;
|
||||
if (value == null) return null;
|
||||
if (_meta is EqualUnmodifiableMapView) return _meta;
|
||||
// ignore: implicit_dynamic_type
|
||||
return EqualUnmodifiableMapView(value);
|
||||
}
|
||||
|
||||
@override final SnScrappedLink? preview;
|
||||
@override final SnWebFeed? feed;
|
||||
@override final String? content;
|
||||
@override final DateTime? publishedAt;
|
||||
@override final String feedId;
|
||||
@override final DateTime createdAt;
|
||||
@override final DateTime updatedAt;
|
||||
@override final DateTime? deletedAt;
|
||||
|
||||
/// Create a copy of SnWebArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@pragma('vm:prefer-inline')
|
||||
_$SnWebArticleCopyWith<_SnWebArticle> get copyWith => __$SnWebArticleCopyWithImpl<_SnWebArticle>(this, _$identity);
|
||||
|
||||
@override
|
||||
Map<String, dynamic> toJson() {
|
||||
return _$SnWebArticleToJson(this, );
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnWebArticle&&(identical(other.id, id) || other.id == id)&&(identical(other.title, title) || other.title == title)&&(identical(other.url, url) || other.url == url)&&(identical(other.author, author) || other.author == author)&&const DeepCollectionEquality().equals(other._meta, _meta)&&(identical(other.preview, preview) || other.preview == preview)&&(identical(other.feed, feed) || other.feed == feed)&&(identical(other.content, content) || other.content == content)&&(identical(other.publishedAt, publishedAt) || other.publishedAt == publishedAt)&&(identical(other.feedId, feedId) || other.feedId == feedId)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
|
||||
}
|
||||
|
||||
@JsonKey(includeFromJson: false, includeToJson: false)
|
||||
@override
|
||||
int get hashCode => Object.hash(runtimeType,id,title,url,author,const DeepCollectionEquality().hash(_meta),preview,feed,content,publishedAt,feedId,createdAt,updatedAt,deletedAt);
|
||||
|
||||
@override
|
||||
String toString() {
|
||||
return 'SnWebArticle(id: $id, title: $title, url: $url, author: $author, meta: $meta, preview: $preview, feed: $feed, content: $content, publishedAt: $publishedAt, feedId: $feedId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/// @nodoc
|
||||
abstract mixin class _$SnWebArticleCopyWith<$Res> implements $SnWebArticleCopyWith<$Res> {
|
||||
factory _$SnWebArticleCopyWith(_SnWebArticle value, $Res Function(_SnWebArticle) _then) = __$SnWebArticleCopyWithImpl;
|
||||
@override @useResult
|
||||
$Res call({
|
||||
String id, String title, String url, String? author, Map<String, dynamic>? meta, SnScrappedLink? preview, SnWebFeed? feed, String? content, DateTime? publishedAt, String feedId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
|
||||
});
|
||||
|
||||
|
||||
@override $SnScrappedLinkCopyWith<$Res>? get preview;@override $SnWebFeedCopyWith<$Res>? get feed;
|
||||
|
||||
}
|
||||
/// @nodoc
|
||||
class __$SnWebArticleCopyWithImpl<$Res>
|
||||
implements _$SnWebArticleCopyWith<$Res> {
|
||||
__$SnWebArticleCopyWithImpl(this._self, this._then);
|
||||
|
||||
final _SnWebArticle _self;
|
||||
final $Res Function(_SnWebArticle) _then;
|
||||
|
||||
/// Create a copy of SnWebArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? title = null,Object? url = null,Object? author = freezed,Object? meta = freezed,Object? preview = freezed,Object? feed = freezed,Object? content = freezed,Object? publishedAt = freezed,Object? feedId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
|
||||
return _then(_SnWebArticle(
|
||||
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
|
||||
as String,title: null == title ? _self.title : title // ignore: cast_nullable_to_non_nullable
|
||||
as String,url: null == url ? _self.url : url // ignore: cast_nullable_to_non_nullable
|
||||
as String,author: freezed == author ? _self.author : author // ignore: cast_nullable_to_non_nullable
|
||||
as String?,meta: freezed == meta ? _self._meta : meta // ignore: cast_nullable_to_non_nullable
|
||||
as Map<String, dynamic>?,preview: freezed == preview ? _self.preview : preview // ignore: cast_nullable_to_non_nullable
|
||||
as SnScrappedLink?,feed: freezed == feed ? _self.feed : feed // ignore: cast_nullable_to_non_nullable
|
||||
as SnWebFeed?,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
|
||||
as String?,publishedAt: freezed == publishedAt ? _self.publishedAt : publishedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,feedId: null == feedId ? _self.feedId : feedId // ignore: cast_nullable_to_non_nullable
|
||||
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
|
||||
as DateTime?,
|
||||
));
|
||||
}
|
||||
|
||||
/// Create a copy of SnWebArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnScrappedLinkCopyWith<$Res>? get preview {
|
||||
if (_self.preview == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnScrappedLinkCopyWith<$Res>(_self.preview!, (value) {
|
||||
return _then(_self.copyWith(preview: value));
|
||||
});
|
||||
}/// Create a copy of SnWebArticle
|
||||
/// with the given fields replaced by the non-null parameter values.
|
||||
@override
|
||||
@pragma('vm:prefer-inline')
|
||||
$SnWebFeedCopyWith<$Res>? get feed {
|
||||
if (_self.feed == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $SnWebFeedCopyWith<$Res>(_self.feed!, (value) {
|
||||
return _then(_self.copyWith(feed: value));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// dart format on
|
||||
94
lib/discovery/discovery_models/webfeed.g.dart
Normal file
94
lib/discovery/discovery_models/webfeed.g.dart
Normal file
@@ -0,0 +1,94 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'webfeed.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// JsonSerializableGenerator
|
||||
// **************************************************************************
|
||||
|
||||
_SnWebFeedConfig _$SnWebFeedConfigFromJson(Map<String, dynamic> json) =>
|
||||
_SnWebFeedConfig(scrapPage: json['scrap_page'] as bool? ?? false);
|
||||
|
||||
Map<String, dynamic> _$SnWebFeedConfigToJson(_SnWebFeedConfig instance) =>
|
||||
<String, dynamic>{'scrap_page': instance.scrapPage};
|
||||
|
||||
_SnWebFeed _$SnWebFeedFromJson(Map<String, dynamic> json) => _SnWebFeed(
|
||||
id: json['id'] as String,
|
||||
url: json['url'] as String,
|
||||
title: json['title'] as String,
|
||||
description: json['description'] as String?,
|
||||
preview: json['preview'] == null
|
||||
? null
|
||||
: SnScrappedLink.fromJson(json['preview'] as Map<String, dynamic>),
|
||||
config: json['config'] == null
|
||||
? const SnWebFeedConfig()
|
||||
: SnWebFeedConfig.fromJson(json['config'] as Map<String, dynamic>),
|
||||
publisherId: json['publisher_id'] as String,
|
||||
articles:
|
||||
(json['articles'] as List<dynamic>?)
|
||||
?.map((e) => SnWebArticle.fromJson(e as Map<String, dynamic>))
|
||||
.toList() ??
|
||||
const [],
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnWebFeedToJson(_SnWebFeed instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'url': instance.url,
|
||||
'title': instance.title,
|
||||
'description': instance.description,
|
||||
'preview': instance.preview?.toJson(),
|
||||
'config': instance.config.toJson(),
|
||||
'publisher_id': instance.publisherId,
|
||||
'articles': instance.articles.map((e) => e.toJson()).toList(),
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
||||
|
||||
_SnWebArticle _$SnWebArticleFromJson(Map<String, dynamic> json) =>
|
||||
_SnWebArticle(
|
||||
id: json['id'] as String,
|
||||
title: json['title'] as String,
|
||||
url: json['url'] as String,
|
||||
author: json['author'] as String?,
|
||||
meta: json['meta'] as Map<String, dynamic>?,
|
||||
preview: json['preview'] == null
|
||||
? null
|
||||
: SnScrappedLink.fromJson(json['preview'] as Map<String, dynamic>),
|
||||
feed: json['feed'] == null
|
||||
? null
|
||||
: SnWebFeed.fromJson(json['feed'] as Map<String, dynamic>),
|
||||
content: json['content'] as String?,
|
||||
publishedAt: json['published_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['published_at'] as String),
|
||||
feedId: json['feed_id'] as String,
|
||||
createdAt: DateTime.parse(json['created_at'] as String),
|
||||
updatedAt: DateTime.parse(json['updated_at'] as String),
|
||||
deletedAt: json['deleted_at'] == null
|
||||
? null
|
||||
: DateTime.parse(json['deleted_at'] as String),
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$SnWebArticleToJson(_SnWebArticle instance) =>
|
||||
<String, dynamic>{
|
||||
'id': instance.id,
|
||||
'title': instance.title,
|
||||
'url': instance.url,
|
||||
'author': instance.author,
|
||||
'meta': instance.meta,
|
||||
'preview': instance.preview?.toJson(),
|
||||
'feed': instance.feed?.toJson(),
|
||||
'content': instance.content,
|
||||
'published_at': instance.publishedAt?.toIso8601String(),
|
||||
'feed_id': instance.feedId,
|
||||
'created_at': instance.createdAt.toIso8601String(),
|
||||
'updated_at': instance.updatedAt.toIso8601String(),
|
||||
'deleted_at': instance.deletedAt?.toIso8601String(),
|
||||
};
|
||||
40
lib/discovery/discovery_service.dart
Normal file
40
lib/discovery/discovery_service.dart
Normal file
@@ -0,0 +1,40 @@
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:island/discovery/discovery_models/autocomplete_response.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
|
||||
final autocompleteServiceProvider = Provider<AutocompleteService>((ref) {
|
||||
final dio = ref.watch(apiClientProvider);
|
||||
return AutocompleteService(dio);
|
||||
});
|
||||
|
||||
class AutocompleteService {
|
||||
final Dio _client;
|
||||
|
||||
AutocompleteService(this._client);
|
||||
|
||||
Future<List<AutocompleteSuggestion>> getSuggestions(
|
||||
String roomId,
|
||||
String content,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'/messager/chat/$roomId/autocomplete',
|
||||
data: {'content': content},
|
||||
);
|
||||
|
||||
final data = response.data as List<dynamic>;
|
||||
return data.map((json) => AutocompleteSuggestion.fromJson(json)).toList();
|
||||
}
|
||||
|
||||
Future<List<AutocompleteSuggestion>> getGeneralSuggestions(
|
||||
String content,
|
||||
) async {
|
||||
final response = await _client.post(
|
||||
'/sphere/autocomplete',
|
||||
data: {'content': content},
|
||||
);
|
||||
|
||||
final data = response.data as List<dynamic>;
|
||||
return data.map((json) => AutocompleteSuggestion.fromJson(json)).toList();
|
||||
}
|
||||
}
|
||||
721
lib/discovery/explore.dart
Normal file
721
lib/discovery/explore.dart
Normal file
@@ -0,0 +1,721 @@
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:island/accounts/accounts_models/account.dart';
|
||||
import 'package:island/core/models/activity.dart';
|
||||
import 'package:island/notifications/notification.dart';
|
||||
import 'package:island/posts/post/post_list.dart';
|
||||
import 'package:island/posts/posts_models/publisher.dart';
|
||||
import 'package:island/posts/posts_widgets/post/filters/post_subscription_filter.dart';
|
||||
import 'package:island/posts/posts_widgets/post/post_item.dart';
|
||||
import 'package:island/posts/posts_widgets/post/post_item_skeleton.dart';
|
||||
import 'package:island/posts/posts_widgets/publisher/publisher_card.dart';
|
||||
import 'package:island/realms/realms_models/realm.dart';
|
||||
import 'package:island/discovery/discovery_models/webfeed.dart';
|
||||
import 'package:island/accounts/event_calendar.dart';
|
||||
import 'package:island/posts/posts_pod.dart';
|
||||
import 'package:island/accounts/accounts_pod.dart';
|
||||
import 'package:island/auth/login_modal.dart';
|
||||
import 'package:island/core/services/responsive.dart';
|
||||
import 'package:island/realms/realms_widgets/realm/realm_card.dart';
|
||||
import 'package:island/shared/widgets/app_scaffold.dart';
|
||||
import 'package:island/posts/posts_models/post.dart';
|
||||
import 'package:island/shared/widgets/extended_refresh_indicator.dart';
|
||||
import 'package:island/shared/widgets/pagination_list.dart';
|
||||
import 'package:island/posts/posts_widgets/compose_sheet.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:island/discovery/web_article_card.dart';
|
||||
import 'package:island/core/services/event_bus.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
import 'package:super_sliver_list/super_sliver_list.dart';
|
||||
import 'package:island/posts/posts_widgets/post/post_list.dart';
|
||||
|
||||
class ExploreScreen extends HookConsumerWidget {
|
||||
const ExploreScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final currentFilter = useState<String?>(null);
|
||||
final selectedPublisherNames = useState<List<String>>([]);
|
||||
final selectedCategoryIds = useState<List<String>>([]);
|
||||
final selectedTagIds = useState<List<String>>([]);
|
||||
final notifier = ref.watch(activityListProvider.notifier);
|
||||
|
||||
void handleFilterChange(String? filter) {
|
||||
currentFilter.value = filter;
|
||||
notifier.applyFilter(filter);
|
||||
}
|
||||
|
||||
// Listen for post creation events to refresh activities
|
||||
useEffect(() {
|
||||
final subscription = eventBus.on<PostCreatedEvent>().listen((event) {
|
||||
ref.read(activityListProvider.notifier).refresh();
|
||||
});
|
||||
return subscription.cancel;
|
||||
}, []);
|
||||
|
||||
final now = DateTime.now();
|
||||
|
||||
final query = useState(
|
||||
EventCalendarQuery(uname: 'me', year: now.year, month: now.month),
|
||||
);
|
||||
|
||||
final events = ref.watch(eventCalendarProvider(query.value));
|
||||
|
||||
final selectedDay = useState(now);
|
||||
|
||||
final user = ref.watch(userInfoProvider);
|
||||
|
||||
final notificationCount = ref.watch(notificationUnreadCountProvider);
|
||||
|
||||
final isWide = isWideScreen(context);
|
||||
|
||||
final hasSubscriptionsSelected = selectedPublisherNames.value.isNotEmpty;
|
||||
|
||||
final filterBar = Card(
|
||||
margin: EdgeInsets.only(top: MediaQuery.of(context).padding.top),
|
||||
child: Row(
|
||||
children: [
|
||||
Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: hasSubscriptionsSelected
|
||||
? null
|
||||
: () => handleFilterChange(null),
|
||||
icon: Icon(
|
||||
Symbols.explore,
|
||||
fill: currentFilter.value == null ? 1 : null,
|
||||
),
|
||||
tooltip: 'explore'.tr(),
|
||||
isSelected: currentFilter.value == null,
|
||||
color: currentFilter.value == null
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: hasSubscriptionsSelected
|
||||
? null
|
||||
: () => handleFilterChange('subscriptions'),
|
||||
icon: Icon(
|
||||
Symbols.subscriptions,
|
||||
fill: currentFilter.value == 'subscriptions' ? 1 : null,
|
||||
),
|
||||
tooltip: 'exploreFilterSubscriptions'.tr(),
|
||||
isSelected: currentFilter.value == 'subscriptions',
|
||||
color: currentFilter.value == 'subscriptions'
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: hasSubscriptionsSelected
|
||||
? null
|
||||
: () => handleFilterChange('friends'),
|
||||
icon: Icon(
|
||||
Symbols.people,
|
||||
fill: currentFilter.value == 'friends' ? 1 : null,
|
||||
),
|
||||
tooltip: 'exploreFilterFriends'.tr(),
|
||||
isSelected: currentFilter.value == 'friends',
|
||||
color: currentFilter.value == 'friends'
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: null,
|
||||
),
|
||||
],
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
context.pushNamed('articles');
|
||||
},
|
||||
icon: Icon(Symbols.auto_stories),
|
||||
tooltip: 'webArticlesStand'.tr(),
|
||||
),
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.search),
|
||||
const Gap(12),
|
||||
Text('search').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('universalSearch');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.category),
|
||||
const Gap(12),
|
||||
Text('categoriesAndTags').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postCategories');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.shuffle),
|
||||
const Gap(12),
|
||||
Text('postShuffle').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postShuffle');
|
||||
},
|
||||
),
|
||||
],
|
||||
icon: Icon(Symbols.action_key),
|
||||
tooltip: 'search'.tr(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 8, vertical: 4),
|
||||
);
|
||||
|
||||
final userInfo = ref.watch(userInfoProvider);
|
||||
|
||||
final appBar = isWide
|
||||
? null
|
||||
: _buildAppBar(
|
||||
currentFilter.value,
|
||||
handleFilterChange,
|
||||
context,
|
||||
hasSubscriptionsSelected,
|
||||
);
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: appBar,
|
||||
floatingActionButton: userInfo.value != null
|
||||
? FloatingActionButton(
|
||||
child: const Icon(Symbols.create),
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
useRootNavigator: true,
|
||||
builder: (context) => Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const Gap(40),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
leading: const Icon(Symbols.post_add_rounded),
|
||||
title: Text('postCompose').tr(),
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
await PostComposeSheet.show(context);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
),
|
||||
leading: const Icon(Symbols.article),
|
||||
title: Text('articleCompose').tr(),
|
||||
onTap: () async {
|
||||
Navigator.of(context).pop();
|
||||
GoRouter.of(context).pushNamed('articleCompose');
|
||||
},
|
||||
),
|
||||
const Gap(16),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
).padding(bottom: MediaQuery.of(context).padding.bottom)
|
||||
: null,
|
||||
body: isWide
|
||||
? _buildWideBody(
|
||||
context,
|
||||
ref,
|
||||
filterBar,
|
||||
user,
|
||||
notificationCount,
|
||||
query,
|
||||
events,
|
||||
selectedDay,
|
||||
currentFilter.value,
|
||||
selectedPublisherNames,
|
||||
selectedCategoryIds,
|
||||
selectedTagIds,
|
||||
)
|
||||
: _buildNarrowBody(context, ref, currentFilter.value),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActivityList(BuildContext context, WidgetRef ref) {
|
||||
final isWide = isWideScreen(context);
|
||||
|
||||
return PaginationWidget(
|
||||
provider: activityListProvider,
|
||||
notifier: activityListProvider.notifier,
|
||||
// Sliver list cannot provide refresh handled by the pagination list
|
||||
isRefreshable: false,
|
||||
isSliver: true,
|
||||
footerSkeletonChild: const PostItemSkeleton(maxWidth: double.infinity),
|
||||
contentBuilder: (data, footer) =>
|
||||
_ActivityListView(data: data, isWide: isWide, footer: footer),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPostList(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
List<String> selectedPublishers,
|
||||
List<String> selectedCategories,
|
||||
List<String> selectedTags,
|
||||
) {
|
||||
return SliverPostList(
|
||||
queryKey: 'explore_filtered',
|
||||
query: PostListQuery(
|
||||
publishers: selectedPublishers,
|
||||
categories: selectedCategories,
|
||||
tags: selectedTags,
|
||||
),
|
||||
padding: EdgeInsets.zero,
|
||||
itemPadding: const EdgeInsets.only(bottom: 8),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildWideBody(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
Widget filterBar,
|
||||
AsyncValue<SnAccount?> user,
|
||||
AsyncValue<int?> notificationCount,
|
||||
ValueNotifier<EventCalendarQuery> query,
|
||||
AsyncValue<List<dynamic>> events,
|
||||
ValueNotifier<DateTime> selectedDay,
|
||||
String? currentFilter,
|
||||
ValueNotifier<List<String>> selectedPublishers,
|
||||
ValueNotifier<List<String>> selectedCategories,
|
||||
ValueNotifier<List<String>> selectedTags,
|
||||
) {
|
||||
// Use post list when subscription filter is active and publishers are selected
|
||||
final usePostList =
|
||||
selectedPublishers.value.isNotEmpty ||
|
||||
selectedCategories.value.isNotEmpty ||
|
||||
selectedTags.value.isNotEmpty;
|
||||
final bodyView = usePostList
|
||||
? _buildPostList(
|
||||
context,
|
||||
ref,
|
||||
selectedPublishers.value,
|
||||
selectedCategories.value,
|
||||
selectedTags.value,
|
||||
)
|
||||
: _buildActivityList(context, ref);
|
||||
|
||||
final notifier = usePostList
|
||||
? null // Post list handles its own refreshing
|
||||
: ref.watch(activityListProvider.notifier);
|
||||
|
||||
final activityState = ref.watch(activityListProvider);
|
||||
|
||||
return Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: ExtendedRefreshIndicator(
|
||||
onRefresh: () async {
|
||||
await notifier?.refresh();
|
||||
},
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverGap(12),
|
||||
if (activityState.value?.isLoading ?? false)
|
||||
SliverToBoxAdapter(
|
||||
child: LinearProgressIndicator().padding(bottom: 8),
|
||||
),
|
||||
SliverToBoxAdapter(child: filterBar),
|
||||
const SliverGap(8),
|
||||
bodyView,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (user.value != null)
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 8,
|
||||
children: [
|
||||
Gap(4 + MediaQuery.paddingOf(context).top),
|
||||
PostSubscriptionFilterWidget(
|
||||
initialSelectedPublishers: selectedPublishers.value,
|
||||
initialSelectedCategories: selectedCategories.value,
|
||||
initialSelectedTags: selectedTags.value,
|
||||
onSelectedPublishersChanged: (names) {
|
||||
selectedPublishers.value = names;
|
||||
},
|
||||
onSelectedCategoriesChanged: (ids) {
|
||||
selectedCategories.value = ids;
|
||||
},
|
||||
onSelectedTagsChanged: (ids) {
|
||||
selectedTags.value = ids;
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else
|
||||
Flexible(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(Symbols.emoji_people_rounded, size: 40),
|
||||
const Gap(8),
|
||||
Text(
|
||||
'Welcome to\nthe Solar Network',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
textAlign: TextAlign.center,
|
||||
).bold(),
|
||||
const Gap(2),
|
||||
Text(
|
||||
'Login to explore more!',
|
||||
style: Theme.of(context).textTheme.bodyLarge,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const Gap(4),
|
||||
TextButton.icon(
|
||||
onPressed: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
useRootNavigator: true,
|
||||
isScrollControlled: true,
|
||||
builder: (context) => LoginModal(),
|
||||
);
|
||||
},
|
||||
icon: const Icon(Symbols.login),
|
||||
label: Text('login').tr(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 36, vertical: 16).center(),
|
||||
),
|
||||
],
|
||||
).padding(horizontal: 12);
|
||||
}
|
||||
|
||||
PreferredSizeWidget _buildAppBar(
|
||||
String? currentFilter,
|
||||
void Function(String?) handleFilterChange,
|
||||
BuildContext context,
|
||||
bool hasSubscriptionsSelected,
|
||||
) {
|
||||
final foregroundColor = Theme.of(context).appBarTheme.foregroundColor;
|
||||
|
||||
return AppBar(
|
||||
flexibleSpace: Container(
|
||||
height: 48,
|
||||
margin: EdgeInsets.only(
|
||||
left: 8,
|
||||
right: 8,
|
||||
top: 4 + MediaQuery.of(context).padding.top,
|
||||
bottom: 4,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Row(
|
||||
spacing: 8,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: hasSubscriptionsSelected
|
||||
? null
|
||||
: () => handleFilterChange(null),
|
||||
icon: Icon(
|
||||
Symbols.explore,
|
||||
color: foregroundColor,
|
||||
fill: currentFilter == null ? 1 : null,
|
||||
),
|
||||
tooltip: 'explore'.tr(),
|
||||
isSelected: currentFilter == null,
|
||||
color: currentFilter == null ? foregroundColor : null,
|
||||
),
|
||||
IconButton(
|
||||
onPressed: hasSubscriptionsSelected
|
||||
? null
|
||||
: () => handleFilterChange('subscriptions'),
|
||||
icon: Icon(
|
||||
Symbols.subscriptions,
|
||||
color: foregroundColor,
|
||||
fill: currentFilter == 'subscription' ? 1 : null,
|
||||
),
|
||||
tooltip: 'exploreFilterSubscriptions'.tr(),
|
||||
isSelected: currentFilter == 'subscriptions',
|
||||
),
|
||||
IconButton(
|
||||
onPressed: hasSubscriptionsSelected
|
||||
? null
|
||||
: () => handleFilterChange('friends'),
|
||||
icon: Icon(
|
||||
Symbols.people,
|
||||
color: foregroundColor,
|
||||
fill: currentFilter == 'friends' ? 1 : null,
|
||||
),
|
||||
tooltip: 'exploreFilterFriends'.tr(),
|
||||
isSelected: currentFilter == 'friends',
|
||||
),
|
||||
const Spacer(),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
context.pushNamed('articles');
|
||||
},
|
||||
icon: Icon(Symbols.auto_stories, color: foregroundColor),
|
||||
tooltip: 'webArticlesStand'.tr(),
|
||||
),
|
||||
PopupMenuButton(
|
||||
itemBuilder: (context) => [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.search),
|
||||
const Gap(12),
|
||||
Text('search').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('universalSearch');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.category),
|
||||
const Gap(12),
|
||||
Text('categoriesAndTags').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postCategories');
|
||||
},
|
||||
),
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.shuffle),
|
||||
const Gap(12),
|
||||
Text('postShuffle').tr(),
|
||||
],
|
||||
),
|
||||
onTap: () {
|
||||
context.pushNamed('postShuffle');
|
||||
},
|
||||
),
|
||||
],
|
||||
icon: Icon(Symbols.action_key, color: foregroundColor),
|
||||
tooltip: 'search'.tr(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildNarrowBody(
|
||||
BuildContext context,
|
||||
WidgetRef ref,
|
||||
String? currentFilter,
|
||||
) {
|
||||
final bodyView = _buildActivityList(context, ref);
|
||||
|
||||
final notifier = ref.watch(activityListProvider.notifier);
|
||||
|
||||
return Expanded(
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: ExtendedRefreshIndicator(
|
||||
onRefresh: notifier.refresh,
|
||||
child: CustomScrollView(slivers: [SliverGap(8), bodyView]),
|
||||
),
|
||||
).padding(horizontal: 8),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _DiscoveryActivityItem extends StatelessWidget {
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
const _DiscoveryActivityItem({required this.data});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final items = data['items'] as List;
|
||||
final type = items.firstOrNull?['type'] ?? 'unknown';
|
||||
|
||||
var flexWeights = isWideScreen(context) ? <int>[3, 2, 1] : <int>[4, 1];
|
||||
if (type == 'post') flexWeights = <int>[3, 2];
|
||||
|
||||
final height = type == 'post' ? 280.0 : 180.0;
|
||||
|
||||
final contentWidget = switch (type) {
|
||||
'post' => SuperListView.separated(
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: items.length,
|
||||
separatorBuilder: (context, index) => const Gap(12),
|
||||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
itemBuilder: (context, index) {
|
||||
final item = items[index];
|
||||
return Container(
|
||||
width: 320,
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 1 / MediaQuery.of(context).devicePixelRatio,
|
||||
color: Theme.of(context).dividerColor.withOpacity(0.5),
|
||||
),
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: const BorderRadius.all(Radius.circular(8)),
|
||||
child: SingleChildScrollView(
|
||||
child: PostActionableItem(
|
||||
item: SnPost.fromJson(item['data']),
|
||||
isCompact: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
_ => CarouselView.weighted(
|
||||
flexWeights: flexWeights,
|
||||
consumeMaxWeight: false,
|
||||
enableSplash: false,
|
||||
shape: const RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8)),
|
||||
),
|
||||
itemSnapping: false,
|
||||
children: [
|
||||
for (final item in items)
|
||||
switch (type) {
|
||||
'realm' => RealmDiscoveryCard(
|
||||
realm: SnRealm.fromJson(item['data']),
|
||||
maxWidth: 280,
|
||||
),
|
||||
'publisher' => PublisherDiscoveryCard(
|
||||
publisher: SnPublisher.fromJson(item['data']),
|
||||
maxWidth: 280,
|
||||
),
|
||||
'article' => WebArticleDiscoveryCard(
|
||||
article: SnWebArticle.fromJson(item['data']),
|
||||
maxWidth: 280,
|
||||
),
|
||||
_ => const Placeholder(),
|
||||
},
|
||||
],
|
||||
),
|
||||
};
|
||||
|
||||
return Card(
|
||||
margin: EdgeInsets.zero,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Icon(switch (type) {
|
||||
'realm' => Symbols.public,
|
||||
'publisher' => Symbols.account_circle,
|
||||
'article' => Symbols.auto_stories,
|
||||
'post' => Symbols.shuffle,
|
||||
_ => Symbols.explore,
|
||||
}, size: 19),
|
||||
const Gap(8),
|
||||
Text(
|
||||
(switch (type) {
|
||||
'realm' => 'discoverRealms',
|
||||
'publisher' => 'discoverPublishers',
|
||||
'article' => 'discoverWebArticles',
|
||||
'post' => 'discoverShuffledPost',
|
||||
_ => 'unknown',
|
||||
}).tr(),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
).padding(top: 1),
|
||||
],
|
||||
).padding(horizontal: 20, top: 8, bottom: 4),
|
||||
SizedBox(
|
||||
height: height,
|
||||
child: contentWidget,
|
||||
).padding(bottom: 8, horizontal: 8),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ActivityListView extends HookConsumerWidget {
|
||||
final List<SnTimelineEvent> data;
|
||||
final bool isWide;
|
||||
final Widget footer;
|
||||
|
||||
const _ActivityListView({
|
||||
required this.data,
|
||||
required this.isWide,
|
||||
required this.footer,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final notifier = ref.watch(activityListProvider.notifier);
|
||||
|
||||
return SliverList.separated(
|
||||
itemCount: data.length + 1,
|
||||
separatorBuilder: (_, _) => const Gap(8),
|
||||
itemBuilder: (context, index) {
|
||||
if (index == data.length) {
|
||||
return footer;
|
||||
}
|
||||
|
||||
final item = data[index];
|
||||
if (item.data == null) {
|
||||
return const SizedBox.shrink();
|
||||
}
|
||||
Widget itemWidget;
|
||||
|
||||
switch (item.type) {
|
||||
case 'posts.new':
|
||||
case 'posts.new.replies':
|
||||
itemWidget = PostActionableItem(
|
||||
borderRadius: 8,
|
||||
item: SnPost.fromJson(item.data!),
|
||||
onRefresh: () {
|
||||
notifier.refresh();
|
||||
},
|
||||
onUpdate: (post) {
|
||||
notifier.updateOne(index, item.copyWith(data: post.toJson()));
|
||||
},
|
||||
);
|
||||
itemWidget = Card(margin: EdgeInsets.zero, child: itemWidget);
|
||||
break;
|
||||
case 'discovery':
|
||||
itemWidget = _DiscoveryActivityItem(data: item.data!);
|
||||
break;
|
||||
default:
|
||||
itemWidget = const Placeholder();
|
||||
}
|
||||
|
||||
return itemWidget;
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
600
lib/discovery/search.dart
Normal file
600
lib/discovery/search.dart
Normal file
@@ -0,0 +1,600 @@
|
||||
import 'dart:async';
|
||||
import 'package:easy_localization/easy_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||
import 'package:gap/gap.dart';
|
||||
import 'package:island/accounts/accounts_widgets/account/account_name.dart';
|
||||
import 'package:island/accounts/accounts_widgets/account/account_picker.dart';
|
||||
import 'package:island/accounts/accounts_widgets/activitypub/actor_list_item.dart';
|
||||
import 'package:island/core/models/activitypub.dart';
|
||||
import 'package:island/accounts/accounts_models/account.dart';
|
||||
import 'package:island/posts/post/post_list.dart';
|
||||
import 'package:island/posts/posts_widgets/post/filters/post_filter.dart';
|
||||
import 'package:island/posts/posts_widgets/post/post_item.dart';
|
||||
import 'package:island/posts/posts_widgets/post/post_item_skeleton.dart';
|
||||
import 'package:island/core/services/activitypub_service.dart';
|
||||
import 'package:island/core/services/responsive.dart';
|
||||
import 'package:island/drive/drive_widgets/cloud_files.dart';
|
||||
import 'package:island/realms/realms_widgets/realm/realm_list.dart';
|
||||
import 'package:island/shared/widgets/alert.dart';
|
||||
import 'package:island/shared/widgets/app_scaffold.dart';
|
||||
import 'package:island/shared/widgets/extended_refresh_indicator.dart';
|
||||
import 'package:island/shared/widgets/pagination_list.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
import 'package:styled_widget/styled_widget.dart';
|
||||
|
||||
const kSearchPostListId = 'search';
|
||||
|
||||
enum SearchTab { posts, accounts, realms }
|
||||
|
||||
class UniversalSearchScreen extends HookConsumerWidget {
|
||||
final SearchTab initialTab;
|
||||
|
||||
const UniversalSearchScreen({super.key, this.initialTab = SearchTab.posts});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final tabController = useTabController(
|
||||
initialLength: 3,
|
||||
initialIndex: initialTab.index,
|
||||
);
|
||||
final searchQuery = useState<String>('');
|
||||
|
||||
return AppScaffold(
|
||||
isNoBackground: false,
|
||||
appBar: AppBar(
|
||||
title: SearchBar(
|
||||
constraints: const BoxConstraints(maxWidth: 400, minHeight: 32),
|
||||
hintText: 'search'.tr(),
|
||||
hintStyle: WidgetStatePropertyAll(TextStyle(fontSize: 14)),
|
||||
textStyle: WidgetStatePropertyAll(TextStyle(fontSize: 14)),
|
||||
onChanged: (value) {
|
||||
searchQuery.value = value;
|
||||
},
|
||||
leading: Icon(
|
||||
Symbols.search,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
elevation: 0,
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
TabBar(
|
||||
controller: tabController,
|
||||
tabs: [
|
||||
Tab(text: 'posts'.tr()),
|
||||
Tab(text: 'accounts'.tr()),
|
||||
Tab(text: 'realms'.tr()),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
controller: tabController,
|
||||
children: [
|
||||
_PostsSearchTab(searchQuery: searchQuery),
|
||||
_AccountSearchTab(searchQuery: searchQuery),
|
||||
_RealmsSearchTab(searchQuery: searchQuery),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _RealmsSearchTab extends HookConsumerWidget {
|
||||
final ValueNotifier<String> searchQuery;
|
||||
|
||||
const _RealmsSearchTab({required this.searchQuery});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
return Stack(
|
||||
children: [
|
||||
CustomScrollView(
|
||||
slivers: [
|
||||
const SliverGap(8),
|
||||
SliverRealmList(
|
||||
query: searchQuery.value,
|
||||
key: ValueKey(searchQuery.value),
|
||||
),
|
||||
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
||||
],
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _PostsSearchTab extends HookConsumerWidget {
|
||||
final ValueNotifier<String> searchQuery;
|
||||
|
||||
const _PostsSearchTab({required this.searchQuery});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final debounce = useMemoized(() => Duration(milliseconds: 500));
|
||||
final debounceTimer = useRef<Timer?>(null);
|
||||
final showFilters = useState(false);
|
||||
final pubNameController = useTextEditingController();
|
||||
final realmController = useTextEditingController();
|
||||
|
||||
final categoryTabController = useTabController(initialLength: 3);
|
||||
final queryState = useState(const PostListQuery());
|
||||
|
||||
final noti = ref.read(
|
||||
postListProvider(PostListQueryConfig(id: kSearchPostListId)).notifier,
|
||||
);
|
||||
|
||||
useEffect(() {
|
||||
return () {
|
||||
pubNameController.dispose();
|
||||
realmController.dispose();
|
||||
debounceTimer.value?.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
void onSearchChanged(String query, {bool skipDebounce = false}) {
|
||||
queryState.value = queryState.value.copyWith(queryTerm: query);
|
||||
|
||||
if (skipDebounce) {
|
||||
noti.applyFilter(queryState.value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (debounceTimer.value?.isActive ?? false) debounceTimer.value!.cancel();
|
||||
debounceTimer.value = Timer(debounce, () {
|
||||
noti.applyFilter(queryState.value);
|
||||
});
|
||||
}
|
||||
|
||||
void toggleFilterDisplay() {
|
||||
showFilters.value = !showFilters.value;
|
||||
}
|
||||
|
||||
Widget buildFilterPanel() {
|
||||
return PostFilterWidget(
|
||||
categoryTabController: categoryTabController,
|
||||
initialQuery: queryState.value,
|
||||
onQueryChanged: (newQuery) {
|
||||
queryState.value = newQuery;
|
||||
noti.applyFilter(newQuery);
|
||||
},
|
||||
hideSearch: true,
|
||||
);
|
||||
}
|
||||
|
||||
// Listen to search query changes and update the search
|
||||
useEffect(() {
|
||||
final query = searchQuery.value;
|
||||
if (query.isNotEmpty) {
|
||||
// Use Future.delayed to defer the provider modification
|
||||
Future.delayed(Duration.zero, () {
|
||||
onSearchChanged(query, skipDebounce: true);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}, [searchQuery.value]);
|
||||
|
||||
return Consumer(
|
||||
builder: (context, ref, child) {
|
||||
final searchState = ref.watch(
|
||||
postListProvider(PostListQueryConfig(id: kSearchPostListId)),
|
||||
);
|
||||
|
||||
return isWideScreen(context)
|
||||
? Row(
|
||||
children: [
|
||||
Flexible(
|
||||
flex: 4,
|
||||
child: ExtendedRefreshIndicator(
|
||||
onRefresh: noti.refresh,
|
||||
child: CustomScrollView(
|
||||
slivers: [
|
||||
SliverGap(4),
|
||||
PaginationList(
|
||||
provider: postListProvider(
|
||||
PostListQueryConfig(id: kSearchPostListId),
|
||||
),
|
||||
notifier: postListProvider(
|
||||
PostListQueryConfig(id: kSearchPostListId),
|
||||
).notifier,
|
||||
isSliver: true,
|
||||
isRefreshable: false,
|
||||
footerSkeletonChild: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
),
|
||||
child: const PostItemSkeleton(
|
||||
maxWidth: double.infinity,
|
||||
),
|
||||
),
|
||||
itemBuilder: (context, index, post) {
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
child: PostActionableItem(
|
||||
item: post,
|
||||
borderRadius: 8,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (searchState.value?.items.isEmpty == true &&
|
||||
searchQuery.value.isNotEmpty &&
|
||||
!searchState.isLoading)
|
||||
SliverFillRemaining(
|
||||
child: Center(child: Text('noResultsFound'.tr())),
|
||||
),
|
||||
SliverGap(MediaQuery.of(context).padding.bottom + 16),
|
||||
],
|
||||
).padding(left: 16),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
flex: 3,
|
||||
child: Align(
|
||||
alignment: Alignment.topLeft,
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.only(right: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Gap(8),
|
||||
Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
Symbols.tune,
|
||||
).padding(horizontal: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'filters'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Symbols.filter_alt,
|
||||
fill: showFilters.value ? 1 : null,
|
||||
),
|
||||
onPressed: toggleFilterDisplay,
|
||||
tooltip: 'toggleFilters'.tr(),
|
||||
),
|
||||
const Gap(4),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(8),
|
||||
if (showFilters.value) buildFilterPanel(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: CustomScrollView(
|
||||
slivers: [
|
||||
const SliverGap(8),
|
||||
SliverToBoxAdapter(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Card(
|
||||
margin: EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Symbols.tune).padding(horizontal: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
'filters'.tr(),
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodyLarge,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: Icon(
|
||||
Symbols.filter_alt,
|
||||
fill: showFilters.value ? 1 : null,
|
||||
),
|
||||
onPressed: toggleFilterDisplay,
|
||||
tooltip: 'toggleFilters'.tr(),
|
||||
),
|
||||
const Gap(4),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const Gap(4),
|
||||
if (showFilters.value) buildFilterPanel(),
|
||||
],
|
||||
),
|
||||
),
|
||||
PaginationList(
|
||||
provider: postListProvider(
|
||||
PostListQueryConfig(id: kSearchPostListId),
|
||||
),
|
||||
notifier: postListProvider(
|
||||
PostListQueryConfig(id: kSearchPostListId),
|
||||
).notifier,
|
||||
isSliver: true,
|
||||
isRefreshable: false,
|
||||
footerSkeletonChild: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: const PostItemSkeleton(maxWidth: double.infinity),
|
||||
),
|
||||
itemBuilder: (context, index, post) {
|
||||
return Card(
|
||||
margin: EdgeInsets.symmetric(
|
||||
horizontal: 8,
|
||||
vertical: 4,
|
||||
),
|
||||
child: PostActionableItem(item: post, borderRadius: 8),
|
||||
);
|
||||
},
|
||||
),
|
||||
if (searchState.value?.items.isEmpty == true &&
|
||||
searchQuery.value.isNotEmpty &&
|
||||
!searchState.isLoading)
|
||||
SliverFillRemaining(
|
||||
child: Center(child: Text('noResultsFound'.tr())),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AccountSearchTab extends HookConsumerWidget {
|
||||
final ValueNotifier<String> searchQuery;
|
||||
|
||||
const _AccountSearchTab({required this.searchQuery});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final debounce = useMemoized(() => const Duration(milliseconds: 500));
|
||||
final debounceTimer = useRef<Timer?>(null);
|
||||
final fediverseResults = useState<List<SnActivityPubActor>>([]);
|
||||
final internalResults = useState<List<SnAccount>>([]);
|
||||
final isSearching = useState(false);
|
||||
|
||||
useEffect(() {
|
||||
return () {
|
||||
debounceTimer.value?.cancel();
|
||||
};
|
||||
}, []);
|
||||
|
||||
Future<void> performSearch(String query) async {
|
||||
if (query.trim().isEmpty) {
|
||||
fediverseResults.value = [];
|
||||
internalResults.value = [];
|
||||
return;
|
||||
}
|
||||
|
||||
isSearching.value = true;
|
||||
try {
|
||||
// Search for fediverse users
|
||||
final activityPubService = ref.read(activityPubServiceProvider);
|
||||
final fediverseFuture = activityPubService.searchUsers(query);
|
||||
|
||||
// Search for internal users
|
||||
final internalFuture = ref.read(
|
||||
searchAccountsProvider(query: query).future,
|
||||
);
|
||||
|
||||
// Wait for both searches to complete
|
||||
final [fediverseData, internalData] = await Future.wait([
|
||||
fediverseFuture,
|
||||
internalFuture,
|
||||
]);
|
||||
|
||||
fediverseResults.value = fediverseData as List<SnActivityPubActor>;
|
||||
internalResults.value = internalData as List<SnAccount>;
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
} finally {
|
||||
isSearching.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
void onSearchChanged(String query) {
|
||||
if (debounceTimer.value?.isActive ?? false) {
|
||||
debounceTimer.value!.cancel();
|
||||
}
|
||||
debounceTimer.value = Timer(debounce, () {
|
||||
performSearch(query);
|
||||
});
|
||||
}
|
||||
|
||||
void updateActorIsFollowing(String actorId, bool isFollowing) {
|
||||
fediverseResults.value = fediverseResults.value
|
||||
.map(
|
||||
(a) => a.id == actorId ? a.copyWith(isFollowing: isFollowing) : a,
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
Future<void> handleFollow(SnActivityPubActor actor) async {
|
||||
try {
|
||||
updateActorIsFollowing(actor.id, true);
|
||||
final service = ref.read(activityPubServiceProvider);
|
||||
await service.followRemoteUser(actor.uri);
|
||||
showSnackBar(
|
||||
'followedUser'.tr(
|
||||
args: [
|
||||
'${actor.username?.isNotEmpty ?? false ? actor.username : actor.displayName}',
|
||||
],
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
updateActorIsFollowing(actor.id, false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> handleUnfollow(SnActivityPubActor actor) async {
|
||||
try {
|
||||
updateActorIsFollowing(actor.id, false);
|
||||
final service = ref.read(activityPubServiceProvider);
|
||||
await service.unfollowRemoteUser(actor.uri);
|
||||
showSnackBar(
|
||||
'unfollowedUser'.tr(
|
||||
args: [
|
||||
'${actor.username?.isNotEmpty ?? false ? actor.username : actor.displayName}',
|
||||
],
|
||||
),
|
||||
);
|
||||
} catch (err) {
|
||||
showErrorAlert(err);
|
||||
updateActorIsFollowing(actor.id, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Listen to search query changes and update the search
|
||||
useEffect(() {
|
||||
final query = searchQuery.value;
|
||||
if (query.isNotEmpty) {
|
||||
// Use Future.delayed to defer the provider modification
|
||||
Future.delayed(Duration.zero, () {
|
||||
onSearchChanged(query);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}, [searchQuery.value]);
|
||||
|
||||
// Combine and display results - local users first
|
||||
final allResults = [
|
||||
...internalResults.value.map(
|
||||
(account) => {'type': 'internal', 'data': account},
|
||||
),
|
||||
...fediverseResults.value.map(
|
||||
(actor) => {'type': 'fediverse', 'data': actor},
|
||||
),
|
||||
];
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: isSearching.value
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: allResults.isEmpty
|
||||
? Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Symbols.search,
|
||||
size: 64,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const Gap(16),
|
||||
if (searchQuery.value.isEmpty)
|
||||
Text(
|
||||
'searchUsersEmpty'.tr(),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
)
|
||||
else
|
||||
Text(
|
||||
'searchUsersNoResults'.tr(),
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
: ExtendedRefreshIndicator(
|
||||
onRefresh: () => performSearch(searchQuery.value),
|
||||
child: ListView.separated(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
itemCount: allResults.length,
|
||||
separatorBuilder: (context, index) => const Gap(8),
|
||||
itemBuilder: (context, index) {
|
||||
final result = allResults[index];
|
||||
if (result['type'] == 'fediverse') {
|
||||
final actor = result['data'] as SnActivityPubActor;
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: ApActorListItem(
|
||||
actor: actor,
|
||||
isFollowing: actor.isFollowing ?? false,
|
||||
isLoading: false,
|
||||
onFollow: () => handleFollow(actor),
|
||||
onUnfollow: () => handleUnfollow(actor),
|
||||
),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
final account = result['data'] as SnAccount;
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 560),
|
||||
child: ListTile(
|
||||
contentPadding: const EdgeInsets.only(
|
||||
left: 16,
|
||||
right: 12,
|
||||
),
|
||||
leading: Stack(
|
||||
children: [
|
||||
ProfilePictureWidget(
|
||||
file: account.profile.picture,
|
||||
),
|
||||
],
|
||||
),
|
||||
title: AccountName(
|
||||
account: account,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
subtitle: Row(
|
||||
children: [
|
||||
Text('@${account.name}'),
|
||||
if (account.profile.bio.isNotEmpty)
|
||||
Expanded(
|
||||
child: Text(
|
||||
account.profile.bio,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(
|
||||
context,
|
||||
).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: const SizedBox(
|
||||
width: 88,
|
||||
), // To align with ApActorListItem
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
210
lib/discovery/web_article_card.dart
Normal file
210
lib/discovery/web_article_card.dart
Normal file
@@ -0,0 +1,210 @@
|
||||
import 'package:cached_network_image/cached_network_image.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:go_router/go_router.dart';
|
||||
import 'package:island/discovery/discovery_models/webfeed.dart';
|
||||
import 'package:island/core/services/time.dart';
|
||||
import 'package:material_symbols_icons/symbols.dart';
|
||||
|
||||
class WebArticleCard extends StatelessWidget {
|
||||
final SnWebArticle article;
|
||||
final double? maxWidth;
|
||||
final bool showDetails;
|
||||
|
||||
const WebArticleCard({
|
||||
super.key,
|
||||
required this.article,
|
||||
this.maxWidth,
|
||||
this.showDetails = false,
|
||||
});
|
||||
|
||||
void _onTap(BuildContext context) {
|
||||
context.pushNamed('articleDetail', pathParameters: {'id': article.id});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () => _onTap(context),
|
||||
child: Column(
|
||||
children: [
|
||||
if (article.preview?.imageUrl != null)
|
||||
AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: CachedNetworkImage(
|
||||
imageUrl: article.preview!.imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
),
|
||||
),
|
||||
ListTile(
|
||||
isThreeLine: true,
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 20,
|
||||
vertical: 4,
|
||||
),
|
||||
trailing: const Icon(Symbols.chevron_right),
|
||||
title: Text(article.title),
|
||||
subtitle: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${article.createdAt.formatSystem()} · ${article.createdAt.formatRelative(context)}',
|
||||
),
|
||||
Text(
|
||||
article.feed?.title ?? 'Unknown Source',
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class WebArticleDiscoveryCard extends StatelessWidget {
|
||||
final SnWebArticle article;
|
||||
final double? maxWidth;
|
||||
final bool showDetails;
|
||||
|
||||
const WebArticleDiscoveryCard({
|
||||
super.key,
|
||||
required this.article,
|
||||
this.maxWidth,
|
||||
this.showDetails = false,
|
||||
});
|
||||
|
||||
void _onTap(BuildContext context) {
|
||||
context.pushNamed('articleDetail', pathParameters: {'id': article.id});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final theme = Theme.of(context);
|
||||
final colorScheme = theme.colorScheme;
|
||||
|
||||
return ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: maxWidth ?? double.infinity),
|
||||
child: Card(
|
||||
margin: EdgeInsets.zero,
|
||||
clipBehavior: Clip.antiAlias,
|
||||
child: InkWell(
|
||||
onTap: () => _onTap(context),
|
||||
child: AspectRatio(
|
||||
aspectRatio: 16 / 9,
|
||||
child: Stack(
|
||||
fit: StackFit.expand,
|
||||
children: [
|
||||
// Image or fallback
|
||||
article.preview?.imageUrl != null
|
||||
? CachedNetworkImage(
|
||||
imageUrl: article.preview!.imageUrl!,
|
||||
fit: BoxFit.cover,
|
||||
width: double.infinity,
|
||||
height: double.infinity,
|
||||
)
|
||||
: ColoredBox(
|
||||
color: colorScheme.secondaryContainer,
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
Icons.article_outlined,
|
||||
size: 48,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Gradient overlay
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
Colors.black.withOpacity(0.7),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Title
|
||||
Align(
|
||||
alignment: Alignment.bottomLeft,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
left: 12,
|
||||
right: 12,
|
||||
bottom: 8,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (showDetails)
|
||||
const SizedBox(height: 8)
|
||||
else
|
||||
Spacer(),
|
||||
Text(
|
||||
article.title,
|
||||
style: theme.textTheme.titleSmall?.copyWith(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
height: 1.3,
|
||||
),
|
||||
maxLines: showDetails ? 3 : 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
if (showDetails &&
|
||||
article.author?.isNotEmpty == true) ...[
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
article.author!,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: Colors.white.withOpacity(0.9),
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
if (showDetails) const Spacer(),
|
||||
if (showDetails && article.publishedAt != null) ...[
|
||||
Text(
|
||||
'${article.publishedAt!.formatSystem()} · ${article.publishedAt!.formatRelative(context)}',
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
],
|
||||
Text(
|
||||
article.feed?.title ?? 'Unknown Source',
|
||||
style: const TextStyle(
|
||||
fontSize: 9,
|
||||
color: Colors.white70,
|
||||
),
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
118
lib/discovery/webfeed.dart
Normal file
118
lib/discovery/webfeed.dart
Normal file
@@ -0,0 +1,118 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:island/discovery/discovery_models/webfeed.dart';
|
||||
import 'package:island/core/network.dart';
|
||||
|
||||
final webFeedListProvider = FutureProvider.autoDispose
|
||||
.family<List<SnWebFeed>, String>((ref, pubName) async {
|
||||
final client = ref.watch(apiClientProvider);
|
||||
final response = await client.get('/insight/publishers/$pubName/feeds');
|
||||
return (response.data as List)
|
||||
.map((json) => SnWebFeed.fromJson(json))
|
||||
.toList();
|
||||
});
|
||||
|
||||
class WebFeedNotifier extends AsyncNotifier<SnWebFeed> {
|
||||
final ({String pubName, String? feedId}) arg;
|
||||
WebFeedNotifier(this.arg);
|
||||
|
||||
@override
|
||||
FutureOr<SnWebFeed> build() async {
|
||||
if (arg.feedId == null || arg.feedId!.isEmpty) {
|
||||
return SnWebFeed(
|
||||
id: '',
|
||||
url: '',
|
||||
title: '',
|
||||
publisherId: arg.pubName,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
deletedAt: null,
|
||||
);
|
||||
}
|
||||
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final response = await client.get(
|
||||
'/insight/publishers/${arg.pubName}/feeds/${arg.feedId}',
|
||||
);
|
||||
return SnWebFeed.fromJson(response.data);
|
||||
} catch (e) {
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> saveFeed(SnWebFeed feed) async {
|
||||
state = const AsyncValue.loading();
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
final url = '/insight/publishers/${feed.publisherId}/feeds';
|
||||
|
||||
final response = feed.id.isEmpty
|
||||
? await client.post(url, data: feed.toJson())
|
||||
: await client.patch('$url/${feed.id}', data: feed.toJson());
|
||||
|
||||
state = AsyncValue.data(SnWebFeed.fromJson(response.data));
|
||||
} catch (error, stackTrace) {
|
||||
state = AsyncValue.error(error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> deleteFeed() async {
|
||||
final feedId = arg.feedId;
|
||||
if (feedId == null || feedId.isEmpty) return;
|
||||
|
||||
state = const AsyncValue.loading();
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.delete('/insight/publishers/${arg.pubName}/feeds/$feedId');
|
||||
state = AsyncValue.data(
|
||||
SnWebFeed(
|
||||
id: '',
|
||||
url: '',
|
||||
title: '',
|
||||
publisherId: arg.pubName,
|
||||
createdAt: DateTime.now(),
|
||||
updatedAt: DateTime.now(),
|
||||
deletedAt: null,
|
||||
),
|
||||
);
|
||||
} catch (error, stackTrace) {
|
||||
state = AsyncValue.error(error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> scrapFeed() async {
|
||||
final feedId = arg.feedId;
|
||||
if (feedId == null || feedId.isEmpty) return;
|
||||
|
||||
state = const AsyncValue.loading();
|
||||
try {
|
||||
final client = ref.read(apiClientProvider);
|
||||
await client.post(
|
||||
'/insight/publishers/${arg.pubName}/feeds/$feedId/scrap',
|
||||
options: Options(
|
||||
sendTimeout: const Duration(seconds: 60),
|
||||
receiveTimeout: const Duration(seconds: 180),
|
||||
),
|
||||
);
|
||||
|
||||
// Reload the feed
|
||||
final response = await client.get(
|
||||
'/sphere/publishers/${arg.pubName}/feeds/$feedId',
|
||||
);
|
||||
state = AsyncValue.data(SnWebFeed.fromJson(response.data));
|
||||
} catch (error, stackTrace) {
|
||||
state = AsyncValue.error(error, stackTrace);
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
final webFeedNotifierProvider = AsyncNotifierProvider.autoDispose
|
||||
.family<WebFeedNotifier, SnWebFeed, ({String pubName, String? feedId})>(
|
||||
WebFeedNotifier.new,
|
||||
);
|
||||
Reference in New Issue
Block a user