Add custom CSS skins feature
This commit is contained in:
parent
c7d645ac28
commit
d169331627
@ -317,7 +317,7 @@ Fictioneer customizes WordPress by using as many standard action and filter hook
|
|||||||
| `wp_default_scripts` | `fictioneer_remove_jquery_migrate` (10)
|
| `wp_default_scripts` | `fictioneer_remove_jquery_migrate` (10)
|
||||||
| `wp_enqueue_scripts` | `fictioneer_add_custom_scripts` (10), `fictioneer_style_queue` (10), `fictioneer_output_customize_css` (9999), `fictioneer_output_customize_preview_css` (9999), `fictioneer_elementor_override_styles` (9999)
|
| `wp_enqueue_scripts` | `fictioneer_add_custom_scripts` (10), `fictioneer_style_queue` (10), `fictioneer_output_customize_css` (9999), `fictioneer_output_customize_preview_css` (9999), `fictioneer_elementor_override_styles` (9999)
|
||||||
| `wp_footer` | `fictioneer_render_category_submenu` (10), `fictioneer_render_tag_submenu` (10), `fictioneer_render_genre_submenu` (10), `fictioneer_render_fandom_submenu` (10), `fictioneer_render_character_submenu` (10), `fictioneer_render_warning_submenu` (10)
|
| `wp_footer` | `fictioneer_render_category_submenu` (10), `fictioneer_render_tag_submenu` (10), `fictioneer_render_genre_submenu` (10), `fictioneer_render_fandom_submenu` (10), `fictioneer_render_character_submenu` (10), `fictioneer_render_warning_submenu` (10)
|
||||||
| `wp_head` | `fictioneer_output_head_seo` (5), `fictioneer_output_rss` (10), `fictioneer_output_schemas` (10), `fictioneer_add_fiction_css` (10), `fictioneer_output_head_fonts` (5), `fictioneer_output_head_translations` (10), `fictioneer_remove_mu_registration_styles` (1), `fictioneer_output_mu_registration_style` (10), `fictioneer_output_head_meta` (1), `fictioneer_output_head_critical_scripts` (9999). `fictioneer_output_head_anti_flicker` (10), `fictioneer_cleanup_discord_meta` (10)
|
| `wp_head` | `fictioneer_output_head_seo` (5), `fictioneer_output_rss` (10), `fictioneer_output_schemas` (10), `fictioneer_add_fiction_css` (10), `fictioneer_output_head_fonts` (5), `fictioneer_output_head_translations` (10), `fictioneer_remove_mu_registration_styles` (1), `fictioneer_output_mu_registration_style` (10), `fictioneer_output_head_meta` (1), `fictioneer_output_head_critical_scripts` (9999). `fictioneer_output_head_anti_flicker` (10), `fictioneer_cleanup_discord_meta` (10), `fictioneer_output_critical_skin_scripts` (9999)
|
||||||
| `wp_insert_comment` | `fictioneer_delete_cached_story_card_by_comment` (10), `fictioneer_increment_story_comment_count` (10)
|
| `wp_insert_comment` | `fictioneer_delete_cached_story_card_by_comment` (10), `fictioneer_increment_story_comment_count` (10)
|
||||||
| `wp_logout` | `fictioneer_remove_logged_in_cookie` (10)
|
| `wp_logout` | `fictioneer_remove_logged_in_cookie` (10)
|
||||||
| `wp_update_nav_menu` | `fictioneer_purge_nav_menu_transients` (10)
|
| `wp_update_nav_menu` | `fictioneer_purge_nav_menu_transients` (10)
|
||||||
|
@ -640,6 +640,18 @@
|
|||||||
"oF" : 1,
|
"oF" : 1,
|
||||||
"pg" : 0
|
"pg" : 0
|
||||||
},
|
},
|
||||||
|
"\/css\/skin-template.css" : {
|
||||||
|
"aP" : 1,
|
||||||
|
"bl" : 0,
|
||||||
|
"ci" : 0,
|
||||||
|
"co" : 0,
|
||||||
|
"ft" : 16,
|
||||||
|
"ma" : 0,
|
||||||
|
"oA" : 1,
|
||||||
|
"oAP" : "\/css\/skin-template-min.css",
|
||||||
|
"oF" : 1,
|
||||||
|
"pg" : 0
|
||||||
|
},
|
||||||
"\/css\/splide.css" : {
|
"\/css\/splide.css" : {
|
||||||
"aP" : 1,
|
"aP" : 1,
|
||||||
"bl" : 0,
|
"bl" : 0,
|
||||||
@ -2145,14 +2157,6 @@
|
|||||||
"oAP" : "\/singular-canvas-site.php",
|
"oAP" : "\/singular-canvas-site.php",
|
||||||
"oF" : 1
|
"oF" : 1
|
||||||
},
|
},
|
||||||
"\/singular-dashboard.php" : {
|
|
||||||
"cB" : 0,
|
|
||||||
"ft" : 8192,
|
|
||||||
"hM" : 0,
|
|
||||||
"oA" : 1,
|
|
||||||
"oAP" : "\/singular-dashboard.php",
|
|
||||||
"oF" : 1
|
|
||||||
},
|
|
||||||
"\/singular-index-advanced.php" : {
|
"\/singular-index-advanced.php" : {
|
||||||
"cB" : 0,
|
"cB" : 0,
|
||||||
"ft" : 8192,
|
"ft" : 8192,
|
||||||
@ -2319,6 +2323,17 @@
|
|||||||
"sC" : 3,
|
"sC" : 3,
|
||||||
"tS" : 0
|
"tS" : 0
|
||||||
},
|
},
|
||||||
|
"\/src\/js\/critical-skin-script.js" : {
|
||||||
|
"bF" : 0,
|
||||||
|
"ft" : 64,
|
||||||
|
"ma" : 0,
|
||||||
|
"mi" : 1,
|
||||||
|
"oA" : 0,
|
||||||
|
"oAP" : "\/js\/critical-skin-script.min.js",
|
||||||
|
"oF" : 2,
|
||||||
|
"sC" : 3,
|
||||||
|
"tS" : 0
|
||||||
|
},
|
||||||
"\/src\/js\/customizer.js" : {
|
"\/src\/js\/customizer.js" : {
|
||||||
"bF" : 0,
|
"bF" : 0,
|
||||||
"ft" : 64,
|
"ft" : 64,
|
||||||
@ -3819,21 +3834,6 @@
|
|||||||
"pg" : 0,
|
"pg" : 0,
|
||||||
"sct" : 0
|
"sct" : 0
|
||||||
},
|
},
|
||||||
"\/src\/scss\/dashboard.scss" : {
|
|
||||||
"aP" : 0,
|
|
||||||
"bl" : 0,
|
|
||||||
"co" : 0,
|
|
||||||
"dP" : 10,
|
|
||||||
"ec" : 0,
|
|
||||||
"ft" : 4,
|
|
||||||
"ma" : 0,
|
|
||||||
"oA" : 0,
|
|
||||||
"oAP" : "\/css\/dashboard.css",
|
|
||||||
"oF" : 2,
|
|
||||||
"oS" : 3,
|
|
||||||
"pg" : 0,
|
|
||||||
"sct" : 0
|
|
||||||
},
|
|
||||||
"\/src\/scss\/editor.scss" : {
|
"\/src\/scss\/editor.scss" : {
|
||||||
"aP" : 0,
|
"aP" : 0,
|
||||||
"bl" : 0,
|
"bl" : 0,
|
||||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
8
css/skin-template.css
Normal file
8
css/skin-template.css
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
/**
|
||||||
|
* Name: SKIN NAME
|
||||||
|
* Author: YOUR NAME
|
||||||
|
* Version: 1.0.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* === Add your CSS below these lines (and remove them to save bytes) =================== */
|
||||||
|
/* === https://github.com/Tetrakern/fictioneer/blob/main/INSTALLATION.md#css-snippets === */
|
@ -634,6 +634,15 @@ if ( get_option( 'fictioneer_enable_bookmarks' ) && is_admin() ) {
|
|||||||
require_once __DIR__ . '/includes/functions/users/_bookmarks.php';
|
require_once __DIR__ . '/includes/functions/users/_bookmarks.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the skins feature.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( get_option( 'fictioneer_enable_css_skins' ) && is_admin() ) {
|
||||||
|
// Only used for AJAX
|
||||||
|
require_once __DIR__ . '/includes/functions/users/_skins.php';
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add content helper functions.
|
* Add content helper functions.
|
||||||
*/
|
*/
|
||||||
|
@ -1810,7 +1810,7 @@ if ( get_option( 'fictioneer_enable_anti_flicker' ) ) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// OUTPUT HEAD CRITICAL SCRIPTS
|
// OUTPUT CRITICAL SCRIPTS
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1833,6 +1833,22 @@ function fictioneer_output_head_critical_scripts() {
|
|||||||
}
|
}
|
||||||
add_action( 'wp_head', 'fictioneer_output_head_critical_scripts', 9999 );
|
add_action( 'wp_head', 'fictioneer_output_head_critical_scripts', 9999 );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outputs critical skin scripts in the <head>
|
||||||
|
*
|
||||||
|
* @since 5.26.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fictioneer_output_critical_skin_scripts() {
|
||||||
|
// Start HTML ---> ?>
|
||||||
|
<script id="fictioneer-skin-script" type="text/javascript" data-jetpack-boost="ignore" data-no-optimize="1" data-no-defer="1" data-no-minify="1">!function(){const e="fcnLoggedIn=",t=document.cookie.split(";");let n=null;for(var c=0;c<t.length;c++){const i=t[c].trim();if(0==i.indexOf(e)){n=decodeURIComponent(i.substring(12,i.length));break}}if(!n)return;const i=JSON.parse(localStorage.getItem("fcnSkins"))??{data:{},active:null,fingerprint:n};if(i?.data?.[i.active]?.css&&n===i?.fingerprint){const e=document.createElement("style");e.textContent=i.data[i.active].css,e.id="fictioneer-active-custom-skin",document.querySelector("head").appendChild(e)}}();</script>
|
||||||
|
<?php // <--- End HTML
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( get_option( 'fictioneer_enable_css_skins' ) ) {
|
||||||
|
add_action( 'wp_head', 'fictioneer_output_critical_skin_scripts', 9999 );
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// ADD EXCERPTS TO PAGES
|
// ADD EXCERPTS TO PAGES
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
|
@ -1468,6 +1468,8 @@ function fictioneer_fast_ajax() {
|
|||||||
'fictioneer_ajax_get_auth',
|
'fictioneer_ajax_get_auth',
|
||||||
'fictioneer_ajax_get_user_data',
|
'fictioneer_ajax_get_user_data',
|
||||||
'fictioneer_ajax_get_avatar',
|
'fictioneer_ajax_get_avatar',
|
||||||
|
'fictioneer_ajax_save_skins',
|
||||||
|
'fictioneer_ajax_get_skins',
|
||||||
// Admin
|
// Admin
|
||||||
'fictioneer_ajax_query_relationship_posts',
|
'fictioneer_ajax_query_relationship_posts',
|
||||||
'fictioneer_ajax_search_posts_to_unlock'
|
'fictioneer_ajax_search_posts_to_unlock'
|
||||||
|
@ -86,7 +86,6 @@ function fictioneer_account_password( $args ) {
|
|||||||
|
|
||||||
if ( current_user_can( 'fcn_admin_panel_access' ) && get_option( 'fictioneer_show_wp_login_link' ) ) {
|
if ( current_user_can( 'fcn_admin_panel_access' ) && get_option( 'fictioneer_show_wp_login_link' ) ) {
|
||||||
add_action( 'fictioneer_account_content', 'fictioneer_account_password', 11 );
|
add_action( 'fictioneer_account_content', 'fictioneer_account_password', 11 );
|
||||||
}
|
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// ACCOUNT OAUTH BINDINGS SECTION
|
// ACCOUNT OAUTH BINDINGS SECTION
|
||||||
@ -108,6 +107,31 @@ function fictioneer_account_oauth( $args ) {
|
|||||||
get_template_part( 'partials/account/_oauth', null, $args );
|
get_template_part( 'partials/account/_oauth', null, $args );
|
||||||
}
|
}
|
||||||
add_action( 'fictioneer_account_content', 'fictioneer_account_oauth', 20 );
|
add_action( 'fictioneer_account_content', 'fictioneer_account_oauth', 20 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SITE SKINS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outputs the HTML for the site skins section
|
||||||
|
*
|
||||||
|
* @since 5.26.0
|
||||||
|
*
|
||||||
|
* @param WP_User $args['user'] Current user.
|
||||||
|
* @param boolean $args['is_admin'] True if the user is an administrator.
|
||||||
|
* @param boolean $args['is_author'] True if the user is an author (by capabilities).
|
||||||
|
* @param boolean $args['is_editor'] True if the user is an editor.
|
||||||
|
* @param boolean $args['is_moderator'] True if the user is a moderator (by capabilities).
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fictioneer_account_site_skins( $args ) {
|
||||||
|
get_template_part( 'partials/account/_skins', null, $args );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( get_option( 'fictioneer_enable_css_skins' ) ) {
|
||||||
|
add_action( 'fictioneer_account_content', 'fictioneer_account_site_skins', 25 );
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// ACCOUNT DATA SECTION
|
// ACCOUNT DATA SECTION
|
||||||
|
@ -731,6 +731,12 @@ define( 'FICTIONEER_OPTIONS', array(
|
|||||||
'group' => 'fictioneer-settings-general-group',
|
'group' => 'fictioneer-settings-general-group',
|
||||||
'sanitize_callback' => 'fictioneer_sanitize_checkbox',
|
'sanitize_callback' => 'fictioneer_sanitize_checkbox',
|
||||||
'default' => 0
|
'default' => 0
|
||||||
|
),
|
||||||
|
'fictioneer_enable_css_skins' => array(
|
||||||
|
'name' => 'fictioneer_enable_css_skins',
|
||||||
|
'group' => 'fictioneer-settings-general-group',
|
||||||
|
'sanitize_callback' => 'fictioneer_sanitize_checkbox',
|
||||||
|
'default' => 0
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
'integers' => array(
|
'integers' => array(
|
||||||
@ -1204,6 +1210,7 @@ function fictioneer_get_option_label( $option ) {
|
|||||||
'fictioneer_show_story_modified_date' => __( 'Show the modified date on story pages', 'fictioneer' ),
|
'fictioneer_show_story_modified_date' => __( 'Show the modified date on story pages', 'fictioneer' ),
|
||||||
'fictioneer_disable_chapter_list_transients' => __( 'Disable chapter list Transient caching', 'fictioneer' ),
|
'fictioneer_disable_chapter_list_transients' => __( 'Disable chapter list Transient caching', 'fictioneer' ),
|
||||||
'fictioneer_disable_shortcode_transients' => __( 'Disable shortcode Transient caching', 'fictioneer' ),
|
'fictioneer_disable_shortcode_transients' => __( 'Disable shortcode Transient caching', 'fictioneer' ),
|
||||||
|
'fictioneer_enable_css_skins' => __( 'Enable CSS skins (requires account)', 'fictioneer' ),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -742,6 +742,16 @@ $images = get_template_directory_uri() . '/img/documentation/';
|
|||||||
?>
|
?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="fictioneer-card__row">
|
||||||
|
<?php
|
||||||
|
fictioneer_settings_label_checkbox(
|
||||||
|
'fictioneer_enable_css_skins',
|
||||||
|
__( 'Enable CSS skins (requires account)', 'fictioneer' ),
|
||||||
|
__( 'Allows logged-in users to apply custom styles.', 'fictioneer' )
|
||||||
|
);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="fictioneer-card__row">
|
<div class="fictioneer-card__row">
|
||||||
<?php
|
<?php
|
||||||
fictioneer_settings_label_checkbox(
|
fictioneer_settings_label_checkbox(
|
||||||
|
136
includes/functions/users/_skins.php
Normal file
136
includes/functions/users/_skins.php
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// AJAX: SAVE SKINS FOR USERS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Saves skins JSON
|
||||||
|
*
|
||||||
|
* Note: Skins are not evaluated server-side, only stored as JSON string.
|
||||||
|
* Everything else happens client-side.
|
||||||
|
*
|
||||||
|
* @since 5.26.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fictioneer_ajax_save_skins() {
|
||||||
|
// Enabled?
|
||||||
|
if ( ! get_option( 'fictioneer_enable_css_skins' ) ) {
|
||||||
|
wp_send_json_error( null, 403 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit
|
||||||
|
fictioneer_check_rate_limit( 'fictioneer_ajax_save_skins', 5 );
|
||||||
|
|
||||||
|
// Setup and validations
|
||||||
|
$user = fictioneer_get_validated_ajax_user();
|
||||||
|
|
||||||
|
if ( ! $user ) {
|
||||||
|
wp_send_json_error( array( 'error' => 'Request did not pass validation.' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( empty( $_POST['skins'] ?? 0 ) ) {
|
||||||
|
wp_send_json_error( array( 'error' => 'Missing arguments.' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanitize
|
||||||
|
$skins = sanitize_text_field( $_POST['skins'] );
|
||||||
|
|
||||||
|
if ( $skins && fictioneer_is_valid_json( wp_unslash( $skins ) ) ) {
|
||||||
|
// Inspect
|
||||||
|
$decoded = json_decode( wp_unslash( $skins ), true );
|
||||||
|
$fingerprint = fictioneer_get_user_fingerprint( $user->ID );
|
||||||
|
|
||||||
|
if ( ! $decoded || ! isset( $decoded['data'] ) ) {
|
||||||
|
wp_send_json_error( array( 'error' => 'Invalid JSON (SKIN-001).' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ( $decoded['fingerprint'] ?? 0 ) !== $fingerprint ) {
|
||||||
|
wp_send_json_error( array( 'error' => 'Invalid JSON (SKIN-002).' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( count( $decoded ) > 3 ) {
|
||||||
|
wp_send_json_error( array( 'error' => 'Invalid JSON (SKIN-003).' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach( $decoded['data'] as $sub_array ) {
|
||||||
|
if ( ! is_array( $sub_array ) ) {
|
||||||
|
wp_send_json_error( array( 'error' => 'Invalid JSON (SKIN-004).' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( count( $sub_array ) > 4 ) {
|
||||||
|
wp_send_json_error( array( 'error' => 'Invalid JSON (SKIN-005).' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed_keys = ['name', 'author', 'version', 'css'];
|
||||||
|
|
||||||
|
foreach ( $sub_array as $sub_key => $value ) {
|
||||||
|
if ( ! in_array( $sub_key, $allowed_keys ) ) {
|
||||||
|
wp_send_json_error( array( 'error' => 'Invalid JSON (SKIN-006).' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $sub_key === 'css' && fictioneer_sanitize_css( $value ) === '' ) {
|
||||||
|
wp_send_json_error( array( 'error' => 'Invalid CSS (SKIN-007).' ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Easier than checking whether the skins have changed
|
||||||
|
delete_user_meta( $user->ID, 'fictioneer_skins' );
|
||||||
|
|
||||||
|
if ( update_user_meta( $user->ID, 'fictioneer_skins', $skins ) ) {
|
||||||
|
wp_send_json_success( array( 'message' => __( 'Skins uploaded successfully.', 'fictioneer' ) ) );
|
||||||
|
} else {
|
||||||
|
wp_send_json_error( array( 'error' => 'Skins could not be uploaded.' ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Something went wrong if we end up here...
|
||||||
|
wp_send_json_error( array( 'error' => 'An unknown error occurred.' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( get_option( 'fictioneer_enable_css_skins' ) ) {
|
||||||
|
add_action( 'wp_ajax_fictioneer_ajax_save_skins', 'fictioneer_ajax_save_skins' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Sends skins JSON
|
||||||
|
*
|
||||||
|
* @since 5.26.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fictioneer_ajax_get_skins() {
|
||||||
|
// Enabled?
|
||||||
|
if ( ! get_option( 'fictioneer_enable_css_skins' ) ) {
|
||||||
|
wp_send_json_error( null, 403 );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limit
|
||||||
|
fictioneer_check_rate_limit( 'fictioneer_ajax_get_skins', 5 );
|
||||||
|
|
||||||
|
// Setup and validations
|
||||||
|
$user = fictioneer_get_validated_ajax_user();
|
||||||
|
|
||||||
|
if ( ! $user ) {
|
||||||
|
wp_send_json_error( array( 'error' => 'Request did not pass validation.' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// Look for saved skins on user...
|
||||||
|
$skins = get_user_meta( $user->ID, 'fictioneer_skins', true );
|
||||||
|
|
||||||
|
// Response
|
||||||
|
if ( $skins ) {
|
||||||
|
wp_send_json_success(
|
||||||
|
array(
|
||||||
|
'skins' => $skins,
|
||||||
|
'message' => __( 'Skins downloaded and applied successfully.', 'fictioneer' )
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_send_json_error();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( get_option( 'fictioneer_enable_css_skins' ) ) {
|
||||||
|
add_action( 'wp_ajax_fictioneer_ajax_get_skins', 'fictioneer_ajax_get_skins' );
|
||||||
|
}
|
10
js/complete.min.js
vendored
10
js/complete.min.js
vendored
File diff suppressed because one or more lines are too long
1
js/critical-skin-script.min.js
vendored
Normal file
1
js/critical-skin-script.min.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
!function(){const e="fcnLoggedIn=",t=document.cookie.split(";");let n=null;for(var c=0;c<t.length;c++){const i=t[c].trim();if(0==i.indexOf(e)){n=decodeURIComponent(i.substring(12,i.length));break}}if(!n)return;const i=JSON.parse(localStorage.getItem("fcnSkins"))??{data:{},active:null,fingerprint:n};if(i?.data?.[i.active]?.css&&n===i?.fingerprint){const e=document.createElement("style");e.textContent=i.data[i.active].css,e.id="fictioneer-active-custom-skin",document.querySelector("head").appendChild(e)}}();
|
2
js/user-profile.min.js
vendored
2
js/user-profile.min.js
vendored
File diff suppressed because one or more lines are too long
2
js/utility.min.js
vendored
2
js/utility.min.js
vendored
File diff suppressed because one or more lines are too long
89
partials/account/_skins.php
Normal file
89
partials/account/_skins.php
Normal file
@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Partial: Account - Skins
|
||||||
|
*
|
||||||
|
* @package WordPress
|
||||||
|
* @subpackage Fictioneer
|
||||||
|
* @since 5.26.0
|
||||||
|
*
|
||||||
|
* @internal $args['user'] Current user.
|
||||||
|
* @internal $args['is_admin'] True if the user is an administrator.
|
||||||
|
* @internal $args['is_author'] True if the user is an author (by capabilities).
|
||||||
|
* @internal $args['is_editor'] True if the user is an editor.
|
||||||
|
* @internal $args['is_moderator'] True if the user is a moderator (by capabilities).
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
// No direct access!
|
||||||
|
defined( 'ABSPATH' ) OR exit;
|
||||||
|
|
||||||
|
// Cookie set?
|
||||||
|
if ( ! isset( $_COOKIE['fcnLoggedIn'] ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup
|
||||||
|
$current_user = $args['user'];
|
||||||
|
$template_download = esc_url( get_template_directory_uri() . '/css/skin-template.css' );
|
||||||
|
|
||||||
|
$translations = array(
|
||||||
|
'wrongFileType' => _x( 'Wrong file type. Please upload a valid CSS file.', 'Custom CSS skin.', 'fictioneer' ),
|
||||||
|
'invalidCss' => _x( 'Invalid file contents. Please check for missing braces ("{}") or forbidden characters ("<").', 'Custom CSS skin.', 'fictioneer' ),
|
||||||
|
'missingMetaData' => _x( 'File is missing the "Name" meta data.', 'Custom CSS skin.', 'fictioneer' ),
|
||||||
|
'tooManySkins' => _x( 'You cannot upload more than 3 skins.', 'Custom CSS skin.', 'fictioneer' ),
|
||||||
|
'wrongFingerprint' => _x( 'Wrong or missing fingerprint hash.', 'Custom CSS skin.', 'fictioneer' ),
|
||||||
|
'fileTooLarge' => _x( 'Maximum file size of 200 KB exceeded.', 'Custom CSS skin.', 'fictioneer' ),
|
||||||
|
'invalidJson' => _x( 'Invalid JSON.', 'Custom CSS skin.', 'fictioneer' ),
|
||||||
|
'error' => _x( 'Error.', 'Custom CSS skin.', 'fictioneer' )
|
||||||
|
);
|
||||||
|
|
||||||
|
?>
|
||||||
|
|
||||||
|
<h3 class="profile__skins-headline"><?php _e( 'Skins', 'fictioneer' ); ?></h3>
|
||||||
|
|
||||||
|
<p class="profile__description"><?php
|
||||||
|
_e( 'You can add up to three custom CSS skins (max. 200 KB each), with one active at a time. These skins apply only to your current device and browser (and may vanish at any time), but you can manually sync them up and down with your account. Be cautious about which skin you trust, as they can mess up the site.', 'fictioneer' );
|
||||||
|
?></p>
|
||||||
|
|
||||||
|
<p class="profile__description"><?php
|
||||||
|
printf(
|
||||||
|
__( 'To create your own skin, <a href="%s" download>download this template</a>.', 'fictioneer' ),
|
||||||
|
$template_download
|
||||||
|
);
|
||||||
|
?></p>
|
||||||
|
|
||||||
|
<template id="template-custom-skin">
|
||||||
|
<div class="custom-skin">
|
||||||
|
<button type="button" class="custom-skin__toggle" data-click-action="skin-toggle">
|
||||||
|
<i class="fa-regular fa-circle off"></i>
|
||||||
|
<i class="fa-solid fa-circle-dot on"></i>
|
||||||
|
</button>
|
||||||
|
<div class="custom-skin__info">
|
||||||
|
<span class="custom-skin__name" data-finder="skin-name">—</span>
|
||||||
|
<span class="custom-skin__spacer"></span>
|
||||||
|
<span class="custom-skin__version" data-finder="skin-version">—</span>
|
||||||
|
<span class="custom-skin__spacer"></span>
|
||||||
|
<span class="custom-skin__author" data-finder="skin-author">—</span>
|
||||||
|
</div>
|
||||||
|
<button type="button" class="custom-skin__delete" data-click-action="skin-delete"><i class="fa-solid fa-trash-can"></i></button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="profile__segment">
|
||||||
|
|
||||||
|
<div class="custom-skin-list"></div>
|
||||||
|
|
||||||
|
<form id="css-upload-form" class="custom-skin _upload">
|
||||||
|
<script type="text/javascript" data-jetpack-boost="ignore" data-no-optimize="1" data-no-defer="1" data-no-minify="1">
|
||||||
|
var fcn_skinTranslations = <?php echo json_encode( $translations ); ?>;
|
||||||
|
</script>
|
||||||
|
<input type="file" id="css-file" name="css-file" accept=".css">
|
||||||
|
<i class="fa-solid fa-plus"></i>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="profile__actions">
|
||||||
|
<button type="button" class="button" data-click-action="skins-sync-up" data-disable-with="<?php esc_attr_e( 'Uploading…', 'fictioneer' ); ?>"><?php _e( 'Sync Up', 'fictioneer' ); ?></button>
|
||||||
|
<button type="button" class="button" data-click-action="skins-sync-down" data-disable-with="<?php esc_attr_e( 'Downloading…', 'fictioneer' ); ?>"><?php _e( 'Sync Down', 'fictioneer' ); ?></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
32
src/js/critical-skin-script.js
Normal file
32
src/js/critical-skin-script.js
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
// To be minified and added to the head
|
||||||
|
|
||||||
|
(function() {
|
||||||
|
const name = 'fcnLoggedIn=';
|
||||||
|
const cookies = document.cookie.split(';');
|
||||||
|
|
||||||
|
let fingerprint = null;
|
||||||
|
|
||||||
|
for (var i = 0; i < cookies.length; i++) {
|
||||||
|
const c = cookies[i].trim();
|
||||||
|
|
||||||
|
if (c.indexOf(name) == 0) {
|
||||||
|
fingerprint = decodeURIComponent(c.substring(name.length, c.length));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fingerprint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const skins = JSON.parse(localStorage.getItem('fcnSkins')) ?? { data: {}, active: null, fingerprint: fingerprint };
|
||||||
|
|
||||||
|
if (skins?.data?.[skins.active]?.css && fingerprint === skins?.fingerprint) {
|
||||||
|
const styleTag = document.createElement('style');
|
||||||
|
|
||||||
|
styleTag.textContent = skins.data[skins.active].css;
|
||||||
|
styleTag.id = 'fictioneer-active-custom-skin';
|
||||||
|
|
||||||
|
document.querySelector('head').appendChild(styleTag);
|
||||||
|
}
|
||||||
|
})();
|
@ -375,3 +375,417 @@ _$('.button-clear-bookmarks')?.addEventListener(
|
|||||||
fcn_setBookmarks(fcn_bookmarks);
|
fcn_setBookmarks(fcn_bookmarks);
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// CSS SKINS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the skins JSON from local storage or a default.
|
||||||
|
*
|
||||||
|
* @since 5.26.0
|
||||||
|
* @return {Object} The skins JSON.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fcn_getSkins() {
|
||||||
|
const fingerprint = fcn_getCookie('fcnLoggedIn');
|
||||||
|
|
||||||
|
if (!fingerprint) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _default = { 'data': {}, 'active': null, 'fingerprint': fingerprint };
|
||||||
|
const skins = fcn_parseJSON(localStorage.getItem('fcnSkins')) ?? _default;
|
||||||
|
|
||||||
|
if (!skins?.fingerprint || fingerprint !== skins.fingerprint) {
|
||||||
|
return _default;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof skins.data !== 'object' || Array.isArray(skins.data)) {
|
||||||
|
skins.data = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return skins;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Saves the skins JSON to local storage.
|
||||||
|
*
|
||||||
|
* @since 5.26.0
|
||||||
|
* @param {Object} skins - The skins to store.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fcn_setSkins(skins) {
|
||||||
|
if (
|
||||||
|
typeof skins !== 'object' || skins === null ||
|
||||||
|
typeof skins.data !== 'object' || Array.isArray(skins.data)
|
||||||
|
) {
|
||||||
|
fcn_showNotification(fcn_skinTranslations.invalidJson, 3, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!skins?.fingerprint || fcn_getCookie('fcnLoggedIn') !== skins.fingerprint) {
|
||||||
|
fcn_showNotification(fcn_skinTranslations.wrongFingerprint, 3, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
localStorage.setItem('fcnSkins', JSON.stringify(skins));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns info object about a CSS skin.
|
||||||
|
*
|
||||||
|
* @since 5.26.0
|
||||||
|
* @param {String} css - The CSS to analyze.
|
||||||
|
* @return {Object} CSS info with name, author, and version.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fcn_getSkinInfo(css) {
|
||||||
|
const nameMatch = css.match(/Name:\s*(.+)/);
|
||||||
|
const authorMatch = css.match(/Author:\s*(.+)/);
|
||||||
|
const versionMatch = css.match(/Version:\s*(.+)/);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: nameMatch ? fcn_sanitizeHTML(nameMatch[1].trim()) : null,
|
||||||
|
author: authorMatch ? fcn_sanitizeHTML(authorMatch[1].trim()) : null,
|
||||||
|
version: versionMatch ? fcn_sanitizeHTML(versionMatch[1].trim()) : null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles the currently selected skin (custom CSS).
|
||||||
|
*
|
||||||
|
* @since 5.26.0
|
||||||
|
* @param {HTMLElement} target - The clicked element.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fcn_toggleSkin(target) {
|
||||||
|
const item = target.closest('.custom-skin');
|
||||||
|
const skins = fcn_getSkins();
|
||||||
|
|
||||||
|
if (item.classList.contains('active')) {
|
||||||
|
_$$('.custom-skin').forEach(element => element.classList.remove('active'));
|
||||||
|
skins.active = null;
|
||||||
|
} else {
|
||||||
|
_$$('.custom-skin').forEach(element => element.classList.remove('active'));
|
||||||
|
item.classList.add('active');
|
||||||
|
skins.active = target.dataset.skinId;
|
||||||
|
}
|
||||||
|
|
||||||
|
fcn_setSkins(skins);
|
||||||
|
fcn_applySkin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the skin (custom CSS).
|
||||||
|
*
|
||||||
|
* @since 5.26.0
|
||||||
|
* @param {HTMLElement} target - The clicked element.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fcn_deleteSkin(target) {
|
||||||
|
const item = target.closest('.custom-skin');
|
||||||
|
const skins = fcn_getSkins();
|
||||||
|
|
||||||
|
if (item.classList.contains('active')) {
|
||||||
|
skins.active = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete skins.data[target.dataset.skinId];
|
||||||
|
_$('#css-upload-form > input').value = '';
|
||||||
|
|
||||||
|
fcn_setSkins(skins);
|
||||||
|
fcn_renderSkinList();
|
||||||
|
fcn_applySkin();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add skin (custom CSS) to document <head>.
|
||||||
|
*
|
||||||
|
* @since 5.26.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fcn_applySkin() {
|
||||||
|
const fingerprint = fcn_getCookie('fcnLoggedIn');
|
||||||
|
|
||||||
|
// Ensure the theme login check is passed
|
||||||
|
if (!fingerprint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get skins from local storage
|
||||||
|
const skins = fcn_getSkins();
|
||||||
|
|
||||||
|
// Cleanup old style tag (if any)
|
||||||
|
_$$$('fictioneer-active-custom-skin')?.remove();
|
||||||
|
|
||||||
|
// Check fingerprint
|
||||||
|
if (skins?.fingerprint !== fingerprint) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if skins data is valid and an active skin is set
|
||||||
|
if (skins?.data?.[skins.active]?.css) {
|
||||||
|
const styleTag = document.createElement('style');
|
||||||
|
|
||||||
|
styleTag.textContent = skins.data[skins.active].css;
|
||||||
|
styleTag.id = 'fictioneer-active-custom-skin';
|
||||||
|
|
||||||
|
_$('head').appendChild(styleTag);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders list of selectable skins.
|
||||||
|
*
|
||||||
|
* @since 5.26.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fcn_renderSkinList() {
|
||||||
|
const container = _$('.custom-skin-list');
|
||||||
|
const fingerprint = fcn_getCookie('fcnLoggedIn');
|
||||||
|
|
||||||
|
// Ensure the theme login check is passed
|
||||||
|
if (!fingerprint || !container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get skins from local storage
|
||||||
|
const skins = fcn_getSkins();
|
||||||
|
|
||||||
|
// Clear previous content
|
||||||
|
container.innerHTML = '';
|
||||||
|
|
||||||
|
// Check fingerprint
|
||||||
|
if (skins?.fingerprint !== fingerprint) {
|
||||||
|
_$$$('css-upload-form').style.display = '';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure skins data exists and has entries
|
||||||
|
if (skins?.data && Object.keys(skins.data).length > 0) {
|
||||||
|
// Loop through the skins and render them
|
||||||
|
Object.entries(skins.data).forEach(([key, skin]) => {
|
||||||
|
const template = _$$$('template-custom-skin').content.cloneNode(true);
|
||||||
|
|
||||||
|
// Active skin?
|
||||||
|
if (skins.active === key) {
|
||||||
|
template.querySelector('.custom-skin').classList.add('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fill template with skin data
|
||||||
|
template.querySelector('[data-click-action="skin-toggle"]').dataset.skinId = key;
|
||||||
|
template.querySelector('[data-click-action="skin-delete"]').dataset.skinId = key;
|
||||||
|
template.querySelector('[data-finder="skin-name"]').innerText = skin.name;
|
||||||
|
template.querySelector('[data-finder="skin-version"]').innerText = skin.version;
|
||||||
|
template.querySelector('[data-finder="skin-author"]').innerText = skin.author;
|
||||||
|
|
||||||
|
// Append the template to the container
|
||||||
|
container.appendChild(template);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add click events
|
||||||
|
_$$('[data-click-action="skin-toggle"]').forEach(button => {
|
||||||
|
button.addEventListener('click', event => fcn_toggleSkin(event.currentTarget));
|
||||||
|
});
|
||||||
|
|
||||||
|
_$$('[data-click-action="skin-delete"]').forEach(button => {
|
||||||
|
button.addEventListener('click', event => fcn_deleteSkin(event.currentTarget));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(skins.data).length > 2) {
|
||||||
|
_$$$('css-upload-form').style.display = 'none';
|
||||||
|
} else {
|
||||||
|
_$$$('css-upload-form').style.display = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
fcn_renderSkinList();
|
||||||
|
|
||||||
|
// Upload
|
||||||
|
_$('#css-upload-form > input')?.addEventListener('input', event => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const input = _$$$('css-file');
|
||||||
|
const file = input.files[0];
|
||||||
|
const skins = fcn_getSkins();
|
||||||
|
const fingerprint = fcn_getCookie('fcnLoggedIn');
|
||||||
|
|
||||||
|
if (Object.keys(skins.data).length > 2) {
|
||||||
|
fcn_showNotification(fcn_skinTranslations.tooManySkins, 3, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skins?.fingerprint !== fingerprint) {
|
||||||
|
fcn_showNotification(fcn_skinTranslations.wrongFingerprint, 3, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!file) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > 200000) {
|
||||||
|
fcn_showNotification(fcn_skinTranslations.fileTooLarge, 3, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.type !== 'text/css') {
|
||||||
|
fcn_showNotification(fcn_skinTranslations.wrongFileType, 3, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const reader = new FileReader();
|
||||||
|
|
||||||
|
reader.onload = event => {
|
||||||
|
const css = event.target.result;
|
||||||
|
const info = fcn_getSkinInfo(css);
|
||||||
|
const skins = fcn_getSkins();
|
||||||
|
|
||||||
|
if (!fcn_validateCss(css)) {
|
||||||
|
fcn_showNotification(fcn_skinTranslations.invalidCss, 5, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!info.name) {
|
||||||
|
fcn_showNotification(fcn_skinTranslations.missingMetaData, 3, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = btoa(info.name);
|
||||||
|
|
||||||
|
skins.data[key] = {
|
||||||
|
name: info.name,
|
||||||
|
version: info.version,
|
||||||
|
author: info.author,
|
||||||
|
css: css
|
||||||
|
};
|
||||||
|
|
||||||
|
fcn_setSkins(skins);
|
||||||
|
fcn_renderSkinList();
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.onerror = () => {
|
||||||
|
console.error(reader.error);
|
||||||
|
fcn_showNotification(reader.error, 3, 'warning');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
reader.readAsText(file);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Uploads the skins to the database.
|
||||||
|
*
|
||||||
|
* @since 5.26.0
|
||||||
|
* @param {HTMLElement} trigger - The event trigger element.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fcn_uploadSkins(trigger) {
|
||||||
|
// Ensure the theme login check is passed
|
||||||
|
if (!fcn_isUserLoggedIn() || trigger.classList.contains('disabled')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get skins from local storage
|
||||||
|
const skins = fcn_getSkins();
|
||||||
|
|
||||||
|
// Toggle button progress
|
||||||
|
fcn_toggleInProgress(trigger);
|
||||||
|
|
||||||
|
// Request
|
||||||
|
fcn_ajaxPost({
|
||||||
|
'action': 'fictioneer_ajax_save_skins',
|
||||||
|
'fcn_fast_ajax': 1,
|
||||||
|
'skins': JSON.stringify(skins)
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
fcn_showNotification(response.data.message, 3, 'success');
|
||||||
|
} else {
|
||||||
|
fcn_showNotification(
|
||||||
|
response.data.failure ?? response.data.error ?? fictioneer_tl.notification.error,
|
||||||
|
3,
|
||||||
|
'warning'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make sure the actual error (if any) is printed to the console too
|
||||||
|
if (response.data.error || response.data.failure) {
|
||||||
|
console.error('Error:', response.data.error ?? response.data.failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error.status && error.statusText) {
|
||||||
|
fcn_showNotification(`${error.status}: ${error.statusText}`, 3, 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
fcn_toggleInProgress(trigger);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_$('[data-click-action="skins-sync-up"]')?.addEventListener('click', event => {
|
||||||
|
fcn_uploadSkins(event.currentTarget);
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* AJAX: Downloads the skins from the database.
|
||||||
|
*
|
||||||
|
* @since 5.26.0
|
||||||
|
* @param {HTMLElement} trigger - The event trigger element.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fcn_downloadSkins(trigger) {
|
||||||
|
// Ensure the theme login check is passed
|
||||||
|
if (!fcn_isUserLoggedIn() || trigger.classList.contains('disabled')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toggle button progress
|
||||||
|
fcn_toggleInProgress(trigger);
|
||||||
|
|
||||||
|
// Request
|
||||||
|
fcn_ajaxPost({
|
||||||
|
'action': 'fictioneer_ajax_get_skins',
|
||||||
|
'fcn_fast_ajax': 1
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
if (response.success) {
|
||||||
|
fcn_showNotification(response.data.message, 3, 'success');
|
||||||
|
fcn_setSkins(JSON.parse(response.data.skins));
|
||||||
|
fcn_renderSkinList();
|
||||||
|
fcn_applySkin()
|
||||||
|
} else {
|
||||||
|
fcn_showNotification(
|
||||||
|
response.data.failure ?? response.data.error ?? fictioneer_tl.notification.error,
|
||||||
|
3,
|
||||||
|
'warning'
|
||||||
|
);
|
||||||
|
|
||||||
|
// Make sure the actual error (if any) is printed to the console too
|
||||||
|
if (response.data.error || response.data.failure) {
|
||||||
|
console.error('Error:', response.data.error ?? response.data.failure);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
if (error.status && error.statusText) {
|
||||||
|
fcn_showNotification(`${error.status}: ${error.statusText}`, 3, 'warning');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error(error);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
_$('#css-upload-form > input').value = '';
|
||||||
|
fcn_toggleInProgress(trigger);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
_$('[data-click-action="skins-sync-down"]')?.addEventListener('click', event => {
|
||||||
|
fcn_downloadSkins(event.currentTarget);
|
||||||
|
});
|
||||||
|
@ -807,6 +807,33 @@ function fcn_sanitizeHTML(html) {
|
|||||||
return temp.innerHTML;
|
return temp.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// SANITIZE CSS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a CSS string.
|
||||||
|
*
|
||||||
|
* @since 5.26.0
|
||||||
|
* @param {String} css - The CSS to validate.
|
||||||
|
* @return {Boolean} True or false.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fcn_validateCss(css) {
|
||||||
|
const openBraces = (css.match(/{/g) || []).length;
|
||||||
|
const closeBraces = (css.match(/}/g) || []).length;
|
||||||
|
|
||||||
|
if (openBraces !== closeBraces) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (css.includes('<')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
// SCREEN COLLISION DETECTION
|
// SCREEN COLLISION DETECTION
|
||||||
// =============================================================================
|
// =============================================================================
|
||||||
@ -973,3 +1000,31 @@ function fcn_isUserLoggedIn() {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// PROGRESSIVE ELEMENT STATUS
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggles progression state of an element.
|
||||||
|
*
|
||||||
|
* @since 5.26.0
|
||||||
|
* @param {HTMLElement} element - The element.
|
||||||
|
* @param {Boolean|null} force - Whether to disable or enable. Defaults to
|
||||||
|
* the opposite of the current state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function fcn_toggleInProgress(element, force = null) {
|
||||||
|
force = force !== null ? force : !element.disabled;
|
||||||
|
|
||||||
|
if (force) {
|
||||||
|
element.dataset.enableWith = element.innerHTML;
|
||||||
|
element.innerHTML = element.dataset.disableWith ?? 'Processing';
|
||||||
|
element.disabled = true;
|
||||||
|
element.classList.add('disabled');
|
||||||
|
} else {
|
||||||
|
element.innerHTML = element.dataset.enableWith;
|
||||||
|
element.disabled = false;
|
||||||
|
element.classList.remove('disabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -209,3 +209,105 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.custom-skin-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 1rem;
|
||||||
|
|
||||||
|
&:not(:empty) {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-skin {
|
||||||
|
flex: 1 1 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--button-secondary-color);
|
||||||
|
background: var(--button-secondary-background);
|
||||||
|
font-size: var(--fs-xs);
|
||||||
|
font-weight: var(--button-font-weight);
|
||||||
|
line-height: 18px;
|
||||||
|
padding: .5rem;
|
||||||
|
border: var(--button-secondary-border);
|
||||||
|
border-radius: var(--layout-border-radius-small);
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&._upload {
|
||||||
|
position: relative;
|
||||||
|
border-style: dashed;
|
||||||
|
padding: 0;
|
||||||
|
height: 2.75rem;
|
||||||
|
|
||||||
|
&:not(:hover) .fa-plus {
|
||||||
|
opacity: .4;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
cursor: pointer;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-plus {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
pointer-events: none;
|
||||||
|
text-align: center;
|
||||||
|
transition: opacity var(--transition-duration);
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--button-color-active);
|
||||||
|
background: var(--button-background-active);
|
||||||
|
border: var(--button-border-active);
|
||||||
|
|
||||||
|
input[type=checkbox]:checked {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-skin__toggle .off {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.active) {
|
||||||
|
.custom-skin__toggle .on {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__info {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__toggle {
|
||||||
|
padding: .25rem 1rem .25rem .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__spacer {
|
||||||
|
&::after {
|
||||||
|
content: '|';
|
||||||
|
margin: 0 .25rem;
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__version,
|
||||||
|
&__author {
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__delete {
|
||||||
|
padding: .25rem .25rem .25rem 1rem;
|
||||||
|
transition: opacity var(--transition-duration);
|
||||||
|
|
||||||
|
&:not(:hover) {
|
||||||
|
opacity: .3;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user