'settings', 'settings' => array( 'checkPath' => '/aimodelka/v1/connection-check', 'keyInConstant' => Config::api_key_in_constant(), 'checkingText' => __( 'Checking…', 'aimodelka' ), 'noKeyText' => __( 'Enter the key first.', 'aimodelka' ), 'failedText' => __( 'The check did not finish. Reload the page and try again.', 'aimodelka' ), ), ) ); } /** * Регистрирует настройку, разделы и поля в Settings API. * * Разделы рисует сама страница — карточками (render_page), поэтому у разделов нет * своих заголовков и описаний; поля в карточку кладёт do_settings_fields(). * * @return void */ public function register_settings(): void { register_setting( self::OPTION_GROUP, Settings::OPTION, array( 'type' => 'array', 'sanitize_callback' => array( $this, 'sanitize' ), 'default' => Settings::defaults(), 'show_in_rest' => false, ) ); add_settings_section( self::SECTION_CONNECTION, '', '__return_false', self::PAGE_SLUG ); add_settings_section( self::SECTION_PHOTOS, '', '__return_false', self::PAGE_SLUG ); add_settings_section( self::SECTION_SAVING, '', '__return_false', self::PAGE_SLUG ); add_settings_section( self::SECTION_SHOP, '', '__return_false', self::PAGE_SLUG ); add_settings_field( 'aimodelka_api_key', __( 'aiModelka key', 'aimodelka' ), array( $this, 'render_api_key_field' ), self::PAGE_SLUG, self::SECTION_CONNECTION, array( 'label_for' => 'aimodelka-api-key' ) ); add_settings_field( 'aimodelka_image_size', __( 'Photo size for the product', 'aimodelka' ), array( $this, 'render_image_size_field' ), self::PAGE_SLUG, self::SECTION_PHOTOS ); add_settings_field( 'aimodelka_file_format', __( 'File type', 'aimodelka' ), array( $this, 'render_file_format_field' ), self::PAGE_SLUG, self::SECTION_PHOTOS ); add_settings_field( 'aimodelka_aspect_ratio', __( 'Photo format', 'aimodelka' ), array( $this, 'render_aspect_ratio_field' ), self::PAGE_SLUG, self::SECTION_PHOTOS ); add_settings_field( 'aimodelka_gallery_position', __( 'New photo in the gallery', 'aimodelka' ), array( $this, 'render_gallery_position_field' ), self::PAGE_SLUG, self::SECTION_SAVING ); add_settings_field( 'aimodelka_old_main', __( 'Previous product image', 'aimodelka' ), array( $this, 'render_old_main_field' ), self::PAGE_SLUG, self::SECTION_SAVING ); add_settings_field( 'aimodelka_ask_variation', __( 'Variations', 'aimodelka' ), array( $this, 'render_ask_variation_field' ), self::PAGE_SLUG, self::SECTION_SAVING ); add_settings_field( 'aimodelka_variation_to_gallery', __( 'Variation photo', 'aimodelka' ), array( $this, 'render_variation_to_gallery_field' ), self::PAGE_SLUG, self::SECTION_SAVING ); add_settings_field( 'aimodelka_storefront_block', __( 'Quick access', 'aimodelka' ), array( $this, 'render_storefront_block_field' ), self::PAGE_SLUG, self::SECTION_SHOP ); } /** * Проверяет и приводит в порядок присланную форму. * * Неизвестные значения заменяются прежними; ключ, не похожий на ключ aiModelka, * не сохраняется — остаётся прежний, а человек видит, что не так. Новый ключ снимает * отметку «ключ не работает»: его ещё никто не отвергал. * * WordPress может вызвать эту функцию дважды за одно сохранение (при самом первом * сохранении настройки), поэтому она даёт тот же результат на своём же результате. * * @param mixed $input Присланные значения. * @return array{api_key: string, image_size: string, file_format: string, aspect_ratio: string, gallery_position: string, old_main: string, ask_variation: string, variation_to_gallery: string, storefront_block: string} */ public function sanitize( $input ): array { $current = Settings::all(); if ( ! is_array( $input ) ) { return $current; } $result = $current; if ( ! Config::api_key_in_constant() && array_key_exists( 'api_key', $input ) ) { $key = trim( sanitize_text_field( (string) $input['api_key'] ) ); if ( '' === $key || 1 === preg_match( '/^amk_[A-Za-z0-9_-]{20,200}$/', $key ) ) { if ( $key !== $current['api_key'] ) { Notices::clear_key_rejected(); } $result['api_key'] = $key; } else { add_settings_error( Settings::OPTION, 'aimodelka_api_key', __( 'This does not look like an aiModelka key: a key starts with amk_. Copy it again from your aiModelka account. The previous key is kept.', 'aimodelka' ) ); } } $choices = array( 'image_size' => Settings::IMAGE_SIZES, 'file_format' => Settings::FILE_FORMATS, 'aspect_ratio' => Settings::ASPECT_RATIOS, 'gallery_position' => Gallery_Layout::POSITIONS, 'old_main' => Gallery_Layout::OLD_MAIN_MODES, 'ask_variation' => Settings::YES_NO, 'variation_to_gallery' => Settings::YES_NO, 'storefront_block' => Settings::YES_NO, ); foreach ( $choices as $field => $allowed ) { $value = $input[ $field ] ?? null; $result[ $field ] = in_array( $value, $allowed, true ) ? $value : $current[ $field ]; } return $result; } /** * Рисует страницу настроек. * * @return void */ public function render_page(): void { if ( ! current_user_can( self::CAPABILITY ) ) { return; } // Одна проверка связи на открытие страницы — до вывода, чтобы её итог встал // в карточку «Связь» (render_api_key_field). $this->connection = $this->check_connection_on_load(); ?>
%1$s %3$s
', esc_html__( 'Create a key in your aiModelka account: Account → API keys.', 'aimodelka' ), esc_url( Config::api_keys_page_url() ), esc_html__( 'Open API keys', 'aimodelka' ) ); printf( '%2$s
', null === $this->connection ? '' : sprintf( ' data-state="%s"', esc_attr( $this->connection['state'] ) ), null === $this->connection ? '' : esc_html( $this->connection['text'] ) ); } /** * Размер фото для товара: 1K или 2K, без цены. В этом размере окно делает варианты * и показывает его на фото по умолчанию; в товар уходит показанный размер. * * @return void */ public function render_image_size_field(): void { $this->render_radios( 'image_size', Settings::image_size(), array( '1K' => __( '1K — variants at the regular price', 'aimodelka' ), '2K' => __( '2K — variants are made in 2K and cost more', 'aimodelka' ), ), __( 'The window makes variants in this size and shows it on the photo by default. The exact price is on the “Generate photo” button before you start.', 'aimodelka' ) ); } /** * Тип файла: JPEG или PNG. * * @return void */ public function render_file_format_field(): void { $this->render_radios( 'file_format', Settings::file_format(), array( 'jpg' => __( 'JPEG', 'aimodelka' ), 'png' => __( 'PNG', 'aimodelka' ), ), __( 'PNG files are several times larger; on a shop page they look the same.', 'aimodelka' ) ); } /** * Формат кадров новой съёмки — те же четыре, что переключателем в окне. * * @return void */ public function render_aspect_ratio_field(): void { $this->render_radios( 'aspect_ratio', Settings::aspect_ratio(), array( '3:4' => __( '3:4 — vertical, as in the aiModelka account', 'aimodelka' ), '1:1' => __( '1:1 — square', 'aimodelka' ), '4:5' => __( '4:5 — vertical, for social networks', 'aimodelka' ), '9:16' => __( '9:16 — tall, for stories', 'aimodelka' ), ), __( 'The format of the shots in a new photo shoot. In the window you can change it for a particular product.', 'aimodelka' ) ); } /** * Куда встаёт новое фото в галерее товара: первым или последним. * * @return void */ public function render_gallery_position_field(): void { $this->render_radios( 'gallery_position', Settings::gallery_position(), array( Gallery_Layout::FIRST => __( 'First', 'aimodelka' ), Gallery_Layout::LAST => __( 'Last', 'aimodelka' ), ), __( 'Where a photo saved with “To product gallery” goes in the product gallery.', 'aimodelka' ) ); } /** * Что делать с прежним изображением товара, когда новое фото становится изображением товара. * * @return void */ public function render_old_main_field(): void { $this->render_radios( 'old_main', Settings::old_main(), array( Gallery_Layout::TO_GALLERY => __( 'Move to the gallery', 'aimodelka' ), Gallery_Layout::REMOVE => __( 'Remove from the product', 'aimodelka' ), ), __( 'When a new photo becomes the product image. The file stays in the Media Library either way.', 'aimodelka' ) ); } /** * Спрашивать ли в окне, для какой вариации фото. * * @return void */ public function render_ask_variation_field(): void { $this->render_radios( 'ask_variation', Settings::ask_variation() ? 'yes' : 'no', array( 'yes' => __( 'Ask when saving which variation the photo is for', 'aimodelka' ), 'no' => __( 'Don’t ask — save it as the image of the whole product', 'aimodelka' ), ), __( 'Only for products with variations. The image is set for every variation with the chosen attribute value — for example, for every size of one colour.', 'aimodelka' ) ); } /** * Начальное положение галочки «Добавить в галерею товара» в вопросе о вариации. * * @return void */ public function render_variation_to_gallery_field(): void { $this->render_radios( 'variation_to_gallery', Settings::variation_to_gallery() ? 'yes' : 'no', array( 'yes' => __( 'Also add it to the product gallery', 'aimodelka' ), 'no' => __( 'Don’t add it to the gallery', 'aimodelka' ), ), __( 'The initial state of the “Also add to the product gallery” checkbox in the question about the variation.', 'aimodelka' ) ); } /** * Флажок блока на витрине. Снятый флажок браузер не отправляет, поэтому перед ним стоит * скрытое поле «no»: без него снять флажок было бы нельзя — проверка формы оставила бы * прежнее значение. * * @return void */ public function render_storefront_block_field(): void { printf( '', esc_attr( Settings::OPTION ), checked( Settings::storefront_block(), true, false ), esc_html__( 'Quick-access buttons on the product page', 'aimodelka' ) ); printf( '%s
', esc_html__( 'Adds aiModelka buttons under the product gallery in your shop. Only people who can edit products see them; customers never do.', 'aimodelka' ) ); } /** * Группа переключателей «один из»: каждый вариант на своей строке, подсказка под группой. * * @param string $field Имя поля внутри записи настроек. * @param string $current Выбранное значение. * @param array%s
', esc_html( $description ) ); } }