From c51df1efdec5558709e3e0b06c0b988268a7d12c Mon Sep 17 00:00:00 2001 From: wgh19 Date: Mon, 20 May 2024 17:42:54 +0800 Subject: [PATCH] add support for novel image --- lib/foundation/image_provider.dart | 118 ++++++++++++++---- lib/network/network.dart | 119 +++++++++++------- lib/pages/image_page.dart | 188 ++++++++++++++++------------- lib/pages/novel_reading_page.dart | 70 ++++++++--- 4 files changed, 323 insertions(+), 172 deletions(-) diff --git a/lib/foundation/image_provider.dart b/lib/foundation/image_provider.dart index 6ef5346..c833ba9 100644 --- a/lib/foundation/image_provider.dart +++ b/lib/foundation/image_provider.dart @@ -45,10 +45,10 @@ abstract class BaseImageProvider> } Future _loadBufferAsync( - T key, - StreamController chunkEvents, - ImageDecoderCallback decode, - ) async { + T key, + StreamController chunkEvents, + ImageDecoderCallback decode, + ) async { try { int retryTime = 1; @@ -83,11 +83,11 @@ abstract class BaseImageProvider> } } - if(stop) { + if (stop) { throw Exception("Image loading is stopped"); } - if(data!.isEmpty) { + if (data!.isEmpty) { throw Exception("Empty image data"); } @@ -147,13 +147,13 @@ class CachedImageProvider extends BaseImageProvider { String get key => url; @override - Future load(StreamController chunkEvents) async{ + Future load(StreamController chunkEvents) async { chunkEvents.add(const ImageChunkEvent( cumulativeBytesLoaded: 0, expectedTotalBytes: 1, )); var cached = await CacheManager().findCache(key); - if(cached != null) { + if (cached != null) { chunkEvents.add(const ImageChunkEvent( cumulativeBytesLoaded: 1, expectedTotalBytes: 1, @@ -161,30 +161,28 @@ class CachedImageProvider extends BaseImageProvider { return await File(cached).readAsBytes(); } var dio = AppDio(); - final time = DateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'").format(DateTime.now()); + final time = + DateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'").format(DateTime.now()); final hash = md5.convert(utf8.encode(time + Network.hashSalt)).toString(); - var res = await dio.get( - url, - options: Options( - responseType: ResponseType.stream, - validateStatus: (status) => status != null && status < 500, - headers: { - "referer": "https://app-api.pixiv.net/", - "user-agent": "PixivAndroidApp/5.0.234 (Android 14; Pixes)", - "x-client-time": time, - "x-client-hash": hash, - "accept-enconding": "gzip", - } - ) - ); - if(res.statusCode != 200) { + var res = await dio.get(url, + options: Options( + responseType: ResponseType.stream, + validateStatus: (status) => status != null && status < 500, + headers: { + "referer": "https://app-api.pixiv.net/", + "user-agent": "PixivAndroidApp/5.0.234 (Android 14; Pixes)", + "x-client-time": time, + "x-client-hash": hash, + "accept-enconding": "gzip", + })); + if (res.statusCode != 200) { throw BadRequestException("Failed to load image: ${res.statusCode}"); } var data = []; var cachingFile = await CacheManager().openWrite(key); await for (var chunk in res.data!.stream) { - var length = res.data!.contentLength+1; - if(length < data.length) { + var length = res.data!.contentLength + 1; + if (length < data.length) { length = data.length + 1; } data.addAll(chunk); @@ -203,3 +201,71 @@ class CachedImageProvider extends BaseImageProvider { return SynchronousFuture(this); } } + +class CachedNovelImageProvider + extends BaseImageProvider { + final String novelId; + final String imageId; + + CachedNovelImageProvider(this.novelId, this.imageId); + + @override + String get key => "$novelId/$imageId"; + + @override + Future load(StreamController chunkEvents) async { + chunkEvents.add(const ImageChunkEvent( + cumulativeBytesLoaded: 0, + expectedTotalBytes: 1, + )); + var cached = await CacheManager().findCache(key); + if (cached != null) { + chunkEvents.add(const ImageChunkEvent( + cumulativeBytesLoaded: 1, + expectedTotalBytes: 1, + )); + return await File(cached).readAsBytes(); + } + var urlRes = await Network().getNovelImage(novelId, imageId); + var url = urlRes.data; + var dio = AppDio(); + final time = + DateFormat("yyyy-MM-dd'T'HH:mm:ss'+00:00'").format(DateTime.now()); + final hash = md5.convert(utf8.encode(time + Network.hashSalt)).toString(); + var res = await dio.get(url, + options: Options( + responseType: ResponseType.stream, + validateStatus: (status) => status != null && status < 500, + headers: { + "referer": "https://app-api.pixiv.net/", + "user-agent": "PixivAndroidApp/5.0.234 (Android 14; Pixes)", + "x-client-time": time, + "x-client-hash": hash, + "accept-enconding": "gzip", + })); + if (res.statusCode != 200) { + throw BadRequestException("Failed to load image: ${res.statusCode}"); + } + var data = []; + var cachingFile = await CacheManager().openWrite(key); + await for (var chunk in res.data!.stream) { + var length = res.data!.contentLength + 1; + if (length < data.length) { + length = data.length + 1; + } + data.addAll(chunk); + await cachingFile.writeBytes(chunk); + chunkEvents.add(ImageChunkEvent( + cumulativeBytesLoaded: data.length, + expectedTotalBytes: length, + )); + } + await cachingFile.close(); + return Uint8List.fromList(data); + } + + @override + Future obtainKey(ImageConfiguration configuration) { + return SynchronousFuture(this); + } +} diff --git a/lib/network/network.dart b/lib/network/network.dart index 6fbedd0..fd234e8 100644 --- a/lib/network/network.dart +++ b/lib/network/network.dart @@ -110,9 +110,9 @@ class Network { contentType: Headers.formUrlEncodedContentType, validateStatus: (i) => true, headers: headers)); - if(res.statusCode != 200) { + if (res.statusCode != 200) { var data = res.data ?? ""; - if(data.contains("Invalid refresh token")) { + if (data.contains("Invalid refresh token")) { throw "Failed to refresh token. Please log out."; } } @@ -134,8 +134,7 @@ class Network { } final res = await dio.get>(path, queryParameters: query, - options: - Options(headers: headers, validateStatus: (status) => true)); + options: Options(headers: headers, validateStatus: (status) => true)); if (res.statusCode == 200) { return Res(res.data!); } else if (res.statusCode == 400) { @@ -161,7 +160,7 @@ class Network { } } - Future> apiGetPlain(String path, + Future> apiGetPlain(String path, {Map? query}) async { try { if (!path.startsWith("http")) { @@ -169,8 +168,7 @@ class Network { } final res = await dio.get(path, queryParameters: query, - options: - Options(headers: headers, validateStatus: (status) => true)); + options: Options(headers: headers, validateStatus: (status) => true)); if (res.statusCode == 200) { return Res(res.data!); } else if (res.statusCode == 400) { @@ -242,14 +240,15 @@ class Network { } } - static const recommendationUrl = "/v1/illust/recommended?include_privacy_policy=true&filter=for_android&include_ranking_illusts=true"; + static const recommendationUrl = + "/v1/illust/recommended?include_privacy_policy=true&filter=for_android&include_ranking_illusts=true"; Future>> getRecommendedIllusts() async { var res = await apiGet(recommendationUrl); if (res.success) { - return Res((res.data["illusts"] as List) - .map((e) => Illust.fromJson(e)) - .toList(), subData: recommendationUrl); + return Res( + (res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(), + subData: recommendationUrl); } else { return Res.error(res.errorMessage); } @@ -268,9 +267,10 @@ class Network { } } - Future>> getUserBookmarks(String uid, [String? nextUrl]) async { - var res = await apiGet(nextUrl ?? - "/v1/user/bookmarks/illust?user_id=$uid&restrict=public"); + Future>> getUserBookmarks(String uid, + [String? nextUrl]) async { + var res = await apiGet( + nextUrl ?? "/v1/user/bookmarks/illust?user_id=$uid&restrict=public"); if (res.success) { return Res( (res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(), @@ -345,7 +345,7 @@ class Network { } } - Future>> getIllustsWithNextUrl(String nextUrl) async{ + Future>> getIllustsWithNextUrl(String nextUrl) async { var res = await apiGet(nextUrl); if (res.success) { return Res( @@ -356,12 +356,16 @@ class Network { } } - Future>> searchUsers(String keyword, [String? nextUrl]) async{ - var path = nextUrl ?? "/v1/search/user?filter=for_android&word=${Uri.encodeComponent(keyword)}"; + Future>> searchUsers(String keyword, + [String? nextUrl]) async { + var path = nextUrl ?? + "/v1/search/user?filter=for_android&word=${Uri.encodeComponent(keyword)}"; var res = await apiGet(path); if (res.success) { return Res( - (res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e)).toList(), + (res.data["user_previews"] as List) + .map((e) => UserPreview.fromJson(e)) + .toList(), subData: res.data["next_url"]); } else { return Res.error(res.errorMessage); @@ -369,7 +373,8 @@ class Network { } Future>> getUserIllusts(String uid) async { - var res = await apiGet("/v1/user/illusts?filter=for_android&user_id=$uid&type=illust"); + var res = await apiGet( + "/v1/user/illusts?filter=for_android&user_id=$uid&type=illust"); if (res.success) { return Res( (res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(), @@ -379,19 +384,24 @@ class Network { } } - Future>> getFollowing(String uid, String type, [String? nextUrl]) async { - var path = nextUrl ?? "/v1/user/following?filter=for_android&user_id=$uid&restrict=$type"; + Future>> getFollowing(String uid, String type, + [String? nextUrl]) async { + var path = nextUrl ?? + "/v1/user/following?filter=for_android&user_id=$uid&restrict=$type"; var res = await apiGet(path); if (res.success) { return Res( - (res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e)).toList(), + (res.data["user_previews"] as List) + .map((e) => UserPreview.fromJson(e)) + .toList(), subData: res.data["next_url"]); } else { return Res.error(res.errorMessage); } } - Future>> getFollowingArtworks(String restrict, [String? nextUrl]) async { + Future>> getFollowingArtworks(String restrict, + [String? nextUrl]) async { var res = await apiGet(nextUrl ?? "/v2/illust/follow?restrict=$restrict"); if (res.success) { return Res( @@ -406,7 +416,9 @@ class Network { var res = await apiGet("/v1/user/recommended?filter=for_android"); if (res.success) { return Res( - (res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e)).toList(), + (res.data["user_previews"] as List) + .map((e) => UserPreview.fromJson(e)) + .toList(), subData: res.data["next_url"]); } else { return Res.error(res.errorMessage); @@ -415,7 +427,8 @@ class Network { /// mode: day, week, month, day_male, day_female, week_original, week_rookie, day_manga, week_manga, month_manga, day_r18_manga, day_r18 Future>> getRanking(String mode, [String? nextUrl]) async { - var res = await apiGet(nextUrl ?? "/v1/illust/ranking?filter=for_android&mode=$mode"); + var res = await apiGet( + nextUrl ?? "/v1/illust/ranking?filter=for_android&mode=$mode"); if (res.success) { return Res( (res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(), @@ -429,7 +442,9 @@ class Network { var res = await apiGet(nextUrl ?? "/v3/illust/comments?illust_id=$id"); if (res.success) { return Res( - (res.data["comments"] as List).map((e) => Comment.fromJson(e)).toList(), + (res.data["comments"] as List) + .map((e) => Comment.fromJson(e)) + .toList(), subData: res.data["next_url"]); } else { return Res.error(res.errorMessage); @@ -456,7 +471,8 @@ class Network { } Future>> getRecommendedMangas() async { - var res = await apiGet("/v1/manga/recommended?filter=for_android&include_ranking_illusts=true&include_privacy_policy=true"); + var res = await apiGet( + "/v1/manga/recommended?filter=for_android&include_ranking_illusts=true&include_privacy_policy=true"); if (res.success) { return Res( (res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList(), @@ -468,13 +484,14 @@ class Network { Future>> getHistory(int page) async { String param = ""; - if(page > 1) { - param = "?offset=${30*(page-1)}"; + if (page > 1) { + param = "?offset=${30 * (page - 1)}"; } var res = await apiGet("/v1/user/browsing-history/illusts$param"); if (res.success) { return Res((res.data["illusts"] as List) - .map((e) => Illust.fromJson(e)).toList()); + .map((e) => Illust.fromJson(e)) + .toList()); } else { return Res.error(res.errorMessage); } @@ -483,19 +500,18 @@ class Network { Future> getMutedTags() async { var res = await apiGet("/v1/mute/list"); if (res.success) { - return res.data["mute_tags"].map((e) => - Tag(e["tag"]["name"], e["tag"]["translated_name"])) + return res.data["mute_tags"] + .map((e) => Tag(e["tag"]["name"], e["tag"]["translated_name"])) .toList(); } else { return []; } } - Future> muteTags(List muteTags, List unmuteTags) async { - var res = await apiPost("/v1/mute/edit", data: { - "add_tags": muteTags, - "delete_tags": unmuteTags - }); + Future> muteTags( + List muteTags, List unmuteTags) async { + var res = await apiPost("/v1/mute/edit", + data: {"add_tags": muteTags, "delete_tags": unmuteTags}); if (res.success) { return const Res(true); } else { @@ -504,20 +520,37 @@ class Network { } Future>> relatedUsers(String id) async { - var res = await apiGet("/v1/user/related?filter=for_android&seed_user_id=$id"); + var res = + await apiGet("/v1/user/related?filter=for_android&seed_user_id=$id"); if (res.success) { - return Res( - (res.data["user_previews"] as List).map((e) => UserPreview.fromJson(e)).toList()); + return Res((res.data["user_previews"] as List) + .map((e) => UserPreview.fromJson(e)) + .toList()); } else { return Res.error(res.errorMessage); } } Future>> relatedIllusts(String id) async { - var res = await apiGet("/v2/illust/related?filter=for_android&illust_id=$id"); + var res = + await apiGet("/v2/illust/related?filter=for_android&illust_id=$id"); if (res.success) { - return Res( - (res.data["illusts"] as List).map((e) => Illust.fromJson(e)).toList()); + return Res((res.data["illusts"] as List) + .map((e) => Illust.fromJson(e)) + .toList()); + } else { + return Res.error(res.errorMessage); + } + } + + Future> getNovelImage(String novelId, String imageId) async { + var res = await apiGetPlain( + "/web/v1/novel/image?novel_id=$novelId&uploaded_image_id=$imageId"); + if (res.success) { + var html = res.data; + int start = html.indexOf(' urls; final int initialPage; - static show(List urls, {int initialPage = 1}) { - App.rootNavigatorKey.currentState - ?.push(AppPageRoute( + static show(List urls, {int initialPage = 0}) { + App.rootNavigatorKey.currentState?.push(AppPageRoute( builder: (context) => ImagePage(urls, initialPage: initialPage))); } @@ -69,61 +68,67 @@ class _ImagePageState extends State with WindowListener { Future getFile() async { var image = widget.urls[currentPage]; - if(image.startsWith("file://")){ + if (image.startsWith("file://")) { return File(image.replaceFirst("file://", "")); } - var file = await CacheManager().findCache(image); - return file == null - ? null - : File(file); + var key = image; + if (key.startsWith("novel:")) { + key = key.split(':').last; + } + var file = await CacheManager().findCache(key); + return file == null ? null : File(file); } String getExtensionName() { var fileName = widget.urls[currentPage].split('/').last; - if(fileName.contains('.')){ + if (fileName.contains('.')) { return '.${fileName.split('.').last}'; } return '.jpg'; } void showMenu() { - menuController.showFlyout(builder: (context) => MenuFlyout( - items: [ - MenuFlyoutItem(text: Text("Save to".tl), onPressed: () async{ - var file = await getFile(); - if(file != null){ - var fileName = file.path.split('/').last; - if(!fileName.contains('.')){ - fileName += getExtensionName(); - } - saveFile(file, fileName); - } - }), - MenuFlyoutItem(text: Text("Share".tl), onPressed: () async{ - var file = await getFile(); - if(file != null){ - var ext = getExtensionName(); - var fileName = file.path.split('/').last; - if(!fileName.contains('.')){ - fileName += ext; - } - var mediaType = switch(ext){ - '.jpg' => 'image/jpeg', - '.jpeg' => 'image/jpeg', - '.png' => 'image/png', - '.gif' => 'image/gif', - '.webp' => 'image/webp', - _ => 'application/octet-stream' - }; - Share.shareXFiles([XFile.fromData( - await file.readAsBytes(), - mimeType: mediaType, - name: fileName)] - ); - } - }), - ], - )); + menuController.showFlyout( + builder: (context) => MenuFlyout( + items: [ + MenuFlyoutItem( + text: Text("Save to".tl), + onPressed: () async { + var file = await getFile(); + if (file != null) { + var fileName = file.path.split('/').last; + if (!fileName.contains('.')) { + fileName += getExtensionName(); + } + saveFile(file, fileName); + } + }), + MenuFlyoutItem( + text: Text("Share".tl), + onPressed: () async { + var file = await getFile(); + if (file != null) { + var ext = getExtensionName(); + var fileName = file.path.split('/').last; + if (!fileName.contains('.')) { + fileName += ext; + } + var mediaType = switch (ext) { + '.jpg' => 'image/jpeg', + '.jpeg' => 'image/jpeg', + '.png' => 'image/png', + '.gif' => 'image/gif', + '.webp' => 'image/webp', + _ => 'application/octet-stream' + }; + Share.shareXFiles([ + XFile.fromData(await file.readAsBytes(), + mimeType: mediaType, name: fileName) + ]); + } + }), + ], + )); } @override @@ -133,12 +138,13 @@ class _ImagePageState extends State with WindowListener { color: FluentTheme.of(context).micaBackgroundColor, child: Listener( onPointerSignal: (event) { - if(event is PointerScrollEvent && + if (event is PointerScrollEvent && !HardwareKeyboard.instance.isControlPressed) { - if(event.scrollDelta.dy > 0 - && controller.page!.toInt() < widget.urls.length - 1) { + if (event.scrollDelta.dy > 0 && + controller.page!.toInt() < widget.urls.length - 1) { controller.jumpToPage(controller.page!.toInt() + 1); - } else if(event.scrollDelta.dy < 0 && controller.page!.toInt() > 0){ + } else if (event.scrollDelta.dy < 0 && + controller.page!.toInt() > 0) { controller.jumpToPage(controller.page!.toInt() - 1); } } @@ -148,19 +154,17 @@ class _ImagePageState extends State with WindowListener { var height = constrains.maxHeight; return Stack( children: [ - Positioned.fill(child: PhotoViewGallery.builder( + Positioned.fill( + child: PhotoViewGallery.builder( pageController: controller, - backgroundDecoration: const BoxDecoration( - color: Colors.transparent - ), + backgroundDecoration: + const BoxDecoration(color: Colors.transparent), itemCount: widget.urls.length, builder: (context, index) { var image = widget.urls[index]; return PhotoViewGalleryPageOptions( - imageProvider: image.startsWith("file://") - ? FileImage(File(image.replaceFirst("file://", ""))) - : CachedImageProvider(image) as ImageProvider, + imageProvider: getImageProvider(image), ); }, onPageChanged: (index) { @@ -177,17 +181,22 @@ class _ImagePageState extends State with WindowListener { height: 36, child: Row( children: [ - const SizedBox(width: 6,), + const SizedBox( + width: 6, + ), IconButton( icon: const Icon(FluentIcons.back).paddingAll(2), - onPressed: () => context.pop() - ), + onPressed: () => context.pop()), const Expanded( - child: DragToMoveArea(child: SizedBox.expand(),), + child: DragToMoveArea( + child: SizedBox.expand(), + ), ), buildActions(), - if(App.isDesktop) - WindowButtons(key: ValueKey(windowButtonKey),), + if (App.isDesktop) + WindowButtons( + key: ValueKey(windowButtonKey), + ), ], ), ), @@ -196,7 +205,10 @@ class _ImagePageState extends State with WindowListener { left: 0, top: height / 2 - 9, child: IconButton( - icon: const Icon(FluentIcons.chevron_left, size: 18,), + icon: const Icon( + FluentIcons.chevron_left, + size: 18, + ), onPressed: () { controller.previousPage( duration: const Duration(milliseconds: 300), @@ -239,25 +251,35 @@ class _ImagePageState extends State with WindowListener { controller: menuController, child: width > 600 ? Button( - onPressed: showMenu, - child: const Row( - children: [ - Icon( - MdIcons.menu, - size: 18, - ), - SizedBox( - width: 8, - ), - Text('Actions'), - ], - )) + onPressed: showMenu, + child: const Row( + children: [ + Icon( + MdIcons.menu, + size: 18, + ), + SizedBox( + width: 8, + ), + Text('Actions'), + ], + )) : IconButton( - icon: const Icon( - MdIcons.more_horiz, - size: 20, - ), - onPressed: showMenu), + icon: const Icon( + MdIcons.more_horiz, + size: 20, + ), + onPressed: showMenu), ); } + + ImageProvider getImageProvider(String url) { + if (url.startsWith("file://")) { + return FileImage(File(url.replaceFirst("file://", ""))); + } else if (url.startsWith("novel:")) { + var ids = url.split(':').last.split('/'); + return CachedNovelImageProvider(ids[0], ids[1]); + } + return CachedImageProvider(url) as ImageProvider; + } } diff --git a/lib/pages/novel_reading_page.dart b/lib/pages/novel_reading_page.dart index b06e868..dae0dac 100644 --- a/lib/pages/novel_reading_page.dart +++ b/lib/pages/novel_reading_page.dart @@ -1,6 +1,10 @@ import 'package:fluent_ui/fluent_ui.dart'; +import 'package:pixes/components/animated_image.dart'; import 'package:pixes/components/loading.dart'; +import 'package:pixes/foundation/image_provider.dart'; import 'package:pixes/network/network.dart'; +import 'package:pixes/pages/image_page.dart'; +import 'package:pixes/utils/ext.dart'; class NovelReadingPage extends StatefulWidget { const NovelReadingPage(this.novel, {super.key}); @@ -14,31 +18,20 @@ class NovelReadingPage extends StatefulWidget { class _NovelReadingPageState extends LoadingState { @override Widget buildContent(BuildContext context, String data) { + var content = buildList(context).toList(); return ScaffoldPage( padding: EdgeInsets.zero, content: SelectionArea( - child: SingleChildScrollView( + child: DefaultTextStyle.merge( + style: const TextStyle(fontSize: 16.0, height: 1.6), + child: ListView.builder( padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(widget.novel.title, - style: const TextStyle( - fontSize: 24.0, fontWeight: FontWeight.bold)), - const SizedBox(height: 12.0), - const Divider( - style: DividerThemeData(horizontalMargin: EdgeInsets.all(0)), - ), - const SizedBox(height: 12.0), - Text(data, - style: const TextStyle( - fontSize: 16.0, - height: 1.6, - )), - ], - ), + itemBuilder: (context, index) { + return content[index]; + }, + itemCount: content.length, ), - ), + )), ); } @@ -46,4 +39,41 @@ class _NovelReadingPageState extends LoadingState { Future> loadData() { return Network().getNovelContent(widget.novel.id.toString()); } + + Iterable buildList(BuildContext context) sync* { + yield Text(widget.novel.title, + style: const TextStyle(fontSize: 24.0, fontWeight: FontWeight.bold)); + yield const SizedBox(height: 12.0); + yield const Divider( + style: DividerThemeData(horizontalMargin: EdgeInsets.all(0)), + ); + yield const SizedBox(height: 12.0); + + var novelContent = data!.split('\n'); + for (var content in novelContent) { + if (content.isEmpty) continue; + if (content.startsWith('[uploadedimage:')) { + var imageId = content.nums; + yield GestureDetector( + onTap: () { + ImagePage.show(["novel:${widget.novel.id.toString()}/$imageId"]); + }, + child: SizedBox( + height: 300, + width: double.infinity, + child: AnimatedImage( + image: + CachedNovelImageProvider(widget.novel.id.toString(), imageId), + filterQuality: FilterQuality.medium, + fit: BoxFit.contain, + height: 300, + width: double.infinity, + ), + ), + ); + } else { + yield Text(content); + } + } + } }