[4chan] Add Support for 4chan (#142)

This commit adds support for 4chan. This means that 4chan can be
selected as a new source type. When the 4chan source is selected a user
can select a board from which he wants to get the RSS feed.
This commit is contained in:
Rico Berger
2024-02-14 13:05:10 +01:00
committed by GitHub
parent 0b077ae973
commit 689d3bd39b
17 changed files with 957 additions and 2 deletions

Binary file not shown.

View File

@@ -8,6 +8,7 @@ import 'package:feeddeck/utils/fd_icons.dart';
/// [FDSourceType] is a enum value which defines the source type. A source can
/// have one of the following types:
/// - [fourchan]
/// - [github]
/// - [googlenews]
/// - [lemmy]
@@ -28,6 +29,7 @@ import 'package:feeddeck/utils/fd_icons.dart';
/// the list, so that we can loop though the types in a ListView / GridView
/// builder via `FDSourceType.values.length - 1`.
enum FDSourceType {
fourchan,
github,
googlenews,
lemmy,
@@ -57,6 +59,8 @@ extension FDSourceTypeExtension on FDSourceType {
/// [toLocalizedString] returns a localized string for a source type.
String toLocalizedString() {
switch (this) {
case FDSourceType.fourchan:
return '4chan';
case FDSourceType.github:
return 'GitHub';
case FDSourceType.googlenews:
@@ -93,6 +97,8 @@ extension FDSourceTypeExtension on FDSourceType {
/// [icon] returns the icon for a source.
IconData get icon {
switch (this) {
case FDSourceType.fourchan:
return FDIcons.fourchan;
case FDSourceType.github:
return FDIcons.github;
case FDSourceType.googlenews:
@@ -129,6 +135,8 @@ extension FDSourceTypeExtension on FDSourceType {
/// [bgColor] returns the background color for the source icon.
Color get bgColor {
switch (this) {
case FDSourceType.fourchan:
return const Color(0xff880000);
case FDSourceType.github:
return const Color(0xff000000);
case FDSourceType.googlenews:
@@ -166,6 +174,8 @@ extension FDSourceTypeExtension on FDSourceType {
/// used toether with the [bgColor].
Color get fgColor {
switch (this) {
case FDSourceType.fourchan:
return const Color(0xffffffff);
case FDSourceType.github:
return const Color(0xffffffff);
case FDSourceType.googlenews:
@@ -268,6 +278,7 @@ class FDSource {
/// [FDSourceOptions] defines all options for the different source types which
/// are available.
class FDSourceOptions {
String? fourchan;
FDGitHubOptions? github;
FDGoogleNewsOptions? googlenews;
String? lemmy;
@@ -284,6 +295,7 @@ class FDSourceOptions {
String? youtube;
FDSourceOptions({
this.fourchan,
this.github,
this.googlenews,
this.lemmy,
@@ -302,6 +314,10 @@ class FDSourceOptions {
factory FDSourceOptions.fromJson(Map<String, dynamic> responseData) {
return FDSourceOptions(
fourchan: responseData.containsKey('fourchan') &&
responseData['fourchan'] != null
? responseData['fourchan']
: null,
github:
responseData.containsKey('github') && responseData['github'] != null
? FDGitHubOptions.fromJson(responseData['github'])
@@ -360,6 +376,7 @@ class FDSourceOptions {
Map<String, dynamic> toJson() {
return {
'fourchan': fourchan,
'github': github?.toJson(),
'googlenews': googlenews?.toJson(),
'lemmy': lemmy,

View File

@@ -1,7 +1,7 @@
import 'package:flutter/widgets.dart';
/// Flutter icons [FDIcons]
/// Copyright (C) 2023 by original authors @ fluttericon.com, fontello.com
/// Flutter icons FDIcons
/// Copyright (C) 2024 by original authors @ fluttericon.com, fontello.com
/// This font was generated by FlutterIcon.com, which is derived from Fontello.
///
/// To use this font, place it in your fonts/ directory and include the
@@ -65,4 +65,6 @@ class FDIcons {
IconData(0xe815, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData lemmy =
IconData(0xe816, fontFamily: _kFontFam, fontPackage: _kFontPkg);
static const IconData fourchan =
IconData(0xe817, fontFamily: _kFontFam, fontPackage: _kFontPkg);
}

View File

@@ -1,3 +1,4 @@
import 'package:feeddeck/widgets/item/details/item_details_fourchan.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -61,6 +62,12 @@ class ItemDetails extends StatelessWidget {
Widget _buildDetails() {
switch (source.type) {
case FDSourceType.fourchan:
return ItemDetailsFourChan(
item: item,
source: source,
);
/// Sources with type [FDSourceType.github] do not provide a details view,
/// because we directly open the link, when the user clicks on the
/// corresponding preview item.

View File

@@ -0,0 +1,40 @@
import 'package:flutter/material.dart';
import 'package:feeddeck/models/item.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/widgets/item/details/utils/item_description.dart';
import 'package:feeddeck/widgets/item/details/utils/item_subtitle.dart';
import 'package:feeddeck/widgets/item/details/utils/item_title.dart';
class ItemDetailsFourChan extends StatelessWidget {
const ItemDetailsFourChan({
super.key,
required this.item,
required this.source,
});
final FDItem item;
final FDSource source;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
children: [
ItemTitle(
itemTitle: item.title,
),
ItemSubtitle(
item: item,
source: source,
),
ItemDescription(
itemDescription: item.description,
sourceFormat: DescriptionFormat.html,
tagetFormat: DescriptionFormat.markdown,
),
],
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:feeddeck/widgets/item/preview/item_preview_fourchan.dart';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
@@ -44,6 +45,11 @@ class ItemPreview extends StatelessWidget {
/// Based on the [source.type] we display a different preview. The preview
/// for each source type is implemented in a separate widget.
switch (source.type) {
case FDSourceType.fourchan:
return ItemPreviewFourChan(
item: item,
source: source,
);
case FDSourceType.github:
return ItemPreviewGithub(
item: item,

View File

@@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'package:feeddeck/models/item.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/widgets/item/preview/utils/details.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_actions.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_description.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_media.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_source.dart';
import 'package:feeddeck/widgets/item/preview/utils/item_title.dart';
class ItemPreviewFourChan extends StatelessWidget {
const ItemPreviewFourChan({
super.key,
required this.item,
required this.source,
});
final FDItem item;
final FDSource source;
@override
Widget build(BuildContext context) {
return ItemActions(
item: item,
onTap: () => showDetails(context, item, source),
children: [
ItemSource(
sourceTitle: source.title,
sourceSubtitle: source.type.toLocalizedString(),
sourceType: source.type,
sourceIcon: source.icon,
itemPublishedAt: item.publishedAt,
itemIsRead: item.isRead,
),
ItemTitle(
itemTitle: item.title,
),
ItemMedia(
itemMedia: item.media,
),
ItemDescription(
itemDescription: item.description,
sourceFormat: DescriptionFormat.html,
tagetFormat: DescriptionFormat.plain,
),
],
);
}
}

View File

@@ -1,3 +1,4 @@
import 'package:feeddeck/widgets/source/add/add_source_fourchan.dart';
import 'package:flutter/material.dart';
import 'package:feeddeck/models/column.dart';
@@ -45,6 +46,10 @@ class _AddSourceState extends State<AddSource> {
/// user selected a source type, the functions returns the form for the
/// selected source type.
Widget _buildBody() {
if (_sourceType == FDSourceType.fourchan) {
return AddSourceFourChan(column: widget.column);
}
if (_sourceType == FDSourceType.github) {
return AddSourceGitHub(column: widget.column);
}

View File

@@ -0,0 +1,440 @@
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import 'package:provider/provider.dart';
import 'package:feeddeck/models/column.dart';
import 'package:feeddeck/models/source.dart';
import 'package:feeddeck/repositories/app_repository.dart';
import 'package:feeddeck/utils/api_exception.dart';
import 'package:feeddeck/utils/constants.dart';
import 'package:feeddeck/utils/openurl.dart';
import 'package:feeddeck/widgets/source/add/add_source_form.dart';
const _helpText = '''
The 4chan source can be used to follow your favorite 4chan boards.
''';
/// The [AddSourceFourChan] widget is used to display the form to add a new
/// 4chan board.
class AddSourceFourChan extends StatefulWidget {
const AddSourceFourChan({
super.key,
required this.column,
});
final FDColumn column;
@override
State<AddSourceFourChan> createState() => _AddSourceFourChanState();
}
class _AddSourceFourChanState extends State<AddSourceFourChan> {
final _formKey = GlobalKey<FormState>();
String _fourChanBoard = 'a';
bool _isLoading = false;
String _error = '';
/// [_addSource] adds a new 4chan board. The user can select a board from the
/// dropdown and we will generate the corresponding URL.
Future<void> _addSource() async {
setState(() {
_isLoading = true;
_error = '';
});
try {
AppRepository app = Provider.of<AppRepository>(context, listen: false);
await app.addSource(
widget.column.id,
FDSourceType.fourchan,
FDSourceOptions(
fourchan: _fourChanBoard,
),
);
setState(() {
_isLoading = false;
_error = '';
});
if (mounted) {
Navigator.of(context).pop();
}
} on ApiException catch (err) {
setState(() {
_isLoading = false;
_error = 'Failed to add source: ${err.message}';
});
} catch (err) {
setState(() {
_isLoading = false;
_error = 'Failed to add source: ${err.toString()}';
});
}
}
@override
Widget build(BuildContext context) {
return AddSourceForm(
onTap: _addSource,
isLoading: _isLoading,
error: _error,
child: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
MarkdownBody(
selectable: true,
data: _helpText,
onTapLink: (text, href, title) {
try {
if (href != null) {
openUrl(href);
}
} catch (_) {}
},
),
const SizedBox(
height: Constants.spacingMiddle,
),
DropdownButton<String>(
value: _fourChanBoard,
isExpanded: true,
underline: Container(height: 1, color: Constants.primary),
onChanged: (String? value) {
setState(() {
_fourChanBoard = value!;
});
},
items: boards.map((FourChanBoard value) {
return DropdownMenuItem(
value: value.id,
child: Text(value.name),
);
}).toList(),
),
],
),
),
);
}
}
/// [FourChanBoard] is the model for a supported 4chan boards.
class FourChanBoard {
String id;
String name;
FourChanBoard({
required this.id,
required this.name,
});
}
/// [boards] is the list of all supported 4chan boards.
final boards = <FourChanBoard>[
FourChanBoard(
id: 'a',
name: 'Anime & Manga',
),
FourChanBoard(
id: 'c',
name: 'Anime/Cute',
),
FourChanBoard(
id: 'w',
name: 'Anime/Wallpapers',
),
FourChanBoard(
id: 'm',
name: 'Mecha',
),
FourChanBoard(
id: 'cgl',
name: 'Cosplay & EGL',
),
FourChanBoard(
id: 'cm',
name: 'Cute/Male',
),
FourChanBoard(
id: 'f',
name: 'Flash',
),
FourChanBoard(
id: 'n',
name: 'Transportation',
),
FourChanBoard(
id: 'jp',
name: 'Otaku Culture',
),
FourChanBoard(
id: 'vt',
name: 'Virtual YouTubers',
),
FourChanBoard(
id: 'v',
name: 'Video Games',
),
FourChanBoard(
id: 'vg',
name: 'Video Game Generals',
),
FourChanBoard(
id: 'vm',
name: 'Video Games/Multiplayer',
),
FourChanBoard(
id: 'vmg',
name: 'Video Games/Mobile',
),
FourChanBoard(
id: 'vp',
name: 'Pokémon',
),
FourChanBoard(
id: 'vr',
name: 'Retro Games',
),
FourChanBoard(
id: 'vrpg',
name: 'Video Games/RPG',
),
FourChanBoard(
id: 'vst',
name: 'Video Games/Strategy',
),
FourChanBoard(
id: 'co',
name: 'Comics & Cartoons',
),
FourChanBoard(
id: 'g',
name: 'Technology',
),
FourChanBoard(
id: 'tv',
name: 'Television & Film',
),
FourChanBoard(
id: 'k',
name: 'Weapons',
),
FourChanBoard(
id: 'o',
name: 'Auto',
),
FourChanBoard(
id: 'an',
name: 'Animals & Nature',
),
FourChanBoard(
id: 'tg',
name: 'Traditional Games',
),
FourChanBoard(
id: 'sp',
name: 'Sports',
),
FourChanBoard(
id: 'xs',
name: 'Extreme Sports',
),
FourChanBoard(
id: 'pw',
name: 'Professional Wrestling',
),
FourChanBoard(
id: 'sci',
name: 'Science & Math',
),
FourChanBoard(
id: 'his',
name: 'History & Humanities',
),
FourChanBoard(
id: 'int',
name: 'International',
),
FourChanBoard(
id: 'out',
name: 'Outdoors',
),
FourChanBoard(
id: 'toy',
name: 'Toys',
),
FourChanBoard(
id: 'i',
name: 'Oekaki',
),
FourChanBoard(
id: 'po',
name: 'Papercraft & Origami',
),
FourChanBoard(
id: 'p',
name: 'Photography',
),
FourChanBoard(
id: 'ck',
name: 'Food & Cooking',
),
FourChanBoard(
id: 'ic',
name: 'Artwork/Critique',
),
FourChanBoard(
id: 'wg',
name: 'Wallpapers/General',
),
FourChanBoard(
id: 'lit',
name: 'Literature',
),
FourChanBoard(
id: 'mu',
name: 'Music',
),
FourChanBoard(
id: 'fa',
name: 'Fashion',
),
FourChanBoard(
id: '3',
name: '3DCG',
),
FourChanBoard(
id: 'gd',
name: 'Graphic Design',
),
FourChanBoard(
id: 'diy',
name: 'Do-It-Yourself',
),
FourChanBoard(
id: 'wsg',
name: 'Worksafe GIF',
),
FourChanBoard(
id: 'qst',
name: 'Quests',
),
FourChanBoard(
id: 'biz',
name: 'Business & Finance',
),
FourChanBoard(
id: 'trv',
name: 'Travel',
),
FourChanBoard(
id: 'fit',
name: 'Fitness',
),
FourChanBoard(
id: 'x',
name: 'Paranormal',
),
FourChanBoard(
id: 'adv',
name: 'Advice',
),
FourChanBoard(
id: 'lgbt',
name: 'LGBT',
),
FourChanBoard(
id: 'mlp',
name: 'Pony',
),
FourChanBoard(
id: 'news',
name: 'Current News',
),
FourChanBoard(
id: 'wsr',
name: 'Worksafe Requests',
),
FourChanBoard(
id: 'vip',
name: 'Very Important Posts',
),
FourChanBoard(
id: 'b',
name: 'Random (NSFW)',
),
FourChanBoard(
id: 'r9k',
name: 'ROBOT9001 (NSFW)',
),
FourChanBoard(
id: 'pol',
name: 'Politically Incorrect (NSFW)',
),
FourChanBoard(
id: 'bant',
name: 'International/Random (NSFW)',
),
FourChanBoard(
id: 'soc',
name: 'Cams & Meetups (NSFW)',
),
FourChanBoard(
id: 's4s',
name: 'Shit 4chan Says (NSFW)',
),
FourChanBoard(
id: 's',
name: 'Sexy Beautiful Women (NSFW)',
),
FourChanBoard(
id: 'hc',
name: 'Hardcore (NSFW)',
),
FourChanBoard(
id: 'hm',
name: 'Handsome Men (NSFW)',
),
FourChanBoard(
id: 'h',
name: 'Hentai (NSFW)',
),
FourChanBoard(
id: 'e',
name: 'Ecchi (NSFW)',
),
FourChanBoard(
id: 'u',
name: 'Yuri (NSFW)',
),
FourChanBoard(
id: 'd',
name: 'Hentai/Alternative (NSFW)',
),
FourChanBoard(
id: 'y',
name: 'Yaoi (NSFW)',
),
FourChanBoard(
id: 't',
name: 'Torrents (NSFW)',
),
FourChanBoard(
id: 'hr',
name: 'High Resolution (NSFW)',
),
FourChanBoard(
id: 'gif',
name: 'Adult GIF (NSFW)',
),
FourChanBoard(
id: 'aco',
name: 'Adult Cartoons (NSFW)',
),
FourChanBoard(
id: 'r',
name: 'Adult Requests (NSFW)',
),
];

View File

@@ -327,6 +327,20 @@
"search": [
"lemmy"
]
},
{
"uid": "2efb1796652ef55a1fca1f7a3eda4b86",
"css": "fourchan",
"code": 59415,
"src": "custom_icons",
"selected": true,
"svg": {
"path": "M461.3 367.5C461.3 367.5 408.5 45 214.4 45.7 83.6 46.2 32.5 171.8 127.3 200.1 127.3 200.1 14.9 240.6 14.9 312.5 14.9 384.3 193 460 461.3 367.5L461.3 367.5ZM541.7 614.3C541.7 614.3 579.4 939 773.3 947.3 904 952.9 960.9 829.8 867.6 797.1 867.6 797.1 981.7 761.9 985.1 690.2 988.4 618.4 814 534.5 541.7 614.3L541.7 614.3ZM388.7 549.7C388.7 549.7 74.9 641 99.1 833.6 115.3 963.4 246.1 999 262.8 901.5 262.8 901.5 316.6 1008.2 387.9 999.5 459.2 990.8 512.9 804.9 388.7 549.8L388.7 549.7ZM623 447C623 447 945 390.7 942.2 196.6 940.3 65.9 814.1 16.1 786.9 111.2 786.9 111.2 745.2-0.7 673.3 0 601.4 0.8 527.7 179.7 623 447Z",
"width": 1000
},
"search": [
"fourchan"
]
}
]
}

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 4096 4096" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
<g id="_4chan" serif:id="4chan" transform="matrix(4.26802,0,0,4.26802,-137.229,-137.223)">
<path d="M474.83,384.842C474.83,384.842 424.165,75.302 237.905,76.022C112.383,76.502 63.359,197.062 154.333,224.174C154.333,224.174 46.448,263.082 46.448,332.019C46.448,400.996 217.353,473.573 474.828,384.801L474.83,384.842ZM552.045,621.726C552.045,621.726 588.193,933.266 774.293,941.263C899.693,946.661 954.355,828.499 864.743,797.15C864.743,797.15 974.308,763.358 977.508,694.503C980.708,625.605 813.361,545.071 552.046,621.727L552.045,621.726ZM405.212,559.744C405.212,559.744 104.028,647.316 127.222,832.176C142.817,956.698 268.377,990.886 284.372,897.356C284.372,897.356 335.955,999.684 404.412,991.366C472.869,983.048 524.374,804.626 405.212,559.746L405.212,559.744ZM630.06,461.098C630.06,461.098 939.04,407.115 936.364,220.856C934.52,95.376 813.44,47.592 787.29,138.843C787.29,138.843 747.302,31.438 678.284,32.155C609.344,32.877 538.569,204.581 630.06,461.098Z" style="fill-rule:nonzero;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB