From 963e538ae563b612334b1970dcb09c0d0a262d9a Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sun, 26 Jan 2025 02:12:03 +0800 Subject: [PATCH] :sparkles: News Reader Basis --- android/app/src/main/AndroidManifest.xml | 4 + api/Reader/List News Sources.bru | 11 + api/Reader/List News.bru | 17 + api/Reader/Trigger Scan News.bru | 17 + assets/translations/en-US.json | 7 +- assets/translations/zh-CN.json | 7 +- ios/Podfile.lock | 14 + lib/providers/navigation.dart | 16 +- lib/router.dart | 95 +-- lib/screens/news/news_detail.dart | 113 +++ lib/screens/news/news_list.dart | 189 +++++ lib/types/news.dart | 38 + lib/types/news.freezed.dart | 660 ++++++++++++++++++ lib/types/news.g.dart | 59 ++ macos/Flutter/GeneratedPluginRegistrant.swift | 2 + pubspec.lock | 64 ++ pubspec.yaml | 1 + web/index.html | 2 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 20 files changed, 1251 insertions(+), 69 deletions(-) create mode 100644 api/Reader/List News Sources.bru create mode 100644 api/Reader/List News.bru create mode 100644 api/Reader/Trigger Scan News.bru create mode 100644 lib/screens/news/news_detail.dart create mode 100644 lib/screens/news/news_list.dart create mode 100644 lib/types/news.dart create mode 100644 lib/types/news.freezed.dart create mode 100644 lib/types/news.g.dart diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 803c22d..bcfa22b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -19,6 +19,10 @@ android:icon="@mipmap/ic_launcher" android:enableOnBackInvokedCallback="true" android:requestLegacyExternalStorage="true"> + + 6.0.3) + - flutter_inappwebview_ios/Core (0.0.1): + - Flutter + - OrderedSet (~> 6.0.3) - flutter_native_splash (2.4.3): - Flutter - flutter_udid (0.0.1): @@ -188,6 +195,7 @@ PODS: - nanopb/encode (= 3.30910.0) - nanopb/decode (3.30910.0) - nanopb/encode (3.30910.0) + - OrderedSet (6.0.3) - package_info_plus (0.4.5): - Flutter - pasteboard (0.0.1): @@ -239,6 +247,7 @@ DEPENDENCIES: - firebase_messaging (from `.symlinks/plugins/firebase_messaging/ios`) - Flutter (from `Flutter`) - flutter_app_update (from `.symlinks/plugins/flutter_app_update/ios`) + - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_udid (from `.symlinks/plugins/flutter_udid/ios`) - flutter_webrtc (from `.symlinks/plugins/flutter_webrtc/ios`) @@ -282,6 +291,7 @@ SPEC REPOS: - GoogleUtilities - Kingfisher - nanopb + - OrderedSet - PromisesObjC - SAMKeychain - SDWebImage @@ -309,6 +319,8 @@ EXTERNAL SOURCES: :path: Flutter flutter_app_update: :path: ".symlinks/plugins/flutter_app_update/ios" + flutter_inappwebview_ios: + :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" flutter_native_splash: :path: ".symlinks/plugins/flutter_native_splash/ios" flutter_udid: @@ -380,6 +392,7 @@ SPEC CHECKSUMS: FirebaseMessaging: e1aca1fcc23e8b9eddb0e33f375ff90944623021 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_app_update: 65f61da626cb111d1b24674abc4b01728d7723bc + flutter_inappwebview_ios: 6f63631e2c62a7c350263b13fa5427aedefe81d4 flutter_native_splash: f71420956eb811e6d310720fee915f1d42852e7a flutter_udid: b2417673f287ee62817a1de3d1643f47b9f508ab flutter_webrtc: 90260f83024b1b96d239a575ea4e3708e79344d1 @@ -396,6 +409,7 @@ SPEC CHECKSUMS: media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e nanopb: fad817b59e0457d11a5dfbde799381cd727c1275 + OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94 package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 pasteboard: 982969ebaa7c78af3e6cc7761e8f5e77565d9ce0 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 diff --git a/lib/providers/navigation.dart b/lib/providers/navigation.dart index 72e0bca..e9f82a1 100644 --- a/lib/providers/navigation.dart +++ b/lib/providers/navigation.dart @@ -58,6 +58,11 @@ class NavigationProvider extends ChangeNotifier { screen: 'realm', label: 'screenRealm', ), + AppNavDestination( + icon: Icon(Symbols.newspaper, weight: 400, opticalSize: 20), + screen: 'news', + label: 'screenNews', + ), AppNavDestination( icon: Icon(Symbols.photo_library, weight: 400, opticalSize: 20), screen: 'album', @@ -83,8 +88,7 @@ class NavigationProvider extends ChangeNotifier { List destinations = []; - int get pinnedDestinationCount => - destinations.where((ele) => ele.isPinned).length; + int get pinnedDestinationCount => destinations.where((ele) => ele.isPinned).length; NavigationProvider() { buildDestinations(kDefaultPinnedDestination); @@ -113,17 +117,13 @@ class NavigationProvider extends ChangeNotifier { } bool isIndexInRange(int min, int max) { - return _currentIndex != null && - _currentIndex! >= min && - _currentIndex! < max; + return _currentIndex != null && _currentIndex! >= min && _currentIndex! < max; } void autoDetectIndex(GoRouter? state) { if (state == null) return; final idx = destinations.indexWhere( - (ele) => - ele.screen == - state.routerDelegate.currentConfiguration.last.route.name, + (ele) => ele.screen == state.routerDelegate.currentConfiguration.last.route.name, ); _currentIndex = idx == -1 ? null : idx; notifyListeners(); diff --git a/lib/router.dart b/lib/router.dart index 4ed1091..b7cca2b 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -19,6 +19,8 @@ import 'package:surface/screens/chat/room.dart'; import 'package:surface/screens/explore.dart'; import 'package:surface/screens/friend.dart'; import 'package:surface/screens/home.dart'; +import 'package:surface/screens/news/news_detail.dart'; +import 'package:surface/screens/news/news_list.dart'; import 'package:surface/screens/notification.dart'; import 'package:surface/screens/post/post_detail.dart'; import 'package:surface/screens/post/post_editor.dart'; @@ -31,7 +33,6 @@ import 'package:surface/screens/settings.dart'; import 'package:surface/screens/sharing.dart'; import 'package:surface/types/post.dart'; import 'package:surface/widgets/about.dart'; -import 'package:surface/widgets/navigation/app_background.dart'; import 'package:surface/widgets/navigation/app_scaffold.dart'; Widget _fadeThroughTransition( @@ -48,18 +49,12 @@ final _appRoutes = [ GoRoute( path: '/', name: 'home', - pageBuilder: (context, state) => CustomTransitionPage( - transitionsBuilder: _fadeThroughTransition, - child: const HomeScreen(), - ), + builder: (context, state) => const HomeScreen(), ), GoRoute( path: '/posts', name: 'explore', - pageBuilder: (context, state) => CustomTransitionPage( - transitionsBuilder: _fadeThroughTransition, - child: const ExploreScreen(), - ), + builder: (context, state) => const ExploreScreen(), routes: [ GoRoute( path: '/write/:mode', @@ -104,64 +99,42 @@ final _appRoutes = [ GoRoute( path: '/account', name: 'account', - pageBuilder: (context, state) => CustomTransitionPage( - transitionsBuilder: _fadeThroughTransition, - child: const AccountScreen(), - ), + builder: (context, state) => const AccountScreen(), ), GoRoute( path: '/chat', name: 'chat', - pageBuilder: (context, state) => CustomTransitionPage( - transitionsBuilder: _fadeThroughTransition, - child: const ChatScreen(), - ), + builder: (context, state) => const ChatScreen(), routes: [ GoRoute( path: '/:scope/:alias', name: 'chatRoom', - builder: (context, state) => AppBackground( - child: ChatRoomScreen( - scope: state.pathParameters['scope']!, - alias: state.pathParameters['alias']!, - ), + builder: (context, state) => ChatRoomScreen( + scope: state.pathParameters['scope']!, + alias: state.pathParameters['alias']!, ), ), GoRoute( path: '/:scope/:alias/call', name: 'chatCallRoom', - builder: (context, state) => AppBackground( - child: CallRoomScreen( - scope: state.pathParameters['scope']!, - alias: state.pathParameters['alias']!, - ), + builder: (context, state) => CallRoomScreen( + scope: state.pathParameters['scope']!, + alias: state.pathParameters['alias']!, ), ), GoRoute( path: '/:scope/:alias/detail', name: 'channelDetail', - builder: (context, state) => AppBackground( - child: ChannelDetailScreen( - scope: state.pathParameters['scope']!, - alias: state.pathParameters['alias']!, - ), + builder: (context, state) => ChannelDetailScreen( + scope: state.pathParameters['scope']!, + alias: state.pathParameters['alias']!, ), ), GoRoute( path: '/manage', name: 'chatManage', - pageBuilder: (context, state) => CustomTransitionPage( - child: ChatManageScreen( - editingChannelAlias: state.uri.queryParameters['editing'], - ), - transitionsBuilder: (context, animation, secondaryAnimation, child) { - return FadeThroughTransition( - animation: animation, - secondaryAnimation: secondaryAnimation, - fillColor: Colors.transparent, - child: child, - ); - }, + builder: (context, state) => ChatManageScreen( + editingChannelAlias: state.uri.queryParameters['editing'], ), ), ], @@ -182,36 +155,40 @@ final _appRoutes = [ GoRoute( path: '/manage', name: 'realmManage', - pageBuilder: (context, state) => CustomTransitionPage( - transitionsBuilder: _fadeThroughTransition, - child: RealmManageScreen( - editingRealmAlias: state.uri.queryParameters['editing'], - ), + builder: (context, state) => RealmManageScreen( + editingRealmAlias: state.uri.queryParameters['editing'], ), ), ], ), + GoRoute( + path: '/news', + name: 'news', + builder: (context, state) => const NewsScreen(), + routes: [ + GoRoute( + path: '/:hash', + name: 'newsDetail', + builder: (context, state) => NewsDetailScreen( + hash: state.pathParameters['hash']!, + ), + ), + ] + ), GoRoute( path: '/album', name: 'album', - pageBuilder: (context, state) => CustomTransitionPage( - transitionsBuilder: _fadeThroughTransition, - child: const AlbumScreen(), - ), + builder: (context, state) => const AlbumScreen(), ), GoRoute( path: '/friend', name: 'friend', - pageBuilder: (context, state) => NoTransitionPage( - child: const FriendScreen(), - ), + builder: (context, state) => const FriendScreen(), ), GoRoute( path: '/notification', name: 'notification', - pageBuilder: (context, state) => NoTransitionPage( - child: const NotificationScreen(), - ), + builder: (context, state) => const NotificationScreen(), ), GoRoute( path: '/auth/login', diff --git a/lib/screens/news/news_detail.dart b/lib/screens/news/news_detail.dart new file mode 100644 index 0000000..6e440e2 --- /dev/null +++ b/lib/screens/news/news_detail.dart @@ -0,0 +1,113 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:surface/providers/sn_network.dart'; +import 'package:surface/types/news.dart'; +import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; + +class NewsDetailScreen extends StatefulWidget { + final String hash; + + const NewsDetailScreen({super.key, required this.hash}); + + @override + State createState() => _NewsDetailScreenState(); +} + +class _NewsDetailScreenState extends State { + SnNewsArticle? _article; + + Future _fetchArticle() async { + try { + final sn = context.read(); + final resp = await sn.client.get('/cgi/re/news/${widget.hash}'); + _article = SnNewsArticle.fromJson(resp.data); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err).then((_) { + if (!mounted) return; + Navigator.pop(context); + }); + } finally { + setState(() {}); + } + } + + @override + void initState() { + super.initState(); + _fetchArticle(); + } + + bool _isReadingFromReader = true; + + String get _htmlContent => """ + + + + + + + ${_article?.content ?? ''} + + + """; + + InAppWebViewController? _webViewController; + + @override + Widget build(BuildContext context) { + return AppScaffold( + appBar: AppBar( + leading: const PageBackButton(), + title: Text(_article?.title ?? 'loading'.tr()), + ), + body: Column( + children: [ + MaterialBanner( + dividerColor: Colors.transparent, + leading: const Icon(Icons.info), + content: Text(_isReadingFromReader ? 'newsReadingFromReader'.tr() : 'newsReadingFromOriginal'.tr()), + actions: [ + TextButton( + child: Text('newsReadingProviderSwap').tr(), + onPressed: () { + setState(() => _isReadingFromReader = !_isReadingFromReader); + if (!_isReadingFromReader) { + _webViewController?.goTo(historyItem: WebHistoryItem(url: WebUri(_article!.url))); + } else { + _webViewController?.goBack(); + } + }, + ), + ], + ), + Expanded( + child: InAppWebView( + key: Key('news-detail-webview-${widget.hash}-$_isReadingFromReader'), + onWebViewCreated: (controller) { + _webViewController = controller; + }, + initialUrlRequest: URLRequest(url: WebUri(_article!.url)), + onLoadStop: (controller, url) { + print("Loaded: $url"); + }, + onLoadError: (controller, url, code, message) { + print("Error loading $url: $message ($code)"); + }, + ), + ), + ], + ), + ); + } +} diff --git a/lib/screens/news/news_list.dart b/lib/screens/news/news_list.dart new file mode 100644 index 0000000..7efa1a6 --- /dev/null +++ b/lib/screens/news/news_list.dart @@ -0,0 +1,189 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; +import 'package:go_router/go_router.dart'; +import 'package:provider/provider.dart'; +import 'package:styled_widget/styled_widget.dart'; +import 'package:surface/providers/sn_network.dart'; +import 'package:surface/types/news.dart'; +import 'package:surface/widgets/app_bar_leading.dart'; +import 'package:surface/widgets/dialog.dart'; +import 'package:surface/widgets/navigation/app_scaffold.dart'; +import 'package:surface/widgets/universal_image.dart'; +import 'package:very_good_infinite_list/very_good_infinite_list.dart'; + +class NewsScreen extends StatefulWidget { + const NewsScreen({super.key}); + + @override + State createState() => _NewsScreenState(); +} + +class _NewsScreenState extends State { + List? _sources; + + @override + initState() { + super.initState(); + _fetchSources(); + } + + Future _fetchSources() async { + try { + final sn = context.read(); + final resp = await sn.client.get('/cgi/re/well-known/sources'); + _sources = List.from( + resp.data?.map((e) => SnNewsSource.fromJson(e)) ?? [], + ); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } finally { + setState(() {}); + } + } + + @override + Widget build(BuildContext context) { + if (_sources == null) { + return AppScaffold( + appBar: AppBar( + leading: AutoAppBarLeading(), + title: Text('screenNews').tr(), + ), + body: Center( + child: CircularProgressIndicator(), + ), + ); + } + + return DefaultTabController( + length: _sources!.length + 1, + child: AppScaffold( + body: NestedScrollView( + headerSliverBuilder: (BuildContext context, bool innerBoxIsScrolled) { + return [ + SliverOverlapAbsorber( + handle: NestedScrollView.sliverOverlapAbsorberHandleFor(context), + sliver: SliverAppBar( + leading: AutoAppBarLeading(), + title: Text('screenNews').tr(), + bottom: TabBar( + isScrollable: true, + tabs: [ + Tab(child: Text('newsAllSources'.tr())), + for (final source in _sources!) Tab(child: Text(source.label)), + ], + ), + ), + ), + ]; + }, + body: TabBarView( + children: [ + _NewsArticleListWidget(), + for (final source in _sources!) _NewsArticleListWidget(source: source.id), + ], + ), + ), + ), + ); + } +} + +class _NewsArticleListWidget extends StatefulWidget { + final String? source; + + const _NewsArticleListWidget({this.source}); + + @override + State<_NewsArticleListWidget> createState() => _NewsArticleListWidgetState(); +} + +class _NewsArticleListWidgetState extends State<_NewsArticleListWidget> { + bool _isBusy = false; + + int? _totalCount; + final List _articles = List.empty(growable: true); + + Future _fetchArticles() async { + setState(() => _isBusy = true); + + try { + final sn = context.read(); + final resp = await sn.client.get('/cgi/re/news', queryParameters: { + 'take': 10, + 'offset': _articles.length, + if (widget.source != null) 'source': widget.source, + }); + _totalCount = resp.data['count']; + _articles.addAll(List.from( + resp.data['data']?.map((e) => SnNewsArticle.fromJson(e)) ?? [], + )); + } catch (err) { + if (!mounted) return; + context.showErrorDialog(err); + } finally { + setState(() => _isBusy = false); + } + } + + @override + void initState() { + super.initState(); + _fetchArticles(); + } + + @override + Widget build(BuildContext context) { + return MediaQuery.removePadding( + context: context, + removeTop: true, + child: RefreshIndicator( + onRefresh: _fetchArticles, + child: InfiniteList( + isLoading: _isBusy, + itemCount: _articles.length, + hasReachedMax: _totalCount != null && _articles.length >= _totalCount!, + onFetchData: () { + _fetchArticles(); + }, + itemBuilder: (context, index) { + final article = _articles[index]; + + final baseUri = Uri.parse(article.url); + final baseUrl = '${baseUri.scheme}://${baseUri.host}'; + + return Card( + child: InkWell( + radius: 8, + onTap: () { + GoRouter.of(context).pushNamed( + 'newsDetail', + pathParameters: {'hash': article.hash}, + ); + }, + child: Column( + children: [ + if (article.thumbnail.isNotEmpty && !article.thumbnail.endsWith('.svg')) + ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(8)), + child: AspectRatio( + aspectRatio: 16 / 9, + child: AutoResizeUniversalImage('$baseUrl/${article.thumbnail}'), + ), + ), + Text(article.title).textStyle(Theme.of(context).textTheme.titleLarge!), + Text(article.description).textStyle(Theme.of(context).textTheme.bodyMedium!), + const Gap(8), + Text(article.source).textStyle(Theme.of(context).textTheme.bodySmall!), + ], + ).padding(all: 8), + ), + ); + }, + ), + ), + ); + } +} diff --git a/lib/types/news.dart b/lib/types/news.dart new file mode 100644 index 0000000..1181022 --- /dev/null +++ b/lib/types/news.dart @@ -0,0 +1,38 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'news.freezed.dart'; +part 'news.g.dart'; + +@freezed +class SnNewsSource with _$SnNewsSource { + const factory SnNewsSource({ + required String id, + required String label, + required String type, + required String source, + required int depth, + required bool enabled, + }) = _SnNewsSource; + + factory SnNewsSource.fromJson(Map json) => _$SnNewsSourceFromJson(json); +} + +@freezed +class SnNewsArticle with _$SnNewsArticle { + const factory SnNewsArticle({ + required int id, + required DateTime createdAt, + required DateTime updatedAt, + required dynamic deletedAt, + required String thumbnail, + required String title, + required String description, + required String content, + required String url, + required String hash, + required String source, + required dynamic publishedAt, + }) = _SnNewsArticle; + + factory SnNewsArticle.fromJson(Map json) => _$SnNewsArticleFromJson(json); +} diff --git a/lib/types/news.freezed.dart b/lib/types/news.freezed.dart new file mode 100644 index 0000000..a157a42 --- /dev/null +++ b/lib/types/news.freezed.dart @@ -0,0 +1,660 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// 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 'news.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +SnNewsSource _$SnNewsSourceFromJson(Map json) { + return _SnNewsSource.fromJson(json); +} + +/// @nodoc +mixin _$SnNewsSource { + String get id => throw _privateConstructorUsedError; + String get label => throw _privateConstructorUsedError; + String get type => throw _privateConstructorUsedError; + String get source => throw _privateConstructorUsedError; + int get depth => throw _privateConstructorUsedError; + bool get enabled => throw _privateConstructorUsedError; + + /// Serializes this SnNewsSource to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SnNewsSource + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SnNewsSourceCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SnNewsSourceCopyWith<$Res> { + factory $SnNewsSourceCopyWith( + SnNewsSource value, $Res Function(SnNewsSource) then) = + _$SnNewsSourceCopyWithImpl<$Res, SnNewsSource>; + @useResult + $Res call( + {String id, + String label, + String type, + String source, + int depth, + bool enabled}); +} + +/// @nodoc +class _$SnNewsSourceCopyWithImpl<$Res, $Val extends SnNewsSource> + implements $SnNewsSourceCopyWith<$Res> { + _$SnNewsSourceCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SnNewsSource + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? label = null, + Object? type = null, + Object? source = null, + Object? depth = null, + Object? enabled = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + label: null == label + ? _value.label + : label // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as String, + source: null == source + ? _value.source + : source // ignore: cast_nullable_to_non_nullable + as String, + depth: null == depth + ? _value.depth + : depth // ignore: cast_nullable_to_non_nullable + as int, + enabled: null == enabled + ? _value.enabled + : enabled // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SnNewsSourceImplCopyWith<$Res> + implements $SnNewsSourceCopyWith<$Res> { + factory _$$SnNewsSourceImplCopyWith( + _$SnNewsSourceImpl value, $Res Function(_$SnNewsSourceImpl) then) = + __$$SnNewsSourceImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + String label, + String type, + String source, + int depth, + bool enabled}); +} + +/// @nodoc +class __$$SnNewsSourceImplCopyWithImpl<$Res> + extends _$SnNewsSourceCopyWithImpl<$Res, _$SnNewsSourceImpl> + implements _$$SnNewsSourceImplCopyWith<$Res> { + __$$SnNewsSourceImplCopyWithImpl( + _$SnNewsSourceImpl _value, $Res Function(_$SnNewsSourceImpl) _then) + : super(_value, _then); + + /// Create a copy of SnNewsSource + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? label = null, + Object? type = null, + Object? source = null, + Object? depth = null, + Object? enabled = null, + }) { + return _then(_$SnNewsSourceImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + label: null == label + ? _value.label + : label // ignore: cast_nullable_to_non_nullable + as String, + type: null == type + ? _value.type + : type // ignore: cast_nullable_to_non_nullable + as String, + source: null == source + ? _value.source + : source // ignore: cast_nullable_to_non_nullable + as String, + depth: null == depth + ? _value.depth + : depth // ignore: cast_nullable_to_non_nullable + as int, + enabled: null == enabled + ? _value.enabled + : enabled // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SnNewsSourceImpl implements _SnNewsSource { + const _$SnNewsSourceImpl( + {required this.id, + required this.label, + required this.type, + required this.source, + required this.depth, + required this.enabled}); + + factory _$SnNewsSourceImpl.fromJson(Map json) => + _$$SnNewsSourceImplFromJson(json); + + @override + final String id; + @override + final String label; + @override + final String type; + @override + final String source; + @override + final int depth; + @override + final bool enabled; + + @override + String toString() { + return 'SnNewsSource(id: $id, label: $label, type: $type, source: $source, depth: $depth, enabled: $enabled)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SnNewsSourceImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.label, label) || other.label == label) && + (identical(other.type, type) || other.type == type) && + (identical(other.source, source) || other.source == source) && + (identical(other.depth, depth) || other.depth == depth) && + (identical(other.enabled, enabled) || other.enabled == enabled)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => + Object.hash(runtimeType, id, label, type, source, depth, enabled); + + /// Create a copy of SnNewsSource + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SnNewsSourceImplCopyWith<_$SnNewsSourceImpl> get copyWith => + __$$SnNewsSourceImplCopyWithImpl<_$SnNewsSourceImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SnNewsSourceImplToJson( + this, + ); + } +} + +abstract class _SnNewsSource implements SnNewsSource { + const factory _SnNewsSource( + {required final String id, + required final String label, + required final String type, + required final String source, + required final int depth, + required final bool enabled}) = _$SnNewsSourceImpl; + + factory _SnNewsSource.fromJson(Map json) = + _$SnNewsSourceImpl.fromJson; + + @override + String get id; + @override + String get label; + @override + String get type; + @override + String get source; + @override + int get depth; + @override + bool get enabled; + + /// Create a copy of SnNewsSource + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SnNewsSourceImplCopyWith<_$SnNewsSourceImpl> get copyWith => + throw _privateConstructorUsedError; +} + +SnNewsArticle _$SnNewsArticleFromJson(Map json) { + return _SnNewsArticle.fromJson(json); +} + +/// @nodoc +mixin _$SnNewsArticle { + int get id => throw _privateConstructorUsedError; + DateTime get createdAt => throw _privateConstructorUsedError; + DateTime get updatedAt => throw _privateConstructorUsedError; + dynamic get deletedAt => throw _privateConstructorUsedError; + String get thumbnail => throw _privateConstructorUsedError; + String get title => throw _privateConstructorUsedError; + String get description => throw _privateConstructorUsedError; + String get content => throw _privateConstructorUsedError; + String get url => throw _privateConstructorUsedError; + String get hash => throw _privateConstructorUsedError; + String get source => throw _privateConstructorUsedError; + dynamic get publishedAt => throw _privateConstructorUsedError; + + /// Serializes this SnNewsArticle to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of SnNewsArticle + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $SnNewsArticleCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $SnNewsArticleCopyWith<$Res> { + factory $SnNewsArticleCopyWith( + SnNewsArticle value, $Res Function(SnNewsArticle) then) = + _$SnNewsArticleCopyWithImpl<$Res, SnNewsArticle>; + @useResult + $Res call( + {int id, + DateTime createdAt, + DateTime updatedAt, + dynamic deletedAt, + String thumbnail, + String title, + String description, + String content, + String url, + String hash, + String source, + dynamic publishedAt}); +} + +/// @nodoc +class _$SnNewsArticleCopyWithImpl<$Res, $Val extends SnNewsArticle> + implements $SnNewsArticleCopyWith<$Res> { + _$SnNewsArticleCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of SnNewsArticle + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? createdAt = null, + Object? updatedAt = null, + Object? deletedAt = freezed, + Object? thumbnail = null, + Object? title = null, + Object? description = null, + Object? content = null, + Object? url = null, + Object? hash = null, + Object? source = null, + Object? publishedAt = freezed, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + deletedAt: freezed == deletedAt + ? _value.deletedAt + : deletedAt // ignore: cast_nullable_to_non_nullable + as dynamic, + thumbnail: null == thumbnail + ? _value.thumbnail + : thumbnail // ignore: cast_nullable_to_non_nullable + as String, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + content: null == content + ? _value.content + : content // ignore: cast_nullable_to_non_nullable + as String, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + hash: null == hash + ? _value.hash + : hash // ignore: cast_nullable_to_non_nullable + as String, + source: null == source + ? _value.source + : source // ignore: cast_nullable_to_non_nullable + as String, + publishedAt: freezed == publishedAt + ? _value.publishedAt + : publishedAt // ignore: cast_nullable_to_non_nullable + as dynamic, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$SnNewsArticleImplCopyWith<$Res> + implements $SnNewsArticleCopyWith<$Res> { + factory _$$SnNewsArticleImplCopyWith( + _$SnNewsArticleImpl value, $Res Function(_$SnNewsArticleImpl) then) = + __$$SnNewsArticleImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {int id, + DateTime createdAt, + DateTime updatedAt, + dynamic deletedAt, + String thumbnail, + String title, + String description, + String content, + String url, + String hash, + String source, + dynamic publishedAt}); +} + +/// @nodoc +class __$$SnNewsArticleImplCopyWithImpl<$Res> + extends _$SnNewsArticleCopyWithImpl<$Res, _$SnNewsArticleImpl> + implements _$$SnNewsArticleImplCopyWith<$Res> { + __$$SnNewsArticleImplCopyWithImpl( + _$SnNewsArticleImpl _value, $Res Function(_$SnNewsArticleImpl) _then) + : super(_value, _then); + + /// Create a copy of SnNewsArticle + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? createdAt = null, + Object? updatedAt = null, + Object? deletedAt = freezed, + Object? thumbnail = null, + Object? title = null, + Object? description = null, + Object? content = null, + Object? url = null, + Object? hash = null, + Object? source = null, + Object? publishedAt = freezed, + }) { + return _then(_$SnNewsArticleImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + deletedAt: freezed == deletedAt + ? _value.deletedAt + : deletedAt // ignore: cast_nullable_to_non_nullable + as dynamic, + thumbnail: null == thumbnail + ? _value.thumbnail + : thumbnail // ignore: cast_nullable_to_non_nullable + as String, + title: null == title + ? _value.title + : title // ignore: cast_nullable_to_non_nullable + as String, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + content: null == content + ? _value.content + : content // ignore: cast_nullable_to_non_nullable + as String, + url: null == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String, + hash: null == hash + ? _value.hash + : hash // ignore: cast_nullable_to_non_nullable + as String, + source: null == source + ? _value.source + : source // ignore: cast_nullable_to_non_nullable + as String, + publishedAt: freezed == publishedAt + ? _value.publishedAt + : publishedAt // ignore: cast_nullable_to_non_nullable + as dynamic, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$SnNewsArticleImpl implements _SnNewsArticle { + const _$SnNewsArticleImpl( + {required this.id, + required this.createdAt, + required this.updatedAt, + required this.deletedAt, + required this.thumbnail, + required this.title, + required this.description, + required this.content, + required this.url, + required this.hash, + required this.source, + required this.publishedAt}); + + factory _$SnNewsArticleImpl.fromJson(Map json) => + _$$SnNewsArticleImplFromJson(json); + + @override + final int id; + @override + final DateTime createdAt; + @override + final DateTime updatedAt; + @override + final dynamic deletedAt; + @override + final String thumbnail; + @override + final String title; + @override + final String description; + @override + final String content; + @override + final String url; + @override + final String hash; + @override + final String source; + @override + final dynamic publishedAt; + + @override + String toString() { + return 'SnNewsArticle(id: $id, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt, thumbnail: $thumbnail, title: $title, description: $description, content: $content, url: $url, hash: $hash, source: $source, publishedAt: $publishedAt)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$SnNewsArticleImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + const DeepCollectionEquality().equals(other.deletedAt, deletedAt) && + (identical(other.thumbnail, thumbnail) || + other.thumbnail == thumbnail) && + (identical(other.title, title) || other.title == title) && + (identical(other.description, description) || + other.description == description) && + (identical(other.content, content) || other.content == content) && + (identical(other.url, url) || other.url == url) && + (identical(other.hash, hash) || other.hash == hash) && + (identical(other.source, source) || other.source == source) && + const DeepCollectionEquality() + .equals(other.publishedAt, publishedAt)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + createdAt, + updatedAt, + const DeepCollectionEquality().hash(deletedAt), + thumbnail, + title, + description, + content, + url, + hash, + source, + const DeepCollectionEquality().hash(publishedAt)); + + /// Create a copy of SnNewsArticle + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$SnNewsArticleImplCopyWith<_$SnNewsArticleImpl> get copyWith => + __$$SnNewsArticleImplCopyWithImpl<_$SnNewsArticleImpl>(this, _$identity); + + @override + Map toJson() { + return _$$SnNewsArticleImplToJson( + this, + ); + } +} + +abstract class _SnNewsArticle implements SnNewsArticle { + const factory _SnNewsArticle( + {required final int id, + required final DateTime createdAt, + required final DateTime updatedAt, + required final dynamic deletedAt, + required final String thumbnail, + required final String title, + required final String description, + required final String content, + required final String url, + required final String hash, + required final String source, + required final dynamic publishedAt}) = _$SnNewsArticleImpl; + + factory _SnNewsArticle.fromJson(Map json) = + _$SnNewsArticleImpl.fromJson; + + @override + int get id; + @override + DateTime get createdAt; + @override + DateTime get updatedAt; + @override + dynamic get deletedAt; + @override + String get thumbnail; + @override + String get title; + @override + String get description; + @override + String get content; + @override + String get url; + @override + String get hash; + @override + String get source; + @override + dynamic get publishedAt; + + /// Create a copy of SnNewsArticle + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$SnNewsArticleImplCopyWith<_$SnNewsArticleImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/types/news.g.dart b/lib/types/news.g.dart new file mode 100644 index 0000000..2c7f788 --- /dev/null +++ b/lib/types/news.g.dart @@ -0,0 +1,59 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'news.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$SnNewsSourceImpl _$$SnNewsSourceImplFromJson(Map json) => + _$SnNewsSourceImpl( + id: json['id'] as String, + label: json['label'] as String, + type: json['type'] as String, + source: json['source'] as String, + depth: (json['depth'] as num).toInt(), + enabled: json['enabled'] as bool, + ); + +Map _$$SnNewsSourceImplToJson(_$SnNewsSourceImpl instance) => + { + 'id': instance.id, + 'label': instance.label, + 'type': instance.type, + 'source': instance.source, + 'depth': instance.depth, + 'enabled': instance.enabled, + }; + +_$SnNewsArticleImpl _$$SnNewsArticleImplFromJson(Map json) => + _$SnNewsArticleImpl( + id: (json['id'] as num).toInt(), + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + deletedAt: json['deleted_at'], + thumbnail: json['thumbnail'] as String, + title: json['title'] as String, + description: json['description'] as String, + content: json['content'] as String, + url: json['url'] as String, + hash: json['hash'] as String, + source: json['source'] as String, + publishedAt: json['published_at'], + ); + +Map _$$SnNewsArticleImplToJson(_$SnNewsArticleImpl instance) => + { + 'id': instance.id, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'deleted_at': instance.deletedAt, + 'thumbnail': instance.thumbnail, + 'title': instance.title, + 'description': instance.description, + 'content': instance.content, + 'url': instance.url, + 'hash': instance.hash, + 'source': instance.source, + 'published_at': instance.publishedAt, + }; diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 9cdba3d..d187442 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -13,6 +13,7 @@ import file_selector_macos import firebase_analytics import firebase_core import firebase_messaging +import flutter_inappwebview_macos import flutter_udid import flutter_webrtc import gal @@ -40,6 +41,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin")) + InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterUdidPlugin.register(with: registry.registrar(forPlugin: "FlutterUdidPlugin")) FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin")) GalPlugin.register(with: registry.registrar(forPlugin: "GalPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 317faac..2ac2676 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -675,6 +675,70 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + sha256: "80092d13d3e29b6227e25b67973c67c7210bd5e35c4b747ca908e31eb71a46d5" + url: "https://pub.dev" + source: hosted + version: "6.1.5" + flutter_inappwebview_android: + dependency: transitive + description: + name: flutter_inappwebview_android + sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" + url: "https://pub.dev" + source: hosted + version: "1.1.3" + flutter_inappwebview_internal_annotations: + dependency: transitive + description: + name: flutter_inappwebview_internal_annotations + sha256: "787171d43f8af67864740b6f04166c13190aa74a1468a1f1f1e9ee5b90c359cd" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + flutter_inappwebview_ios: + dependency: transitive + description: + name: flutter_inappwebview_ios + sha256: "5818cf9b26cf0cbb0f62ff50772217d41ea8d3d9cc00279c45f8aabaa1b4025d" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_macos: + dependency: transitive + description: + name: flutter_inappwebview_macos + sha256: c1fbb86af1a3738e3541364d7d1866315ffb0468a1a77e34198c9be571287da1 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_platform_interface: + dependency: transitive + description: + name: flutter_inappwebview_platform_interface + sha256: cf5323e194096b6ede7a1ca808c3e0a078e4b33cc3f6338977d75b4024ba2500 + url: "https://pub.dev" + source: hosted + version: "1.3.0+1" + flutter_inappwebview_web: + dependency: transitive + description: + name: flutter_inappwebview_web + sha256: "55f89c83b0a0d3b7893306b3bb545ba4770a4df018204917148ebb42dc14a598" + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_inappwebview_windows: + dependency: transitive + description: + name: flutter_inappwebview_windows + sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" + url: "https://pub.dev" + source: hosted + version: "0.6.0" flutter_launcher_icons: dependency: "direct dev" description: diff --git a/pubspec.yaml b/pubspec.yaml index 187da15..af6ffc2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -115,6 +115,7 @@ dependencies: slide_countdown: ^2.0.2 video_compress: ^3.1.3 cached_network_image: ^3.4.1 + flutter_inappwebview: ^6.1.5 dev_dependencies: flutter_test: diff --git a/web/index.html b/web/index.html index 1734104..82f4e82 100644 --- a/web/index.html +++ b/web/index.html @@ -16,6 +16,8 @@ --> + + diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index e1dd346..8bcf9ef 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -11,6 +11,7 @@ #include #include #include +#include #include #include #include @@ -34,6 +35,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("FileSelectorWindows")); FirebaseCorePluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); + FlutterInappwebviewWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterInappwebviewWindowsPluginCApi")); FlutterUdidPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterUdidPluginCApi")); FlutterWebRTCPluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index e9cb10e..ac4942c 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -8,6 +8,7 @@ list(APPEND FLUTTER_PLUGIN_LIST file_saver file_selector_windows firebase_core + flutter_inappwebview_windows flutter_udid flutter_webrtc gal