diff --git a/src/class-tiny-compress.php b/src/class-tiny-compress.php index 7aab1fb1..346eaed6 100644 --- a/src/class-tiny-compress.php +++ b/src/class-tiny-compress.php @@ -212,8 +212,8 @@ private static function compression_cost( $total ) { $total -= $compressions; } - if ( $total > 500 ) { - $compressions = $total - 500; + if ( $total > Tiny_Config::MONTHLY_FREE_COMPRESSIONS ) { + $compressions = $total - Tiny_Config::MONTHLY_FREE_COMPRESSIONS; $cost += $compressions * 0.009; $total -= $compressions; } diff --git a/src/class-tiny-dashboard.php b/src/class-tiny-dashboard.php index 196f3339..cb450a41 100644 --- a/src/class-tiny-dashboard.php +++ b/src/class-tiny-dashboard.php @@ -128,6 +128,17 @@ public function get_widget_data( $optimization_stats ) { is_numeric( $remaining_credits ); $remaining_credits = $has_limited_credits ? intval( $remaining_credits ) : null; + // Paid accounts get the same free credits each month and pay per + // compression once those are used. + $free_credits_left = null; + if ( ! $this->settings->is_on_free_plan() ) { + $compression_count = intval( $this->settings->get_compression_count() ); + $free_credits_left = max( + 0, + Tiny_Config::MONTHLY_FREE_COMPRESSIONS - $compression_count + ); + } + // How many of the remaining images the credits still cover. $images_affordable = 0; if ( $has_limited_credits && $images_remaining > 0 && $estimated_credit_use > 0 ) { @@ -169,6 +180,7 @@ public function get_widget_data( $optimization_stats ) { 'percentage' => $percentage, 'estimated_credit_use' => $estimated_credit_use, 'remaining_credits' => $remaining_credits, + 'free_credits_left' => $free_credits_left, 'seconds_left' => $seconds_left, 'email_address' => $this->settings->get_email_address(), 'auto_compress' => $this->settings->auto_compress_enabled(), diff --git a/src/class-tiny-onboarding.php b/src/class-tiny-onboarding.php new file mode 100644 index 00000000..fb0abee4 --- /dev/null +++ b/src/class-tiny-onboarding.php @@ -0,0 +1,257 @@ +settings = $settings; + } + + public function admin_init() { + add_action( + 'admin_post_tiny_onboarding_save_settings', + array( $this, 'save_settings' ) + ); + add_action( 'load-plugins.php', array( $this, 'redirect_after_activation' ) ); + } + + /** + * Sends a new user to the first onboarding step after activating the plugin. + * + * Hooked to `load-plugins.php`, the screen WordPress shows after activation. + */ + public function redirect_after_activation() { + if ( $this->is_onboarded() || ! current_user_can( 'manage_options' ) ) { + return; + } + + $this->set_is_onboarded( 1 ); + + if ( self::is_bulk_activation() ) { + return; + } + + wp_safe_redirect( $this->get_step_url( 1 ) ); + exit(); + } + + /** + * Onboarding is skipped when activating multiple plugins + * + * @return bool + */ + private static function is_bulk_activation() { + return filter_has_var( INPUT_GET, 'activate-multi' ); + } + + public function admin_menu() { + $this->page_title = __( 'Welcome to TinyPNG', 'tiny-compress-images' ); + + $hook = add_submenu_page( + 'options-general.php', + $this->page_title, + $this->page_title, + 'manage_options', + self::PAGE_SLUG, + array( $this, 'render_step' ) + ); + + if ( ! $hook ) { + return; + } + + remove_submenu_page( 'options-general.php', self::PAGE_SLUG ); + + /** + * because title is retrieved from submenu, which is not part of the menu, + * resolve it through globals + */ + add_action( 'load-' . $hook, array( $this, 'set_page_title' ) ); + } + + /** + * Renders the step from the `step` query arg, or step 1 when it is not a known step. + */ + public function render_step() { + $step = filter_input( INPUT_GET, 'step', FILTER_VALIDATE_INT ); + if ( ! in_array( $step, array( 1, 2, 3 ), true ) ) { + $step = 1; + } + include __DIR__ . '/views/onboarding-' . $step . '.php'; + } + + /** + * Supplies the title of a page that is not listed in the menu. + * + * Hooked to `load-{$hook}` of every onboarding step. + */ + public function set_page_title() { + $GLOBALS['title'] = $this->page_title; + } + + /** + * Checks wether user is onboarded + * defaults to true + * + * @return boolean true if onboarded + */ + private function is_onboarded() { + $onboarding_status_field = self::get_prefixed_name( 'onboarding_status' ); + return 1 === (int) get_option( $onboarding_status_field, 1 ); + } + + /** + * Whether the current admin request is the onboarding page. + * + * @return bool + */ + public static function is_onboarding_page() { + return isset( $GLOBALS['plugin_page'] ) && self::PAGE_SLUG === $GLOBALS['plugin_page']; + } + + /** + * Retrieves the url of the given onboarding step + * + * @param int $step + * @return string + */ + public function get_step_url( $step ) { + return admin_url( 'options-general.php?page=' . self::PAGE_SLUG . '&step=' . $step ); + } + + /** + * Sets the onboarding status + * + * @param int $is_onboarded status + */ + public static function set_is_onboarded( $is_onboarded ) { + $onboarding_status_field = self::get_prefixed_name( 'onboarding_status' ); + return update_option( $onboarding_status_field, $is_onboarded ); + } + + /** + * Renders the onboarding progress with the given step as current. + * + * @param int $current_step + * @param bool $confirm_email false when connecting with an API key skipped that step + */ + public function render_steps( $current_step, $confirm_email = true ) { + include __DIR__ . '/views/onboarding-steps.php'; + } + + /** + * Saves settings from onboading page, + * redirects to bulk optimizer. + * + * Hooked to `admin_post_tiny_onboarding_save_settings`. + */ + public function save_settings() { + check_admin_referer( 'tiny-onboarding-save-settings' ); + + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( + esc_html__( + 'Sorry, you are not allowed to manage options for this site.', + 'tiny-compress-images' + ) + ); + } + + $convert_to = isset( $_POST['tinypng_convert_format']['convert_to'] ) ? + sanitize_key( wp_unslash( $_POST['tinypng_convert_format']['convert_to'] ) ) : ''; + if ( ! in_array( $convert_to, array( 'smallest', 'webp', 'avif' ), true ) ) { + $convert_to = 'smallest'; + } + + $delivery_method = isset( $_POST['tinypng_convert_format']['delivery_method'] ) ? + sanitize_key( wp_unslash( $_POST['tinypng_convert_format']['delivery_method'] ) ) : ''; + if ( ! in_array( $delivery_method, array( 'htaccess', 'picture' ), true ) ) { + $delivery_method = 'picture'; + } + + $convert_format = array( 'convert' => 'off' ); + if ( isset( $_POST['tinypng_convert_format']['convert'] ) ) { + $convert_format = array( + 'convert' => 'on', + 'convert_to' => $convert_to, + 'delivery_method' => $delivery_method, + ); + } + + $timing_name = self::get_prefixed_name( 'compression_timing' ); + $timing = isset( $_POST[ $timing_name ] ) ? + sanitize_key( wp_unslash( $_POST[ $timing_name ] ) ) : ''; + if ( in_array( $timing, array( 'background', 'auto', 'manual' ), true ) ) { + update_option( $timing_name, $timing ); + } + update_option( self::get_prefixed_name( 'convert_format' ), $convert_format ); + + $next = isset( $_POST['next'] ) ? sanitize_key( wp_unslash( $_POST['next'] ) ) : ''; + if ( 'settings' === $next ) { + wp_safe_redirect( admin_url( 'options-general.php?page=tinify' ) ); + } else { + wp_safe_redirect( admin_url( 'upload.php?page=tiny-bulk-optimization' ) ); + } + exit(); + } + + /** + * Decides on activation whether this site still needs onboarding. + * + * A site that already has a key, or that has compressed before, keeps the + * default onboarded state so it is never sent through onboarding again. + */ + public static function on_activate() { + $api_key = Tiny_Settings::get_api_key(); + $compression_count = get_option( self::get_prefixed_name( 'status' ) ); + + if ( empty( $api_key ) && empty( $compression_count ) ) { + self::set_is_onboarded( 0 ); + } else { + self::set_is_onboarded( 1 ); + } + } +} diff --git a/src/class-tiny-plugin.php b/src/class-tiny-plugin.php index 4a00d97b..59539323 100644 --- a/src/class-tiny-plugin.php +++ b/src/class-tiny-plugin.php @@ -89,6 +89,7 @@ public function init() { ); $this->tiny_compatibility(); + new Tiny_Onboarding( $this->settings ); } public function cli_init() { diff --git a/src/class-tiny-settings.php b/src/class-tiny-settings.php index 045e38e7..6ebfc15a 100644 --- a/src/class-tiny-settings.php +++ b/src/class-tiny-settings.php @@ -108,16 +108,10 @@ public function admin_init() { ); } - if ( current_user_can( 'manage_options' ) ) { + if ( current_user_can( 'manage_options' ) && ! Tiny_Onboarding::is_onboarding_page() ) { $this->setup_incomplete_checks(); } - $field = self::get_prefixed_name( 'api_key' ); - register_setting( 'tinify', $field ); - - $field = self::get_prefixed_name( 'api_key_pending' ); - register_setting( 'tinify', $field ); - $field = self::get_prefixed_name( 'compression_timing' ); register_setting( 'tinify', $field ); @@ -171,14 +165,29 @@ public function image_sizes_notice() { check_ajax_referer( 'tiny-compress' ); if ( current_user_can( 'manage_options' ) ) { - $selected_sizes = isset( $_GET['image_sizes_selected'] ) ? + $selected_sizes = isset( $_GET['image_sizes_selected'] ) ? intval( $_GET['image_sizes_selected'] ) : 0; + $resize_original = isset( $_GET['resize_original'] ); + $compress_wr2x = isset( $_GET['compress_wr2x'] ); + $convert = isset( $_GET['convert'] ); + + echo '
'; - echo ''; - echo ''; - echo '
'; + return ucfirst( str_replace( '_', ' ', $size ) ) . ' (' . $dimensions . ')'; } - public function render_size_checkboxes_description( + /** + * Credits one new upload uses: one per size, one more when the original is + * resized, doubled for retina sizes and doubled again for WebP or AVIF versions. + */ + public static function get_credits_per_image( $active_sizes_count, $resize_original_enabled, $compress_wr2x, $conversion_enabled ) { - echo ''; - esc_html_e( - 'Remember each selected size counts as a compression.', - 'tiny-compress-images' - ); - echo '
'; - echo ''; - if ( $resize_original_enabled ) { - ++$active_sizes_count; - } + $credits = $active_sizes_count; + if ( $compress_wr2x ) { - $active_sizes_count *= 2; + $credits *= 2; } - if ( $conversion_enabled ) { - $active_sizes_count *= 2; + $credits *= 2; } + if ( $resize_original_enabled ) { + ++$credits; + } + return $credits; + } + + public function render_size_checkboxes_description( + $active_sizes_count, + $resize_original_enabled, + $compress_wr2x, + $conversion_enabled + ) { + echo '
'; if ( $active_sizes_count < 1 ) { esc_html_e( 'With these settings no images will be compressed.', 'tiny-compress-images' ); } else { - $free_images_per_month = floor( - Tiny_Config::MONTHLY_FREE_COMPRESSIONS / $active_sizes_count + $credits = self::get_credits_per_image( + $active_sizes_count, + $resize_original_enabled, + $compress_wr2x, + $conversion_enabled ); - - $strong = array( - 'strong' => array(), + printf( + esc_html( + /* translators: %s: number of image sizes */ + _n( + '%s size selected.', + '%s sizes selected.', + $active_sizes_count, + 'tiny-compress-images' + ) + ), + esc_html( number_format_i18n( $active_sizes_count ) ) ); - + echo ' '; printf( - wp_kses( - /* translators: %1$s: number of images */ - __( - // phpcs:ignore Generic.Files.LineLength - 'With these settings you can compress at least %1$s images for free each month.', + esc_html( + /* translators: %s: number of credits */ + _n( + 'Each new image uses %s credit.', + 'Each new image uses %s credits.', + $credits, 'tiny-compress-images' - ), - $strong + ) ), - esc_html( $free_images_per_month ) + esc_html( number_format_i18n( $credits ) ) ); - - if ( self::wr2x_active() ) { - echo '
'; - echo '';
- esc_html_e(
- 'If selected, retina sizes will be compressed when generated by WP Retina 2x',
- 'tiny-compress-images'
- );
- echo '
';
- esc_html_e(
- 'Each retina size will count as an additional compression.',
- 'tiny-compress-images'
- );
- }
- } // End if().
+ }
echo '
'; - echo '' . esc_html__( 'Warning', 'tiny-compress-images' ) . ' — '; - $message = esc_html__( - // phpcs:ignore Generic.Files.LineLength - 'For compression to work you will need to configure WP Offload S3 to keep a copy of the images on the server.', + if ( self::wr2x_active() ) { + echo '
'; + esc_html_e( + 'If selected, retina sizes will be compressed when generated by WP Retina 2x', 'tiny-compress-images' ); - echo esc_html( $message ); - echo '
'; - } else { - echo '
';
+ echo '
';
+ esc_html_e(
+ 'Each retina size will count as an additional compression.',
+ 'tiny-compress-images'
+ );
+ echo '
'; + printf( + esc_html( + /* translators: %s: number of credits */ + _n( + 'With your settings, each new image uses %s credit.', + 'With your settings, each new image uses %s credits.', + $credits, + 'tiny-compress-images' + ) + ), + esc_html( number_format_i18n( $credits ) ) + ); echo '
'; } @@ -860,22 +802,11 @@ public function after_compress_callback( $compressor ) { public function render_account_status() { $key = $this->get_api_key(); - if ( empty( $key ) ) { - $compressor = $this->get_compressor(); - if ( $compressor->can_create_key() ) { - include __DIR__ . '/views/account-status-create-advanced.php'; - } else { - include __DIR__ . '/views/account-status-create-simple.php'; - } - } else { + if ( ! empty( $key ) ) { if ( $this->refresh_api_key_pending() ) { $status = (object) array( 'ok' => true, 'pending' => true, - 'message' => __( - 'An email has been sent to activate your account', - 'tiny-compress-images' - ), ); } else { $status = $this->compressor->get_status(); @@ -885,16 +816,40 @@ public function render_account_status() { } } + /** + * Renders the form to create a Tinify account or to connect one with an API key. + */ + public function render_account_connect() { + $user = wp_get_current_user(); + $name = trim( $user->user_firstname . ' ' . $user->user_lastname ); + $email = trim( $user->user_email ); + $api_key = $this->get_api_key(); + $has_api_key = $api_key && ! $this->get_api_key_pending(); + $api_key_masked = str_repeat( '•', 8 ) . substr( $api_key, -4 ); + $show_create = ! $has_api_key; + include __DIR__ . '/views/account-status-connect.php'; + } + + /** + * Renders the switch for WebP or AVIF versions, with the format and the delivery to use. + * Server rules are recommended, so they are selected when Apache can rewrite and + * no delivery was chosen yet. + */ + public function render_modern_formats() { + $convert_name = self::get_prefixed_name( 'convert_format' ); + $convert = $this->get_conversion_enabled(); + $convert_to = self::get_convert_format_option( 'convert_to', 'smallest' ); + $server_rules = self::can_render_delivery_method(); + $delivery = self::get_convert_format_option( + 'delivery_method', + $server_rules ? 'htaccess' : 'picture' + ); + include __DIR__ . '/views/settings-modern-formats.php'; + } + public function render_pending_status() { $key = $this->get_api_key(); - if ( empty( $key ) ) { - $compressor = $this->get_compressor(); - if ( $compressor->can_create_key() ) { - include __DIR__ . '/views/account-status-create-advanced.php'; - } else { - include __DIR__ . '/views/account-status-create-simple.php'; - } - } else { + if ( ! empty( $key ) ) { include __DIR__ . '/views/account-status-loading.php'; } } @@ -934,6 +889,8 @@ public function create_api_key() { } try { + $name = sanitize_text_field( wp_unslash( $_POST['name'] ) ); + $email = sanitize_email( wp_unslash( $_POST['email'] ) ); $site = str_replace( array( 'http://', 'https://' ), '', @@ -941,18 +898,17 @@ public function create_api_key() { ); $identifier = 'WordPress plugin for ' . $site; $link = $this->get_absolute_url(); - $compressor->create_key( - sanitize_email( wp_unslash( $_POST['email'] ) ), - array( - 'name' => sanitize_text_field( wp_unslash( $_POST['name'] ) ), - 'identifier' => $identifier, - 'link' => $link, - ) + $options = array( + 'name' => $name, + 'identifier' => $identifier, + 'link' => $link, ); + $compressor->create_key( $email, $options ); update_option( self::get_prefixed_name( 'api_key_pending' ), true ); update_option( self::get_prefixed_name( 'api_key' ), $compressor->get_key() ); update_option( self::get_prefixed_name( 'status' ), 0 ); + update_option( self::get_prefixed_name( 'email_address' ), $email ); $status = (object) array( 'ok' => true, @@ -993,7 +949,10 @@ public function update_api_key() { ); } else { $key = sanitize_text_field( wp_unslash( $_POST['key'] ) ); - $status = Tiny_Compress::create( $key )->get_status(); + $status = Tiny_Compress::create( + $key, + array( $this, 'after_compress_callback' ) + )->get_status(); } if ( $status->ok ) { @@ -1017,40 +976,21 @@ public function get_wr2x_option() { ); } + /** + * Wether or not wr2x is active + * + * @return bool true if active + */ public function compress_wr2x_images() { $option = $this->get_wr2x_option(); return self::wr2x_active() && $option['tinify']; } - public function render_format_conversion() { - include __DIR__ . '/views/settings-conversion.php'; - } - - private static function render_radiobutton( - $group_name, - $option_id, - $option_value, - $current_value, - $label, - $descr - ) { - $checked = ( $current_value === $option_value ); - echo ''; - echo ''; - echo ''; - echo '
'; - } - - /** * @return bool true if apache with mod_rewrite loaded */ - private static function can_render_delivery_method() { + public static function can_render_delivery_method() { global $is_apache; if ( ! $is_apache ) { return false; @@ -1067,17 +1007,6 @@ private static function can_render_delivery_method() { return in_array( 'mod_rewrite', $modules, true ); } - /** - * If possible, render the delivery method settings view. - */ - public function render_delivery_method() { - if ( ! self::can_render_delivery_method() ) { - return; - } - - include __DIR__ . '/views/settings-conversion-delivery.php'; - } - private static function get_convert_format_option( $option, $default_value ) { $setting = get_option( self::get_prefixed_name( 'convert_format' ) ); if ( isset( $setting[ $option ] ) && $setting[ $option ] ) { diff --git a/src/css/admin.css b/src/css/admin.css index cfaa00ba..7f87a708 100644 --- a/src/css/admin.css +++ b/src/css/admin.css @@ -1,330 +1,770 @@ +body { + --tiny-theme-default: var(--wp-admin-theme-color, #2271b1); + --tiny-text-primary: #12141d; + --tiny-text-secondary: #5b5b67; + --tiny-text-inverse: #fff; + --tiny-surface-primary: #fff; + --tiny-surface-subtle: #ededf1; + --tiny-surface-page: #f8f8fa; + --tiny-border-default: #ededf1; + --tiny-border-emphasis: #d4d4db; + --tiny-neutral-300: #b8b8c1; + --tiny-neutral-500: #7a7a86; + --tiny-neutral-700: #4a4b56; + --tiny-neutral-900: #1e2029; + --tiny-red-300: #fca5a5; + --tiny-red-500: #ef4444; + --tiny-green-500: #06d28b; + --tiny-green-700: #008560; + --tiny-shadow-xs: 0 1px 2px rgba(18, 20, 29, 0.05); + --tiny-shadow-sm: 0 1px 2px rgba(18, 20, 29, 0.06); +} + div.misc-pub-section.tiny-compress-images { border-top: 1px solid #e1e1e1; padding-bottom: 16px; } /* Settings */ -div.tiny-account-status { +.incompatible-plugins.error h3 { + margin: 10px 0; +} + +.incompatible-plugins.error table { + margin-bottom: 10px; +} + +.incompatible-plugins.error table td.bullet { + width: 20px; +} + +.incompatible-plugins.error table td.name { + padding-right: 100px; +} + +/* Account connect */ +.tiny-account-connect { box-sizing: border-box; - display: table-cell; - width: 570px; - border: 1px solid #e1e1e1; - background-color: white; + display: flex; + width: 100%; + background: var(--tiny-surface-primary); + border: 1px solid var(--tiny-border-default); + border-radius: 12px; } -@media only screen and (max-width: 800px) { - div.tiny-account-status { - max-width: 100%; +.tiny-account-connect-column { + display: flex; + flex: 1 1 0%; + flex-direction: column; + gap: 16px; + min-width: 0; + padding: 24px; +} + +.tiny-account-connect-column+.tiny-account-connect-column { + border-left: 1px solid var(--tiny-border-default); +} + +@media screen and (max-width: 600px) { + .tiny-account-connect { + flex-direction: column; + } + + .tiny-account-connect-column+.tiny-account-connect-column { + border-top: 1px solid var(--tiny-border-default); + border-left: 0; } } -div.tiny-account-status div.status { +.tiny-card-head h2 { + margin: 0; + padding: 0; + color: var(--tiny-text-primary); + font-size: 15px; + font-weight: 600; + line-height: 20px; +} + +.tiny-card-head p { + margin: 4px 0 0; + color: var(--tiny-text-secondary); +} + +.tiny-field { + margin: 0; +} + +.tiny-field label { + display: block; + margin-bottom: 6px; + color: var(--tiny-neutral-700); + font-size: 14px; + font-weight: 500; + line-height: 20px; +} + +.tiny-field input[type="text"], +.tiny-field input[type="number"] { box-sizing: border-box; - padding: 22px 28px 14px 28px; - position: relative; + width: 100%; + height: auto; + margin: 0; + padding: 9px 14px; + color: var(--tiny-neutral-900); + line-height: 24px; + background-color: var(--tiny-surface-primary); + border: 1px solid var(--tiny-neutral-300); + border-radius: 2px; + box-shadow: var(--tiny-shadow-xs); } -div.tiny-account-status h4 { - margin-top: 0; - margin-bottom: 9px; - line-height: 1.5; +.tiny-field input[type="text"]::placeholder { + color: var(--tiny-neutral-500); } -div.tiny-account-status p { - margin-top: 0; - margin-bottom: 8px; +.tiny-field input[type="text"]:focus, +.tiny-field input[type="number"]:focus { + border-color: var(--tiny-theme-default); + box-shadow: 0 0 0 1px var(--tiny-theme-default); + outline: 2px solid transparent; } -div.tiny-account-status button+p { - margin-top: 8px; +.tiny-field-invalid input[type="text"] { + padding-right: 38px; + background: var(--tiny-surface-primary) url("../images/alert-circle.svg") no-repeat right 14px center; + border-color: var(--tiny-red-300); } -div.tiny-account-status p:first-child { - margin-bottom: 10px; +.tiny-field-invalid input[type="text"]:focus { + border-color: var(--tiny-red-300); + box-shadow: 0 0 0 1px var(--tiny-red-300); } -div.tiny-account-status p:last-child { - margin-bottom: 0; +.tiny-field-error { + display: none; + margin: 6px 0 0; + color: var(--tiny-red-500); + font-size: 14px; + line-height: 20px; } -div.tiny-account-status p.introduction { - margin-bottom: 12px; +.tiny-field-error a { + color: inherit; } -div.tiny-account-status p.status { - font-weight: bold; +.tiny-field-invalid .tiny-field-error { + display: block; } -div.tiny-account-status p.status a { +.tiny-actions { + display: flex; + align-items: center; + gap: 16px; + margin: 0; +} + +.tiny-actions .button { + height: auto; + min-height: 40px; + padding: 9px 16px; + font-size: 14px; + font-weight: 600; + line-height: 20px; + border-radius: 2px; +} + +.tiny-actions a, +.tiny-actions .button-link { + color: var(--tiny-theme-default); + font-size: 14px; + font-weight: 600; + line-height: 20px; text-decoration: none; - font-weight: normal; } -div.tiny-account-status p.status span { - position: relative; - margin-right: 30px; +.tiny-actions .spinner { + float: none; + margin: 0; +} + +/* Onboarding */ +.tiny-onboarding { + display: flex; + flex-direction: column; + align-items: center; + gap: 24px; + max-width: 720px; + /* Matches #wpcontent's left padding, so the page centers between menu and window edge. */ + margin: 64px auto 0; + padding-right: 20px; +} + +.tiny-onboarding-steps { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: 12px; + margin: 0; + padding: 0; + list-style: none; +} + +.tiny-onboarding-steps li { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + color: var(--tiny-text-secondary); + font-size: 14px; + line-height: 20px; +} + +/* The connector to the previous step. */ +.tiny-onboarding-steps li+li:before { + content: ""; + width: 40px; + height: 2px; + margin-right: 4px; + background: var(--tiny-border-emphasis); + border-radius: 1px; } -div.tiny-account-status p.status span:after { +.tiny-onboarding-steps li.tiny-step-done+li:before { + background: var(--tiny-theme-default); +} + +@media screen and (max-width: 600px) { + .tiny-onboarding-steps li+li:before { + display: none; + } +} + +.tiny-step-dot { box-sizing: border-box; + display: flex; + justify-content: center; + align-items: center; + width: 24px; + height: 24px; + color: var(--tiny-text-secondary); + font-size: 12px; + font-weight: 600; + line-height: 16px; + letter-spacing: 0.12px; + background: var(--tiny-surface-primary); + border: 1px solid var(--tiny-border-default); + border-radius: 12px; +} + +.tiny-step-current { + color: var(--tiny-text-primary); + font-weight: 600; +} + +.tiny-step-current .tiny-step-dot { + color: var(--tiny-text-inverse); + background: var(--tiny-theme-default); + border: 0; +} + +.tiny-step-done .tiny-step-dot { + color: var(--tiny-theme-default); + background: none; + border: 0; +} + +.tiny-onboarding-panda { + display: block; + width: 112px; + height: 112px; +} + +.tiny-onboarding-header { + text-align: center; +} + +.tiny-onboarding-header h1 { + margin: 0 0 8px; + padding: 0; + color: var(--tiny-text-primary); + font-size: 24px; + font-weight: 600; + line-height: 32px; +} + +.tiny-onboarding-header p { + margin: 0; + color: var(--tiny-text-secondary); + font-size: 14px; + line-height: 20px; +} + +.tiny-onboarding-card, +.tiny-settings .tiny-card { + box-sizing: border-box; + width: 100%; + background: var(--tiny-surface-primary); + border: 1px solid var(--tiny-border-default); + border-radius: 12px; +} + +.tiny-card-section { + display: flex; + flex-direction: column; + gap: 16px; + padding: 24px; +} + +.tiny-card-section+.tiny-card-section { + border-top: 1px solid var(--tiny-border-default); +} + +.tiny-card-section[hidden] { + display: none; +} + +.tiny-card-section .notice { + margin: 0; +} + +.tiny-option { + display: flex; + align-items: flex-start; + gap: 12px; + margin: 0; + cursor: pointer; +} + +.tiny-option-text { + display: flex; + flex-direction: column; + color: var(--tiny-text-secondary); +} + +.tiny-option-label { + color: var(--tiny-neutral-700); + font-weight: 500; +} + +.tiny-option input[role="switch"] { + box-sizing: border-box; + position: relative; + flex: none; + width: 44px; + min-width: 44px; + height: 24px; + margin: 4px 0; + background: var(--tiny-surface-subtle); + border: 0; + border-radius: 12px; + box-shadow: none; + transition: background-color 0.15s; +} + +.tiny-option input[role="switch"]:before, +.tiny-option input[role="switch"]:checked:before { + content: ""; position: absolute; + top: 2px; + left: 2px; + float: none; width: 20px; height: 20px; - font-size: 28px; - font-family: dashicons; - font-weight: 400; - font-style: normal; + margin: 0; + background: var(--tiny-surface-primary); + border-radius: 50%; + box-shadow: var(--tiny-shadow-sm); + transition: transform 0.15s; } -div.tiny-account-status div.status-loading { - padding-left: 56px; +.tiny-option input[role="switch"]:checked { + background: var(--tiny-theme-default); } -div.tiny-account-status div.status-loading:before { - position: absolute; - left: 26px; - top: 22px; +.tiny-option input[role="switch"]:checked:before { + transform: translateX(20px); +} + +.tiny-check { + align-items: center; +} + +.tiny-check input[type="checkbox"] { + box-sizing: border-box; + flex: none; + width: 20px; + min-width: 20px; height: 20px; + margin: 0; + background: var(--tiny-surface-primary); + border: 1px solid var(--tiny-neutral-300); + border-radius: 2px; + box-shadow: none; +} + +.tiny-check input[type="checkbox"]:checked { + background: var(--tiny-theme-default); + border-color: var(--tiny-theme-default); +} + +.tiny-check input[type="checkbox"]:checked:before { + content: ""; + display: block; + float: none; + width: 5px; + height: 10px; + margin: 2px 0 0 6px; + border: solid var(--tiny-text-inverse); + border-width: 0 2px 2px 0; + transform: rotate(45deg); +} + +/* Radio button, drawn over WordPress' own radio styles. */ +.tiny-option input[type="radio"] { + box-sizing: border-box; + flex: none; width: 20px; - background: url("../images/spinner.gif") no-repeat center; - background-size: 20px 20px; + min-width: 20px; + height: 20px; + margin: 2px 0 0; + background: var(--tiny-surface-primary); + border: 1px solid var(--tiny-neutral-300); + border-radius: 50%; + box-shadow: none; +} + +.tiny-option input[type="radio"]:checked { + background: var(--tiny-theme-default); + border-color: var(--tiny-theme-default); +} + +.tiny-option input[type="radio"]:checked:before { content: ""; + display: block; + float: none; + width: 8px; + height: 8px; + margin: 5px; + text-indent: 0; + background: var(--tiny-text-inverse); + border-radius: 50%; } -div.tiny-account-status div.status-loading p { - margin-bottom: 10px; +.tiny-option input[type="checkbox"]:focus, +.tiny-option input[type="radio"]:focus { + box-shadow: 0 0 0 2px var(--tiny-surface-primary), 0 0 0 4px var(--tiny-theme-default); + outline: 2px solid transparent; } -@media (-webkit-min-device-pixel-ratio: 1.25), -(min-resolution: 120dpi) { - div.tiny-account-status div.status-loading:before { - background-image: url("../images/spinner-2x.gif"); - } +.tiny-format-options { + display: flex; + flex-direction: column; + gap: 16px; + padding-left: 52px; } -div.tiny-account-status div.status-success p { - display: inline-block; - position: relative; - margin-bottom: 10px; +.tiny-format-options[hidden] { + display: none; } -div.tiny-account-status div.status-success p.status span:after { - right: -18px; - top: -11px; - color: #30d030; - content: "\f147"; +.tiny-option-group { + margin: 0; + color: var(--tiny-text-primary); + font-weight: 600; } -div.tiny-account-status div.status-failure p.status span:after { - right: -20px; - top: -11px; - color: #d54e21; - content: "\f158"; +.tiny-format-note { + margin: 0; + color: var(--tiny-text-secondary); } -div.tiny-account-status div.status-pending p.status span:after { - right: -26px; - top: -9px; - color: #0086ba; +/* Settings page */ +.tiny-settings h1 { + margin: 22px 0 8px; + padding: 0; + color: var(--tiny-text-primary); font-size: 24px; - content: "\f466"; + font-weight: 600; + line-height: 32px; } -div.tiny-account-status div.status-pending p.status span { - margin-right: 38px; +.tiny-settings-intro { + margin: 0; + color: var(--tiny-text-secondary); + font-size: 14px; + line-height: 20px; } -div.tiny-account-status div.failure input { - border-color: #dc3232; - background-color: #fdf1f1; +.tiny-settings .wp-header-end { + visibility: hidden; + margin: -1px 0 0; } -div.tiny-account-status div.failure p.message { - color: #dc3232; +.tiny-settings-columns { + display: flex; + flex-wrap: wrap; + align-items: flex-start; + gap: 24px; + margin-top: 24px; + max-width: 1200px; } -div.tiny-account-status div.update { - padding: 22px 28px; +.tiny-settings-main { + display: flex; + flex: 999 1 560px; + flex-direction: column; + gap: 24px; + min-width: 0; } -div.tiny-account-status div.upgrade { - padding: 22px 28px; - background-color: #f5f9fa; +.tiny-settings-sidebar { + display: flex; + flex: 1 1 320px; + flex-direction: column; + gap: 24px; + min-width: 0; } -div.tiny-account-status div.upgrade p { - margin-bottom: 0; +.tiny-sizes { + display: flex; + flex-direction: column; + gap: 10px; } -div.tiny-account-status div.upgrade div.button-container { - float: right; - margin-left: 20px; - margin-bottom: 5px; +.tiny-credit-summary { + padding: 16px; + color: var(--tiny-text-primary); + font-size: 14px; + font-weight: 600; + line-height: 20px; + background: var(--tiny-surface-page); + border-radius: 8px; } -@media only screen and (max-width: 400px) { - div.tiny-account-status div.upgrade div.button-container { - float: none; - margin-left: 0; - margin-bottom: 10px; - } +.tiny-credit-summary p, +.tiny-card-footer p { + margin: 0; } -div.tiny-account-status.wide { - width: auto; - max-width: 700px; +.tiny-credit-summary p+p { + margin-top: 8px; + font-weight: 400; } -div.tiny-account-status.wide div.create { - box-sizing: border-box; - display: table-cell; - width: 50%; - padding: 22px 28px; - border-right: 1px solid #e5e5e5; +.tiny-resize-inputs { + flex-direction: row; + flex-wrap: wrap; } -div.tiny-account-status.wide div.update { - box-sizing: border-box; - display: table-cell; - width: 50%; - padding-left: 29px; +.tiny-resize-inputs input[type="number"] { + width: 120px; } -@media screen and (max-width: 600px) { - div.tiny-account-status.wide div.create { - display: block; - width: auto; - padding-bottom: 20px; - border-right: 0; - border-bottom: 1px solid #e5e5e5; - } +.tiny-account-loading { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + color: var(--tiny-text-secondary); + font-size: 14px; + line-height: 20px; +} - div.tiny-account-status.wide div.update { - display: block; - width: auto; - padding-top: 29px; - } +.tiny-account-loading .spinner { + float: none; + margin: 0; } -div.tiny-account-status.wide div.create input, -div.tiny-account-status.wide div.update input { - width: 100%; - margin-bottom: 8px; +.tiny-account-details, +.tiny-credit-rows { + display: flex; + flex-direction: column; + margin: 0; } -div.tiny-account-status.wide div.create button, -div.tiny-account-status.wide div.update button { - width: 100%; - margin-top: 4px; - margin-bottom: 6px; +.tiny-account-details { + gap: 8px; } -div.tiny-account-status.wide div.create button.loading, -div.tiny-account-status.wide div.update button.loading { - background-image: url(../images/button-spinner.gif) !important; - background-size: 16px 16px !important; - background-position: 95% 50% !important; - background-repeat: no-repeat !important; +.tiny-credit-rows { + gap: 10px; } -@media (-webkit-min-device-pixel-ratio: 1.25), -(min-resolution: 120dpi) { +.tiny-account-details div, +.tiny-credit-rows div { + display: flex; + gap: 10px; +} - div.tiny-account-status.wide div.create button.loading, - div.tiny-account-status.wide div.update button.loading { - background-image: url("../images/button-spinner-2x.gif") !important; - } +.tiny-account-details div { + justify-content: space-between; } -.incompatible-plugins.error h3 { - margin: 10px 0; +.tiny-account-details dt, +.tiny-credit-rows dd { + color: var(--tiny-text-secondary); } -.incompatible-plugins.error table { - margin-bottom: 10px; +.tiny-account-details dd, +.tiny-credit-rows dd { + min-width: 0; + margin: 0; + overflow-wrap: anywhere; } -.incompatible-plugins.error table td.bullet { - width: 20px; +.tiny-account-details dd { + color: var(--tiny-text-primary); + font-weight: 500; + text-align: right; } -.incompatible-plugins.error table td.name { - padding-right: 100px; +.tiny-credit-rows dt { + flex: none; + width: 72px; + color: var(--tiny-text-primary); + font-weight: 600; } -table.tinify-settings p.intro { - color: #777777; +.tiny-credits { + display: flex; + flex-direction: column; + gap: 8px; } -table.tinify-settings div.sizes, -table.tinify-settings div.conversion-options { - margin: 20px 0 5px; +.tiny-credits-figure { + display: flex; + align-items: baseline; + gap: 8px; + margin: 0; + color: var(--tiny-text-secondary); + font-size: 14px; + line-height: 20px; } -table.tinify-settings input[type="checkbox"], -table.tinify-settings input[type="radio"] { - margin-right: 12px; +.tiny-credits-figure strong { + color: var(--tiny-text-primary); + font-size: 24px; + font-weight: 600; + line-height: 32px; } -div.optimization-options { - margin-top: 20px; +.tiny-credits-empty strong { + color: var(--tiny-red-500); } -p.tiny-radio { - margin-right: 12px; +.tiny-credits-bar { + display: flex; + height: 8px; + overflow: hidden; + background: var(--tiny-border-emphasis); + border-radius: 4px; } -p.tiny-radio span.description, -p.tiny-check span.description { - display: block; +.tiny-credits-bar span { + height: 100%; + background: var(--tiny-green-500); } -@media only screen and (max-width: 782px) { - p.tiny-radio span.description { - margin-left: 38px; - } +.tiny-credits-keys { + display: flex; + flex-direction: column; + gap: 4px; } -p.tiny-radio.disabled label, -p.tiny-radio.disabled span.description { - opacity: 0.5; +.tiny-credits-key { + display: flex; + align-items: center; + gap: 8px; + margin: 0; + color: var(--tiny-text-secondary); + font-size: 12px; + font-weight: 500; + line-height: 16px; + letter-spacing: 0.12px; } -p.tiny-radio span, -p.tiny-check span.description { - margin-left: 28px; - color: #777777; - line-height: 30px; +.tiny-credits-key:before { + content: ""; + width: 10px; + height: 10px; + background: var(--tiny-green-500); + border-radius: 2px; } -div.tiny-resize-available { - margin-top: 5px; +.tiny-credits-bar .tiny-credits-bundle, +.tiny-credits-key.tiny-credits-bundle:before { + background: var(--tiny-green-700); } -div.tiny-resize-resolution { - margin: 10px 0 20px 28px; +.tiny-account-links { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 12px; + margin: 0; + color: var(--tiny-text-secondary); + font-size: 14px; + line-height: 20px; } -@media only screen and (max-width: 782px) { - div.tiny-resize-resolution { - margin-left: 38px; - } +.tiny-account-links a, +.tiny-credit-option a { + color: var(--tiny-theme-default); + font-weight: 600; + text-decoration: none; +} + +.tiny-account-links .tiny-link-secondary { + color: var(--tiny-text-secondary); + font-size: inherit; + font-weight: 500; + line-height: inherit; + text-decoration: none; +} + +.tiny-card-footer { + display: flex; + flex-direction: column; + gap: 12px; + padding: 24px; + color: var(--tiny-text-primary); + font-size: 14px; + font-weight: 600; + line-height: 20px; + background: var(--tiny-surface-page); + border-top: 1px solid var(--tiny-border-default); + border-radius: 0 0 12px 12px; +} + +.tiny-card-footer h3 { + margin: 0; + font-size: 14px; + line-height: 20px; } -div.tiny-resize-resolution span { - color: #777777; +.tiny-credit-option { + display: flex; + flex-direction: column; + gap: 4px; + font-weight: 400; +} + +.tiny-credit-option strong { + font-weight: 500; } -div.tiny-resize-inputs { - margin-top: 10px; +.tiny-credit-option>span { + color: var(--tiny-text-secondary); } -div.tiny-resize-inputs input { - margin-right: 6px; +.tiny-credit-option a { + margin-top: 4px; } -input[type=number][name*="tinypng_resize_original"] { - width: 65px; +.tiny-credit-option+.tiny-credit-option { + padding-top: 12px; + border-top: 1px solid var(--tiny-border-default); } /* Media library */ @@ -448,37 +888,6 @@ input[type=number][name*="tinypng_resize_original"] { margin: 5px 0; } -fieldset.tinypng_convert_fields { - margin: 18px 28px; -} - -fieldset.tinypng_convert_fields p { - display: flex; -} - -fieldset.tinypng_convert_fields p input[type=radio] { - margin-top: 2px; -} - -fieldset.tinypng_convert_fields label span { - display: flex; - margin-left: 0; - margin-top: 2px; - line-height: 1.5; -} - -fieldset.tinypng_convert_fields[disabled] { - opacity: 0.6; -} - -.tiny-d-flex { - display: flex; -} - -.tiny-mt-2 { - margin-top: 10px; -} - .tiny-dialog { padding: 20px; border: 1px solid #ccd0d4; diff --git a/src/images/alert-circle.svg b/src/images/alert-circle.svg new file mode 100644 index 00000000..9a6ad929 --- /dev/null +++ b/src/images/alert-circle.svg @@ -0,0 +1 @@ + diff --git a/src/images/panda-juggle.png b/src/images/panda-juggle.png new file mode 100644 index 00000000..9a1b5dbb Binary files /dev/null and b/src/images/panda-juggle.png differ diff --git a/src/images/panda-thumbs-up.png b/src/images/panda-thumbs-up.png new file mode 100644 index 00000000..66b5b74f Binary files /dev/null and b/src/images/panda-thumbs-up.png differ diff --git a/src/images/panda-unbox.png b/src/images/panda-unbox.png new file mode 100644 index 00000000..1493639a Binary files /dev/null and b/src/images/panda-unbox.png differ diff --git a/src/js/admin.js b/src/js/admin.js index 9430368e..f69652b3 100644 --- a/src/js/admin.js +++ b/src/js/admin.js @@ -69,13 +69,11 @@ function downloadDiagnostics() { try { - jQuery('#download-diagnostics-spinner').show(); jQuery('#tiny-download-diagnostics').attr('disabled', true); const downloadURL = `${ajaxurl}?action=tiny_download_diagnostics&security=${tinyCompress.nonce}`; window.location.href = downloadURL; } finally { jQuery('#tiny-download-diagnostics').attr('disabled', false); - jQuery('#download-diagnostics-spinner').hide(); } } jQuery('#tiny-download-diagnostics').click(downloadDiagnostics); @@ -196,70 +194,127 @@ } } - function toggleChangeKey(event) { - jQuery('div.tiny-account-status div.update').toggle(); - jQuery('div.tiny-account-status div.status').toggle(); - jQuery('div.tiny-account-status div.upgrade').toggle(); - return false; + /** + * Posts the account connect request for the clicked button and opens next + * when it worked. Returns the error message when it did not. + */ + async function postAccountConnect(button, params, next) { + const spinner = button.closest('.tiny-actions').querySelector('.spinner'); + button.disabled = true; + spinner.classList.add('is-active'); + + let message; + try { + const response = await fetch(ajaxurl, { + method: 'POST', + body: new URLSearchParams({ _nonce: tinyCompress.nonce, ...params }), + }); + if (!response.ok) { + throw new Error(response.statusText); + } + const status = await response.json(); + if (status.ok) { + window.location.href = next; + return null; + } + message = status.message; + } catch (err) { + message = 'Something went wrong, try again soon'; + } + + button.disabled = false; + spinner.classList.remove('is-active'); + return message; } - function submitKey(event) { + /** + * Creates an account and opens data-create-key-next. A failure is shown + * under the name field when it is empty, otherwise under the email field. + */ + async function createAccount(event) { event.preventDefault(); - jQuery(event.target).attr({disabled: true}).addClass('loading'); - - var action; - var parent = jQuery(event.target).closest('div'); - var key; - var email; - var name; - - if (jQuery(event.target).data('tiny-action') === 'update-key') { - action = 'update'; - key = parent.find('#tinypng_api_key').val(); - } else if (jQuery(event.target).data('tiny-action') === 'create-key') { - action = 'create'; - name = parent.find('#tinypng_api_key_name').val(); - email = parent.find('#tinypng_api_key_email').val(); - } else { - return false; + + const button = event.target; + const nameInput = document.querySelector('#tinypng_api_key_name'); + const emailInput = document.querySelector('#tinypng_api_key_email'); + const nameField = nameInput.closest('.tiny-field'); + const emailField = emailInput.closest('.tiny-field'); + + nameField.classList.remove('tiny-field-invalid'); + emailField.classList.remove('tiny-field-invalid'); + + const message = await postAccountConnect(button, { + action: 'tiny_settings_create_api_key', + name: nameInput.value, + email: emailInput.value, + }, button.closest('[data-create-key-next]').dataset.createKeyNext); + + if (message) { + const field = nameInput.value ? emailField : nameField; + field.querySelector('.tiny-field-error').textContent = message; + field.classList.add('tiny-field-invalid'); } + } - jQuery.ajax({ - url: ajaxurl, - type: 'POST', - data: { - _nonce: tinyCompress.nonce, - action: 'tiny_settings_' + action + '_api_key', - key: key, - name: name, - email: email - }, - success: function(json) { - var status = jQuery.parseJSON(json); - - if (status.ok) { - var target = jQuery('#tiny-account-status'); - if (target.length) { - jQuery.get(ajaxurl + (ajaxurl.indexOf( '?' ) > 0 ? '&' : '?') + 'action=tiny_account_status', function(data) { - jQuery(event.target).attr({disabled: false}).removeClass('loading'); - target.replaceWith(data); - }); - } - jQuery('div.tiny-notice[data-name="setting"]').remove(); - } else { - jQuery(event.target).attr({disabled: false}).removeClass('loading'); - parent.addClass('failure'); - parent.find('p.message').text(status.message).show(); - } - }, - error: function() { - jQuery(event.target).attr({disabled: false}).removeClass('loading'); - parent.addClass('failure'); - parent.find('p.message').text('Something went wrong, try again soon').show(); - } - }); + /** + * Connects the API key in the key field and opens data-update-key-next. + */ + async function connectApiKey(event) { + event.preventDefault(); - return false; + const button = event.target; + const keyInput = document.querySelector('#tinypng_api_key'); + const field = keyInput.closest('.tiny-field'); + + field.classList.remove('tiny-field-invalid'); + if (!keyInput.value) { + field.classList.add('tiny-field-invalid'); + return; + } + + const message = await postAccountConnect(button, { + action: 'tiny_settings_update_api_key', + key: keyInput.value, + }, button.closest('[data-update-key-next]').dataset.updateKeyNext); + + if (message) { + field.classList.add('tiny-field-invalid'); + } + } + + /** + * Disconnects the account and opens data-update-key-next. The server + * deletes the API key when it gets an empty one. + */ + async function disconnectApiKey(event) { + event.preventDefault(); + + const button = event.target; + const message = await postAccountConnect(button, { + action: 'tiny_settings_update_api_key', + key: '', + }, button.closest('[data-update-key-next]').dataset.updateKeyNext); + + if (message) { + window.alert(message); + } + } + + /** + * Opens the next onboarding step once the account is activated from the email. + * The account status markup has status-success once the key works. + */ + async function continueWhenActivated() { + const separator = ajaxurl.includes('?') ? '&' : '?'; + try { + const html = await fetch(`${ajaxurl}${separator}action=tiny_account_status`).then(r => r.text()); + const status = new DOMParser().parseFromString(html, 'text/html'); + if (status.querySelector('.status-success')) { + window.location.href = document.querySelector('.tiny-onboarding').dataset.activatedNext; + } + } finally { + setTimeout(continueWhenActivated, 5000); + } } function dismissNotice(event) { @@ -287,35 +342,80 @@ return false; } - function updateResizeSettings() { - if (propOf('#tinypng_sizes_0', 'checked')) { - jQuery('.tiny-resize-available').show(); - jQuery('.tiny-resize-unavailable').hide(); - } else { - jQuery('.tiny-resize-available').hide(); - jQuery('.tiny-resize-unavailable').show(); - } + function showFormatOptions(event) { + document.querySelector('#tiny-format-options').hidden = !event.target.checked; + } + + function showResizeInputs(event) { + document.querySelector('#tiny-resize-inputs').hidden = !event.target.checked; + } - var original_enabled = propOf('#tinypng_resize_original_enabled', 'checked'); - jQuery('#tinypng_resize_original_width, #tinypng_resize_original_height').each(function (i, el) { - el.disabled = !original_enabled; + /** + * Resizing, backups and metadata only apply to the original image, so their + * sections are hidden while the original size is not compressed. + */ + function showOriginalSettings(event) { + document.querySelectorAll('.tiny-resize-available').forEach(function(section) { + section.hidden = !event.target.checked; }); + document.querySelector('.tiny-resize-unavailable').hidden = event.target.checked; } - function updatePreserveSettings() { - if (propOf('#tinypng_sizes_0', 'checked')) { - jQuery('.tiny-preserve').show(); - } else { - jQuery('.tiny-preserve').hide(); - jQuery('#tinypng_preserve_data_creation').attr('checked', false); - jQuery('#tinypng_preserve_data_copyright').attr('checked', false); - jQuery('#tinypng_preserve_data_location').attr('checked', false); + function showAccountConnect() { + document.querySelector('.tiny-settings-connect').hidden = false; + document.querySelector('#tinypng_api_key').focus(); + } + + /** + * Replaces the account status placeholder with the status from the API. + */ + async function loadAccountStatus() { + const placeholder = document.querySelector('#tiny-account-status[data-state=pending]'); + if (!placeholder) { + return; + } + + const separator = ajaxurl.includes('?') ? '&' : '?'; + try { + const response = await fetch(`${ajaxurl}${separator}action=tiny_account_status`); + placeholder.outerHTML = await response.text(); + } catch (err) { + console.error(err); } } - function updateSettings() { - updateResizeSettings(); - updatePreserveSettings(); + /** + * Updates the credits per image under the sizes and in the sidebar. + */ + async function refreshCreditSummary() { + const totalSelectedSizes = document.querySelectorAll('input[name*=tinypng_sizes]:checked').length; + const compressWr2x = document.querySelector('#tinypng_sizes_wr2x')?.checked ?? false; + const compressOriginal = document.querySelector('#tinypng_sizes_0')?.checked ?? false; + const params = new URLSearchParams({ + action: 'tiny_image_sizes_notice', + image_sizes_selected: compressWr2x ? totalSelectedSizes - 1 : totalSelectedSizes, + _ajax_nonce: tinyCompress.nonce, + }); + if (compressOriginal && document.querySelector('#tinypng_resize_original_enabled').checked) { + params.set('resize_original', 'true'); + } + if (compressWr2x) { + params.set('compress_wr2x', 'true'); + } + if (document.querySelector('#tiny-convert').checked) { + params.set('convert', 'true'); + } + + const separator = ajaxurl.includes('?') ? '&' : '?'; + try { + const html = await fetch(`${ajaxurl}${separator}${params}`).then(r => r.text()); + const summary = new DOMParser().parseFromString(html, 'text/html'); + ['#tiny-image-sizes-notice', '#tiny-credits-per-image'].forEach(function(selector) { + document.querySelector(selector).innerHTML = summary.querySelector(selector).innerHTML; + }); + } catch (err) { + console.error(err); + } } var adminpage = ''; @@ -333,16 +433,6 @@ } } - function propOf(selector, property) { - if (typeof jQuery.fn.prop === 'function') { - /* Added in 1.6. Before jQuery 1.6, the .attr() method sometimes took - property values into account. */ - return jQuery(selector).prop(property); - } else { - return jQuery(selector).attr(property); - } - } - function setPropOf(selector, property, value) { if (typeof jQuery.fn.prop === 'function') { /* Added in 1.6. Before jQuery 1.6, the .attr() method sometimes took @@ -380,66 +470,34 @@ case 'post-php': eventOn('click', 'button.tiny-compress', compressImage); break; - case 'settings_page_tinify': - changeEnterKeyTarget('div.tiny-account-status create', '[data-tiny-action=create-key]'); - changeEnterKeyTarget('div.tiny-account-status update', '[data-tiny-action=update-key]'); - - eventOn('click', '[data-tiny-action=create-key]', submitKey); - eventOn('click', '[data-tiny-action=update-key]', submitKey); - eventOn('click', '#change-key', toggleChangeKey); - eventOn('click', '#cancel-change-key', toggleChangeKey); - - var target = jQuery('#tiny-account-status[data-state=pending]'); - if (target.length) { - jQuery.get(ajaxurl + (ajaxurl.indexOf( '?' ) > 0 ? '&' : '?') + 'action=tiny_account_status', function(data) { - target.replaceWith(data); - }); - } - - async function refreshSizeDescriptionNotice() { - const totalSelectedSizes = document.querySelectorAll('input[name*=tinypng_sizes]:checked').length; - const compressWr2x = document.querySelector('#tinypng_sizes_wr2x')?.checked ?? false; - const selectedCount = compressWr2x ? totalSelectedSizes - 1 : totalSelectedSizes; + case 'settings_page_tiny-onboarding': + changeEnterKeyTarget('#tinypng_api_key_name, #tinypng_api_key_email', '[data-tiny-action=create-key]'); + changeEnterKeyTarget('#tinypng_api_key', '[data-tiny-action=update-key]'); - const separator = ajaxurl.includes('?') ? '&' : '?'; - let imageCountUrl = `${ajaxurl}${separator}action=tiny_image_sizes_notice&image_sizes_selected=${selectedCount}&_ajax_nonce=${tinyCompress.nonce}`; + eventOn('click', '[data-tiny-action=create-key]', createAccount); + eventOn('click', '[data-tiny-action=update-key]', connectApiKey); + eventOn('change', '#tiny-convert', showFormatOptions); - const resizeOriginalChecked = document.querySelector('#tinypng_resize_original_enabled')?.checked; - const compressOriginalChecked = document.querySelector('#tinypng_sizes_0')?.checked; - if (resizeOriginalChecked && compressOriginalChecked) { - imageCountUrl += '&resize_original=true'; - } - if (compressWr2x) { - imageCountUrl += '&compress_wr2x=true'; - } - try { - const sizeDescriptionHtml = await fetch(imageCountUrl).then(r => r.text()); - document.querySelector('#tiny-image-sizes-notice').innerHTML = sizeDescriptionHtml; - } catch (err) { - document.querySelector('#tiny-image-sizes-notice').innerHTML = ''; - console.error(err); - } + if (document.querySelector('[data-activated-next]')) { + setTimeout(continueWhenActivated, 5000); } - - eventOn('click', 'input[name*=tinypng_sizes], #tinypng_resize_original_enabled', refreshSizeDescriptionNotice); - eventOn('click', '#tinypng_auto_compress_enabled', updateSettings); - jQuery('#tinypng_sizes_0, #tinypng_resize_original_enabled').click(updateSettings); - updateSettings(); + break; + case 'settings_page_tinify': + changeEnterKeyTarget('#tinypng_api_key_name, #tinypng_api_key_email', '[data-tiny-action=create-key]'); + changeEnterKeyTarget('#tinypng_api_key', '[data-tiny-action=update-key]'); + eventOn('click', '[data-tiny-action=create-key]', createAccount); + eventOn('click', '[data-tiny-action=update-key]', connectApiKey); + eventOn('click', '[data-tiny-action=clear-key]', disconnectApiKey); + eventOn('click', '[data-tiny-action=change-account]', showAccountConnect); + loadAccountStatus(); + eventOn('change', '#tiny-convert', showFormatOptions); + eventOn('change', '#tinypng_resize_original_enabled', showResizeInputs); + eventOn('change', '#tinypng_sizes_0', showOriginalSettings); + eventOn('change', 'input[name*=tinypng_sizes], #tinypng_resize_original_enabled, #tiny-convert', refreshCreditSummary); } jQuery('.tiny-notice a.tiny-dismiss').click(dismissNotice); jQuery(function() { jQuery('.tiny-notice.is-dismissible button').unbind('click').click(dismissNotice); }); - - - function onConvertChange(e) { - const newValue = e.target.checked; - if (newValue) { - jQuery('#tinypng_convert_convert_to').removeAttr('disabled'); - } else { - jQuery('#tinypng_convert_convert_to').attr('disabled', true); - } - } - jQuery('#tinypng_conversion_convert').on('change', onConvertChange); }).call(); diff --git a/src/views/account-status-connect.php b/src/views/account-status-connect.php new file mode 100644 index 00000000..e591b2d0 --- /dev/null +++ b/src/views/account-status-connect.php @@ -0,0 +1,110 @@ +%s', + esc_html__( 'API dashboard', 'tiny-compress-images' ) +); +?> ++ +
++ + + +
+ ++ + + +
+ ++ + +
++ +
++ + + + + +
+ ++ + + + + + + +
+- ok ) { - if ( isset( $status->message ) ) { - echo esc_html( $status->message ); - } else { - esc_html_e( 'Your account is connected', 'tiny-compress-images' ); - } - } else { - esc_html_e( 'Connection unsuccessful', 'tiny-compress-images' ); - } - ?> - - '; - esc_html_e( 'Change API key', 'tiny-compress-images' ); - echo ''; - } - ?> -
-- ok ) { - $strong = array( - 'strong' => array(), - ); - $compressions = self::get_compression_count(); - $remaining_credits = self::get_remaining_credits(); - if ( self::is_on_free_plan() ) { - printf( - wp_kses( - /* translators: %s: number of remaining credits */ - __( - 'You are on a free plan with %s compressions left this month.', // WPCS: Needed for proper translation. - 'tiny-compress-images' - ), - $strong - ), - intval( $remaining_credits ) - ); - } elseif ( ! $status->pending ) { - printf( - /* translators: %s: number of compressions */ - esc_html__( - 'You have made %s compressions this month.', - 'tiny-compress-images' - ), - intval( $compressions ) - ); - } - } elseif ( isset( $status->message ) ) { - echo esc_html__( 'Error', 'tiny-compress-images' ) . ': '; - echo esc_html( $status->message ); - } else { - esc_html_e( - 'API status could not be checked, enable cURL for more information', - 'tiny-compress-images' - ); - } // End if(). - ?> -
-- -
-- -
- - - - - - - - -- %s', - esc_html__( 'API dashboard', 'tiny-compress-images' ) - ); - echo wp_kses_post( - sprintf( - /* translators: %s: link saying API dashboard */ - __( 'Enter your API key. Go to your %s to retrieve it.', 'tiny-compress-images' ), - $link - ) - ); - ?> -
- - - - - - -- %s', - esc_html__( 'TinyPNG developer section', 'tiny-compress-images' ) - ); - - esc_html_e( 'Enter your API key.', 'tiny-compress-images' ); - echo ' '; - - echo wp_kses_post( - sprintf( - /* translators: %s: link saying TinyPNG developer section */ - __( 'If needed you can go to the %s to retrieve it.', 'tiny-compress-images' ), - $link - ) - ); - ?> -
- - - - - - -+
+
- -
+ + +
++ — + +
+- /> - -
-| - |
- - - -- > - - -
-
-
-
- |
-
|---|
+
\ No newline at end of file + + diff --git a/src/views/settings-original-image.php b/src/views/settings-original-image.php index 872055a3..cc625c15 100644 --- a/src/views/settings-original-image.php +++ b/src/views/settings-original-image.php @@ -1,119 +1,50 @@ array(), -); - +$tiny_resize_name = self::get_prefixed_name( 'resize_original[enabled]' ); +$tiny_backup_name = self::get_prefixed_name( 'backup[enabled]' ); +$tiny_resize_on = $this->get_resize_enabled(); ?> +
- get_backup_enabled();
- ?>
- />
-