Compare commits

...

76 Commits

Author SHA1 Message Date
Tetrakern
d145405955 Fix missing thumbnail in Discord story embed 2025-01-18 20:14:15 +01:00
Tetrakern
623d7ffa60 Bumb version to 5.27.2 2025-01-18 05:53:09 +01:00
Tetrakern
7463162a15 Bumb version to 5.27.2-beta6 2025-01-17 13:59:10 +01:00
Tetrakern
a1caff1ab7 Fix Patreon data expiration check 2025-01-17 13:56:17 +01:00
Tetrakern
cbc904e35b Update ACTIONS.md 2025-01-17 13:40:53 +01:00
Tetrakern
93f88321fa Update Patreon API 2025-01-17 13:05:40 +01:00
Tetrakern
a78d54105e Fix fictioneer_after_oauth_user args patreon_tiers 2025-01-17 13:03:32 +01:00
Tetrakern
c8d286ed27 Bumb version to 5.27.2-beta5 2025-01-16 19:07:37 +01:00
Tetrakern
d81630c1c7 Remove debug console output
lmao
2025-01-16 19:05:25 +01:00
ccesar
9be7dfbe21
update pt_BR trabnslation (#68)
Co-authored-by: ada <ada@dod.mil>
2025-01-16 19:04:11 +01:00
Tetrakern
0a98fcacc3 Fix story checkmark on story pages 2025-01-16 19:01:24 +01:00
Tetrakern
f958cca073 Fix large card checkmarks toggle 2025-01-16 18:44:28 +01:00
Tetrakern
dfde0cc075 Update ACTIONS.md 2025-01-14 00:31:49 +01:00
Tetrakern
da1b854353 Fix author access for scheduled chapters 2025-01-13 11:32:01 +01:00
Tetrakern
b5450f695c Update DEVELOPMENT.md 2025-01-13 00:56:30 +01:00
Tetrakern
c6c64edc5d Use 307 for scheduled chapter redirect 2025-01-13 00:55:40 +01:00
Tetrakern
26047ab14d Add option to redirect scheduled chapter 404 pages 2025-01-13 00:50:29 +01:00
Tetrakern
bb827ec1ef Allow latest chapters to be standalone 2025-01-12 23:41:37 +01:00
Tetrakern
c9d3f526d8 Update DOCUMENTATION.md 2025-01-12 19:39:13 +01:00
Tetrakern
b783d3cb9a Update DOCUMENTATION.md 2025-01-12 18:04:02 +01:00
Tetrakern
9360506c2e Add post status CSS to shortcode outputs 2025-01-12 18:00:18 +01:00
Tetrakern
d614b973c4 Add post_status param to shortcodes 2025-01-12 17:44:38 +01:00
Tetrakern
9b3913d0b9 Update SortableJS to 1.15.6 2025-01-12 16:17:16 +01:00
Tetrakern
9647f07524 Update Font Awesome to 6.7.2 2025-01-12 16:07:07 +01:00
Tetrakern
32a0fea1c9 Update fictioneer.pot 2025-01-12 16:02:18 +01:00
Tetrakern
2f88220fa7 Change advanced search toggle to icon 2025-01-12 16:01:34 +01:00
Tetrakern
aa9a6e5459 Fix follow dot not vanishing in mobile menu 2025-01-12 07:44:35 +01:00
Tetrakern
7b4d2256e9 Hide floating suggestion button if comments are disabled 2025-01-12 07:08:20 +01:00
Tetrakern
83a8500edb Bumb version to 5.27.2-beta4 2025-01-11 18:15:47 +01:00
Tetrakern
0948428500 Hide lightbox loader after 1s
In case of images with transparency. Either it is loaded at this point or not, the loader has no relation to the image status anyway.
2025-01-11 17:57:30 +01:00
Tetrakern
9679d7d4fc Add DoubleClickjack protection to frontend profile 2025-01-08 18:59:34 +01:00
Tetrakern
42ae3a0224 Bumb version to 5.27.2-beta3 2025-01-08 12:45:08 +01:00
Tetrakern
0f5e890179 Update fictioneer.pot 2025-01-08 01:51:02 +01:00
Tetrakern
87af4114b7 Fix missing top margin in article cards block 2025-01-08 01:08:57 +01:00
Tetrakern
f8cd013826 Update DOCUMENTATION.md 2025-01-08 00:03:53 +01:00
Tetrakern
d1aee5e2ca Add fictioneer_filter_shortcode_terms_query_args filter 2025-01-08 00:02:01 +01:00
Tetrakern
e3b17c898b Update DOCUMENTATION.md 2025-01-07 23:36:45 +01:00
Tetrakern
ed609f32b5 Add fictioneer_terms shortcode 2025-01-07 19:45:50 +01:00
Tetrakern
17df19a097 Increase specificity of latest comments block CSS
This was not always overwriting the default block styles.
2025-01-07 13:26:28 +01:00
Tetrakern
36aac5bd3e Add filters for RSS query args 2025-01-07 11:34:40 +01:00
Tetrakern
42367f0ae2 Bumb version to 5.27.2-beta2 2025-01-07 09:57:20 +01:00
Tetrakern
2326e7be0f Update doc comments 2025-01-07 09:55:25 +01:00
Tetrakern
78e68ffb9e Update fictioneer.pot 2025-01-07 09:52:40 +01:00
Tetrakern
2c2f541f9b Refactor Patreon fields bulk edit 2025-01-07 09:51:24 +01:00
Tetrakern
e3ca8a0799 Allow bulk edit of chapter story 2025-01-07 09:47:11 +01:00
Tetrakern
91b8083b0a Add fictioneer_filter_patreon_tier_unlock filter 2025-01-05 08:20:13 +01:00
Tetrakern
0b9df8e84a Add fictioneer_expired_post_password action 2025-01-05 08:05:04 +01:00
Tetrakern
ac71ecce7a Update MIGRATION.md 2025-01-04 19:16:54 +01:00
Tetrakern
691ed90d08 Update FILTERS.md 2025-01-04 17:45:59 +01:00
Tetrakern
37561eb521 Update FILTERS.md 2025-01-04 17:01:13 +01:00
Tetrakern
0f65c9f3d6 Add fictioneer_filter_list_chapter_title_row
And extend fictioneer_filter_list_chapter_prefix.
2025-01-03 18:45:14 +01:00
Tetrakern
be32074540 Fix logo width in FF 2025-01-03 05:15:16 +01:00
Tetrakern
6cb4670fa9 Update fictioneer.pot 2025-01-03 01:00:07 +01:00
Tetrakern
614a58a4a3 Clarify sidebar customizer setting 2025-01-03 00:58:14 +01:00
Tetrakern
426ca24a2f Fix missing comment mod menu 2025-01-01 06:06:34 +01:00
Tetrakern
9de0420ef7 Update FILTERS.md 2024-12-31 17:17:16 +01:00
Tetrakern
f80278b459 Add FICTIONEER_ENABLE_ALL_AUTHOR_PROFILES 2024-12-31 17:14:51 +01:00
Tetrakern
431966fc00 Update FILTERS.md 2024-12-30 17:54:40 +01:00
Tetrakern
b7fbfdc899 Fortify follows and reminders data structure 2024-12-30 07:09:30 +01:00
Tetrakern
6b907f5079 Fix checkmarks initialization 2024-12-30 06:11:00 +01:00
Tetrakern
1500fcab43 Fix mobile menu toggle with bookmarks disabled 2024-12-28 17:16:50 +01:00
Tetrakern
7391c15087 Bumb version to 5.27.1 2024-12-28 09:12:54 +01:00
Tetrakern
3480a94a11 Fix icon menu item button font weight 2024-12-27 07:41:00 +01:00
Tetrakern
6f394d5842 Fix chapter group meta field permission check 2024-12-23 00:06:39 +01:00
Tetrakern
d0714c2a13 Bumb version to 5.27.1-beta1 2024-12-22 21:48:12 +01:00
Tetrakern
fde4762e94 Improve guard clause for date update block 2024-12-22 07:56:28 +01:00
Tetrakern
c35cbd46ed Fix errant text-indent for FA icons in paragraphs 2024-12-21 23:40:20 +01:00
Tetrakern
e20976adb5 Fix text extraction utility and TTS 2024-12-21 23:39:42 +01:00
Tetrakern
67cc797010 Fix comment form Stimulus controller for LiteSpeed
Only relevant if logged-in users are severed cached files and not loaded via AJAX.
2024-12-21 01:45:25 +01:00
Tetrakern
b0d4dc8e0c Improve fictioneer_fix_logged_in_cookie()
This should normally not happen, but some cache shenanigans can cause it.
2024-12-20 00:59:03 +01:00
Tetrakern
d35f7fb1d1 Fix AJAYX auth for anonymized caching
Should have tested this sooner but alas.
2024-12-19 16:33:22 +01:00
Tetrakern
7a224a87c8 Optimize focus outline offset in navigation 2024-12-18 15:28:10 +01:00
Tetrakern
2ac900eb30 Aggregate styles for [hidden] and .hidden 2024-12-18 15:15:40 +01:00
Tetrakern
d28b70f601 Optimize focus style application 2024-12-18 15:13:57 +01:00
Tetrakern
c57950a65d Account for story post comments in count
You cannot normally comment on stories, only chapters, but in case someone changes that for likely no good reason, these comments are now also counted.
2024-12-18 00:41:18 +01:00
Tetrakern
96f7cf4b66 Improve navigation menu hover style with submenus 2024-12-17 18:47:52 +01:00
88 changed files with 3338 additions and 2425 deletions

View File

@ -5,7 +5,7 @@ The following [action hooks](https://developer.wordpress.org/reference/functions
This is an example of how to add a Discord invite link to the chapter top actions via the the `fictioneer_chapter_actions_top_center` hook. The link will feature a [Font Awesome Discord icon](https://fontawesome.com/icons/discord?f=brands) and be located between the formatting modal toggle (priority: 10) and fullscreen buttons (priority: 20). Note that no arguments of the hook are used because we do not need any of them here.
```php
// Add this to your child theme's functions.php
// Add this to your child themes functions.php
function child_theme_discord_invite_link() {
// Start HTML ---> ?>
<a href="http://www.your-discord-invite-link.com" target="_blank" rel="noopener" class="button _secondary">
@ -119,15 +119,28 @@ Fires after an user has been successfully created or logged-in via the OAuth 2.0
* $user (WP_User) The user object.
**$args:**
* $channel (string) Either `discord`, `patreon`, `twitch`, or `google`.
* $uid (string) External unique user ID from the linked account. Unsanitized.
* $username (string) The external username. Unsanitized.
* $nickname (string) The external nickname (or same as username). Unsanitized.
* $email (string) The external email address. Unsanitized.
* $avatar_url (string) The external avatar URL. Unsanitized.
* $patreon_tiers (array) The relevant Patreon tiers or an empty array. Unsanitized.
* $new (boolean) Whether this is a newly created user.
* $merged (boolean) Whether the account has been newly linked to an existing user.
* 'channel' (string) Either `discord`, `patreon`, `twitch`, or `google`.
* 'uid' (string) External unique user ID from the linked account. Unsanitized.
* 'username' (string) The external username. Unsanitized.
* 'nickname' (string) The external nickname (or same as username). Unsanitized.
* 'email' (string) The external email address. Unsanitized.
* 'avatar_url' (string) The external avatar URL. Unsanitized.
* 'patreon_tiers' (array) Associative array (Tier ID => Array) with the relevant Patreon tiers or an empty array. Unsanitized.
* 'tier' (string) Tier display title.
* 'title' (string) Tier display title.
* 'description' (string) Tier display description or empty.
* 'published' (boolean) Whether the tier is currently published.
* 'amount_cents' (int) Monetary amount associated with this tier (in U.S. cents).
* 'timestamp' (int) Unix timestamp (GMT) of the authentication in seconds.
* 'id' (int) Tier ID (also used as array key).
* 'patreon_membership' (array) Array with the Patreon membership data or an empty array. Unsanitized.
* 'lifetime_support_cents' (int) The total amount that the member has ever paid to the campaign in the campaigns currency. `0` if never paid.
* 'last_charge_date' (string|null) Datetime (UTC ISO) of last attempted charge. `null` if never charged.
* 'last_charge_status' (string|null) The result of the last attempted charge. The only successful status is `'Paid'`. `null` if never charged. One of `'Paid'`, `'Declined'`, `'Deleted'`, `'Pending'`, `'Refunded'`, `'Fraud'`, `'Refunded by Patreon'`, `'Other'`, `'Partially Refunded'`, `'Free Trial'`.
* 'next_charge_date' (string|null) Datetime (UTC ISO) of next charge. `null` if annual pledge downgrade.
* 'patron_status' (string|null) One of `'active_patron'`, `'declined_patron'`, `'former_patron'`. A `null` value indicates the member has never pledged.
* 'new' (boolean) Whether this is a newly created user.
* 'merged' (boolean) Whether the account has been newly linked to an existing user.
---
@ -399,6 +412,23 @@ Fires right after the content section in the `single-fcn_chapter.php` template,
* `fictioneer_chapter_support_links( $args )` Support links set for the chapter/story/author. Priority 20.
* `fictioneer_chapter_footer( $args )` Chapter footer. Priority 99.
**Example:**
```php
function child_display_pw_expiration_date_in_chapter( $args ) {
$password_expiration_date_utc = get_post_meta( $args['chapter_id'], 'fictioneer_post_password_expiration_date', true );
if ( empty( $password_expiration_date_utc ) ) {
return;
}
$local_time = get_date_from_gmt( $password_expiration_date_utc );
$formatted_datetime = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $local_time ) );
echo $formatted_datetime;
}
add_action( 'fictioneer_chapter_after_content', 'child_display_pw_expiration_date_in_chapter', 5 );
```
---
### `do_action( 'fictioneer_chapter_after_main', $args )`
@ -607,6 +637,14 @@ List page template hook. Fires right at the top of an empty result list in the `
---
### `do_action( 'fictioneer_expired_post_password', $post )`
Fires after a post password has been expired, which happens when a visitor tries to access the post.
**$args:**
* $post (WP_Post) The post that had its password expired.
---
### `do_action( 'fictioneer_footer', $args )`
Fires outside the `#site` container and before the `wp_footer` hook, near the end of the document. Not to be confused with the `fictioneer_site_footer` hook.

View File

@ -276,6 +276,8 @@ Fictioneer customizes WordPress by using as many standard action and filter hook
| `admin_post_*` | `fictioneer_delete_all_epubs` (10), `fictioneer_tools_add_moderator_role` (10), `fictioneer_tools_move_story_tags_to_genres` (10), `fictioneer_tools_duplicate_story_tags_to_genres` (10), `fictioneer_tools_purge_theme_caches` (10), `fictioneer_tools_move_chapter_tags_to_genres` (10), `fictioneer_tools_duplicate_chapter_tags_to_genres` (10), `fictioneer_tools_append_default_genres` (10), `fictioneer_tools_append_default_tags` (10), `fictioneer_tools_remove_unused_tags` (10), `fictioneer_tools_reset_post_relationship_registry` (10), `fictioneer_admin_profile_unset_oauth` (10), `fictioneer_admin_profile_clear_data_node` (10), `fictioneer_update_frontend_profile` (10), `fictioneer_cancel_frontend_email_change` (10), `fictioneer_add_role` (10), `fictioneer_remove_role` (10), `fictioneer_rename_role` (10), `fictioneer_connection_get_patreon_tiers` (10), `fictioneer_connection_delete_patreon_tiers` (10)
| `after_setup_theme` | `fictioneer_theme_setup` (10)
| `bulk_edit_custom_box` | `fictioneer_add_patreon_bulk_edit_tiers` (10), `fictioneer_add_patreon_bulk_edit_amount` (10)
| `bulk_edit_posts` | `fictioneer_save_chapter_bulk_edit` (10)
| `comment_form_top` | `fictioneer_fix_comment_form_stimulus_controller` (10)
| `comment_post` | `fictioneer_comment_post` (20), `fictioneer_post_comment_to_discord` (99)
| `current_screen` | `fictioneer_restrict_admin_only_pages` (10), `fictioneer_restrict_comment_edit` (10)
| `customize_controls_enqueue_scripts` | `fictioneer_enqueue_customizer_scripts` (10)
@ -302,14 +304,14 @@ Fictioneer customizes WordPress by using as many standard action and filter hook
| `publish_to_draft` | `fictioneer_chapter_to_draft` (10)
| `rest_api_init` | `fictioneer_register_endpoint_get_story_comments` (10)
| `restrict_manage_posts` | `fictioneer_add_chapter_story_filter_dropdown` (10)
| `save_post` | `fictioneer_refresh_chapters_schema` (20), `fictioneer_refresh_chapter_schema` (20), `fictioneer_refresh_collections_schema` (20), `fictioneer_refresh_post_caches` (20), `fictioneer_refresh_post_schema` (20), `fictioneer_refresh_recommendations_schema` (20), `fictioneer_refresh_recommendation_schema` (20), `fictioneer_refresh_stories_schema` (20), `fictioneer_refresh_story_schema` (20), `fictioneer_save_seo_metabox` (10), `fictioneer_save_word_count` (10), `fictioneer_track_chapter_and_story_updates` (10), `fictioneer_update_modified_date_on_story_for_chapter` (10), `fictioneer_update_shortcode_relationships` (10), `fictioneer_purge_transients_after_update` (10), `fictioneer_save_story_metaboxes` (10), `fictioneer_save_chapter_metaboxes` (10), `fictioneer_save_extra_metabox` (10), `fictioneer_save_support_links_metabox` (10), `fictioneer_save_collection_metaboxes` (10), `fictioneer_save_recommendation_metaboxes` (10), `fictioneer_save_post_metaboxes` (10), `fictioneer_delete_cached_story_card_after_update` (10), `fictioneer_rebuild_story_data_collection` (999), `fictioneer_post_chapter_to_discord` (99), `fictioneer_bulk_edit_save_patreon` (10), `fictioneer_bulk_edit_save_chapter_fields` (10)
| `save_post` | `fictioneer_refresh_chapters_schema` (20), `fictioneer_refresh_chapter_schema` (20), `fictioneer_refresh_collections_schema` (20), `fictioneer_refresh_post_caches` (20), `fictioneer_refresh_post_schema` (20), `fictioneer_refresh_recommendations_schema` (20), `fictioneer_refresh_recommendation_schema` (20), `fictioneer_refresh_stories_schema` (20), `fictioneer_refresh_story_schema` (20), `fictioneer_save_seo_metabox` (10), `fictioneer_save_word_count` (10), `fictioneer_track_chapter_and_story_updates` (10), `fictioneer_update_modified_date_on_story_for_chapter` (10), `fictioneer_update_shortcode_relationships` (10), `fictioneer_purge_transients_after_update` (10), `fictioneer_save_story_metaboxes` (10), `fictioneer_save_chapter_metaboxes` (10), `fictioneer_save_extra_metabox` (10), `fictioneer_save_support_links_metabox` (10), `fictioneer_save_collection_metaboxes` (10), `fictioneer_save_recommendation_metaboxes` (10), `fictioneer_save_post_metaboxes` (10), `fictioneer_delete_cached_story_card_after_update` (10), `fictioneer_rebuild_story_data_collection` (999), `fictioneer_post_chapter_to_discord` (99), `fictioneer_post_story_to_discord` (99)
| `send_headers` | `fictioneer_block_pages_from_indexing` (10)
| `set_logged_in_cookie` | `fictioneer_set_logged_in_cookie` (10)
| `show_user_profile` | `fictioneer_custom_profile_fields` (20)
| `shutdown` | `fictioneer_save_story_card_cache` (10)
| `switch_theme` | `fictioneer_theme_deactivation` (10)
| `template_redirect` | `fictioneer_generate_epub` (10), `fictioneer_oauth2_process` (10), `fictioneer_logout` (10), `fictioneer_disable_attachment_pages` (10), `fictioneer_gate_unpublished_content` (10), `fictioneer_serve_sitemap` (10), `fictioneer_redirect_story` (10)
| `transition_post_status` | `fictioneer_log_story_chapter_status_changes` (10), `fictioneer_chapter_future_to_publish` (10), `fictioneer_post_story_to_discord` (99)
| `template_redirect` | `fictioneer_generate_epub` (10), `fictioneer_oauth2_process` (10), `fictioneer_logout` (10), `fictioneer_disable_attachment_pages` (10), `fictioneer_gate_unpublished_content` (10), `fictioneer_serve_sitemap` (10), `fictioneer_redirect_story` (10), `fictioneer_redirect_scheduled_chapter_404` (10)
| `transition_post_status` | `fictioneer_log_story_chapter_status_changes` (10), `fictioneer_chapter_future_to_publish` (10)
| `trashed_post` | `fictioneer_refresh_post_caches` (20), `fictioneer_track_chapter_and_story_updates` (10), `fictioneer_update_modified_date_on_story_for_chapter` (10), `fictioneer_purge_transients_after_update` (10), `fictioneer_remove_chapter_from_story` (10)
| `untrash_post` | `fictioneer_refresh_post_caches` (20), `fictioneer_track_chapter_and_story_updates` (10), `fictioneer_update_modified_date_on_story_for_chapter` (10), `fictioneer_purge_transients_after_update` (10)
| `update_option_*` | `fictioneer_update_option_disable_extended_chapter_list_meta_queries` (10), `fictioneer_update_option_disable_extended_story_list_meta_queries` (10)

View File

@ -799,6 +799,31 @@ Renders a subscribe button for the specified story.
[fictioneer_subscribe_button story_id="228"]
```
### Terms Shortcode
Renders a group of terms similar to those on story pages, either globally or for a specific post.
* **type:** The queried taxonomy. Choose between `category`, `tag`, `genre`, `fandom`, `character`, and `warning`. Default `tag`.
* **post_id:** Query only terms for a specific post. Default `0` (none).
* **count:** Limit the number of items. Default `-1` (all).
* **order:** Either DESC (descending) or ASC (ascending). Default `desc`.
* **orderby:** The default is `count`, but you can also use `name` and [more](https://developer.wordpress.org/reference/classes/wp_term_query/__construct/).
* **show_empty:** Whether to show empty terms. Default `false`.
* **show_count:** Whether to show the term count. Default `false`.
* **class:** Additional CSS classes, separated by whitespace.
* **inner_class:** Additional CSS classes for nested elements (if any), separated by whitespace.
* **style:** Inline CSS style applied to the wrapping element.
* **inner_style:** Inline CSS style applied to nested elements (if any).
* **empty:** Override message for empty query results.
```
[fictioneer_terms count="15"]
```
```
[fictioneer_terms type="genre" post_id="228" inner_class="_secondary" show_count="true" style="margin: 2rem 0;" empty=""]
```
### Font Awesome Shortcode
Renders a *free* [Font Awesome](https://fontawesome.com/) icon, which you could technically do manually in the code editor as well. Somewhat more convenient, I guess? Just omit the shortcode block and write it directly into the text. This shortcode also works if your role lacks the shortcode capability.
@ -815,6 +840,7 @@ Renders a multi-column grid of paginated medium cards ordered by publishing date
* **post_type:** Comma-separated list of post types to query. Default `post`.
* **post_ids:** Comma-separated list of post IDs, if you want to pick from a curated pool.
* **post_status:** Either `publish` or `future`, albeit others are possible (but why?). Note that by default, any post status except `publish` redirects to a 404 page for guests and users without higher permissions. Default `publish`.
* **per_page:** Number of posts per page. Defaults to theme settings.
* **count:** Limit articles to any positive number, disabling the pagination.
* **order:** Either `desc` (descending) or `asc` (ascending). Default `desc`.
@ -875,6 +901,7 @@ Renders paginated blog posts akin to the main blog page, but with options. Only
* **ignore_sticky:** Whether sticky posts should be ignored or not. Default `false`.
* **ignore_protected:** Whether protected posts should be ignored or not. Default `false`.
* **only_protected:** Whether to query only protected posts or not. Default `false`.
* **post_status:** Either `publish` or `future`, albeit others are possible (but why?). Note that by default, any post status except `publish` redirects to a 404 page for guests and users without higher permissions. Default `publish`.
* **author:** Only show posts of a specific author. Make sure to use the url-safe nice_name.
* **author_ids:** Only show posts of a comma-separated list of author IDs.
* **exclude_author_ids:** Comma-separated list of author IDs to exclude.
@ -926,6 +953,7 @@ Renders a multi-column grid of small bookmark cards, ordered by date of creation
Renders a list of chapters identical to those on story pages, ordered by sequence in the source. Must have either the **story_id** or **chapter_ids** parameter, but not both.
* **story_id:** ID of a single story. You need either this or **chapters**.
* **post_status:** Either `publish` or `future`, albeit others are possible (but why?). Note that by default, any post status except `publish` redirects to a 404 page for guests and users without higher permissions. Default `publish`.
* **chapter_ids:** Comma-separated list of chapter IDs. You need either this or **story**.
* **count:** Limit chapters to any positive number. Default `-1` (all).
* **offset:** Skip a number of chapters, which can make sense if you query all.
@ -998,6 +1026,7 @@ Renders a multi-column grid of small cards, showing the latest four chapters ord
* **spoiler:** The excerpt is obfuscated, set `true` if you want to reveal it. Default `false`.
* **source:** Whether to show the author and story nodes. Default `true`.
* **post_ids:** Comma-separated list of chapter post IDs, if you want to pick from a curated pool.
* **post_status:** Either `publish` or `future`, albeit others are possible (but why?). Note that by default, any post status except `publish` redirects to a 404 page for guests and users without higher permissions. Default `publish`.
* **ignore_protected:** Whether protected posts should be ignored or not. Default `false`.
* **only_protected:** Whether to query only protected posts or not. Default `false`.
* **author_ids:** Only show posts of a comma-separated list of author IDs.
@ -1064,6 +1093,7 @@ Renders the last blog post or a list of blog posts, ignoring sticky posts, order
* **count:** Limit posts to any positive number, although you should keep it reasonable. Default `1`.
* **author:** Only show posts of a specific author. Make sure to use the url-safe nice_name.
* **post_ids:** Comma-separated list of post IDs, if you want to pick from a curated pool.
* **post_status:** Either `publish` or `future`, albeit others are possible (but why?). Note that by default, any post status except `publish` redirects to a 404 page for guests and users without higher permissions. Default `publish`.
* **ignore_protected:** Whether protected posts should be ignored or not. Default `false`.
* **only_protected:** Whether to query only protected posts or not. Default `false`.
* **author_ids:** Only show posts of a comma-separated list of author IDs.
@ -1100,6 +1130,7 @@ Renders a multi-column grid of small cards, showing the latest four recommendati
* **order:** Either `desc` (descending) or `asc` (ascending). Default `desc`.
* **orderby:** The default is `date`, but you can also use `modified` and [more](https://developer.wordpress.org/reference/classes/wp_query/#order-orderby-parameters).
* **post_ids:** Comma-separated list of post IDs, if you want to pick from a curated pool.
* **post_status:** Either `publish` or `future`, albeit others are possible (but why?). Note that by default, any post status except `publish` redirects to a 404 page for guests and users without higher permissions. Default `publish`.
* **ignore_protected:** Whether protected posts should be ignored or not. Default `false`.
* **only_protected:** Whether to query only protected posts or not. Default `false`.
* **author_ids:** Only show posts of a comma-separated list of author IDs.
@ -1153,6 +1184,7 @@ Renders a multi-column grid of small cards, showing the latest four stories orde
* **order:** Either `desc` (descending) or `asc` (ascending). Default `desc`.
* **orderby:** The default is `date`, but you can also use `modified` and [more](https://developer.wordpress.org/reference/classes/wp_query/#order-orderby-parameters).
* **post_ids:** Comma-separated list of story post IDs, if you want to pick from a curated pool.
* **post_status:** Either `publish` or `future`, albeit others are possible (but why?). Note that by default, any post status except `publish` redirects to a 404 page for guests and users without higher permissions. Default `publish`.
* **ignore_protected:** Whether protected posts should be ignored or not. Default `false`.
* **only_protected:** Whether to query only protected posts or not. Default `false`.
* **author_ids:** Only show posts of a comma-separated list of author IDs.
@ -1231,6 +1263,7 @@ Renders a multi-column grid of small cards, showing the latest four updated stor
* **author:** Only show updates of a specific author. Make sure to use the url-safe nice_name.
* **order:** Either `desc` (descending) or `asc` (ascending). Default `desc`.
* **post_ids:** Comma-separated list of post IDs, if you want to pick from a curated pool.
* **post_status:** Either `publish` or `future`, albeit others are possible (but why?). Note that by default, any post status except `publish` redirects to a 404 page for guests and users without higher permissions. Default `publish`.
* **ignore_protected:** Whether protected posts should be ignored or not. Default `false`.
* **only_protected:** Whether to query only protected posts or not. Default `false`.
* **author_ids:** Only show posts of a comma-separated list of author IDs.
@ -1345,6 +1378,7 @@ Renders dynamic grid of thumbnails with title, showing the latest eight posts of
* **order:** Either `desc` (descending) or `asc` (ascending). Default `desc`.
* **orderby:** The default is `date`, but you can also use `rand` and [more](https://developer.wordpress.org/reference/classes/wp_query/#order-orderby-parameters).
* **post_ids:** Comma-separated list of post IDs, if you want to pick from a curated pool.
* **post_status:** Either `publish` or `future`, albeit others are possible (but why?). Note that by default, any post status except `publish` redirects to a 404 page for guests and users without higher permissions. Default `publish`.
* **ignore_protected:** Whether protected posts should be ignored or not. Default `false`.
* **only_protected:** Whether to query only protected posts or not. Default `false`.
* **author_ids:** Only show posts of a comma-separated list of author IDs.

View File

@ -878,9 +878,33 @@ Filters the chapter message array passed to `fictioneer_discord_send_message()`
**Parameters:**
* $message (array) The message and fields posted to the Discord webhook.
* 'content' (string) - The actual discord message.
* 'embeds' (array) - Array of array with embedded items. Only uses the `0` index item.
* `0` (array) - First item in the array of embeds.
* 'title' (string) - Title.
* 'description' (string) - Description.
* 'url' (string) - URL.
* 'color' (string) - Color integer. Defaults to `FICTIONEER_DISCORD_EMBED_COLOR` (`9692513`).
* 'author' (array) - Array of author data.
* 'name' (string) - Author name.
* 'icon_url' (string) - Avatar URL.
* 'timestamp' (string) - [ISO 8601](https://wordpress.org/documentation/article/customize-date-and-time-format/) timestamp.
* 'footer' (string|null) - Optional. Title of the story.
* 'thumbnail' (array|null) - Optional. Array of thumbnail data.
* 'url' (string) - Thumbnail URL.
* $post (WP_Post) The new chapter being published.
* $story_id (int|null) The ID of the associated story if set. Unsafe.
**Example:**
```php
function child_remove_discord_chapter_message_embed_description( $message ) {
$message['embeds'][0]['description'] = '';
return $message;
}
add_filter( 'fictioneer_filter_discord_chapter_message', 'child_remove_discord_chapter_message_embed_description' );
```
---
### `apply_filters( 'fictioneer_filter_discord_comment_message', $message, $comment, $post, $user )`
@ -903,10 +927,11 @@ Filters the story message array passed to `fictioneer_discord_send_message()` in
---
### `apply_filters( 'fictioneer_filter_discord_chapter_webhook', $post, $story_id )`
### `apply_filters( 'fictioneer_filter_discord_chapter_webhook', $webhook, $post, $story_id )`
Filters the webhook used for the Discord notification about a new chapters.
**Parameters:**
* $webhook (string) - The Discord webhook.
* $post (WP_Post) - The chapter post.
* $story_id (int|null) - The ID of the story post (if any). Unsafe.
@ -1049,11 +1074,46 @@ Filters the boolean return value of the `fictioneer_is_editor( $user_id )` funct
---
### `apply_filters( 'fictioneer_filter_list_chapter_prefix', $prefix )`
### `apply_filters( 'fictioneer_filter_list_chapter_prefix', $prefix, $chapter_id, $context )`
Filters the prefix string (if any) in front of a chapter title before it is rendered in the `_story-content.php` partial or `fictioneer_chapter_list` shortcode. Useful if you want to add a wrapper for styling purposes.
**Parameters:**
* $prefix (string) Chapter prefix.
* $chapter_id (int) - Post ID of the chapter.
* $context (string) Either `'story'` or `'shortcode'`.
---
### `apply_filters( 'fictioneer_filter_list_chapter_title_row', $output, $chapter_id, $prefix, $has_password, $context )`
Filters the intermediate output HTML of the chapter list title row before it is rendered in the `_story-content.php` partial or `fictioneer_chapter_list` shortcode.
**Parameters:**
* $output (string) The chapter title row HTML.
* $chapter_id (int) - Post ID of the chapter.
* $prefix (string) Filtered chapter prefix.
* $has_password (boolean) Whether the chapter has a password.
* $context (string) Either `'story'` or `'shortcode'`.
**Example:**
```php
function child_display_pw_expiration_date_in_chapter_lists( $output, $chapter_id, $prefix, $has_password ) {
if ( ! $has_password ) {
return $output;
}
$password_expiration_date_utc = get_post_meta( $chapter_id, 'fictioneer_post_password_expiration_date', true );
if ( empty( $password_expiration_date_utc ) ) {
return $output;
}
$local_time = get_date_from_gmt( $password_expiration_date_utc );
$formatted_datetime = date_i18n( get_option( 'date_format' ) . ' ' . get_option( 'time_format' ), strtotime( $local_time ) );
return "[{$formatted_datetime}] {$output}";
}
add_filter( 'fictioneer_filter_list_chapter_title_row', 'child_display_pw_expiration_date_in_chapter_lists', 10, 4 );
```
---
@ -1185,6 +1245,21 @@ Filters the HTML of the message displayed below the Patreon unlock button. The m
---
### `apply_filters( 'fictioneer_filter_patreon_tier_unlock', $required, $post, $user, $patreon_post_data )`
Filters the boolean to determine whether a post is unlocked via Patreon tier.
**Parameters:**
* $required (boolean) Whether the post is unlocked.
* $post (WP_Post) The post object.
* $user (WP_User) The current user.
* $patreon_post_data (array) Relevant Patreon data for the post.
* 'gated' (boolean) - Whether the post has tiers or a cent amount assigned.
* 'gate_tiers' (array) - Tiers that unlock the post.
* 'gated' (int) - Minimum cent amount that unlocks the post.
* 'gate_lifetime_cents' (int) - Minimum lifetime cent amount that unlocks the post.
---
### `apply_filters( 'fictioneer_filter_post_card_footer', $footer_items, $post, $args )`
Filters the intermediate output array in the `_card-post.php` partial before it is imploded and rendered. Contains statistics with icons such as the author, publishing date, and comments.
@ -1346,6 +1421,43 @@ Filters the RSS link returned by the `fictioneer_get_rss_link( $post_type, $post
---
### `apply_filters( 'fictioneer_filter_rss_main_query_args', $query_args )`
Filters the query arguments for the main RSS feed in the `rss-rss-main.php` template.
**Parameters:**
* $query_args (array) Associative array of query arguments.
**Example:**
```php
function child_exclude_protected_posts_from_main_rss( $query_args ) {
$query_args['has_password'] = false;
return $query_args;
}
add_filter( 'fictioneer_filter_rss_story_query_args', 'child_exclude_protected_posts_from_main_rss' );
```
---
### `apply_filters( 'fictioneer_filter_rss_story_query_args', $query_args, $story_id )`
Filters the query arguments for the story RSS feed in the `rss-rss-story.php` template.
**Parameters:**
* $query_args (array) Associative array of query arguments.
* $story_id (int) Post ID of the story.
**Example:**
```php
function child_exclude_protected_posts_from_story_rss( $query_args ) {
$query_args['has_password'] = false;
return $query_args;
}
add_filter( 'fictioneer_filter_rss_story_query_args', 'child_exclude_protected_posts_from_story_rss' );
```
---
### `apply_filters( 'fictioneer_filter_safe_title', $title, $post_id, $context, $args )`
Filters the string returned by the `fictioneer_get_safe_title( $post_id )` function, after all tags and line breaks have been stripped. No further sanitization is applied here, so you can add HTML again.
@ -1463,7 +1575,7 @@ Filters the WP_Query arguments in the `fictioneer_blog` shortcode.
* $posts_per_page (int) `$args['posts_per_page']`
* $tax_query (array|null) `fictioneer_get_shortcode_tax_query( $args )`
**$attr:**
**$args:**
* $posts_per_page (int) The number of posts per page. Defaults to WordPress.
* $page (int) Current main query page number. Default `1`.
* $ignore_sticky (boolean) Optional. Whether to ignore sticky posts. Default `false`.
@ -1813,6 +1925,33 @@ Filters the WP_Query arguments in the `fictioneer_showcase` shortcode. The optio
---
### `apply_filters( 'fictioneer_filter_shortcode_terms_query_args', $query_args, $attr )`
Filters the WP_Term_Query arguments in the `fictioneer_terms` shortcode.
**$query_args:**
* 'fictioneer_query_name' (string) `'terms_shortcode'`
* 'taxonomy' (string) As specified in the shortcode. Either `'category'`, `'post_tag'`, `'fcn_genre'`, `'fcn_fandom'`, `'fcn_character'`, or `'fcn_content_warning'`.
* 'orderby' (string) As specified in the shortcode. Default `'count'`.
* 'order' (string) As specified in the shortcode. Default `'desc'`.
* 'number' (int|null) As specified in the shortcode. Default `null`.
* 'hide_empty' (bool) As specified in the shortcode. Default `true`.
**$attr:**
* 'term_type' (string|null) Either `'category'`, `'tag'`, `'genre'`, `'fandom'`, `'character'`, or `'warning'`. Default `'tag'`.
* 'post_id' (int|null) Limit terms to a specific post. Default `0`.
* 'count' (int|null) Limit number of queried terms. Default `-1` (all).
* 'orderby' (string|null) Default `'count'`.
* 'order' (string|null) Default `'desc'`.
* 'show_empty' (boolean|null) Whether to query unused terms. Default `false`.
* 'show_count' (boolean|null) Whether to show the term counts. Default `false`.
* 'classes' (string|null) String of additional outer CSS classes. Default empty.
* 'inner_classes' (string|null) String of additional inner CSS classes. Default empty.
* 'style' (string|null) String of additional outer inline styles. Default empty.
* 'inner_style' (string|null) String of additional inner inline styles. Default empty.
* 'empty' (string|null) Override message for empty query result. Default empty.
---
### `apply_filters( 'fictioneer_filter_sitemap_page_template_excludes', $excludes )`
Filters the exclusion array of page templates for the custom theme sitemap. By default, these are `'user-profile.php'`, `'singular-bookmarks.php'`, `'singular-bookshelf.php'`, and `'singular-bookshelf-ajax.php'`.

View File

@ -1579,4 +1579,5 @@ define( 'CONSTANT_NAME', value );
| FICTIONEER_ENABLE_ASYNC_ONLOAD_PATTERN | boolean | Whether the [onload pattern](https://www.filamentgroup.com/lab/load-css-simpler/) for asynchronous CSS loading is used. Default `true`.
| FICTIONEER_SHOW_LATEST_CHAPTERS_ON_STORY_CARDS | boolean | Whether to show the latest instead of the first chapters on story cards. Default `false`.
| FICTIONEER_LIST_SCHEDULED_CHAPTERS | boolean | Whether to show scheduled chapters in lists. Default `false`.
| FICTIONEER_ENABLE_ALL_AUTHOR_PROFILES | boolean | Whether to enable all author profile pages. Default `false`.
| FICTIONEER_EXAMPLE_CHAPTER_ICONS | array | Collection of example Font Awesome icon class strings.

View File

@ -7,6 +7,9 @@ You can migrate an existing database through several means. The easiest, albeit
Add the following script to your `functions.php` file, for example via the **Theme File Editor** under **Appearance**. This serves as the base for the described migration methods and as a template if you want to write your own script. Make sure to remove the code once you are done; you do not want to leave this in a production environment. You can find a list of all story and chapter meta fields in the [development guide](DEVELOPMENT.md#story-meta-fields).
```php
// This is just the base migration script to convert and assign chapters;
// you also need to add one of the method scripts described afterwards!
function fictioneer_migrate_chapters( $query_args = [], $story_args = [], $preview = true ) {
// Security check (only allow admins to execute script)
if ( ! current_user_can( 'manage_options' ) ) {
@ -160,7 +163,7 @@ function fictioneer_migrate_chapters( $query_args = [], $story_args = [], $previ
}
```
## Simple Migration
## Method 1: Simple Migration
Add the following script to your `functions.php` file. This script will query ALL posts and convert them to chapters, optionally appending them to a prepared story post. You can customize the parameters if needed. The action can be called by opening `/wp-admin/admin-post.php?action=fictioneer_migrate` on your site. You will first get a preview of which chapters are about to be migrated; if you are happy with the preview, click the "Start migration!" link.
@ -168,7 +171,7 @@ Add the following script to your `functions.php` file. This script will query AL
function fictioneer_migrate() {
// Story data if you want to append the chapters
$story_args = array(
'story_id' => null, // Leave null if you only want to convert chapters
'story_id' => null, // Existing story post ID; leave null if you only want to convert chapters
'age_rating' => 'Everyone', // Choose: 'Everyone', 'Teen', 'Mature', or 'Adult' (capitalized and in English)
'status' => 'Ongoing', // Choose: 'Ongoing', 'Completed', 'Oneshot', 'Hiatus', or 'Cancelled' (capitalized and in English)
'description' => null, // Used on cards; falls back to excerpt
@ -206,7 +209,7 @@ function fictioneer_migrate() {
add_action( 'admin_post_fictioneer_migrate', 'fictioneer_migrate' );
```
## Migrate by Author(s)
## Method 2: Migrate by Author(s)
Add the following script to your `functions.php` file. Unlike the simple approach, this script will loop over a predefined set of author data (created by you) to migrate specific posts to specific stories. In this case, a category is used as a discriminator, but other attributes can work too. The action can be called by opening `/wp-admin/admin-post.php?action=fictioneer_migrate_by_author` on your site. Note that you will NOT get a preview; the migration happens immediately!

View File

@ -15,8 +15,11 @@ $author = get_userdata( $author_id );
// Return home if not a valid author
if (
! FICTIONEER_ENABLE_ALL_AUTHOR_PROFILES &&
(
! fictioneer_is_author( $author_id ) ||
count_user_posts( $author_id, ['fcn_story', 'fcn_chapter'] ) < 1
)
) {
wp_redirect( home_url() );
exit();

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -5,9 +5,9 @@
// =============================================================================
// Version
define( 'FICTIONEER_VERSION', '5.27.0' );
define( 'FICTIONEER_VERSION', '5.27.2' );
define( 'FICTIONEER_MAJOR_VERSION', '5' );
define( 'FICTIONEER_RELEASE_TAG', 'v5.27.0' );
define( 'FICTIONEER_RELEASE_TAG', 'v5.27.2' );
if ( ! defined( 'CHILD_VERSION' ) ) {
define( 'CHILD_VERSION', null );
@ -99,14 +99,14 @@ define(
// String: Font Awesome CDN URL
if ( ! defined( 'FICTIONEER_FA_CDN' ) ) {
define( 'FICTIONEER_FA_CDN', 'https://use.fontawesome.com/releases/v6.6.0/css/all.css' );
define( 'FICTIONEER_FA_CDN', 'https://use.fontawesome.com/releases/v6.7.2/css/all.css' );
}
// String: Font Awesome CDN integrity (generated by https://www.srihash.org/)
if ( ! defined( 'FICTIONEER_FA_INTEGRITY' ) ) {
define(
'FICTIONEER_FA_INTEGRITY',
'sha384-h/hnnw1Bi4nbpD6kE7nYfCXzovi622sY5WBxww8ARKwpdLj5kUWjRuyiXaD1U2JT'
'sha384-nRgPTkuX86pH8yjPJUAFuASXQSSl2/bBUiNV47vSYpKFxHJhbcrGnmlYpYJMeD7a'
);
}
@ -444,6 +444,11 @@ if ( ! defined( 'FICTIONEER_LIST_SCHEDULED_CHAPTERS' ) ) {
define( 'FICTIONEER_LIST_SCHEDULED_CHAPTERS', false );
}
// Boolean: Whether to enable all author profile pages
if ( ! defined( 'FICTIONEER_ENABLE_ALL_AUTHOR_PROFILES' ) ) {
define( 'FICTIONEER_ENABLE_ALL_AUTHOR_PROFILES', false );
}
// =============================================================================
// GLOBAL
// =============================================================================

View File

@ -1535,6 +1535,16 @@ function fictioneer_add_layout_customizer_settings( $manager ) {
'right' => _x( 'Right', 'Customizer header sidebar option.', 'fictioneer' )
);
if ( get_option( 'fictioneer_disable_all_widgets' ) ) {
$description = sprintf(
'%s <div style="margin: 10px 0;">%s</div>',
__( 'Choose whether and where to render the sidebar. You can set it up under Widgets; it will not appear before that.', 'fictioneer' ),
__( '<strong>Note:</strong> Widgets are currently disabled under Fictioneer > General > Performance. You need to enable them first.', 'fictioneer' )
);
} else {
$description = __( 'Choose whether and where to render the sidebar. You can set it up under Widgets; it will not appear before that.', 'fictioneer' );
}
$manager->add_control(
'sidebar_style',
array(
@ -1542,12 +1552,12 @@ function fictioneer_add_layout_customizer_settings( $manager ) {
'priority' => 6,
'section' => 'layout',
'label' => __( 'Sidebar', 'fictioneer' ),
'description' => __( 'Choose whether and where to render the sidebar. You can set it up under Widgets.', 'fictioneer' ),
'description' => $description,
'choices' => apply_filters( 'fictioneer_filter_customizer_sidebar_style', $sidebar_styles )
)
);
// Disable story sidebar
// Disable mobile sidebar
$manager->add_setting(
'sidebar_hide_on_mobile',
array(

View File

@ -101,8 +101,8 @@ function fictioneer_generate_test_content() {
// Disable Discord
remove_action( 'comment_post', 'fictioneer_post_comment_to_discord', 99 );
remove_action( 'transition_post_status', 'fictioneer_post_story_to_discord', 99 );
remove_action( 'transition_post_status', 'fictioneer_post_chapter_to_discord', 99 );
remove_action( 'save_post', 'fictioneer_post_story_to_discord', 99 );
remove_action( 'save_post', 'fictioneer_post_chapter_to_discord', 99 );
// Setup
$user_count = absint( $_GET['users'] ?? 1 );

View File

@ -2786,7 +2786,7 @@ function fictioneer_render_skin_interface() {
<template data-css-skin-target="template">
<div class="custom-skin" data-css-skin-finder="skin-item">
<button type="button" class="custom-skin__toggle" data-action="click->css-skin#toggle">
<button type="button" class="custom-skin__toggle" data-action="click->css-skin#toggle" data-fictioneer-target="dcjProtected" disabled>
<i class="fa-regular fa-circle off"></i>
<i class="fa-solid fa-circle-dot on"></i>
</button>
@ -2797,7 +2797,7 @@ function fictioneer_render_skin_interface() {
<span class="custom-skin__spacer"></span>
<span class="custom-skin__author" data-css-skin-finder="author">&mdash;</span>
</div>
<button type="button" class="custom-skin__delete" data-action="click->css-skin#delete"><i class="fa-solid fa-trash-can"></i></button>
<button type="button" class="custom-skin__delete" data-action="click->css-skin#delete" data-fictioneer-target="dcjProtected" disabled><i class="fa-solid fa-trash-can"></i></button>
</div>
</template>
@ -2824,8 +2824,8 @@ function fictioneer_render_skin_interface() {
</div>
<div class="profile__actions custom-skin-actions">
<button type="button" class="button" data-action="click->css-skin#upload" data-disable-with="<?php esc_attr_e( 'Uploading…', 'fictioneer' ); ?>"><?php _e( 'Sync Up', 'fictioneer' ); ?></button>
<button type="button" class="button" data-action="click->css-skin#download" data-disable-with="<?php esc_attr_e( 'Downloading…', 'fictioneer' ); ?>"><?php _e( 'Sync Down', 'fictioneer' ); ?></button>
<button type="button" class="button" data-action="click->css-skin#upload" data-disable-with="<?php esc_attr_e( 'Uploading…', 'fictioneer' ); ?>" data-fictioneer-target="dcjProtected" disabled><?php _e( 'Sync Up', 'fictioneer' ); ?></button>
<button type="button" class="button" data-action="click->css-skin#download" data-disable-with="<?php esc_attr_e( 'Downloading…', 'fictioneer' ); ?>" data-fictioneer-target="dcjProtected" disabled><?php _e( 'Sync Down', 'fictioneer' ); ?></button>
<div class="invisible custom-skin-action-status" data-css-skin-target="action-status-message"><span class="dashicons dashicons-saved"></span></div>
</div>

View File

@ -328,10 +328,10 @@ function fictioneer_patreon_tiers_valid( $user = null ) {
// Setup
$patreon_tiers = get_user_meta( $user_id, 'fictioneer_patreon_tiers', true );
$patreon_tiers = is_array( $patreon_tiers ) ? $patreon_tiers : [];
$last_updated = empty( $patreon_tiers ) ? 0 : ( $patreon_tiers[0]['timestamp'] ?? 0 );
$last_updated = empty( $patreon_tiers ) ? 0 : ( reset( $patreon_tiers )['timestamp'] ?? 0 );
// Check
$valid = time() <= $last_updated + FICTIONEER_PATREON_EXPIRATION_TIME;
$valid = current_time( 'U', true ) <= ( $last_updated + FICTIONEER_PATREON_EXPIRATION_TIME );
// Filter and return
return apply_filters( 'fictioneer_filter_user_patreon_validation', $valid, $user_id, $patreon_tiers );
@ -345,7 +345,7 @@ function fictioneer_patreon_tiers_valid( $user = null ) {
* @param int|WP_User|null $user The user object or user ID. Defaults to current user.
*
* @return array Empty array if not a patron, associative array otherwise. Includes the
* keys 'valid', 'is_follower', 'lifetime_support_cents', 'last_charge_date',
* keys 'valid', 'lifetime_support_cents', 'last_charge_date',
* 'last_charge_status', 'next_charge_date', 'patron_status', and 'tiers'.
* Tiers is an array of tiers with the keys 'id', 'title', 'description',
* 'published', 'amount_cents', and 'timestamp'.

View File

@ -191,15 +191,29 @@ if ( get_option( 'fictioneer_discord_channel_comments_webhook' ) ) {
*
* @since 5.6.0
* @since 5.21.2 - Refactored.
* @since 5.27.2 - Switch back to save_post hook to ensure the thumbnail is saved.
*
* @param string $new_status New post status.
* @param string $new_status Old post status.
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
* @param bool $update Whether this is an existing post being updated. Unreliable.
*/
function fictioneer_post_story_to_discord( $new_status, $old_status, $post ) {
// Only if story going from non-publish status to publish
if ( $post->post_type !== 'fcn_story' || $new_status !== 'publish' || $old_status === 'publish' ) {
function fictioneer_post_story_to_discord( $post_id, $post, $update ) {
// Prevent multi-fire
if ( fictioneer_multi_save_guard( $post_id ) ) {
return;
}
// Only if published chapter
if ( $post->post_type !== 'fcn_story' || $post->post_status !== 'publish' ) {
return;
}
// Only if published less than 10 minutes ago
$post_timestamp = get_post_time( 'U', true, $post_id );
$current_timestamp = current_time( 'U', true );
if ( $update && ( $current_timestamp - $post_timestamp ) >= 600 ) {
return;
}
@ -210,14 +224,13 @@ function fictioneer_post_story_to_discord( $new_status, $old_status, $post ) {
// Data
$title = html_entity_decode( get_the_title( $post ) );
$url = get_permalink( $post->ID );
// Message
$message = array(
'content' => sprintf(
_x( "New story published: [%s](%s)!\n_ _", 'Discord message for new story.', 'fictioneer' ),
$title,
$url
get_permalink( $post_id )
),
'embeds' => array(
array(
@ -258,11 +271,11 @@ function fictioneer_post_story_to_discord( $new_status, $old_status, $post ) {
update_post_meta( $post->ID, 'fictioneer_discord_post_trigger', true );
// Unhook if done to avoid additional triggers (if any)
remove_action( 'transition_post_status', 'fictioneer_post_story_to_discord', 99 );
remove_action( 'save_post', 'fictioneer_post_story_to_discord', 99 );
}
if ( get_option( 'fictioneer_discord_channel_stories_webhook' ) ) {
add_action( 'transition_post_status', 'fictioneer_post_story_to_discord', 99, 3 );
add_action( 'save_post', 'fictioneer_post_story_to_discord', 99, 3 );
}
// =============================================================================
@ -278,7 +291,7 @@ if ( get_option( 'fictioneer_discord_channel_stories_webhook' ) ) {
*
* @param int $post_id Post ID.
* @param WP_Post $post Post object.
* @param bool $update Whether this is an existing post being updated.
* @param bool $update Whether this is an existing post being updated. Unreliable.
*/
function fictioneer_post_chapter_to_discord( $post_id, $post, $update ) {

View File

@ -490,7 +490,8 @@ function fictioneer_oauth2_make_user( $user_data, $cookie ) {
'nickname' => $user_data['nickname'],
'email' => $user_data['email'],
'avatar_url' => $user_data['avatar'],
'patreon_tiers' => $user_data['patreon_tiers'] ?? [],
'patreon_tiers' => $user_data['tiers'] ?? [],
'patreon_membership' => $user_data['membership'] ?? [],
'new' => $new,
'merged' => $merged
)
@ -790,7 +791,7 @@ function fictioneer_oauth2_patreon( $token_response, $cookie ) {
// Build params
$params = '?fields' . urlencode( '[user]' ) . '=email,first_name,image_url,is_email_verified';
$params .= '&fields' . urlencode( '[tier]' ) . '=title,amount_cents,published,description';
$params .= '&fields' . urlencode( '[member]' ) . '=lifetime_support_cents,is_follower,last_charge_date,last_charge_status,next_charge_date,patron_status';
$params .= '&fields' . urlencode( '[member]' ) . '=lifetime_support_cents,campaign_lifetime_support_cents,last_charge_date,last_charge_status,next_charge_date,patron_status';
$params .= '&include=memberships.currently_entitled_tiers';
// Retrieve user data from Patreon
@ -827,13 +828,13 @@ function fictioneer_oauth2_patreon( $token_response, $cookie ) {
// Tiers data
foreach ( $user->included as $node ) {
if ( isset( $node->type ) && $node->type === 'tier' ) {
$tiers[] = array(
$tiers[ $node->id ] = array(
'tier' => sanitize_text_field( $node->attributes->title ),
'title' => sanitize_text_field( $node->attributes->title ),
'description' => wp_kses_post( $node->attributes->description ?? '' ),
'published' => filter_var( $node->attributes->published ?? 0, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ),
'amount_cents' => absint( $node->attributes->amount_cents ?? 0 ),
'timestamp' => time(),
'timestamp' => current_time( 'U', true ),
'id' => $node->id
);
$tier_ids[] = $node->id;
@ -846,12 +847,10 @@ function fictioneer_oauth2_patreon( $token_response, $cookie ) {
isset( $node->type ) &&
$node->type === 'member' &&
isset( $node->attributes ) &&
isset( $node->attributes->lifetime_support_cents ) &&
isset( $node->relationships->currently_entitled_tiers->data ) &&
in_array( $node->relationships->currently_entitled_tiers->data[0]->id, $tier_ids )
) {
$membership['is_follower'] = $node->attributes->is_follower ?? 0;
$membership['lifetime_support_cents'] = $node->attributes->lifetime_support_cents ?? 0;
$membership['lifetime_support_cents'] = $node->attributes->lifetime_support_cents ?? $node->attributes->campaign_lifetime_support_cents ?? 0;
$membership['last_charge_date'] = $node->attributes->last_charge_date ?? null;
$membership['last_charge_status'] = $node->attributes->last_charge_status ?? null;
$membership['next_charge_date'] = $node->attributes->next_charge_date ?? null;

View File

@ -383,6 +383,8 @@ function fictioneer_expire_post_password( $required, $post ) {
fictioneer_refresh_post_caches( $post->ID );
wp_update_post( array( 'ID' => $post->ID, 'post_password' => '' ) );
do_action( 'fictioneer_expired_post_password', $post );
$required = false;
}
}

View File

@ -1176,7 +1176,7 @@ function fictioneer_ajax_get_chapter_group_options() {
wp_send_json_error( array( 'error' => 'Request did not pass validation.' ) );
}
if ( ! $user || ! current_user_can( 'edit_fcn_stories' ) ) {
if ( ! $user || ! current_user_can( 'edit_fcn_chapters' ) ) {
wp_send_json_error( array( 'error' => 'User did not pass validation.' ) );
}

View File

@ -4156,7 +4156,7 @@ add_action( 'save_post', 'fictioneer_save_recommendation_metaboxes' );
// =============================================================================
/**
* Hide Patreon columns in list table views by default
* Hides Patreon columns in list table views by default.
*
* @since 5.17.0
*
@ -4182,7 +4182,7 @@ function fictioneer_default_hide_patreon_posts_columns( $hidden, $screen ) {
add_filter( 'default_hidden_columns', 'fictioneer_default_hide_patreon_posts_columns', 10, 2 );
/**
* Add Patreon columns to list table views
* Adds Patreon columns to list table views.
*
* @since 5.17.0
*
@ -4202,7 +4202,7 @@ function fictioneer_add_posts_columns_patreon( $post_columns ) {
}
/**
* Output Patreon values in list table views
* Renders Patreon values in list table views.
*
* @since 5.17.0
*
@ -4249,7 +4249,7 @@ function fictioneer_manage_posts_column_patreon( $column_name, $post_id ) {
}
/**
* Add Patreon tiers to bulk edit
* Adds Patreon tiers to bulk edit.
*
* @since 5.17.0
*
@ -4286,6 +4286,9 @@ function fictioneer_add_patreon_bulk_edit_tiers( $column_name, $post_type ) {
);
}
// Nonce
wp_nonce_field( 'fictioneer_bulk_edit_patreon', 'fictioneer_bulk_edit_patreon_nonce', false );
// Start HTML ---> ?>
<fieldset class="inline-edit-col-left">
<div class="inline-edit-patreon-tiers-wrap">
@ -4296,17 +4299,17 @@ function fictioneer_add_patreon_bulk_edit_tiers( $column_name, $post_type ) {
<div class="bulk-edit-box">
<input type="hidden" name="fictioneer_patreon_lock_tiers_bulk" value="no_change">
<input type="hidden" name="fictioneer_bulk_edit_patreon_lock_tiers" value="no_change">
<label class="bulk-edit-checkbox-label-pair">
<input type="checkbox" name="fictioneer_patreon_lock_tiers_bulk" value="remove">
<input type="checkbox" name="fictioneer_bulk_edit_patreon_lock_tiers" value="remove">
<span><?php _e( 'Remove Tiers', 'fictioneer' ); ?></span>
</label>
<?php foreach ( $tier_options as $key => $tier ) : ?>
<label class="bulk-edit-checkbox-label-pair">
<input type="checkbox" name="fictioneer_patreon_lock_tiers_bulk[]" value="<?php echo $key; ?>">
<input type="checkbox" name="fictioneer_bulk_edit_patreon_lock_tiers[]" value="<?php echo $key; ?>">
<span><?php echo $tier; ?></span>
</label>
@ -4320,7 +4323,7 @@ function fictioneer_add_patreon_bulk_edit_tiers( $column_name, $post_type ) {
}
/**
* Add Patreon amount_cents to bulk edit
* Adds Patreon amount_cents to bulk edit.
*
* @since 5.17.0
*
@ -4337,6 +4340,9 @@ function fictioneer_add_patreon_bulk_edit_amount( $column_name, $post_type ) {
return;
}
// Nonce
wp_nonce_field( 'fictioneer_bulk_edit_patreon', 'fictioneer_bulk_edit_patreon_nonce', false );
// Start HTML ---> ?>
<fieldset class="inline-edit-col-left">
<div class="inline-edit-patreon-amount-cents-wrap">
@ -4346,7 +4352,7 @@ function fictioneer_add_patreon_bulk_edit_amount( $column_name, $post_type ) {
?></span>
<div>
<input type="number" name="fictioneer_patreon_lock_amount_bulk" autocomplete="off" min="0">
<input type="number" name="fictioneer_bulk_edit_patreon_lock_amount" autocomplete="off" min="0">
</div>
</div>
@ -4355,55 +4361,92 @@ function fictioneer_add_patreon_bulk_edit_amount( $column_name, $post_type ) {
}
/**
* Save Patreon bulk edit fields
* Saves bulk edit Patreon meta.
*
* @since 5.17.0
* @since 5.27.2
* @link https://developer.wordpress.org/reference/hooks/bulk_edit_posts/
*
* @param int $post_id ID of the updated post.
* @param int[] $updated_post_ids An array of updated post IDs.
* @param int[] $shared_post_data Associative array containing the post data.
*/
function fictioneer_bulk_edit_save_patreon( $post_id ) {
// Abort if...
function fictioneer_save_patreon_bulk_edit( $updated_post_ids, $shared_post_data ) {
// Validate
if (
! wp_verify_nonce( $_REQUEST['_wpnonce'] ?? 0, 'bulk-posts' ) ||
( $_REQUEST['action2'] ?? 0 ) === 'trash' ||
! wp_verify_nonce( $shared_post_data['fictioneer_bulk_edit_patreon_nonce'] ?? 0, 'fictioneer_bulk_edit_patreon' ) ||
( $shared_post_data['action2'] ?? 0 ) === 'trash' ||
! in_array(
get_post_type( $post_id ),
$shared_post_data['post_type'] ?? 0,
['post', 'page', 'fcn_story', 'fcn_chapter', 'fcn_collection', 'fcn_recommendation']
) ||
! ( current_user_can( 'manage_options' ) || current_user_can( 'fcn_assign_patreon_tiers' ) )
! ( current_user_can( 'manage_options' ) || current_user_can( 'fcn_assign_patreon_tiers' ) ) ||
empty( $updated_post_ids )
) {
return;
}
// Setup
$tiers = $_REQUEST['fictioneer_patreon_lock_tiers_bulk'] ?? 0;
$amount = $_REQUEST['fictioneer_patreon_lock_amount_bulk'] ?? '';
global $wpdb;
// Patreon tiers
// Setup
$tiers = $shared_post_data['fictioneer_bulk_edit_patreon_lock_tiers'] ?? 'no_change';
$amount = sanitize_text_field( $shared_post_data['fictioneer_bulk_edit_patreon_lock_amount'] ?? '' );
$user_id = get_current_user_id();
// Nothing to do?
if ( $tiers === 'no_change' && $amount === '' ) {
return;
}
// Prepare post author map
$post_author_map = [];
$posts_and_authors = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID, post_author
FROM {$wpdb->posts}
WHERE ID IN (" . implode( ',', array_fill( 0, count( $updated_post_ids ), '%d' ) ) . ")",
$updated_post_ids
), ARRAY_A
);
foreach ( $posts_and_authors as $post ) {
$post_author_map[ (int) $post['ID'] ] = (int) $post['post_author'];
}
// Update Patreon tiers
if ( $tiers !== 'no_change' ) {
// Remove or update...
foreach ( $updated_post_ids as $post_id ) {
if ( ! current_user_can( 'manage_options' ) && $user_id !== $post_author_map[ (int) $post_id ] ) {
continue;
}
if ( $tiers === 'remove' ) {
fictioneer_update_post_meta( $post_id, 'fictioneer_patreon_lock_tiers', 0 );
} else {
} elseif ( is_array( $tiers ) ) {
$allowed_tiers = get_option( 'fictioneer_connection_patreon_tiers' );
$allowed_tiers = is_array( $allowed_tiers ) ? $allowed_tiers : [];
if ( ! empty( $allowed_tiers ) ) {
$tiers = array_intersect( $tiers, array_keys( $allowed_tiers ) );
$tiers = array_map( 'absint', $tiers );
$tiers = array_unique( $tiers );
$tiers = array_map( 'strval', $tiers ); // Safer to match with LIKE in SQL
$tiers = array_unique( $tiers );
fictioneer_update_post_meta( $post_id, 'fictioneer_patreon_lock_tiers', $tiers );
}
}
}
}
// Patreon amount cents
// Update Patreon amount cents
if ( $amount !== '' ) {
foreach ( $updated_post_ids as $post_id ) {
if ( ! current_user_can( 'manage_options' ) && $user_id !== $post_author_map[ (int) $post_id ] ) {
continue;
}
fictioneer_update_post_meta( $post_id, 'fictioneer_patreon_lock_amount', absint( $amount ) );
}
}
}
if (
@ -4417,8 +4460,7 @@ if (
add_action( 'bulk_edit_custom_box', 'fictioneer_add_patreon_bulk_edit_tiers', 10, 2 );
add_action( 'bulk_edit_custom_box', 'fictioneer_add_patreon_bulk_edit_amount', 10, 2 );
add_action( 'save_post', 'fictioneer_bulk_edit_save_patreon' );
add_action( 'bulk_edit_posts', 'fictioneer_save_patreon_bulk_edit', 10, 2 );
}
// =============================================================================
@ -4426,7 +4468,226 @@ if (
// =============================================================================
/**
* Add chapter meta fields to bulk edit
* Saves bulk edit chapter meta.
*
* @since 5.27.2
* @link https://developer.wordpress.org/reference/hooks/bulk_edit_posts/
*
* @param int[] $updated_post_ids An array of updated post IDs.
* @param int[] $shared_post_data Associative array containing the post data.
*/
function fictioneer_save_chapter_bulk_edit( $updated_post_ids, $shared_post_data ) {
// Validate
if (
! wp_verify_nonce( $shared_post_data['fictioneer_bulk_edit_chapters_nonce'] ?? 0, 'fictioneer_bulk_edit_chapters' ) ||
( $shared_post_data['action2'] ?? 0 ) === 'trash' ||
( $shared_post_data['screen'] ?? 0 ) !== 'edit-fcn_chapter' ||
( $shared_post_data['post_type'] ?? 0 ) !== 'fcn_chapter' ||
empty( $updated_post_ids )
) {
return;
}
global $wpdb;
// Setup
$story_id = sanitize_text_field( $shared_post_data['bulk_edit_fictioneer_chapter_story_id'] ?? '' );
$icon = sanitize_text_field( $shared_post_data['bulk_edit_fictioneer_chapter_icon'] ?? '' );
$text_icon = sanitize_text_field( $shared_post_data['bulk_edit_fictioneer_chapter_text_icon'] ?? '' );
$prefix = sanitize_text_field( $shared_post_data['bulk_edit_fictioneer_chapter_prefix'] ?? '' );
$group = sanitize_text_field( $shared_post_data['bulk_edit_fictioneer_chapter_group'] ?? '' );
$user_id = get_current_user_id();
$user_is_editor = current_user_can( 'edit_others_fcn_chapters' ) || current_user_can( 'manage_options' );
// Nothing to do?
if ( $story_id === '' && $icon === '' && $text_icon === '' && $prefix === '' && $group === '' ) {
return;
}
// Prepare post author map
$post_author_map = [];
$posts_and_authors = $wpdb->get_results(
$wpdb->prepare(
"SELECT ID, post_author
FROM {$wpdb->posts}
WHERE ID IN (" . implode( ',', array_fill( 0, count( $updated_post_ids ), '%d' ) ) . ")",
$updated_post_ids
), ARRAY_A
);
foreach ( $posts_and_authors as $post ) {
$post_author_map[ (int) $post['ID'] ] = (int) $post['post_author'];
}
// Update icon
if ( $icon ) {
foreach ( $updated_post_ids as $post_id ) {
if ( ! $user_is_editor && $user_id !== $post_author_map[ (int) $post_id ] ) {
continue;
}
if ( strpos( $icon, 'fa-' ) === 0 && $icon !== FICTIONEER_DEFAULT_CHAPTER_ICON ) {
update_post_meta( $post_id, 'fictioneer_chapter_icon', $icon );
} elseif ( $icon === '_remove' ) {
delete_post_meta( $post_id, 'fictioneer_chapter_icon' );
}
}
}
// Update text icon
if ( $text_icon && get_option( 'fictioneer_enable_advanced_meta_fields' ) ) {
foreach ( $updated_post_ids as $post_id ) {
if ( ! $user_is_editor && $user_id !== $post_author_map[ (int) $post_id ] ) {
continue;
}
if ( $text_icon === '_remove' ) {
fictioneer_update_post_meta( $post_id, 'fictioneer_chapter_text_icon', 0 );
} else {
fictioneer_update_post_meta( $post_id, 'fictioneer_chapter_text_icon', mb_substr( $text_icon, 0, 10, 'UTF-8' ) );
}
}
}
// Update prefix
if ( $prefix && get_option( 'fictioneer_enable_advanced_meta_fields' ) ) {
foreach ( $updated_post_ids as $post_id ) {
if ( ! $user_is_editor && $user_id !== $post_author_map[ (int) $post_id ] ) {
continue;
}
if ( $prefix === '_remove' ) {
fictioneer_update_post_meta( $post_id, 'fictioneer_chapter_prefix', 0 );
} else {
fictioneer_update_post_meta( $post_id, 'fictioneer_chapter_prefix', $prefix );
}
}
}
// Update chapter group
if ( $group ) {
foreach ( $updated_post_ids as $post_id ) {
if ( ! $user_is_editor && $user_id !== $post_author_map[ (int) $post_id ] ) {
continue;
}
if ( $group === '_remove' ) {
fictioneer_update_post_meta( $post_id, 'fictioneer_chapter_group', 0 );
} else {
fictioneer_update_post_meta( $post_id, 'fictioneer_chapter_group', $group );
}
}
}
// Update story ID
if ( $story_id !== '' ) {
$story_id = intval( $story_id );
$story_author_id = (int) get_post_field( 'post_author', $story_id );
if ( $user_is_editor || $story_author_id === $user_id ) {
if ( $story_id < 1 || fictioneer_validate_id( $story_id, 'fcn_story' ) ) {
fictioneer_bulk_edit_chapter_story( $updated_post_ids, $story_id, $post_author_map );
}
}
}
// Clear story meta caches (if not already done)
if ( $story_id === '' ) {
$updated_stories = [];
foreach ( $updated_post_ids as $chapter_id ) {
$current_story_id = intval( fictioneer_get_chapter_story_id( $chapter_id ) );
if ( ! $current_story_id ) {
continue;
}
if ( ! in_array( $current_story_id, $updated_stories ) ) {
wp_update_post( array( 'ID' => $current_story_id, 'post_type' => 'fcn_story' ) ); // Trigger hooks
$updated_stories[] = $current_story_id;
}
}
}
}
add_action( 'bulk_edit_posts', 'fictioneer_save_chapter_bulk_edit', 10, 2 );
/**
* Bulk edit chapter stories.
*
* @since 5.27.2
*
* @param int[] $chapter_ids Array of post IDs.
* @param int $story_id ID of the story or 0 to unset.
* @param int[] $author_map Array of post ID => author ID.
*/
function fictioneer_bulk_edit_chapter_story( $chapter_ids, $story_id, $author_map ) {
// Setup
$user_id = get_current_user_id();
$user_is_editor = current_user_can( 'edit_others_fcn_chapters' ) || current_user_can( 'manage_options' );
$updated_stories = [];
$allowed_chapter_ids = $user_is_editor
? $chapter_ids
: array_filter( $chapter_ids, fn( $post_id ) => $user_id === $author_map[ (int) $post_id ] );
// Unset old story
foreach ( $allowed_chapter_ids as $chapter_id ) {
$current_story_id = intval( fictioneer_get_chapter_story_id( $chapter_id ) );
if ( ! $current_story_id || $current_story_id === $story_id ) {
continue;
}
$other_story_chapters = fictioneer_get_story_chapter_ids( $current_story_id );
$other_story_chapters = array_diff( $other_story_chapters, [ strval( $chapter_id ) ] );
update_post_meta( $current_story_id, 'fictioneer_story_chapters', $other_story_chapters );
if ( $story_id < 1 ) {
delete_post_meta( $chapter_id, 'fictioneer_chapter_story' );
fictioneer_set_chapter_story_parent( $chapter_id, 0 );
}
if ( ! in_array( $current_story_id, $updated_stories ) ) {
update_post_meta( $current_story_id, 'fictioneer_chapters_modified', current_time( 'mysql', 1 ) );
wp_update_post( array( 'ID' => $current_story_id, 'post_type' => 'fcn_story' ) ); // Trigger hooks
$updated_stories[] = $current_story_id;
}
}
// Assign new story
if ( $story_id > 0 ) {
$previous_story_chapter_ids = fictioneer_get_story_chapter_ids( $story_id );
$new_story_chapter_ids = $previous_story_chapter_ids;
foreach ( $allowed_chapter_ids as $chapter_id ) {
if ( ! in_array( $chapter_id, $previous_story_chapter_ids ) ) {
update_post_meta( $chapter_id, 'fictioneer_chapter_story', $story_id );
fictioneer_set_chapter_story_parent( $chapter_id, $story_id );
if ( get_option( 'fictioneer_enable_chapter_appending' ) ) {
$new_story_chapter_ids[] = $chapter_id;
}
}
}
$new_story_chapter_ids = array_unique( $new_story_chapter_ids );
if ( $previous_story_chapter_ids !== $new_story_chapter_ids ) {
update_post_meta( $story_id, 'fictioneer_story_chapters', array_map( 'strval', $new_story_chapter_ids ) );
update_post_meta( $story_id, 'fictioneer_chapters_modified', current_time( 'mysql', 1 ) );
update_post_meta( $story_id, 'fictioneer_chapters_added', current_time( 'mysql', 1 ) );
wp_update_post( array( 'ID' => $story_id, 'post_type' => 'fcn_story' ) ); // Trigger hooks
}
}
}
/**
* Adds chapter meta fields to bulk edit.
*
* @since 5.24.1
*
@ -4435,21 +4696,30 @@ if (
*/
function fictioneer_add_bulk_edit_chapter_meta( $column_name, $post_type ) {
// Check post type
if ( $post_type !== 'fcn_chapter' ) {
return;
}
// Make sure this function is only executed once
static $added = false;
if ( $added ) {
// Abort if...
if ( $post_type !== 'fcn_chapter' || $added ) {
return;
}
$added = true;
// Nonce
wp_nonce_field( 'fictioneer_bulk_edit_chapters', 'fictioneer_bulk_edit_chapters_nonce', false );
// Start HTML ---> ?>
<?php if ( current_user_can( 'edit_others_fcn_chapters' ) || current_user_can( 'manage_options' ) ) : ?>
<fieldset class="inline-edit-col-right">
<div class="inline-edit-chapter-story-wrap">
<label class="inline-edit-chapter-story">
<span class="title"><?php _ex( 'Story', 'Chapter story meta field label.', 'fictioneer' ); ?></span>
<input type="text" name="bulk_edit_fictioneer_chapter_story_id" autocomplete="off" autocorrect="off" placeholder="<?php _e( 'Post ID (0 to remove)', 'fictioneer' ); ?>">
</label>
</div>
</fieldset>
<?php endif; ?>
<p class="bulk-edit-help"><?php _e( 'Use <code>_remove</code> to remove current values.', 'fictioneer' ); ?></p>
<?php if ( ! get_option( 'fictioneer_hide_chapter_icons' ) ) : ?>
@ -4496,72 +4766,6 @@ function fictioneer_add_bulk_edit_chapter_meta( $column_name, $post_type ) {
}
add_action( 'bulk_edit_custom_box', 'fictioneer_add_bulk_edit_chapter_meta', 10, 2 );
/**
* Save chapter bulk edit fields
*
* @since 5.24.1
*
* @param int $post_id ID of the updated post.
*/
function fictioneer_bulk_edit_save_chapter_fields( $post_id ) {
// Abort if...
if (
! wp_verify_nonce( $_REQUEST['_wpnonce'] ?? 0, 'bulk-posts' ) ||
( $_REQUEST['action2'] ?? 0 ) === 'trash' ||
! fictioneer_validate_save_action_user( $post_id, 'fcn_chapter' )
) {
return;
}
// Setup
$icon = sanitize_text_field( $_REQUEST['bulk_edit_fictioneer_chapter_icon'] ?? '' );
$text_icon = sanitize_text_field( $_REQUEST['bulk_edit_fictioneer_chapter_text_icon'] ?? '' );
$prefix = sanitize_text_field( $_REQUEST['bulk_edit_fictioneer_chapter_prefix'] ?? '' );
$group = sanitize_text_field( $_REQUEST['bulk_edit_fictioneer_chapter_group'] ?? '' );
// Update icon
if ( $icon && ! get_option( 'fictioneer_hide_chapter_icons' ) ) {
if ( strpos( $icon, 'fa-' ) === 0 && $icon !== FICTIONEER_DEFAULT_CHAPTER_ICON ) {
fictioneer_update_post_meta( $post_id, 'fictioneer_chapter_icon', $icon );
} elseif ( $icon === '_remove' ) {
fictioneer_update_post_meta( $post_id, 'fictioneer_chapter_icon', 0 );
}
}
// Update text icon
if (
$text_icon &&
! get_option( 'fictioneer_hide_chapter_icons' ) &&
get_option( 'fictioneer_enable_advanced_meta_fields' )
) {
if ( $text_icon === '_remove' ) {
fictioneer_update_post_meta( $post_id, 'fictioneer_chapter_text_icon', 0 );
} else {
fictioneer_update_post_meta( $post_id, 'fictioneer_chapter_text_icon', mb_substr( $text_icon, 0, 10, 'UTF-8' ) );
}
}
// Update prefix
if ( $prefix && get_option( 'fictioneer_enable_advanced_meta_fields' ) ) {
if ( $prefix === '_remove' ) {
fictioneer_update_post_meta( $post_id, 'fictioneer_chapter_prefix', 0 );
} else {
fictioneer_update_post_meta( $post_id, 'fictioneer_chapter_prefix', $prefix );
}
}
// Update group
if ( $group ) {
if ( $group === '_remove' ) {
fictioneer_update_post_meta( $post_id, 'fictioneer_chapter_group', 0 );
} else {
fictioneer_update_post_meta( $post_id, 'fictioneer_chapter_group', $group );
}
}
}
add_action( 'save_post', 'fictioneer_bulk_edit_save_chapter_fields' );
// =============================================================================
// PASSWORD EXPIRATION NOTE IN POST TABLE
// =============================================================================

View File

@ -546,6 +546,8 @@ function fictioneer_bypass_password( $required, $post ) {
( $patreon_user_data['lifetime_support_cents'] ?? -1 ) >= $patreon_check_lifetime_amount_cents
);
$required = apply_filters( 'fictioneer_filter_patreon_tier_unlock', $required, $post, $user, $patreon_post_data );
if ( ! $required ) {
break;
}
@ -1561,7 +1563,7 @@ if ( ! current_user_can( 'manage_options' ) ) {
function fictioneer_prevent_publish_date_update( $data, $postarr ) {
// New post?
if ( $postarr['post_status'] === 'auto-draft' || empty( $postarr['post_date_gmt'] ) ) {
if ( empty( $postarr['ID'] ) || $postarr['post_status'] === 'auto-draft' || empty( $postarr['post_date_gmt'] ) ) {
return $data;
}

View File

@ -167,6 +167,7 @@ function fictioneer_get_default_shortcode_args( $attr, $def_count = -1 ) {
'orderby' => $attr['orderby'] ?? '',
'page' => max( 1, get_query_var( 'page' ) ?: get_query_var( 'paged' ) ),
'posts_per_page' => absint( $attr['per_page'] ?? 0 ) ?: get_option( 'posts_per_page' ),
'post_status' => sanitize_key( $attr['post_status'] ?? 'publish' ),
'post_ids' => fictioneer_explode_list( $attr['post_ids'] ?? '' ),
'author' => sanitize_title( $attr['author'] ?? '' ),
'author_ids' => fictioneer_explode_list( $attr['author_ids'] ?? '' ),
@ -411,6 +412,7 @@ function fictioneer_get_splide_inline_init() {
* collections, recommendations, and stories.
* @param string|null $attr['count'] Optional. Maximum number of items. Default 8.
* @param string|null $attr['author'] Optional. Limit posts to a specific author.
* @param string|null $attr['post_status'] Optional. Choose a valid post status. Default 'publish'.
* @param string|null $attr['order'] Optional. Order direction. Default 'DESC'.
* @param string|null $attr['orderby'] Optional. Order argument. Default 'date'.
* @param string|null $attr['post_ids'] Optional. Limit posts to specific post IDs.
@ -528,6 +530,7 @@ add_shortcode( 'fictioneer_showcase', 'fictioneer_shortcode_showcase' );
* @param string|null $attr['count'] Optional. Maximum number of items. Default 4.
* @param string|null $attr['author'] Optional. Limit posts to a specific author.
* @param string|null $attr['type'] Optional. Choose between 'default', 'simple', and 'compact'.
* @param string|null $attr['post_status'] Optional. Choose a valid post status. Default 'publish'.
* @param string|null $attr['order'] Optional. Order argument. Default 'DESC'.
* @param string|null $attr['orderby'] Optional. Orderby argument. Default 'date'.
* @param string|null $attr['spoiler'] Optional. Whether to show spoiler content.
@ -635,6 +638,7 @@ add_shortcode( 'fictioneer_latest_chapters', 'fictioneer_shortcode_latest_chapte
* @param string|null $attr['count'] Optional. Maximum number of items. Default 4.
* @param string|null $attr['author'] Optional. Limit posts to a specific author.
* @param string|null $attr['type'] Optional. Choose between 'default' and 'compact'.
* @param string|null $attr['post_status'] Optional. Choose a valid post status. Default 'publish'.
* @param string|null $attr['order'] Optional. Order argument. Default 'DESC'.
* @param string|null $attr['orderby'] Optional. Orderby argument. Default 'date'.
* @param string|null $attr['post_ids'] Optional. Limit posts to specific post IDs.
@ -746,6 +750,7 @@ add_shortcode( 'fictioneer_latest_stories', 'fictioneer_shortcode_latest_stories
* @param string|null $attr['count'] Optional. Maximum number of items. Default 4.
* @param string|null $attr['author'] Optional. Limit posts to a specific author.
* @param string|null $attr['type'] Optional. Choose between 'default', 'simple', 'single', and 'compact'.
* @param string|null $attr['post_status'] Optional. Choose a valid post status. Default 'publish'.
* @param string|null $attr['single'] Optional. Whether to show only one chapter item. Default false.
* @param string|null $attr['post_ids'] Optional. Limit posts to specific post IDs.
* @param string|null $attr['ignore_protected'] Optional. Whether to ignore protected posts. Default false.
@ -860,6 +865,7 @@ add_shortcode( 'fictioneer_latest_updates', 'fictioneer_shortcode_latest_story_u
* @param string|null $attr['count'] Optional. Maximum number of items. Default 4.
* @param string|null $attr['author'] Optional. Limit posts to a specific author.
* @param string|null $attr['type'] Optional. Choose between 'default' and 'compact'.
* @param string|null $attr['post_status'] Optional. Choose a valid post status. Default 'publish'.
* @param string|null $attr['order'] Optional. Order argument. Default 'DESC'.
* @param string|null $attr['orderby'] Optional. Orderby argument. Default 'date'.
* @param string|null $attr['post_ids'] Optional. Limit posts to specific post IDs.
@ -952,6 +958,7 @@ add_shortcode( 'fictioneer_latest_recommendations', 'fictioneer_shortcode_latest
*
* @param string|null $attr['count'] Optional. Maximum number of items. Default 1.
* @param string|null $attr['author'] Optional. Limit posts to a specific author.
* @param string|null $attr['post_status'] Optional. Choose a valid post status. Default 'publish'.
* @param string|null $attr['post_ids'] Optional. Limit posts to specific post IDs.
* @param string|null $attr['ignore_protected'] Optional. Whether to ignore protected posts. Default false.
* @param string|null $attr['only_protected'] Optional. Whether to query only protected posts. Default false.
@ -1108,6 +1115,7 @@ function fictioneer_shortcode_chapter_list_empty( $attr ) {
* @since 5.0.0
*
* @param string $attr['story_id'] Either/Or. The ID of the story the chapters belong to.
* @param string|null $attr['post_status'] Optional. Choose a valid post status. Default 'publish'.
* @param string|null $attr['chapter_ids'] Either/Or. Comma-separated list of chapter IDs.
* @param string|null $attr['count'] Optional. Maximum number of items. Default -1 (all).
* @param string|null $attr['offset'] Optional. Skip a number of posts.
@ -1133,6 +1141,7 @@ function fictioneer_shortcode_chapter_list( $attr ) {
$offset = max( 0, intval( $attr['offset'] ?? 0 ) );
$group = empty( $attr['group'] ) ? false : strtolower( trim( $attr['group'] ) );
$heading = empty( $attr['heading'] ) ? false : $attr['heading'];
$post_status = sanitize_key( $attr['post_status'] ?? 'publish' );
$story_id = fictioneer_validate_id( $attr['story_id'] ?? -1, 'fcn_story' );
$prefer_chapter_icon = get_option( 'fictioneer_override_chapter_status_icons' );
$hide_icons = get_option( 'fictioneer_hide_chapter_icons' );
@ -1181,7 +1190,7 @@ function fictioneer_shortcode_chapter_list( $attr ) {
$query_args = array(
'fictioneer_query_name' => 'fictioneer_shortcode_chapter_list',
'post_type' => 'fcn_chapter',
'post_status' => 'publish',
'post_status' => $post_status,
'post__in' => $chapters, // Cannot be empty!
'ignore_sticky_posts' => true,
'orderby' => 'post__in', // Preserve order from meta box
@ -1300,12 +1309,20 @@ function fictioneer_shortcode_chapter_list( $attr ) {
class="chapter-group__list-item-link truncate _1-1 <?php echo $has_password ? '_password' : ''; ?>"
><?php
$title_output = '';
$prefix = apply_filters( 'fictioneer_filter_list_chapter_prefix', $prefix, $chapter_id, 'shortcode' );
if ( ! empty( $prefix ) ) {
// Mind space between prefix and title
echo apply_filters( 'fictioneer_filter_list_chapter_prefix', $prefix ) . ' ';
$title_output .= $prefix . ' ';
}
echo $title;
$title_output .= $title;
echo apply_filters(
'fictioneer_filter_list_chapter_title_row',
$title_output, $chapter_id, $prefix, $has_password, 'shortcode'
);
?></a>
@ -1563,6 +1580,7 @@ add_shortcode( 'fictioneer_search', 'fictioneer_shortcode_search' );
* @param string|null $attr['ignore_protected'] Optional. Whether to ignore protected posts. Default false.
* @param string|null $attr['only_protected'] Optional. Whether to query only protected posts. Default false.
* @param string|null $attr['author'] Optional. Limit posts to a specific author.
* @param string|null $attr['post_status'] Optional. Choose a valid post status. Default 'publish'.
* @param string|null $attr['author_ids'] Optional. Only include posts by these author IDs.
* @param string|null $attr['exclude_author_ids'] Optional. Exclude posts with these author IDs.
* @param string|null $attr['exclude_tag_ids'] Optional. Exclude posts with these tags.
@ -1583,7 +1601,7 @@ function fictioneer_shortcode_blog( $attr ) {
$query_args = array(
'fictioneer_query_name' => 'blog_shortcode',
'post_type' => 'post',
'post_status' => 'publish',
'post_status' => $args['post_status'],
'paged' => $args['page'],
'posts_per_page' => $args['posts_per_page'],
'ignore_sticky_posts' => $args['ignore_sticky']
@ -1707,6 +1725,7 @@ add_shortcode( 'fictioneer_blog', 'fictioneer_shortcode_blog' );
* @since 5.7.3
*
* @param string|null $attr['post_type'] Optional. The post types to query. Default 'post'.
* @param string|null $attr['post_status'] Optional. Choose a valid post status. Default 'publish'.
* @param string|null $attr['post_ids'] Optional. Limit posts to specific post IDs.
* @param string|null $attr['per_page'] Optional. Number of posts per page.
* @param string|null $attr['count'] Optional. Maximum number of posts. Default -1.
@ -2456,3 +2475,121 @@ function fictioneer_shortcode_tooltip( $atts, $content = null ) {
return $html;
}
add_shortcode( 'fcnt', 'fictioneer_shortcode_tooltip' );
// =============================================================================
// TERMS SHORTCODE
// =============================================================================
/**
* Shortcode to show taxonomies.
*
* @since 5.27.2
*
* @param string|null $attr['term_type'] Term type. Default 'post_tag'.
* @param int|null $attr['post_id'] Post ID. Default 0.
* @param int|null $attr['count'] Maximum number of terms. Default -1 (all).
* @param string|null $attr['orderby'] Orderby argument. Default 'count'.
* @param string|null $attr['order'] Order argument. Default 'DESC'.
* @param bool|null $attr['show_empty'] Whether to show empty terms. Default false.
* @param bool|null $attr['show_count'] Whether to show term counts. Default false.
* @param string|null $attr['classes'] Additional section CSS classes. Default empty.
* @param string|null $attr['inner_classes'] Additional term CSS classes. Default empty.
* @param string|null $attr['style'] Inline section CSS style. Default empty.
* @param string|null $attr['inner_style'] Inline term CSS style. Default empty.
* @param string|null $attr['empty'] Override message for empty query result.
*
* @return string The shortcode HTML.
*/
function fictioneer_shortcode_terms( $attr ) {
// Setup
$type = sanitize_key( $attr['type'] ?? 'tag' );
$post_id = absint( $attr['post_id'] ?? 0 );
$count = max( -1, intval( $attr['count'] ?? -1 ) );
$orderby = sanitize_text_field( $attr['orderby'] ?? 'count' );
$order = sanitize_text_field( $attr['order'] ?? 'DESC' );
$show_empty = filter_var( $attr['show_empty'] ?? 0, FILTER_VALIDATE_BOOLEAN );
$show_count = filter_var( $attr['show_count'] ?? 0, FILTER_VALIDATE_BOOLEAN );
$classes = esc_attr( wp_strip_all_tags( $attr['classes'] ?? $attr['class'] ?? '' ) );
$inner_classes = esc_attr( wp_strip_all_tags( $attr['inner_classes'] ?? $attr['inner_class'] ?? '' ) );
$style = esc_attr( wp_strip_all_tags( $attr['style'] ?? '' ) );
$inner_style = esc_attr( wp_strip_all_tags( $attr['inner_style'] ?? '' ) );
$empty = sanitize_text_field( $attr['empty'] ?? __( 'No taxonomies specified yet.', 'fictioneer' ) );
$term_map = array(
'tag' => 'post_tag',
'genre' => 'fcn_genre',
'fandom' => 'fcn_fandom',
'character' => 'fcn_character',
'warning' => 'fcn_content_warning'
);
$term_type = $term_map[ $type ] ?? $type;
// Term exists?
if ( ! taxonomy_exists( $term_type ) ) {
return 'Error: Taxonomy does not exist.';
}
// Post exists?
if ( $post_id && ! get_post( $post_id ) ) {
return 'Error: Post not found.';
}
// Prepare query args
$args = array(
'fictioneer_query_name' => 'terms_shortcode',
'taxonomy' => $term_type,
'orderby' => $orderby,
'order' => $order,
'hide_empty' => ! $show_empty
);
if ( $count > 0 ) {
$args['number'] = $count;
}
// Apply filters
$args = apply_filters( 'fictioneer_filter_shortcode_terms_query_args', $args, $attr );
// Query terms
if ( $post_id ) {
$terms = wp_get_post_terms( $post_id, $term_type, $args );
} else {
$terms = get_terms( $args );
}
// All good?
if ( is_wp_error( $terms ) ) {
return 'Error: ' . $terms->get_error_message();
}
// Build and return HTML
$html = '';
if ( ! empty( $terms ) ) {
foreach ( $terms as $term ) {
$html .= sprintf(
'<a href="%s" class="tag-pill _taxonomy-%s _taxonomy-slug-%s %s" style="%s">%s</a>',
get_tag_link( $term ),
str_replace( 'fcn_', '', $term->taxonomy ),
$term->slug,
'tag-pill ' . $inner_classes,
$inner_style,
$show_count
? sprintf( _x( '%1$s (%2$s)', 'Terms shortcode with count.', 'fictioneer' ), $term->name, $term->count )
: $term->name
);
}
} else {
$html = $empty;
}
return sprintf(
'<section class="terms-shortcode tag-group %s" style="%s">%s</section>',
"_{$term_type} {$classes} " . wp_unique_id( 'shortcode-id-' ),
$style,
$html
);
}
add_shortcode( 'fictioneer_terms', 'fictioneer_shortcode_terms' );

View File

@ -1814,7 +1814,7 @@ function fictioneer_output_head_anti_flicker() {
<?php // <--- End HTML
wp_print_inline_script_tag(
'document.addEventListener("readystatechange", () => {if (document.readyState === "interactive") document.body.style.visibility = "visible";});console.log("foo");',
'document.addEventListener("readystatechange", () => {if (document.readyState === "interactive") document.body.style.visibility = "visible";});',
array(
'id' => 'fictioneer-anti-flicker',
'type' => 'text/javascript',
@ -2598,8 +2598,15 @@ add_action( 'wp_logout', 'fictioneer_remove_logged_in_cookie' );
*/
function fictioneer_fix_logged_in_cookie() {
if ( ! get_current_user_id() && isset( $_COOKIE['fcnLoggedIn'] ) ) {
$user_id = get_current_user_id();
if ( ! $user_id && isset( $_COOKIE['fcnLoggedIn'] ) ) {
fictioneer_remove_logged_in_cookie();
}
if ( $user_id && ! isset( $_COOKIE['fcnLoggedIn'] ) ) {
// Unknown how long WP login cookie still lasts, so just take an hour
fictioneer_set_logged_in_cookie( null, time() + HOUR_IN_SECONDS, null, $user_id );
}
}
add_action( 'init', 'fictioneer_fix_logged_in_cookie' );

View File

@ -1684,7 +1684,7 @@ function fictioneer_replace_br_with_whitespace( $text ) {
// =============================================================================
/**
* Add noindex and nofollow headers to certain requests
* Adds noindex and nofollow headers to certain requests
*
* @since 5.23.1
*/
@ -1700,3 +1700,53 @@ function fictioneer_block_pages_from_indexing() {
}
}
add_action( 'send_headers', 'fictioneer_block_pages_from_indexing' );
// =============================================================================
// REDIRECT SCHEDULED CHAPTER 404
// =============================================================================
/**
* Redirects scheduled chapter 404 to story or home
*
* @since 5.27.2
*/
function fictioneer_redirect_scheduled_chapter_404() {
if ( current_user_can( 'manage_options' ) || current_user_can( 'edit_others_fcn_chapters' ) ) {
return; // Default behavior
}
global $wp_query, $post;
if ( isset( $wp_query->query_vars['p'] ) && ( $wp_query->query_vars['post_type'] ?? 0 ) === 'fcn_chapter' ) {
$post_id = absint( $wp_query->query_vars['p'] );
$user_id = get_current_user_id();
$author_id = (int) get_post_field( 'post_author', $post_id );
if ( $user_id && $user_id === $author_id ) {
return; // Default behavior
}
if ( ! $post ) {
$post_status = get_post_status( $post_id );
} else {
$post_status = $post->post_status;
}
if ( $post_status === 'future' ) {
$story_id = fictioneer_get_chapter_story_id( $post_id );
if ( $story_id ) {
wp_safe_redirect( get_permalink( $story_id ), 307 );
} else {
wp_safe_redirect( home_url(), 307 );
}
exit;
}
}
}
if ( get_option( 'fictioneer_redirect_scheduled_chapter_404' ) ) {
add_action( 'template_redirect', 'fictioneer_redirect_scheduled_chapter_404' );
}

View File

@ -430,7 +430,8 @@ if ( ! function_exists( 'fictioneer_get_story_data' ) ) {
$meta_cache['comment_count_timestamp'] = time();
// Update post database comment count
fictioneer_sql_update_comment_count( $story_id, $comment_count );
$story_comment_count = get_approved_comments( $story_id, array( 'count' => true ) ) ?: 0;
fictioneer_sql_update_comment_count( $story_id, $comment_count + $story_comment_count );
// Update meta cache and purge
update_post_meta( $story_id, 'fictioneer_story_data_collection', $meta_cache );
@ -583,7 +584,8 @@ if ( ! function_exists( 'fictioneer_get_story_data' ) ) {
update_post_meta( $story_id, 'fictioneer_story_total_word_count', $word_count );
// Update post database comment count
fictioneer_sql_update_comment_count( $story_id, $comment_count );
$story_comment_count = get_approved_comments( $story_id, array( 'count' => true ) ) ?: 0;
fictioneer_sql_update_comment_count( $story_id, $comment_count + $story_comment_count );
// Done
return $result;

View File

@ -21,6 +21,33 @@ if ( ! function_exists( 'fictioneer_comment_form' ) ) {
}
}
/**
* Some cache plugins need the controller added after loading
*
* @since 5.27.1
*/
function fictioneer_fix_comment_form_stimulus_controller() {
wp_print_inline_script_tag(
'(()=>{let e=document.getElementById("commentform");if(e){let t="fictioneer-comment-form",l=e.dataset.controller||"";l.split(" ").includes(t)||(e.dataset.controller=l+(l?" ":"")+t)}})();',
array(
'id' => 'fictioneer-fix-comment-form-controller-scripts',
'type' => 'text/javascript',
'data-jetpack-boost' => 'ignore',
'data-no-optimize' => '1',
'data-no-defer' => '1',
'data-no-minify' => '1'
)
);
}
if (
! get_option( 'fictioneer_enable_ajax_comment_form' ) &&
! get_option( 'fictioneer_enable_ajax_comments' )
) {
add_action( 'comment_form_top', 'fictioneer_fix_comment_form_stimulus_controller' );
}
// =============================================================================
// COMMENT TOOLBAR
// =============================================================================

View File

@ -375,14 +375,16 @@ add_filter( 'wp_update_comment_data', 'fictioneer_track_comment_edit', 10, 2 );
// RENDER COMMENT MODERATION MENU
// =============================================================================
function fictioneer_comment_moderation_template() {
// Abort if...
if ( ! get_option( 'fictioneer_enable_ajax_comment_moderation' ) ) {
return;
}
/**
* Renders the comment moderation menu template
*
* @since 4.27.0
*/
function fictioneer_comment_moderation_template() {
// Start HTML ---> ?>
<template id="template-comment-frontend-moderation-menu">
<?php if ( get_option( 'fictioneer_enable_ajax_comment_moderation' ) ) : ?>
<button data-action="click->fictioneer-comment#trash"><?php _e( 'Trash', 'fictioneer' ); ?></button>
<button data-action="click->fictioneer-comment#spam"><?php _e( 'Spam', 'fictioneer' ); ?></button>
<button data-action="click->fictioneer-comment#offensive"><?php _e( 'Offensive', 'fictioneer' ); ?></button>
@ -396,6 +398,7 @@ function fictioneer_comment_moderation_template() {
<button data-action="click->fictioneer-comment#sticky"><?php _e( 'Sticky', 'fictioneer' ); ?></button>
<button data-action="click->fictioneer-comment#unsticky"><?php _e( 'Unsticky', 'fictioneer' ); ?></button>
<?php endif; ?>
<?php endif; ?>
<a class="comment-edit-link" data-fictioneer-comment-target="editLink"><?php _e( 'Edit' ); ?></a>

View File

@ -772,8 +772,11 @@ function fictioneer_chapter_suggestion_tools() {
return;
}
// Setup
$hide_if_logged_out = get_option( 'comment_registration' ) ? 'hide-if-logged-out' : ''; // Safer for cached site
// Start HTML ---> ?>
<div id="selection-tools" class="invisible suggestion-tools" data-nosnippet>
<div id="selection-tools" class="invisible suggestion-tools <?php echo $hide_if_logged_out; ?>" data-nosnippet>
<button id="button-add-suggestion" type="button" class="button button--suggestion">
<i class="fa-solid fa-highlighter"></i>
<span><?php _e( 'Add Suggestion', 'fictioneer' ); ?></span>

View File

@ -632,30 +632,42 @@ function fictioneer_story_chapters( $args ) {
class="chapter-group__list-item-link truncate _1-1 <?php echo $chapter['password'] ? '_password' : ''; ?>"
><?php
$title_output = '';
// Non-published chapter prefixes
if ( in_array( $chapter['status'], ['future', 'trash', 'private'] ) ) {
$status_prefix = fcntr( "{$chapter['status']}_prefix" );
if ( $status_prefix ) {
echo '<span class="chapter-group__list-item-status">' . $status_prefix . '</span> '; // Mind the space
$title_output .= '<span class="chapter-group__list-item-status">' . $status_prefix . '</span> '; // Mind the space
}
}
$chapter['prefix'] = apply_filters(
'fictioneer_filter_list_chapter_prefix',
$chapter['prefix'], $chapter['id'], 'story'
);
if ( ! empty( $chapter['prefix'] ) ) {
// Mind space between prefix and title
echo apply_filters( 'fictioneer_filter_list_chapter_prefix', $chapter['prefix'] ) . ' ';
$title_output .= $chapter['prefix'] . ' ';
}
if ( ! empty( $chapter['list_title'] ) && $chapter['title'] !== $chapter['list_title'] ) {
printf(
$title_output .= sprintf(
' <span class="chapter-group__list-item-title list-view">%s</span><span class="grid-view">%s</span>',
$chapter['title'],
wp_strip_all_tags( $chapter['list_title'] )
);
} else {
echo $chapter['title'];
$title_output .= $chapter['title'];
}
echo apply_filters(
'fictioneer_filter_list_chapter_title_row',
$title_output, $chapter['id'], $chapter['prefix'], $chapter['password'], 'story'
);
?></a>
<?php if ( $chapter['password'] ) : ?>

View File

@ -749,6 +749,12 @@ define( 'FICTIONEER_OPTIONS', array(
'group' => 'fictioneer-settings-general-group',
'sanitize_callback' => 'fictioneer_sanitize_checkbox',
'default' => 0
),
'fictioneer_redirect_scheduled_chapter_404' => array(
'name' => 'fictioneer_redirect_scheduled_chapter_404',
'group' => 'fictioneer-settings-general-group',
'sanitize_callback' => 'fictioneer_sanitize_checkbox',
'default' => 0
)
),
'integers' => array(
@ -1225,6 +1231,7 @@ function fictioneer_get_option_label( $option ) {
'fictioneer_enable_css_skins' => __( 'Enable CSS skins (requires account)', 'fictioneer' ),
'fictioneer_exclude_non_stories_from_cloud_counts' => __( 'Only count stories in taxonomy clouds', 'fictioneer' ),
'fictioneer_enable_ffcnr_auth' => __( 'Enable FFCNR user authentication', 'fictioneer' ),
'fictioneer_redirect_scheduled_chapter_404' => __( 'Redirect scheduled chapters to story or home', 'fictioneer' ),
);
}

View File

@ -313,6 +313,16 @@ $images = get_template_directory_uri() . '/img/documentation/';
?>
</div>
<div class="fictioneer-card__row">
<?php
fictioneer_settings_label_checkbox(
'fictioneer_redirect_scheduled_chapter_404',
__( 'Redirect scheduled chapters to story or home', 'fictioneer' ),
__( 'Without user permission, they show a 404 error page.', 'fictioneer' )
);
?>
</div>
<div class="fictioneer-card__row">
<?php
fictioneer_settings_label_checkbox(
@ -1156,7 +1166,7 @@ $images = get_template_directory_uri() . '/img/documentation/';
fictioneer_settings_label_checkbox(
'fictioneer_disable_all_widgets',
__( 'Disable all widgets', 'fictioneer' ),
__( 'The theme does not use widgets by default and removing them slightly boosts performance.', 'fictioneer' )
__( 'Required for sidebar. If not used, removing widgets slightly boosts performance.', 'fictioneer' )
);
?>
</div>

File diff suppressed because one or more lines are too long

2
js/chapter.min.js vendored

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
function fcn_toggleCheckmark(e,t=null,a=null){const r=window.FictioneerApp.Controllers.fictioneerCheckmarks;if(!r)return void fcn_showNotification("Error: Checkmarks Controller not connected.",3,"warning");const s=FcnUtils.userData();let i="story";if((e=parseInt(e??0))<1)return void fcn_showNotification("Error: Invalid story ID.",3,"warning");if(null!==t){if((t=parseInt(t))<1)return void fcn_showNotification("Error: Invalid chapter ID.",3,"warning");i="chapter"}s.checkmarks.data[e]||(s.checkmarks.data[e]=[]),null===a&&(a="chapter"===i?!s.checkmarks.data[e]?.includes(t):!s.checkmarks.data[e]?.includes(e));const c="chapter"===i?t:e;a?s.checkmarks.data[e].push(c):(FcnUtils.removeArrayItemOnce(s.checkmarks.data[e],c),"chapter"===i&&FcnUtils.removeArrayItemOnce(s.checkmarks.data[e],e)),s.checkmarks.data[e]=s.checkmarks.data[e].filter(((e,t,a)=>a.indexOf(e)==t)),s.lastLoaded=0,FcnUtils.setUserData(s),r.refreshView(),clearTimeout(r.timeout),r.timeout=setTimeout((()=>{FcnUtils.remoteAction("fictioneer_ajax_set_checkmark",{payload:{update:s.checkmarks.data[e].join(" "),story_id:e}})}),FcnGlobals.debounceRate)}application.register("fictioneer-checkmarks",class extends Stimulus.Controller{static get targets(){return["chapterCheck","storyCheck","ribbon"]}timeout=0;initialize(){fcn()?.userReady?this.#e=!0:document.addEventListener("fcnUserDataReady",(()=>{this.refreshView(),this.#e=!0,this.#t()}))}connect(){window.FictioneerApp.Controllers.fictioneerCheckmarks=this,this.#e&&(this.refreshView(),this.#t())}data(){return this.checkmarksCachedData=FcnUtils.userData().checkmarks?.data,Array.isArray(this.checkmarksCachedData)&&0===this.checkmarksCachedData.length&&(this.checkmarksCachedData={}),this.checkmarksCachedData}toggleChapter({params:{chapter:e,story:t}}){fcn_toggleCheckmark(t,e)}toggleStory({params:{story:e}}){fcn_toggleCheckmark(e)}clear(){const e=FcnUtils.userData();e.checkmarks={data:{},updated:Date.now()},fcn().setUserData(e),this.refreshView()}refreshView(){const e=this.data();!e||Object.keys(e).length<1?this.uncheckAll():Object.entries(e).forEach((([e,t])=>{e=parseInt(e);const a=t?.includes(e);this.hasChapterCheckTarget&&(a?this.chapterCheckTargets.forEach((e=>{e.classList.toggle("marked",!0),e.setAttribute("aria-checked",!0)})):this.chapterCheckTargets.forEach((a=>{const r=parseInt(a.dataset.fictioneerCheckmarksChapterParam);if(parseInt(a.dataset.fictioneerCheckmarksStoryParam)===e){const e=t?.includes(r);a.classList.toggle("marked",e),a.setAttribute("aria-checked",e)}}))),this.hasStoryCheckTarget&&(this.storyCheckTarget.classList.toggle("marked",a),this.storyCheckTarget.setAttribute("aria-checked",a)),this.hasRibbonTarget&&this.ribbonTarget.classList.toggle("hidden",!a)}))}uncheckAll(){this.hasChapterCheckTarget&&this.chapterCheckTargets.forEach((e=>{e.classList.toggle("marked",!1),e.setAttribute("aria-checked",!1)})),this.hasStoryCheckTarget&&(this.storyCheckTarget.classList.toggle("marked",!1),this.storyCheckTarget.setAttribute("aria-checked",!1))}#e=!1;#a=!1;#r(){const e=FcnUtils.loggedIn();return e||(this.#s(),this.#a=!0),e}#i(){return this.#r()&&JSON.stringify(this.checkmarksCachedData??0)!==JSON.stringify(this.data())}#c(){this.refreshInterval||(this.refreshInterval=setInterval((()=>{!this.#a&&this.#i()&&this.refreshView()}),3e4+1e3*Math.random()))}#t(){this.#c(),this.visibilityStateCheck=()=>{this.#r()&&("visible"===document.visibilityState?(this.#a=!1,this.refreshView(),this.#c()):(this.#a=!0,clearInterval(this.refreshInterval),this.refreshInterval=null))},document.addEventListener("visibilitychange",this.visibilityStateCheck)}#s(){clearInterval(this.refreshInterval),document.removeEventListener("visibilitychange",this.visibilityStateCheck)}});
function fcn_toggleCheckmark(e,t=null,a=null){const r=window.FictioneerApp.Controllers.fictioneerCheckmarks;if(!r)return void fcn_showNotification("Error: Checkmarks Controller not connected.",3,"warning");const s=FcnUtils.userData();let i="story";if((e=parseInt(e??0))<1)return void fcn_showNotification("Error: Invalid story ID.",3,"warning");if(null!==t){if((t=parseInt(t))<1)return void fcn_showNotification("Error: Invalid chapter ID.",3,"warning");i="chapter"}("object"!=typeof s.checkmarks.data||null===s.checkmarks.data||Array.isArray(s.checkmarks.data))&&(s.checkmarks.data={}),s.checkmarks.data[e]||(s.checkmarks.data[e]=[]),null===a&&(a="chapter"===i?!s.checkmarks.data[e]?.includes(t):!s.checkmarks.data[e]?.includes(e));const c="chapter"===i?t:e;a?s.checkmarks.data[e].push(c):(FcnUtils.removeArrayItemOnce(s.checkmarks.data[e],c),"chapter"===i&&FcnUtils.removeArrayItemOnce(s.checkmarks.data[e],e)),s.checkmarks.data[e]=s.checkmarks.data[e].filter(((e,t,a)=>a.indexOf(e)==t)),s.lastLoaded=0,FcnUtils.setUserData(s),r.refreshView(),clearTimeout(r.timeout),r.timeout=setTimeout((()=>{FcnUtils.remoteAction("fictioneer_ajax_set_checkmark",{payload:{update:s.checkmarks.data[e].join(" "),story_id:e}})}),FcnGlobals.debounceRate)}application.register("fictioneer-checkmarks",class extends Stimulus.Controller{static get targets(){return["chapterCheck","storyCheck","ribbon"]}timeout=0;initialize(){fcn()?.userReady?this.#e=!0:document.addEventListener("fcnUserDataReady",(()=>{this.refreshView(),this.#e=!0,this.#t()}))}connect(){window.FictioneerApp.Controllers.fictioneerCheckmarks=this,this.#e&&(this.refreshView(),this.#t())}data(){return this.checkmarksCachedData=FcnUtils.userData().checkmarks?.data,Array.isArray(this.checkmarksCachedData)&&0===this.checkmarksCachedData.length&&(this.checkmarksCachedData={}),this.checkmarksCachedData}toggleChapter({params:{chapter:e,story:t}}){fcn_toggleCheckmark(t,e)}toggleStory({params:{story:e}}){fcn_toggleCheckmark(e)}clear(){const e=FcnUtils.userData();e.checkmarks={data:{},updated:Date.now()},fcn().setUserData(e),this.refreshView()}refreshView(){const e=this.data();if(!e||Object.keys(e).length<1)return void this.uncheckAll();let t=!1;Object.entries(e).forEach((([e,a])=>{e=parseInt(e),this.hasStoryCheckTarget&&e==this.storyCheckTarget.dataset.fictioneerCheckmarksStoryParam&&(t=a?.includes(e)),this.hasChapterCheckTarget&&(t?this.chapterCheckTargets.forEach((e=>{e.classList.toggle("marked",!0),e.setAttribute("aria-checked",!0)})):this.chapterCheckTargets.forEach((t=>{const r=parseInt(t.dataset.fictioneerCheckmarksChapterParam);if(parseInt(t.dataset.fictioneerCheckmarksStoryParam)===e){const e=a?.includes(r);t.classList.toggle("marked",e),t.setAttribute("aria-checked",e)}}))),this.hasStoryCheckTarget&&(this.storyCheckTarget.classList.toggle("marked",t),this.storyCheckTarget.setAttribute("aria-checked",t)),this.hasRibbonTarget&&this.ribbonTarget.classList.toggle("hidden",!t)}))}uncheckAll(){this.hasChapterCheckTarget&&this.chapterCheckTargets.forEach((e=>{e.classList.toggle("marked",!1),e.setAttribute("aria-checked",!1)})),this.hasStoryCheckTarget&&(this.storyCheckTarget.classList.toggle("marked",!1),this.storyCheckTarget.setAttribute("aria-checked",!1))}#e=!1;#a=!1;#r(){const e=FcnUtils.loggedIn();return e||(this.#s(),this.#a=!0),e}#i(){return this.#r()&&JSON.stringify(this.checkmarksCachedData??0)!==JSON.stringify(this.data())}#c(){this.refreshInterval||(this.refreshInterval=setInterval((()=>{!this.#a&&this.#i()&&this.refreshView()}),3e4+1e3*Math.random()))}#t(){this.#c(),this.visibilityStateCheck=()=>{this.#r()&&("visible"===document.visibilityState?(this.#a=!1,this.refreshView(),this.#c()):(this.#a=!0,clearInterval(this.refreshInterval),this.refreshInterval=null))},document.addEventListener("visibilitychange",this.visibilityStateCheck)}#s(){clearInterval(this.refreshInterval),document.removeEventListener("visibilitychange",this.visibilityStateCheck)}});

10
js/complete.min.js vendored

File diff suppressed because one or more lines are too long

2
js/follows.min.js vendored
View File

@ -1 +1 @@
function fcn_toggleFollow(t,e=null){const s=window.FictioneerApp.Controllers.fictioneerFollows;if(!s)return void fcn_showNotification("Error: Follows Controller not connected.",3,"warning");const a=FcnUtils.userData();(e=e??!a.follows.data[t])?a.follows.data[t]={story_id:parseInt(t),timestamp:Date.now()}:delete a.follows.data[t],a.lastLoaded=0,FcnUtils.setUserData(a),s.refreshView(),clearTimeout(s.timeout),s.timeout=setTimeout((()=>{FcnUtils.remoteAction("fictioneer_ajax_toggle_follow",{payload:{set:e,story_id:t}})}),FcnGlobals.debounceRate)}application.register("fictioneer-follows",class extends Stimulus.Controller{static get targets(){return["toggleButton","newDisplay","scrollList","mobileScrollList","mobileMarkRead"]}followsLoaded=!1;markedRead=!1;timeout=0;initialize(){fcn()?.userReady?this.#t=!0:document.addEventListener("fcnUserDataReady",(()=>{this.refreshView(),this.#t=!0,this.#e()}))}connect(){window.FictioneerApp.Controllers.fictioneerFollows=this,this.#t&&(this.refreshView(),this.#e())}data(){return this.followsCachedData=FcnUtils.userData().follows?.data,Array.isArray(this.followsCachedData)&&0===this.followsCachedData.length&&(this.followsCachedData={}),this.followsCachedData}isFollowed(t){return!(!FcnUtils.loggedIn()||!this.data()?.[t])}unreadCount(){return parseInt(FcnUtils.userData()?.follows?.new??0)}toggleFollow({params:{id:t}}){this.#s()&&(fcn_toggleFollow(t,!this.isFollowed(t)),this.refreshView())}clear(){const t=FcnUtils.userData();t.follows={data:{}},fcn().setUserData(t),this.refreshView()}refreshView(){this.toggleButtonTargets.forEach((t=>{const e=t.dataset.storyId;t.classList.toggle("_followed",this.isFollowed(e))}));const t=this.unreadCount();this.newDisplayTargets.forEach((e=>{e.classList.toggle("_new",t>0),t>0&&(e.dataset.newCount=t,this.hasMobileMarkReadTarget&&this.mobileMarkReadTarget.classList.remove("hidden"))}))}loadFollowsHtml(){this.followsLoaded||FcnUtils.aGet({action:"fictioneer_ajax_get_follows_notifications",fcn_fast_ajax:1}).then((t=>{t.data.html&&(this.hasScrollListTarget&&(this.scrollListTarget.innerHTML=t.data.html),this.hasMobileScrollListTarget&&(this.mobileScrollListTarget.innerHTML=t.data.html),!1===FcnUtils.userData().loggedIn&&(fcn().removeUserData(),fcn().fetchUserData()))})).catch((t=>{429===t.status?fcn_showNotification(fictioneer_tl.notification.slowDown,3,"warning"):t.status&&t.statusText&&fcn_showNotification(`${t.status}: ${t.statusText}`,5,"warning"),this.hasScrollListTarget&&this.scrollListTarget.remove(),this.hasMobileScrollListTarget&&this.mobileScrollListTarget.remove()})).then((()=>{this.followsLoaded=!0,this.hasNewDisplayTarget&&this.newDisplayTargets.forEach((t=>{t.classList.add("_loaded")}))}))}markRead(){if(!this.followsLoaded&&this.unreadCount()>0||this.markedRead)return;this.markedRead=!0,this.newDisplayTargets.forEach((t=>{t.classList.remove("_new")})),this.hasMobileMarkReadTarget&&this.mobileMarkReadTarget.classList.add("hidden");const t=FcnUtils.userData();t.new=0,t.lastLoaded=0,FcnUtils.setUserData(t),FcnUtils.aPost({action:"fictioneer_ajax_mark_follows_read",fcn_fast_ajax:1}).catch((t=>{t.status&&t.statusText&&fcn_showNotification(`${t.status}: ${t.statusText}`,5,"warning")}))}#t=!1;#a=!1;#s(){const t=FcnUtils.loggedIn();return t||(this.#i(),this.#a=!0),t}#o(){return this.#s()&&JSON.stringify(this.followsCachedData??0)!==JSON.stringify(this.data())}#l(){this.refreshInterval||(this.refreshInterval=setInterval((()=>{!this.#a&&this.#o()&&this.refreshView()}),3e4+1e3*Math.random()))}#e(){this.#l(),this.visibilityStateCheck=()=>{this.#s()&&("visible"===document.visibilityState?(this.#a=!1,this.refreshView(),this.#l()):(this.#a=!0,clearInterval(this.refreshInterval),this.refreshInterval=null))},document.addEventListener("visibilitychange",this.visibilityStateCheck)}#i(){clearInterval(this.refreshInterval),document.removeEventListener("visibilitychange",this.visibilityStateCheck)}});
function fcn_toggleFollow(t,e=null){const s=window.FictioneerApp.Controllers.fictioneerFollows;if(!s)return void fcn_showNotification("Error: Follows Controller not connected.",3,"warning");const a=FcnUtils.userData();("object"!=typeof a.follows.data||null===a.follows.data||Array.isArray(a.follows.data))&&(a.follows.data={}),(e=e??!a.follows.data[t])?a.follows.data[t]={story_id:parseInt(t),timestamp:Date.now()}:delete a.follows.data[t],a.lastLoaded=0,FcnUtils.setUserData(a),s.refreshView(),clearTimeout(s.timeout),s.timeout=setTimeout((()=>{FcnUtils.remoteAction("fictioneer_ajax_toggle_follow",{payload:{set:e,story_id:t}})}),FcnGlobals.debounceRate)}application.register("fictioneer-follows",class extends Stimulus.Controller{static get targets(){return["toggleButton","newDisplay","scrollList","mobileScrollList","mobileMarkRead"]}followsLoaded=!1;markedRead=!1;timeout=0;initialize(){fcn()?.userReady?this.#t=!0:document.addEventListener("fcnUserDataReady",(()=>{this.refreshView(),this.#t=!0,this.#e()}))}connect(){window.FictioneerApp.Controllers.fictioneerFollows=this,this.#t&&(this.refreshView(),this.#e())}data(){return this.followsCachedData=FcnUtils.userData().follows?.data,Array.isArray(this.followsCachedData)&&0===this.followsCachedData.length&&(this.followsCachedData={}),this.followsCachedData}isFollowed(t){return!(!FcnUtils.loggedIn()||!this.data()?.[t])}unreadCount(){return parseInt(FcnUtils.userData()?.follows?.new??0)}toggleFollow({params:{id:t}}){this.#s()&&(fcn_toggleFollow(t,!this.isFollowed(t)),this.refreshView())}clear(){const t=FcnUtils.userData();t.follows={data:{}},fcn().setUserData(t),this.refreshView()}refreshView(){this.toggleButtonTargets.forEach((t=>{const e=t.dataset.storyId;t.classList.toggle("_followed",this.isFollowed(e))}));const t=this.unreadCount();this.newDisplayTargets.forEach((e=>{e.classList.toggle("_new",t>0),t>0&&(e.dataset.newCount=t,this.hasMobileMarkReadTarget&&this.mobileMarkReadTarget.classList.remove("hidden"))}))}loadFollowsHtml(){this.followsLoaded||FcnUtils.aGet({action:"fictioneer_ajax_get_follows_notifications",fcn_fast_ajax:1}).then((t=>{t.data.html&&(this.hasScrollListTarget&&(this.scrollListTarget.innerHTML=t.data.html),this.hasMobileScrollListTarget&&(this.mobileScrollListTarget.innerHTML=t.data.html),!1===FcnUtils.userData().loggedIn&&(fcn().removeUserData(),fcn().fetchUserData()))})).catch((t=>{429===t.status?fcn_showNotification(fictioneer_tl.notification.slowDown,3,"warning"):t.status&&t.statusText&&fcn_showNotification(`${t.status}: ${t.statusText}`,5,"warning"),this.hasScrollListTarget&&this.scrollListTarget.remove(),this.hasMobileScrollListTarget&&this.mobileScrollListTarget.remove()})).then((()=>{this.followsLoaded=!0,this.hasNewDisplayTarget&&this.newDisplayTargets.forEach((t=>{t.classList.add("_loaded")}))}))}markRead(){if(!this.followsLoaded&&this.unreadCount()>0||this.markedRead)return;this.markedRead=!0,this.newDisplayTargets.forEach((t=>{t.classList.remove("_new")})),_$$(".follow-item").forEach((t=>{t.classList.remove("_new")})),this.hasMobileMarkReadTarget&&this.mobileMarkReadTarget.classList.add("hidden");const t=FcnUtils.userData();t.new=0,t.lastLoaded=0,FcnUtils.setUserData(t),FcnUtils.aPost({action:"fictioneer_ajax_mark_follows_read",fcn_fast_ajax:1}).catch((t=>{t.status&&t.statusText&&fcn_showNotification(`${t.status}: ${t.statusText}`,5,"warning")}))}#t=!1;#a=!1;#s(){const t=FcnUtils.loggedIn();return t||(this.#i(),this.#a=!0),t}#o(){return this.#s()&&JSON.stringify(this.followsCachedData??0)!==JSON.stringify(this.data())}#l(){this.refreshInterval||(this.refreshInterval=setInterval((()=>{!this.#a&&this.#o()&&this.refreshView()}),3e4+1e3*Math.random()))}#e(){this.#l(),this.visibilityStateCheck=()=>{this.#s()&&("visible"===document.visibilityState?(this.#a=!1,this.refreshView(),this.#l()):(this.#a=!0,clearInterval(this.refreshInterval),this.refreshInterval=null))},document.addEventListener("visibilitychange",this.visibilityStateCheck)}#i(){clearInterval(this.refreshInterval),document.removeEventListener("visibilitychange",this.visibilityStateCheck)}});

View File

@ -1 +1 @@
application.register("fictioneer-mobile-menu",class extends Stimulus.Controller{static get targets(){return["frame","bookmarks","panelBookmarks"]}advanced=!!_$(".mobile-menu._advanced-mobile-menu");open=!1;editingBookmarks=!1;currentFrame=null;bookmarkTemplate=_$$$("mobile-bookmark-template");chapterId=_$("article")?.id;connect(){window.FictioneerApp.Controllers.fictioneerMobileMenu=this}toggle(e=null){this.open=e??!this.open,this.advanced?this.#e(this.open):this.#o(this.open)}clickOutside({detail:{target:e}}){this.open&&"site"===e?.id&&this.toggle(!1)}openFrame({params:{frame:e}}){const o=`mobile-frame-${e}`;this.editingBookmarks=!1,this.panelBookmarksTarget.dataset.editing=!1,this.hasFrameTarget&&this.frameTargets.forEach((e=>{e.classList.toggle("_active",e.id===o),e.id===o&&(this.currentFrame=e)}))}back(){this.openFrame({params:{frame:"main"}})}setMobileBookmarks(){const e=window.FictioneerApp.Controllers.fictioneerBookmarks;if(e&&this.hasBookmarksTarget){const o=Object.entries(e.data());if(this.bookmarksTarget.innerHTML="",!o||o.length<1){const e=document.createElement("li");return e.classList.add("no-bookmarks"),e.textContent=this.bookmarksTarget.dataset.empty,void this.bookmarksTarget.appendChild(e)}const t=document.createDocumentFragment();o.forEach((([e,{color:o,progress:r,link:s,chapter:a,"paragraph-id":i}])=>{const n=this.bookmarkTemplate.content.cloneNode(!0),l=n.querySelector(".mobile-menu__bookmark");l.classList.add(`bookmark-${e}`),l.dataset.color=o,n.querySelector(".mobile-menu__bookmark-progress > div > div").style.width=`${r.toFixed(1)}%`,n.querySelector(".mobile-menu__bookmark a").href=`${s}#paragraph-${i}`,n.querySelector(".mobile-menu__bookmark a span").innerText=a,n.querySelector(".mobile-menu-bookmark-delete-button").setAttribute("data-fictioneer-mobile-menu-id-param",e),t.appendChild(n)})),this.bookmarksTarget.appendChild(t)}}deleteBookmark({params:{id:e}}){const o=window.FictioneerApp.Controllers.fictioneerBookmarks;o?(o.remove({params:{id:e}}),this.setMobileBookmarks()):fcn_showNotification("Error: Bookmarks Controller not connected.",3,"warning")}toggleBookmarksEdit(){this.editingBookmarks=!this.editingBookmarks,this.panelBookmarksTarget.dataset.editing=this.editingBookmarks}scrollToComments(){this.#t(_$$$("comments"))}scrollToBookmark(){const e=window.FictioneerApp.Controllers.fictioneerBookmarks;if(!e)return void fcn_showNotification("Error: Bookmarks Controller not connected.",3,"warning");const o=e.data()?.[this.chapterId]?.["paragraph-id"],t=_$(`[data-paragraph-id="${o}"]`);this.#t(t)}changeLightness({currentTarget:e}){fcn_updateDarken(fcn_siteSettings.darken+parseFloat(e.value))}#t(e){this.toggle(!1),e&&setTimeout((()=>{FcnUtils.scrollTo(e)}),200)}#o(e){this.back(),e?(document.body.classList.add("mobile-menu-open","scrolling-down"),document.body.classList.remove("scrolling-up")):document.body.classList.remove("mobile-menu-open")}#e(e){const o=_$$$("wpadminbar")?.offsetHeight??0,t=window.scrollY,r=FcnGlobals.eSite.scrollTop;this.back(),e?(document.body.classList.add("mobile-menu-open","scrolling-down","scrolled-to-top"),document.body.classList.remove("scrolling-up"),FcnGlobals.eSite.classList.add("transformed-scroll","transformed-site"),FcnGlobals.eSite.scrollTop=t-o):(FcnGlobals.eSite.classList.remove("transformed-site","transformed-scroll"),document.body.classList.remove("mobile-menu-open"),document.documentElement.style.scrollBehavior="auto",window.scroll(0,r+o),document.documentElement.style.scrollBehavior="")}});
application.register("fictioneer-mobile-menu",class extends Stimulus.Controller{static get targets(){return["frame","bookmarks","panelBookmarks"]}advanced=!!_$(".mobile-menu._advanced-mobile-menu");open=!1;editingBookmarks=!1;currentFrame=null;bookmarkTemplate=_$$$("mobile-bookmark-template");chapterId=_$("article")?.id;connect(){window.FictioneerApp.Controllers.fictioneerMobileMenu=this}toggle(e=null){this.open=e??!this.open,this.advanced?this.#e(this.open):this.#o(this.open)}clickOutside({detail:{target:e}}){this.open&&"site"===e?.id&&this.toggle(!1)}openFrame({params:{frame:e}}){const o=`mobile-frame-${e}`;this.editingBookmarks=!1,this.hasPanelBookmarksTarget&&(this.panelBookmarksTarget.dataset.editing=!1),this.hasFrameTarget&&this.frameTargets.forEach((e=>{e.classList.toggle("_active",e.id===o),e.id===o&&(this.currentFrame=e)}))}back(){this.openFrame({params:{frame:"main"}})}setMobileBookmarks(){const e=window.FictioneerApp.Controllers.fictioneerBookmarks;if(e&&this.hasBookmarksTarget){const o=Object.entries(e.data());if(this.bookmarksTarget.innerHTML="",!o||o.length<1){const e=document.createElement("li");return e.classList.add("no-bookmarks"),e.textContent=this.bookmarksTarget.dataset.empty,void this.bookmarksTarget.appendChild(e)}const t=document.createDocumentFragment();o.forEach((([e,{color:o,progress:r,link:a,chapter:s,"paragraph-id":i}])=>{const n=this.bookmarkTemplate.content.cloneNode(!0),l=n.querySelector(".mobile-menu__bookmark");l.classList.add(`bookmark-${e}`),l.dataset.color=o,n.querySelector(".mobile-menu__bookmark-progress > div > div").style.width=`${r.toFixed(1)}%`,n.querySelector(".mobile-menu__bookmark a").href=`${a}#paragraph-${i}`,n.querySelector(".mobile-menu__bookmark a span").innerText=s,n.querySelector(".mobile-menu-bookmark-delete-button").setAttribute("data-fictioneer-mobile-menu-id-param",e),t.appendChild(n)})),this.bookmarksTarget.appendChild(t)}}deleteBookmark({params:{id:e}}){const o=window.FictioneerApp.Controllers.fictioneerBookmarks;o?(o.remove({params:{id:e}}),this.setMobileBookmarks()):fcn_showNotification("Error: Bookmarks Controller not connected.",3,"warning")}toggleBookmarksEdit(){this.editingBookmarks=!this.editingBookmarks,this.panelBookmarksTarget.dataset.editing=this.editingBookmarks}scrollToComments(){this.#t(_$$$("comments"))}scrollToBookmark(){const e=window.FictioneerApp.Controllers.fictioneerBookmarks;if(!e)return void fcn_showNotification("Error: Bookmarks Controller not connected.",3,"warning");const o=e.data()?.[this.chapterId]?.["paragraph-id"],t=_$(`[data-paragraph-id="${o}"]`);this.#t(t)}changeLightness({currentTarget:e}){fcn_updateDarken(fcn_siteSettings.darken+parseFloat(e.value))}#t(e){this.toggle(!1),e&&setTimeout((()=>{FcnUtils.scrollTo(e)}),200)}#o(e){this.back(),e?(document.body.classList.add("mobile-menu-open","scrolling-down"),document.body.classList.remove("scrolling-up")):document.body.classList.remove("mobile-menu-open")}#e(e){const o=_$$$("wpadminbar")?.offsetHeight??0,t=window.scrollY,r=FcnGlobals.eSite.scrollTop;this.back(),e?(document.body.classList.add("mobile-menu-open","scrolling-down","scrolled-to-top"),document.body.classList.remove("scrolling-up"),FcnGlobals.eSite.classList.add("transformed-scroll","transformed-site"),FcnGlobals.eSite.scrollTop=t-o):(FcnGlobals.eSite.classList.remove("transformed-site","transformed-scroll"),document.body.classList.remove("mobile-menu-open"),document.documentElement.style.scrollBehavior="auto",window.scroll(0,r+o),document.documentElement.style.scrollBehavior="")}});

2
js/reminders.min.js vendored
View File

@ -1 +1 @@
function fcn_toggleReminder(e,t=null){const i=window.FictioneerApp.Controllers.fictioneerReminders;if(!i)return void fcn_showNotification("Error: Reminders Controller not connected.",3,"warning");const r=FcnUtils.userData();(t=t??!r.reminders.data[e])?r.reminders.data[e]={story_id:parseInt(e),timestamp:Date.now()}:delete r.reminders.data[e],r.lastLoaded=0,FcnUtils.setUserData(r),i.refreshView(),clearTimeout(i.timeout),i.timeout=setTimeout((()=>{FcnUtils.remoteAction("fictioneer_ajax_toggle_reminder",{payload:{set:t,story_id:e}})}),FcnGlobals.debounceRate)}application.register("fictioneer-reminders",class extends Stimulus.Controller{static get targets(){return["toggleButton"]}remindersLoaded=!1;timeout=0;initialize(){fcn()?.userReady?this.#e=!0:document.addEventListener("fcnUserDataReady",(()=>{this.refreshView(),this.#e=!0,this.#t()}))}connect(){window.FictioneerApp.Controllers.fictioneerReminders=this,this.#e&&(this.refreshView(),this.#t())}data(){return this.remindersCachedData=FcnUtils.userData().reminders?.data,Array.isArray(this.remindersCachedData)&&0===this.remindersCachedData.length&&(this.remindersCachedData={}),this.remindersCachedData}isRemembered(e){return!(!FcnUtils.loggedIn()||!this.data()?.[e])}toggleReminder({params:{id:e}}){this.#i()&&(fcn_toggleReminder(e,!this.isRemembered(e)),this.refreshView())}clear(){const e=FcnUtils.userData();e.reminders={data:{}},fcn().setUserData(e),this.refreshView()}refreshView(){this.toggleButtonTargets.forEach((e=>{const t=e.dataset.storyId;e.classList.toggle("_remembered",this.isRemembered(t))}))}#e=!1;#r=!1;#i(){const e=FcnUtils.loggedIn();return e||(this.#s(),this.#r=!0),e}#a(){return this.#i()&&JSON.stringify(this.remindersCachedData??0)!==JSON.stringify(this.data())}#n(){this.refreshInterval||(this.refreshInterval=setInterval((()=>{!this.#r&&this.#a()&&this.refreshView()}),3e4+1e3*Math.random()))}#t(){this.#n(),this.visibilityStateCheck=()=>{this.#i()&&("visible"===document.visibilityState?(this.#r=!1,this.refreshView(),this.#n()):(this.#r=!0,clearInterval(this.refreshInterval),this.refreshInterval=null))},document.addEventListener("visibilitychange",this.visibilityStateCheck)}#s(){clearInterval(this.refreshInterval),document.removeEventListener("visibilitychange",this.visibilityStateCheck)}});
function fcn_toggleReminder(e,t=null){const r=window.FictioneerApp.Controllers.fictioneerReminders;if(!r)return void fcn_showNotification("Error: Reminders Controller not connected.",3,"warning");const i=FcnUtils.userData();("object"!=typeof i.reminders.data||null===i.reminders.data||Array.isArray(i.reminders.data))&&(i.reminders.data={}),(t=t??!i.reminders.data[e])?i.reminders.data[e]={story_id:parseInt(e),timestamp:Date.now()}:delete i.reminders.data[e],i.lastLoaded=0,FcnUtils.setUserData(i),r.refreshView(),clearTimeout(r.timeout),r.timeout=setTimeout((()=>{FcnUtils.remoteAction("fictioneer_ajax_toggle_reminder",{payload:{set:t,story_id:e}})}),FcnGlobals.debounceRate)}application.register("fictioneer-reminders",class extends Stimulus.Controller{static get targets(){return["toggleButton"]}remindersLoaded=!1;timeout=0;initialize(){fcn()?.userReady?this.#e=!0:document.addEventListener("fcnUserDataReady",(()=>{this.refreshView(),this.#e=!0,this.#t()}))}connect(){window.FictioneerApp.Controllers.fictioneerReminders=this,this.#e&&(this.refreshView(),this.#t())}data(){return this.remindersCachedData=FcnUtils.userData().reminders?.data,Array.isArray(this.remindersCachedData)&&0===this.remindersCachedData.length&&(this.remindersCachedData={}),this.remindersCachedData}isRemembered(e){return!(!FcnUtils.loggedIn()||!this.data()?.[e])}toggleReminder({params:{id:e}}){this.#r()&&(fcn_toggleReminder(e,!this.isRemembered(e)),this.refreshView())}clear(){const e=FcnUtils.userData();e.reminders={data:{}},fcn().setUserData(e),this.refreshView()}refreshView(){this.toggleButtonTargets.forEach((e=>{const t=e.dataset.storyId;e.classList.toggle("_remembered",this.isRemembered(t))}))}#e=!1;#i=!1;#r(){const e=FcnUtils.loggedIn();return e||(this.#s(),this.#i=!0),e}#a(){return this.#r()&&JSON.stringify(this.remindersCachedData??0)!==JSON.stringify(this.data())}#n(){this.refreshInterval||(this.refreshInterval=setInterval((()=>{!this.#i&&this.#a()&&this.refreshView()}),3e4+1e3*Math.random()))}#t(){this.#n(),this.visibilityStateCheck=()=>{this.#r()&&("visible"===document.visibilityState?(this.#i=!1,this.refreshView(),this.#n()):(this.#i=!0,clearInterval(this.refreshInterval),this.refreshInterval=null))},document.addEventListener("visibilitychange",this.visibilityStateCheck)}#s(){clearInterval(this.refreshInterval),document.removeEventListener("visibilitychange",this.visibilityStateCheck)}});

4
js/sortable.min.js vendored

File diff suppressed because one or more lines are too long

2
js/tts.min.js vendored

File diff suppressed because one or more lines are too long

2
js/utility.min.js vendored

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

Binary file not shown.

File diff suppressed because it is too large Load Diff

View File

@ -19,6 +19,7 @@
* @internal $args['orderby'] Sorting of posts. Default 'date'.
* @internal $args['page'] The current page. Default 1.
* @internal $args['post_ids'] Array of post IDs. Default empty.
* @internal $args['post_status'] Queried post status. Default 'publish'.
* @internal $args['author_ids'] Array of author IDs. Default empty.
* @internal $args['excluded_authors'] Array of author IDs to exclude. Default empty.
* @internal $args['excluded_cats'] Array of category IDs to exclude. Default empty.
@ -50,7 +51,7 @@ $show_terms = ! in_array( $args['terms'], ['none', 'false'] );
$query_args = array(
'fictioneer_query_name' => 'article_cards',
'post_type' => $args['post_type'],
'post_status' => 'publish',
'post_status' => $args['post_status'],
'post__in' => $args['post_ids'], // May be empty!
'order' => $args['order'],
'orderby' => $args['orderby'],
@ -175,6 +176,8 @@ if ( $splide ) {
}
// Extra classes
$card_classes[] = '_' . $args['post_status'];
if ( get_theme_mod( 'card_style', 'default' ) !== 'default' ) {
$card_classes[] = '_' . get_theme_mod( 'card_style' );
}

View File

@ -97,7 +97,6 @@ $thumbnail_args = array(
data-controller="fictioneer-large-card"
data-fictioneer-large-card-post-id-value="<?php echo $post_id; ?>"
data-fictioneer-large-card-story-id-value="<?php echo $story_id; ?>"
data-fictioneer-large-card-chapter-id-value="<?php echo $post_id; ?>"
data-action="click->fictioneer-large-card#cardClick"
<?php echo $card_attributes; ?>
>

View File

@ -17,6 +17,7 @@
* @internal $args['spoiler'] Whether to obscure or show chapter excerpt.
* @internal $args['source'] Whether to show author and story.
* @internal $args['post_ids'] Array of post IDs. Default empty.
* @internal $args['post_status'] Queried post status. Default 'publish'.
* @internal $args['author_ids'] Array of author IDs. Default empty.
* @internal $args['excluded_authors'] Array of author IDs to exclude. Default empty.
* @internal $args['excluded_cats'] Array of category IDs to exclude. Default empty.
@ -51,7 +52,7 @@ $card_counter = 0;
$query_args = array(
'fictioneer_query_name' => 'latest_chapters_compact',
'post_type' => 'fcn_chapter',
'post_status' => 'publish',
'post_status' => $args['post_status'],
'post__in' => $args['post_ids'], // May be empty!
'order' => $args['order'],
'orderby' => $args['orderby'],
@ -156,7 +157,7 @@ if ( $splide ) {
$post_id = $post->ID;
$story_id = fictioneer_get_chapter_story_id( $post_id );
if ( get_post_status( $story_id ) !== 'publish' ) {
if ( $story_id && get_post_status( $story_id ) !== 'publish' ) {
continue;
}
@ -168,6 +169,8 @@ if ( $splide ) {
$card_classes = [];
// Extra card classes
$card_classes[] = '_' . $args['post_status'];
if ( ! empty( $post->post_password ) ) {
$card_classes[] = '_password';
}

View File

@ -16,6 +16,7 @@
* @internal $args['orderby'] Sorting of posts. Default 'date'.
* @internal $args['spoiler'] Whether to obscure or show chapter excerpt.
* @internal $args['post_ids'] Array of post IDs. Default empty.
* @internal $args['post_status'] Queried post status. Default 'publish'.
* @internal $args['author_ids'] Array of author IDs. Default empty.
* @internal $args['excluded_authors'] Array of author IDs to exclude. Default empty.
* @internal $args['excluded_cats'] Array of category IDs to exclude. Default empty.
@ -56,7 +57,7 @@ $content_list_style = get_theme_mod( 'content_list_style', 'default' );
$query_args = array(
'fictioneer_query_name' => 'latest_chapters_list',
'post_type' => 'fcn_chapter',
'post_status' => 'publish',
'post_status' => $args['post_status'],
'post__in' => $args['post_ids'], // May be empty!
'orderby' => $args['orderby'],
'order' => $args['order'],
@ -160,9 +161,8 @@ if ( $splide ) {
// Setup
$post_id = $post->ID;
$story_id = fictioneer_get_chapter_story_id( $post_id );
$story = $story_id ? fictioneer_get_story_data( $story_id, false ) : null; // Does not refresh comment count!
if ( get_post_status( $story_id ) !== 'publish' || ! $story ) {
if ( $story_id && get_post_status( $story_id ) !== 'publish' ) {
continue;
}
@ -173,11 +173,12 @@ if ( $splide ) {
// Continue setup
$title = fictioneer_get_safe_title( $post_id, 'shortcode-latest-chapters-list' );
$story = $story_id ? fictioneer_get_story_data( $story_id, false ) : null; // Does not refresh comment count!
$permalink = get_permalink( $post_id );
$chapter_rating = get_post_meta( $post_id, 'fictioneer_chapter_rating', true );
$words = fictioneer_get_word_count( $post_id );
if ( ! $chapter_rating && $story_id ) {
if ( ! $chapter_rating && $story ) {
$chapter_rating = get_post_meta( $story_id, 'fictioneer_story_rating', true );
}
@ -200,7 +201,7 @@ if ( $splide ) {
}
// Extra classes
$classes = [];
$classes = [ '_' . $args['post_status'] ];
if ( ! empty( $post->post_password ) ) {
$classes[] = '_password';
@ -249,7 +250,7 @@ if ( $splide ) {
// Meta
$meta = [];
if ( $args['source'] ) {
if ( $story && $args['source'] ) {
$meta['story'] = sprintf(
_x(
'<span class="post-list-item__meta-in-story"><span>in </span><a href="%1$s">%2$s</a></span>',
@ -268,7 +269,7 @@ if ( $splide ) {
) . '</span>';
}
if ( $args['footer_status'] ) {
if ( $story && $args['footer_status'] ) {
$meta['status'] = '<span class="post-item-item__meta-status _' . strtolower( $story['status'] ) . '">' . fcntr( $story['status'] ) . '</span>';
}

View File

@ -16,6 +16,7 @@
* @internal $args['orderby'] Sorting of posts. Default 'date'.
* @internal $args['spoiler'] Whether to obscure or show chapter excerpt.
* @internal $args['post_ids'] Array of post IDs. Default empty.
* @internal $args['post_status'] Queried post status. Default 'publish'.
* @internal $args['author_ids'] Array of author IDs. Default empty.
* @internal $args['excluded_authors'] Array of author IDs to exclude. Default empty.
* @internal $args['excluded_cats'] Array of category IDs to exclude. Default empty.
@ -56,7 +57,7 @@ $card_counter = 0;
$query_args = array(
'fictioneer_query_name' => 'latest_chapters',
'post_type' => 'fcn_chapter',
'post_status' => 'publish',
'post_status' => $args['post_status'],
'post__in' => $args['post_ids'], // May be empty!
'orderby' => $args['orderby'],
'order' => $args['order'],
@ -161,22 +162,25 @@ if ( $splide ) {
$post_id = $post->ID;
$story_id = fictioneer_get_chapter_story_id( $post_id );
if ( get_post_status( $story_id ) !== 'publish' ) {
if ( $story_id && get_post_status( $story_id ) !== 'publish' ) {
continue;
}
$title = fictioneer_get_safe_title( $post_id, 'shortcode-latest-chapters' );
$permalink = get_permalink();
$chapter_rating = get_post_meta( $post_id, 'fictioneer_chapter_rating', true );
$story = $story_id ? fictioneer_get_story_data( $story_id, false ) : null; // Does not refresh comment count!
$text_icon = get_post_meta( $post_id, 'fictioneer_chapter_text_icon', true );
$grid_or_vertical = $args['vertical'] ? '_vertical' : '_grid';
$card_classes = [];
if ( ! $chapter_rating && $story_id ) {
if ( ! $chapter_rating && $story ) {
$chapter_rating = get_post_meta( $story_id, 'fictioneer_story_rating', true );
}
// Extra card classes
$card_classes[] = '_' . $args['post_status'];
if ( ! empty( $post->post_password ) ) {
$card_classes[] = '_password';
}
@ -259,7 +263,7 @@ if ( $splide ) {
'post_id' => $post_id,
'title' => $title,
'classes' => 'card__image cell-img',
'permalink' => get_permalink(),
'permalink' => $permalink,
'lightbox' => $args['lightbox'],
'vertical' => $args['vertical'],
'seamless' => $args['seamless'],
@ -270,7 +274,7 @@ if ( $splide ) {
}
?>
<h3 class="card__title _small cell-title"><a href="<?php the_permalink(); ?>" class="truncate _1-1"><?php
<h3 class="card__title _small cell-title"><a href="<?php echo $permalink; ?>" class="truncate _1-1"><?php
$list_title = get_post_meta( $post_id, 'fictioneer_chapter_list_title', true );
$list_title = trim( wp_strip_all_tags( $list_title ) );

View File

@ -13,6 +13,7 @@
* @internal $args['author'] The author provided by the shortcode. Default false.
* @internal $args['count'] The number of posts provided by the shortcode. Default 1.
* @internal $args['post_ids'] Array of post IDs. Default empty.
* @internal $args['post_status'] Queried post status. Default 'publish'.
* @internal $args['author_ids'] Array of author IDs. Default empty.
* @internal $args['excluded_authors'] Array of author IDs to exclude. Default empty.
* @internal $args['excluded_cats'] Array of category IDs to exclude. Default empty.
@ -32,7 +33,7 @@ defined( 'ABSPATH' ) OR exit;
$query_args = array(
'fictioneer_query_name' => 'latest_posts',
'post_type' => 'post',
'post_status' => 'publish',
'post_status' => $args['post_status'],
'post__in' => $args['post_ids'], // May be empty!
'order' => 'DESC',
'orderby' => 'date',
@ -88,6 +89,9 @@ $query_args = apply_filters( 'fictioneer_filter_shortcode_latest_posts_query_arg
// Query post
$latest_entries = fictioneer_shortcode_query( $query_args );
// Classes
$args['classes'] .= ' _' . $args['post_status'];
?>
<section class="latest-posts <?php echo $args['classes']; ?>">

View File

@ -15,6 +15,7 @@
* @internal $args['order'] Order of posts. Default 'DESC'.
* @internal $args['orderby'] Sorting of posts. Default 'date'.
* @internal $args['post_ids'] Array of post IDs. Default empty.
* @internal $args['post_status'] Queried post status. Default 'publish'.
* @internal $args['author_ids'] Array of author IDs. Default empty.
* @internal $args['excluded_authors'] Array of author IDs to exclude. Default empty.
* @internal $args['excluded_cats'] Array of category IDs to exclude. Default empty.
@ -48,7 +49,7 @@ $show_terms = ! in_array( $args['terms'], ['none', 'false'] ) &&
$query_args = array (
'fictioneer_query_name' => 'latest_recommendations_compact',
'post_type' => 'fcn_recommendation',
'post_status' => 'publish',
'post_status' => $args['post_status'],
'post__in' => $args['post_ids'], // May be empty!
'order' => $args['order'],
'orderby' => $args['orderby'],
@ -143,7 +144,9 @@ if ( $splide ) {
$grid_or_vertical = $args['vertical'] ? '_vertical' : '_grid';
$card_classes = [];
// Extra classes
// Extra card classes
$card_classes[] = '_' . $args['post_status'];
if ( $show_terms ) {
$card_classes[] = '_info';
}

View File

@ -15,6 +15,7 @@
* @internal $args['order'] Order of posts. Default 'DESC'.
* @internal $args['orderby'] Sorting of posts. Default 'date'.
* @internal $args['post_ids'] Array of post IDs. Default empty.
* @internal $args['post_status'] Queried post status. Default 'publish'.
* @internal $args['author_ids'] Array of author IDs. Default empty.
* @internal $args['excluded_authors'] Array of author IDs to exclude. Default empty.s
* @internal $args['excluded_cats'] Array of category IDs to exclude. Default empty.
@ -46,7 +47,7 @@ $show_terms = ! get_option( 'fictioneer_hide_taxonomies_on_recommendation_cards'
$query_args = array (
'fictioneer_query_name' => 'latest_recommendations',
'post_type' => 'fcn_recommendation',
'post_status' => 'publish',
'post_status' => $args['post_status'],
'post__in' => $args['post_ids'], // May be empty!
'order' => $args['order'],
'orderby' => $args['orderby'],
@ -140,7 +141,9 @@ if ( $splide ) {
$grid_or_vertical = $args['vertical'] ? '_vertical' : '_grid';
$card_classes = [];
// Extra classes
// Extra card classes
$card_classes[] = '_' . $args['post_status'];
if ( get_theme_mod( 'card_style', 'default' ) !== 'default' ) {
$card_classes[] = '_' . get_theme_mod( 'card_style' );
}

View File

@ -16,6 +16,7 @@
* @internal $args['order'] Order of posts. Default 'DESC'.
* @internal $args['orderby'] Sorting of posts. Default 'date'.
* @internal $args['post_ids'] Array of post IDs. Default empty.
* @internal $args['post_status'] Queried post status. Default 'publish'.
* @internal $args['author_ids'] Array of author IDs. Default empty.
* @internal $args['excluded_authors'] Array of author IDs to exclude. Default empty.
* @internal $args['excluded_cats'] Array of category IDs to exclude. Default empty.
@ -56,7 +57,7 @@ $show_terms = ! in_array( $args['terms'], ['none', 'false'] ) &&
$query_args = array(
'fictioneer_query_name' => 'latest_stories_compact',
'post_type' => 'fcn_story',
'post_status' => 'publish',
'post_status' => $args['post_status'],
'post__in' => $args['post_ids'], // May be empty!
'order' => $args['order'],
'orderby' => $args['orderby'],
@ -171,7 +172,9 @@ if ( $splide ) {
);
$short_description = mb_strlen( $short_description, 'UTF-8' ) < 30 ? get_the_excerpt() : $short_description;
// Extra classes
// Extra card classes
$card_classes[] = '_' . $args['post_status'];
if ( $show_terms ) {
$card_classes[] = '_info';
}

View File

@ -16,6 +16,7 @@
* @internal $args['order'] Order of posts. Default 'DESC'.
* @internal $args['orderby'] Sorting of posts. Default 'date'.
* @internal $args['post_ids'] Array of post IDs. Default empty.
* @internal $args['post_status'] Queried post status. Default 'publish'.
* @internal $args['author_ids'] Array of author IDs. Default empty.
* @internal $args['excluded_authors'] Array of author IDs to exclude. Default empty.
* @internal $args['excluded_cats'] Array of category IDs to exclude. Default empty.
@ -57,7 +58,7 @@ $content_list_style = get_theme_mod( 'content_list_style', 'default' );
$query_args = array(
'fictioneer_query_name' => 'latest_stories_list',
'post_type' => 'fcn_story',
'post_status' => 'publish',
'post_status' => $args['post_status'],
'post__in' => $args['post_ids'], // May be empty!
'order' => $args['order'],
'orderby' => $args['orderby'],
@ -184,7 +185,7 @@ if ( $splide ) {
}
// Extra classes
$classes = [];
$classes = [ '_' . $args['post_status'] ];
if ( $is_sticky ) {
$classes[] = '_sticky';

View File

@ -16,6 +16,7 @@
* @internal $args['order'] Order of posts. Default 'DESC'.
* @internal $args['orderby'] Sorting of posts. Default 'date'.
* @internal $args['post_ids'] Array of post IDs. Default empty.
* @internal $args['post_status'] Queried post status. Default 'publish'.
* @internal $args['author_ids'] Array of author IDs. Default empty.
* @internal $args['excluded_authors'] Array of author IDs to exclude. Default empty.
* @internal $args['excluded_cats'] Array of category IDs to exclude. Default empty.
@ -57,7 +58,7 @@ $show_terms = ! in_array( $args['terms'], ['none', 'false'] ) &&
$query_args = array(
'fictioneer_query_name' => 'latest_stories',
'post_type' => 'fcn_story',
'post_status' => 'publish',
'post_status' => $args['post_status'],
'post__in' => $args['post_ids'], // May be empty!
'order' => $args['order'],
'orderby' => $args['orderby'],
@ -167,6 +168,9 @@ if ( $splide ) {
$grid_or_vertical = $args['vertical'] ? '_vertical' : '_grid';
$card_classes = [];
// Extra card classes
$card_classes[] = '_' . $args['post_status'];
if ( $is_sticky ) {
$card_classes[] = '_sticky';
}

View File

@ -18,6 +18,7 @@
* @internal $args['author'] Author provided by the shortcode.
* @internal $args['order'] Order of posts. Default 'DESC'.
* @internal $args['post_ids'] Array of post IDs. Default empty.
* @internal $args['post_status'] Queried post status. Default 'publish'.
* @internal $args['author_ids'] Array of author IDs. Default empty.
* @internal $args['excluded_authors'] Array of author IDs to exclude. Default empty.
* @internal $args['excluded_cats'] Array of category IDs to exclude. Default empty.
@ -59,7 +60,7 @@ $card_counter = 0;
$query_args = array(
'fictioneer_query_name' => 'latest_updates_compact',
'post_type' => 'fcn_story',
'post_status' => 'publish',
'post_status' => $args['post_status'],
'post__in' => $args['post_ids'], // May be empty!
'order' => $args['order'],
'orderby' => 'meta_value',
@ -181,7 +182,9 @@ if ( $splide ) {
continue;
}
// Extra classes
// Extra card classes
$card_classes[] = '_' . $args['post_status'];
if ( ! empty( $post->post_password ) ) {
$card_classes[] = '_password';
}

View File

@ -18,6 +18,7 @@
* @internal $args['author'] Author provided by the shortcode.
* @internal $args['order'] Order of posts. Default 'DESC'.
* @internal $args['post_ids'] Array of post IDs. Default empty.
* @internal $args['post_status'] Queried post status. Default 'publish'.
* @internal $args['author_ids'] Array of author IDs. Default empty.
* @internal $args['excluded_authors'] Array of author IDs to exclude. Default empty.
* @internal $args['excluded_cats'] Array of category IDs to exclude. Default empty.
@ -62,7 +63,7 @@ $content_list_style = get_theme_mod( 'content_list_style', 'default' );
$query_args = array(
'fictioneer_query_name' => 'latest_updates_list',
'post_type' => 'fcn_story',
'post_status' => 'publish',
'post_status' => $args['post_status'],
'post__in' => $args['post_ids'], // May be empty!
'order' => $args['order'],
'orderby' => 'meta_value',
@ -205,7 +206,7 @@ if ( $splide ) {
}
// Extra classes
$classes = [];
$classes = [ '_' . $args['post_status'] ];
if ( ! empty( $post->post_password ) ) {
$classes[] = '_password';

View File

@ -19,6 +19,7 @@
* @internal $args['author'] Author provided by the shortcode.
* @internal $args['order'] Order of posts. Default 'DESC'.
* @internal $args['post_ids'] Array of post IDs. Default empty.
* @internal $args['post_status'] Queried post status. Default 'publish'.
* @internal $args['author_ids'] Array of author IDs. Default empty.
* @internal $args['excluded_authors'] Array of author IDs to exclude. Default empty.
* @internal $args['excluded_cats'] Array of category IDs to exclude. Default empty.
@ -65,7 +66,7 @@ $show_terms = ! get_option( 'fictioneer_hide_taxonomies_on_story_cards' ) &&
$query_args = array(
'fictioneer_query_name' => 'latest_updates',
'post_type' => 'fcn_story',
'post_status' => 'publish',
'post_status' => $args['post_status'],
'post__in' => $args['post_ids'], // May be empty!
'order' => $args['order'],
'orderby' => 'meta_value',
@ -186,7 +187,9 @@ if ( $splide ) {
continue;
}
// Extra classes
// Extra card classes
$card_classes[] = '_' . $args['post_status'];
if ( ! empty( $post->post_password ) ) {
$card_classes[] = '_password';
}

View File

@ -18,6 +18,7 @@
* @internal $args['orderby'] Order argument. Default 'date'.
* @internal $args['author'] Author provided by the shortcode.
* @internal $args['post_ids'] Array of post IDs. Default empty.
* @internal $args['post_status'] Queried post status. Default 'publish'.
* @internal $args['author_ids'] Array of author IDs. Default empty.
* @internal $args['excluded_authors'] Array of author IDs to exclude. Default empty.
* @internal $args['excluded_cats'] Array of category IDs to exclude. Default empty.
@ -46,7 +47,7 @@ $splide = $args['splide'] ?? 0;
$query_args = array (
'fictioneer_query_name' => 'showcase',
'post_type' => $args['post_type'],
'post_status' => 'publish',
'post_status' => $args['post_status'],
'post__in' => $args['post_ids'], // May be empty!
'order' => $args['order'],
'orderby' => $args['orderby'],
@ -129,7 +130,7 @@ if ( $args['min_width'] ) {
}
// Classes
$classes = '';
$classes = '_' . $args['post_status'];
if ( $args['aspect_ratio'] ) {
$classes .= ' _aspect-ratio';

View File

@ -39,5 +39,5 @@ $delete_account_prompt = sprintf(
<p class="profile__description"><?php _e( 'You can delete your account and associated user data with it. Submitted <em>content</em> such as comments and posts will remain under the “Deleted User” name unless you remove them <em>prior</em>. Be aware that once you delete your account, there is no going back.', 'fictioneer' ); ?></p>
<div class="profile__actions">
<button id="button-delete-my-account" type="button" class="button _danger" data-nonce="<?php echo wp_create_nonce( 'fictioneer_delete_account' ); ?>" data-id="<?php echo $current_user->ID; ?>" data-confirm="<?php echo $confirmation; ?>" data-warning="<?php echo esc_attr( $delete_account_prompt ); ?>"><?php _e( 'Delete Account', 'fictioneer' ); ?></button>
<button id="button-delete-my-account" type="button" class="button _danger" data-nonce="<?php echo wp_create_nonce( 'fictioneer_delete_account' ); ?>" data-id="<?php echo $current_user->ID; ?>" data-confirm="<?php echo $confirmation; ?>" data-warning="<?php echo esc_attr( $delete_account_prompt ); ?>" data-fictioneer-target="dcjProtected" disabled><?php _e( 'Delete Account', 'fictioneer' ); ?></button>
</div>

View File

@ -130,7 +130,7 @@ $delete_bookmarks_prompt = sprintf(
?>
</div>
</div>
<button class="card__delete button-clear-comments" data-nonce="<?php echo wp_create_nonce( 'fictioneer_clear_comments' ); ?>" data-confirm="<?php echo $confirmation; ?>" data-warning="<?php echo esc_attr( $delete_comments_prompt ); ?>"><i class="fa-solid fa-trash-can"></i></button>
<button class="card__delete button-clear-comments" data-nonce="<?php echo wp_create_nonce( 'fictioneer_clear_comments' ); ?>" data-confirm="<?php echo $confirmation; ?>" data-warning="<?php echo esc_attr( $delete_comments_prompt ); ?>" data-fictioneer-target="dcjProtected" disabled><i class="fa-solid fa-trash-can"></i></button>
</div>
</li>
<?php endif; ?>
@ -152,7 +152,7 @@ $delete_bookmarks_prompt = sprintf(
?>
</div>
</div>
<button class="card__delete button-clear-comment-subscriptions" data-nonce="<?php echo wp_create_nonce( 'fictioneer_clear_comment_subscriptions' ); ?>" data-confirm="<?php echo $confirmation; ?>" data-warning="<?php echo esc_attr( $delete_comment_subscriptions_prompt ); ?>"><i class="fa-solid fa-trash-can"></i></button>
<button class="card__delete button-clear-comment-subscriptions" data-nonce="<?php echo wp_create_nonce( 'fictioneer_clear_comment_subscriptions' ); ?>" data-confirm="<?php echo $confirmation; ?>" data-warning="<?php echo esc_attr( $delete_comment_subscriptions_prompt ); ?>" data-fictioneer-target="dcjProtected" disabled><i class="fa-solid fa-trash-can"></i></button>
</div>
</li>
<?php endif; ?>
@ -183,7 +183,7 @@ $delete_bookmarks_prompt = sprintf(
</div>
</div>
<?php if ( $follows_count > 0 ) : ?>
<button class="card__delete button-clear-follows" data-nonce="<?php echo wp_create_nonce( 'fictioneer_clear_follows' ); ?>" data-confirm="<?php echo $confirmation; ?>" data-warning="<?php echo esc_attr( $delete_follows_prompt ); ?>"><i class="fa-solid fa-trash-can"></i></button>
<button class="card__delete button-clear-follows" data-nonce="<?php echo wp_create_nonce( 'fictioneer_clear_follows' ); ?>" data-confirm="<?php echo $confirmation; ?>" data-warning="<?php echo esc_attr( $delete_follows_prompt ); ?>" data-fictioneer-target="dcjProtected" disabled><i class="fa-solid fa-trash-can"></i></button>
<?php endif; ?>
</div>
</li>
@ -215,7 +215,7 @@ $delete_bookmarks_prompt = sprintf(
</div>
</div>
<?php if ( $reminders_count > 0 ) : ?>
<button class="card__delete button-clear-reminders" data-nonce="<?php echo wp_create_nonce( 'fictioneer_clear_reminders' ); ?>" data-confirm="<?php echo $confirmation; ?>" data-warning="<?php echo esc_attr( $delete_reminders_prompt ); ?>"><i class="fa-solid fa-trash-can"></i></button>
<button class="card__delete button-clear-reminders" data-nonce="<?php echo wp_create_nonce( 'fictioneer_clear_reminders' ); ?>" data-confirm="<?php echo $confirmation; ?>" data-warning="<?php echo esc_attr( $delete_reminders_prompt ); ?>" data-fictioneer-target="dcjProtected" disabled><i class="fa-solid fa-trash-can"></i></button>
<?php endif; ?>
</div>
</li>
@ -252,7 +252,7 @@ $delete_bookmarks_prompt = sprintf(
</div>
</div>
<?php if ( $stories_count > 0 || $chapters_count > 0 ) : ?>
<button class="card__delete button-clear-checkmarks" data-nonce="<?php echo wp_create_nonce( 'fictioneer_clear_checkmarks' ); ?>" data-confirm="<?php echo $confirmation; ?>" data-warning="<?php echo esc_attr( $delete_checkmarks_prompt ); ?>"><i class="fa-solid fa-trash-can"></i></button>
<button class="card__delete button-clear-checkmarks" data-nonce="<?php echo wp_create_nonce( 'fictioneer_clear_checkmarks' ); ?>" data-confirm="<?php echo $confirmation; ?>" data-warning="<?php echo esc_attr( $delete_checkmarks_prompt ); ?>" data-fictioneer-target="dcjProtected" disabled><i class="fa-solid fa-trash-can"></i></button>
<?php endif; ?>
</div>
</li>
@ -275,7 +275,7 @@ $delete_bookmarks_prompt = sprintf(
<?php _e( 'You have currently <strong>%s bookmark(s)</strong> set. Bookmarks are only processed in your browser.', 'fictioneer' ); ?>
</div>
</div>
<button class="card__delete button-clear-bookmarks" data-confirm="<?php echo $confirmation; ?>" data-warning="<?php echo esc_attr( $delete_bookmarks_prompt ); ?>"><i class="fa-solid fa-trash-can"></i></button>
<button class="card__delete button-clear-bookmarks" data-confirm="<?php echo $confirmation; ?>" data-warning="<?php echo esc_attr( $delete_bookmarks_prompt ); ?>" data-fictioneer-target="dcjProtected" disabled><i class="fa-solid fa-trash-can"></i></button>
</div>
</li>
<?php endif; ?>

View File

@ -111,6 +111,8 @@ $unset_oauth_prompt = sprintf(
data-channel="<?php echo $provider[0]; ?>"
data-confirm="<?php echo $confirmation; ?>"
data-warning="<?php echo esc_attr( $unset_oauth_prompt ); ?>"
data-fictioneer-target="dcjProtected"
disabled
><?php fictioneer_icon( 'fa-xmark' ); ?></button>
</div>
<?php

View File

@ -209,7 +209,7 @@ $renaming_disabled = $current_user->fictioneer_admin_disable_renaming;
<input name="user_id" type="hidden" value="<?php echo $current_user->ID; ?>">
<div class="profile__actions">
<input name="submit" type="submit" value="<?php esc_attr_e( 'Update Profile', 'fictioneer' ); ?>" class="button">
<input name="submit" type="submit" value="<?php esc_attr_e( 'Update Profile', 'fictioneer' ); ?>" class="button" data-fictioneer-target="dcjProtected" disabled>
</div>
</form>

View File

@ -7,7 +7,7 @@ Contributors: tetrakern
Requires at least: 6.1.0
Tested up to: 6.7.1
Requires PHP: 7.4
Stable tag: 5.27.0
Stable tag: 5.27.2
License: GNU General Public License v3.0 or later
License URI: http://www.gnu.org/licenses/gpl.html

View File

@ -8,9 +8,9 @@
* @link https://github.com/WordPress/WordPress/blob/master/wp-includes/feed-rss2.php
*/
// Query posts
$posts = get_posts(
array(
// Query
$query_args = array(
'fictioneer_query_name' => 'main_rss',
'post_type' => ['post', 'fcn_story', 'fcn_chapter', 'fcn_recommendation'],
'post_status' => 'publish',
@ -20,10 +20,13 @@ $posts = get_posts(
'posts_per_page' => get_option( 'posts_per_rss' ) + 8, // Buffer in case of invalid results
'update_post_meta_cache' => true,
'update_post_term_cache' => true,
'no_found_rows' => true
)
'no_found_rows' => true // Improve performance
);
$query_args = apply_filters( 'fictioneer_filter_rss_main_query_args', $query_args );
$posts = get_posts( $query_args );
// Filter out hidden posts (faster than meta query)
$posts = array_filter( $posts, function ( $post ) {
// Chapter hidden?

View File

@ -13,6 +13,7 @@
* @link https://github.com/WordPress/WordPress/blob/master/wp-includes/feed-rss2.php
*/
// Get ID from parameter
$story_id = fictioneer_validate_id( $_GET[ 'story_id' ] ?? 0, 'fcn_story' );
$is_hidden = get_post_meta( $story_id ?: 0, 'fictioneer_story_hidden', true );
@ -142,6 +143,8 @@ do_action( 'rss_tag_pre', 'rss2' );
'no_found_rows' => true // Improve performance
);
$query_args = apply_filters( 'fictioneer_filter_rss_story_query_args', $query_args, $story_id );
$chapter_query = new WP_Query( $query_args );
// Loop over chapters

View File

@ -136,7 +136,7 @@ if ( $show_advanced ) {
type="button"
class="search-form__advanced-toggle"
tabindex="0"
><?php _ex( 'Advanced', 'Advanced search toggle.', 'fictioneer' ); ?></button>
><i class="fa-solid fa-sliders"></i></button>
<?php endif; ?>
<button type="submit" class="search-form__submit" aria-label="<?php echo esc_attr__( 'Submit search request', 'fictioneer' ); ?>"><i class="fa-solid fa-magnifying-glass"></i></button>
</div>

View File

@ -128,7 +128,7 @@ window.FictioneerApp.Controllers = window.FictioneerApp.Controllers || {};
application.register('fictioneer', class extends Stimulus.Controller {
static get targets() {
return ['avatarWrapper', 'modal', 'mobileMenuToggle']
return ['avatarWrapper', 'modal', 'mobileMenuToggle', 'dcjProtected']
}
static values = {
@ -145,6 +145,7 @@ application.register('fictioneer', class extends Stimulus.Controller {
userReady = false;
lastModalToggle = null;
currentModal = null;
dcjProtection = true;
/**
* Stimulus Controller initialize lifecycle callback.
@ -172,6 +173,19 @@ application.register('fictioneer', class extends Stimulus.Controller {
// Fire event
document.dispatchEvent(event);
}
if (this.hasDcjProtectedTarget) {
['mousemove', 'touchstart', 'keydown'].forEach(event => {
window.addEventListener(event, this.liftProtection.bind(this), { once: true });
});
}
}
liftProtection() {
if (this.dcjProtection && this.hasDcjProtectedTarget) {
this.dcjProtectedTargets.forEach(element => element.disabled = false);
this.dcjProtection = false;
}
}
/**
@ -245,7 +259,7 @@ application.register('fictioneer', class extends Stimulus.Controller {
// Only update from server after some time has passed (e.g. 60 seconds)
if (
(FcnGlobals.ajaxLimitThreshold < currentUserData.lastLoaded || currentUserData.loggedIn === false) &&
currentUserData.loggedIn !== 'pending'
(currentUserData.loggedIn !== 'pending' || this.ajaxAuthValue)
) {
// Prepare event
const event = new CustomEvent(
@ -2617,8 +2631,7 @@ application.register('fictioneer-large-card', class extends Stimulus.Controller
static values = {
postId: Number,
storyId: Number,
chapterId: Number
storyId: Number
}
initialize() {
@ -2680,7 +2693,7 @@ application.register('fictioneer-large-card', class extends Stimulus.Controller
const checkmarks = this.#data()?.checkmarks?.data?.[this.storyIdValue];
return !!checkmarks &&
(checkmarks.includes(this.chapterIdValue) || checkmarks.includes(this.storyIdValue));
(checkmarks.includes(this.postIdValue) || checkmarks.includes(this.storyIdValue));
}
cardClick(event) {
@ -2717,7 +2730,7 @@ application.register('fictioneer-large-card', class extends Stimulus.Controller
toggleCheckmarks() {
if (this.#loggedIn()) {
fcn_toggleCheckmark(this.storyIdValue, this.chapterIdValue);
fcn_toggleCheckmark(this.storyIdValue, this.postIdValue);
this.#refreshCheckmarkState();
} else {
_$('[data-fictioneer-id-param="login-modal"]')?.click();
@ -2979,6 +2992,7 @@ function fcn_loadConsentBanner() {
function fcn_showLightbox(target) {
const lightbox = _$$$('fictioneer-lightbox');
const loader = lightbox.querySelector('.loader');
const lightboxContent = _$('.lightbox__content');
let valid = false;
@ -2986,6 +3000,7 @@ function fcn_showLightbox(target) {
// Cleanup previous content (if any)
lightboxContent.innerHTML = '';
loader.style.opacity = 1;
// Bookmark source element for later use
target.classList.add('lightbox-last-trigger');
@ -3006,6 +3021,10 @@ function fcn_showLightbox(target) {
lightboxContent.appendChild(img);
lightbox.classList.add('show');
setTimeout(() => {
loader.style.opacity = 0;
}, 1000);
const close = lightbox.querySelector('.lightbox__close');
close?.focus();

View File

@ -269,11 +269,11 @@ application.register('fictioneer-chapter', class extends Stimulus.Controller {
this.progressBar.style.width = `${p}%`;
// If end of chapter has been reached and the user is logged in...
if (!this.checkmarkUpdated && !!this.storyIdValue && p >= 100 && FcnUtils.loggedIn()) {
if (!this.checkmarkUpdated && this.hasStoryIdValue && !!this.storyIdValue && p >= 100 && FcnUtils.loggedIn()) {
this.checkmarkUpdated = true;
// Make sure necessary data is available
if (!this.chapterIdValue || typeof fcn_toggleCheckmark !== 'function') {
if (!this.hasChapterIdValue || !this.chapterIdValue || typeof fcn_toggleCheckmark !== 'function') {
return;
}

View File

@ -65,10 +65,14 @@ application.register('fictioneer-checkmarks', class extends Stimulus.Controller
return;
}
let storyChecked = false;
Object.entries(checkmarks).forEach(([storyId, chapterArray]) => {
storyId = parseInt(storyId);
const storyChecked = chapterArray?.includes(storyId);
if (this.hasStoryCheckTarget && storyId == this.storyCheckTarget.dataset.fictioneerCheckmarksStoryParam) {
storyChecked = chapterArray?.includes(storyId);
}
if (this.hasChapterCheckTarget) {
if (storyChecked) {
@ -222,6 +226,15 @@ function fcn_toggleCheckmark(storyId, chapterId = null, set = null) {
type = 'chapter';
}
// Ensure data object is properly initialized
if (
typeof userData.checkmarks.data !== 'object' ||
userData.checkmarks.data === null ||
Array.isArray(userData.checkmarks.data)
) {
userData.checkmarks.data = {};
}
// Initialize if story is not yet tracked
if (!userData.checkmarks.data[storyId]) {
userData.checkmarks.data[storyId] = [];

View File

@ -151,6 +151,10 @@ application.register('fictioneer-follows', class extends Stimulus.Controller {
display.classList.remove('_new');
});
_$$('.follow-item').forEach(element => {
element.classList.remove('_new');
});
if (this.hasMobileMarkReadTarget) {
this.mobileMarkReadTarget.classList.add('hidden');
}
@ -258,6 +262,15 @@ function fcn_toggleFollow(storyId, set = null) {
const userData = FcnUtils.userData();
// Ensure data object is properly initialized
if (
typeof userData.follows.data !== 'object' ||
userData.follows.data === null ||
Array.isArray(userData.follows.data)
) {
userData.follows.data = {};
}
// Decide force if not given
set = set ?? !userData.follows.data[storyId];

View File

@ -44,7 +44,10 @@ application.register('fictioneer-mobile-menu', class extends Stimulus.Controller
const targetId = `mobile-frame-${frame}`;
this.editingBookmarks = false;
if (this.hasPanelBookmarksTarget) {
this.panelBookmarksTarget.dataset.editing = false;
}
if (this.hasFrameTarget) {
this.frameTargets.forEach(element => {

View File

@ -153,6 +153,15 @@ function fcn_toggleReminder(storyId, set = null) {
const userData = FcnUtils.userData();
// Ensure data object is properly initialized
if (
typeof userData.reminders.data !== 'object' ||
userData.reminders.data === null ||
Array.isArray(userData.reminders.data)
) {
userData.reminders.data = {};
}
// Decide force if not given
set = set ?? !userData.reminders.data[storyId];

View File

@ -283,7 +283,7 @@ function fcn_readTextStack() {
current.classList.remove('current-reading');
}
_$$$(fcn_currentReadingId).classList.add('current-reading');
_$$$(fcn_currentReadingId)?.classList.add('current-reading');
}
fcn_utter.text = fcn_ttsCurrentText;
@ -333,8 +333,7 @@ if (typeof speechSynthesis !== 'undefined' && fcn_ttsInterface) {
// Prepare items to read
fcn_ttsStack = fcn_ttsStack.flatMap(node => {
const result = [];
const inner = node.querySelector('.paragraph-inner');
const text = inner ? inner.textContent : node.textContent;
const text = FcnUtils.extractTextNodes(node);
// Split text into array of sentences using a regex pattern
const sentences = text.replace(regex, '$1|').split('|');

View File

@ -759,14 +759,28 @@ const FcnUtils = {
*
* @since 5.27.0
* @param {HTMLElement} element - The element.
* @param {Set<String>} allowedTags - Set of allowed tag names.
* @return {String} Extracted text or empty string.
*/
extractTextNodes(element) {
return Array.from(element.childNodes)
.filter(node => node.nodeType === Node.TEXT_NODE)
.map(node => node.textContent.trim())
.join(' ');
extractTextNodes(element, allowedTags = new Set(['strong', 'b', 'em', 'i', 'u', 'code', 'a', 's', 'kbd', 'sub', 'sup', 'span', 'label', 'button', 'ins', 'del', 'small', 'mark', 'q', 'abbr', 'time', 'cite'])) {
let result = '';
element.childNodes.forEach(node => {
if (node.nodeType === Node.TEXT_NODE) {
result += node.textContent.replace(/\r?\n/g, ' ');
} else if (node.nodeType === Node.ELEMENT_NODE) {
const tagName = node.tagName.toLowerCase();
if (tagName === 'br') {
result += ' ';
} else if (allowedTags.has(tagName)) {
result += FcnUtils.extractTextNodes(node, allowedTags);
}
}
});
return result.replace(/\s+/g, ' ').trim();
},
/**

View File

@ -7,6 +7,7 @@
}
.small-card-block,
.article-card-block,
.article-card-block .pagination {
margin-top: 2rem;
}

View File

@ -154,7 +154,7 @@
text-indent: 1.5rem;
}
:is(kbd, code, table, ul, ol, form, del, ins, mark, input, select, button, embed, iframe, sub, sup) {
:is(abbr, kbd, code, table, ul, ol, form, del, ins, mark, input, select, button, embed, iframe, sub, sup, i, em, b, strong) {
text-indent: 0;
}
}

View File

@ -28,7 +28,7 @@
.custom-logo {
height: var(--header-logo-height);
max-height: calc(var(--header-height) - 2rem);
width: fit-content;
width: auto;
max-width: 99vw;
object-fit: contain;
pointer-events: none; // Prevents drag and Edge image function nonsense

View File

@ -5,15 +5,14 @@
.icon-menu {
display: flex;
color: var(--navigation-color);
font-size: var(--navigation-font-size);
font-weight: var(--font-weight-navigation);
.menu-item {
:is(a, label, button):where(:not(._no-menu-item-style)) {
display: flex;
align-items: center;
color: var(--navigation-color);
font-size: inherit;
font-size: var(--navigation-font-size);
font-weight: var(--font-weight-navigation);
letter-spacing: 0;
padding: 0 10px;
height: var(--navigation-height);
@ -55,12 +54,6 @@
@include bp(desktop) {
border-radius: var(--layout-border-radius-small);
}
&.menu-item-has-children {
@include bp(desktop) {
border-radius: var(--layout-border-radius-small) var(--layout-border-radius-small) 0 0;
}
}
}
.sub-menu .menu-item :where(a, label, button):where(:not(._no-menu-item-style)) {

View File

@ -45,5 +45,6 @@
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition: opacity .3s;
}
}

View File

@ -204,12 +204,6 @@
@include bp(desktop) {
border-radius: var(--layout-border-radius-small);
}
&.menu-item-has-children {
@include bp(desktop) {
border-radius: var(--layout-border-radius-small) var(--layout-border-radius-small) 0 0;
}
}
}
}
}
@ -220,6 +214,17 @@
height: fit-content;
transition: background-color .1s;
&.menu-item-has-children {
transition: background-color .1s, border-radius .1s;
transition-delay: 0s, .3s;
&:hover {
@include bp(desktop) {
border-radius: var(--layout-border-radius-small) var(--layout-border-radius-small) 0 0;
}
}
}
&:not(:hover) {
.sub-menu {
pointer-events: none;
@ -387,6 +392,10 @@
transform: scale(1);
}
}
> :is(input, button, label, a) {
outline-offset: -2px;
}
}
}
}

View File

@ -10,11 +10,7 @@
.search-form[data-advanced="true"] {
.search-form__bar .search-form__advanced-toggle {
color: var(--fg-500);
&:hover {
color: var(--fg-300);
}
color: var(--button-primary-color-hover);
}
.search-form__current {
@ -44,6 +40,17 @@
padding-right: 96px;
width: 100%;
}
button {
--button-primary-background: transparent;
--button-primary-background-hover: transparent;
--button-primary-color: var(--fg-900);
--button-primary-color-hover: var(--fg-400);
display: grid;
place-content: center;
font-size: 12px;
min-width: 32px;
}
}
&__current {
@ -73,16 +80,6 @@
}
}
&__submit:is(button) {
--button-primary-background: transparent;
--button-primary-background-hover: transparent;
--button-primary-color: var(--fg-900);
--button-primary-color-hover: var(--fg-400);
display: grid;
place-content: center;
width: 32px;
}
&__select-group {
display: grid;
gap: 1rem;
@ -146,15 +143,12 @@
}
&-toggle {
display: grid;
place-content: center;
color: var(--fg-700);
font-size: 11px;
height: 32px;
color: var(--button-primary-color);
padding-left: 32px;
transition: color var(--transition-duration);
&:hover {
color: var(--fg-400);
color: var(--button-primary-color-hover);
}
}

View File

@ -977,7 +977,7 @@ figure.wp-block-pullquote:where(:not(.no-theme-style)) {
// LATEST COMMENTS
// =============================================================================
.wp-block-latest-comments:where(:not(.no-theme-style)) {
.wp-block-latest-comments:is(:not(.no-theme-style)) {
list-style: none;
padding-left: 0;
@ -1009,7 +1009,7 @@ figure.wp-block-pullquote:where(:not(.no-theme-style)) {
.wp-block-latest-comments__comment-avatar {
float: left;
margin: 3px 8px 4px 0;
margin: 3px 8px 3px 0;
border-radius: var(--layout-border-radius-small);
height: 32px;
width: 32px;

View File

@ -13,15 +13,11 @@ html:is(:root) {
-webkit-tap-highlight-color: transparent;
}
[hidden] {
display: none !important;
}
body:where(:not(.user-is-tabbing)) :is(input, select, textarea):focus {
body:where(:not(.user-is-tabbing)) :is(input, select, textarea, button, label, a):focus {
outline: none;
}
body:where(.user-is-tabbing) :is(input, select, textarea):focus {
input[type="checkbox"]:focus {
outline-offset: 3px;
}

View File

@ -304,7 +304,7 @@ html:not(.logged-in) body.logged-in .hide-if-logged-in,
html:not(.logged-in) body:not(.logged-in) .hide-if-logged-out,
.mobile-menu .hide-in-mobile-menu,
.site .hide-outside-mobile-menu,
.hidden,
.hidden, [hidden],
.inside-epub {
display: none !important;
content-visibility: hidden;

View File

@ -711,7 +711,7 @@ body {
// LATEST COMMENTS
// =============================================================================
.wp-block-latest-comments:where(:not(.no-theme-style)) {
.wp-block-latest-comments:is(:not(.no-theme-style)) {
list-style: none;
padding-left: 0 !important;
@ -734,7 +734,7 @@ body {
.wp-block-latest-comments__comment-avatar {
float: left;
margin: 3px 8px 4px 0;
margin: 3px 8px 3px 0;
border-radius: 2px;
height: 32px;
width: 32px;

View File

@ -11,7 +11,7 @@ License URI: http://www.gnu.org/licenses/gpl.html
Requires at least: 6.1.0
Tested up to: 6.7.1
Requires PHP: 7.4
Version: 5.27.0
Version: 5.27.2
Text Domain: fictioneer
Description: Standalone solution for publishing and reading web fictions.