capability(); add_menu_page( __('3D Print Showcase', '3d-print-showcase'), __('3D Print Showcase', '3d-print-showcase'), $capability, $parent, [$this, 'info_page'], $this->menu_icon(), 58 ); // Keep the WordPress administration menu focused on the three primary pages. add_submenu_page($parent, __('Info', '3d-print-showcase'), __('Info', '3d-print-showcase'), $capability, $parent, [$this, 'info_page']); add_submenu_page($parent, __('Settings', '3d-print-showcase'), __('Settings', '3d-print-showcase'), $capability, 'ds82-3dps-settings', [$this, 'settings_page']); add_submenu_page($parent, __('User 3D Viewer', '3d-print-showcase'), __('User 3D Viewer', '3d-print-showcase'), $capability, 'ds82-3dps-user-viewer', [$this, 'user_viewer_page']); // Register detail pages as hidden admin pages. They remain available through // the internal Settings navigation without extending the left admin menu. add_submenu_page(null, __('Materials', '3d-print-showcase'), __('Materials', '3d-print-showcase'), $capability, 'ds82-3dps-materials', [$this, 'materials_page']); add_submenu_page(null, __('Shortcode Builder', '3d-print-showcase'), __('Shortcode Builder', '3d-print-showcase'), $capability, 'ds82-3dps-shortcode-builder', [$this, 'shortcode_builder_page']); add_submenu_page(null, __('Addons', '3d-print-showcase'), __('Addons', '3d-print-showcase'), $capability, 'ds82-3dps-integrations', [$this, 'integrations_page']); add_submenu_page(null, __('Diagnostics', '3d-print-showcase'), __('Diagnostics', '3d-print-showcase'), $capability, 'ds82-3dps-diagnostics', [$this, 'diagnostics_page']); add_submenu_page(null, __('Tools', '3d-print-showcase'), __('Tools', '3d-print-showcase'), $capability, 'ds82-3dps-tools', [$this, 'tools_page']); add_submenu_page(null, __('Documentation', '3d-print-showcase'), __('Documentation', '3d-print-showcase'), $capability, 'ds82-3dps-documentation', [$this, 'documentation_page']); } private function menu_icon(): string { // WordPress allocates a 20 × 20 px menu icon box. The artwork is inset // to approximately 16 × 16 px so it visually matches native Dashicons. $svg = ''; return 'data:image/svg+xml;base64,' . base64_encode($svg); } public function assets(string $hook): void { $page = sanitize_key((string) filter_input(INPUT_GET, 'page', FILTER_UNSAFE_RAW)); if (!str_starts_with($page, 'ds82-3dps')) { return; } wp_enqueue_style('ds82-3dps-admin', DS82_3DPS_URL . 'assets/css/admin.css', [], DS82_3DPS_VERSION); // Keep the primary 3D Print Showcase navigation identical on every // core/add-on administration page. Add-ons may have their own local // sub-navigation, but the primary navigation always comes from the // main plugin's canonical tab registry. wp_enqueue_script('ds82-3dps-admin-navigation', DS82_3DPS_URL . 'assets/js/admin/navigation.js', [], DS82_3DPS_VERSION, true); $canonicalTabs = []; foreach ($this->tabs() as $slug => $label) { $canonicalTabs[] = [ 'slug' => (string) $slug, 'label' => (string) $label, 'url' => admin_url('admin.php?page=' . $slug), ]; } wp_localize_script('ds82-3dps-admin-navigation', 'DS823DPSAdminNavigation', [ 'tabs' => $canonicalTabs, 'currentPage' => $page, ]); if ('ds82-3dps-materials' === $page) { wp_enqueue_script('ds82-3dps-material-admin', DS82_3DPS_URL . 'assets/js/admin/materials.js', [], DS82_3DPS_VERSION, true); } } public function info_page(): void { $this->guard_page(); $installedVersion = DS82_3DPS_VERSION; $remoteVersion = $this->available_plugin_version(); $versionStatus = 'unknown'; if (is_string($remoteVersion) && '' !== $remoteVersion) { $versionStatus = version_compare($installedVersion, $remoteVersion, '>=') ? 'current' : 'update'; } $this->header(__('Info', '3d-print-showcase')); ?>
—

render_changelog(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
footer(); } private function available_plugin_version(): ?string { $cacheKey = 'ds82_3dps_available_version'; $cached = get_transient($cacheKey); if (is_string($cached) && '' !== $cached) { return $cached; } $response = wp_remote_get( 'https://api.wpresstech.cz/wp-json/wp/v2/ds82-pluginy', [ 'timeout' => 10, 'redirection' => 3, 'headers' => [ 'Accept' => 'application/json', ], 'user-agent' => '3D Print Showcase/' . DS82_3DPS_VERSION . '; ' . home_url('/'), ] ); if (is_wp_error($response) || 200 !== wp_remote_retrieve_response_code($response)) { return null; } $data = json_decode(wp_remote_retrieve_body($response), true); if (!is_array($data) || [] === $data) { return null; } $targetSlug = '3d-print-showcase-display-3d-models-with-realistic-filaments'; foreach ($data as $record) { if (!is_array($record) || $targetSlug !== ($record['slug'] ?? '')) { continue; } $rawVersion = $record['acf']['np_verze_pluginu'] ?? null; if (!is_scalar($rawVersion)) { return null; } $version = $this->normalize_version((string) $rawVersion); if (null !== $version) { set_transient($cacheKey, $version, 15 * MINUTE_IN_SECONDS); } return $version; } return null; } private function normalize_version(string $value): ?string { if (preg_match('/(?' . esc_html__('The changelog file is not available.', '3d-print-showcase') . '

'; } $contents = file_get_contents($path); if (!is_string($contents) || '' === trim($contents)) { return '

' . esc_html__('The changelog is empty.', '3d-print-showcase') . '

'; } $html = ''; $listOpen = false; foreach (preg_split('/\R/', $contents) ?: [] as $line) { $line = trim($line); if ('' === $line) { if ($listOpen) { $html .= ''; $listOpen = false; } continue; } if (str_starts_with($line, '# ')) { if ($listOpen) { $html .= ''; $listOpen = false; } $html .= '

' . esc_html(substr($line, 2)) . '

'; continue; } if (str_starts_with($line, '## ')) { if ($listOpen) { $html .= ''; $listOpen = false; } $html .= '

' . esc_html(substr($line, 3)) . '

'; continue; } if (str_starts_with($line, '- ')) { if (!$listOpen) { $html .= ''; $listOpen = false; } $html .= '

' . esc_html($line) . '

'; } if ($listOpen) { $html .= ''; } return '
' . $html . '
'; } public function settings_page(): void { $this->guard_page(); $settings = Settings::all(); $this->header(__('Settings', '3d-print-showcase')); ?>

footer(); } public function user_viewer_page(): void { $this->guard_page(); $settings = Settings::all(); $materials = $this->materials->all(); $selected = array_map('strval', (array) ($settings['user_viewer_materials'] ?? [])); $this->header(__('User 3D Viewer', '3d-print-showcase')); ?>

footer(); } public function shortcode_builder_page(): void { $this->guard_page(); $this->header(__('Shortcode Builder', '3d-print-showcase')); ?>

footer(); } public function documentation_page(): void { $this->guard_page(); $this->header(__('Documentation', '3d-print-showcase')); ?>

[ds82_3d_print_showcase model_id="123"]

[ds82_3d_print_showcase model_url="https://example.com/model.stl"]

[ds82_3d_print_showcase model_id="123" height="650" material="internal:az1" fallback_url="https://example.com/fallback.jpg" auto_rotate="true"]

model_idmodel_id="123"
model_urlmodel_url="https://example.com/model.3mf"
heightheight="600"
materialmaterial="internal:az1"
fallback_urlfallback_url="https://example.com/model.jpg"
auto_rotateauto_rotate="true"

STL, 3MF, GLB, GLTF

footer(); } public function integrations_page(): void { $this->guard_page(); $this->header(__('Addons', '3d-print-showcase')); $availableAddons = AddonCatalog::items(); ?>

'); ?>

footer(); } private function addon_action_button(string $slug, string $operation, string $label, string $class = ''): void { $url = wp_nonce_url(admin_url('admin-post.php?action=ds82_3dps_addon_action&operation=' . rawurlencode($operation) . '&addon=' . rawurlencode($slug)), 'ds82_3dps_addon_action_' . $operation . '_' . $slug); echo '' . esc_html($label) . ''; } public function refresh_addons(): void { $this->guard_action('ds82_3dps_refresh_addons'); check_admin_referer('ds82_3dps_refresh_addons'); AddonCatalog::clear_cache(); AddonCatalog::items(true); delete_site_transient('update_plugins'); $this->redirect('ds82-3dps-integrations', 'addons-refreshed'); } public function addon_action(): void { $this->guard_action('ds82_3dps_addon_action'); $operation = sanitize_key((string) filter_input(INPUT_GET, 'operation', FILTER_UNSAFE_RAW)); $slug = sanitize_key((string) filter_input(INPUT_GET, 'addon', FILTER_UNSAFE_RAW)); check_admin_referer('ds82_3dps_addon_action_' . $operation . '_' . $slug); $addon = AddonCatalog::find($slug); if (!$addon) wp_die(esc_html__('Addon was not found in the remote catalog.', '3d-print-showcase')); $installed = AddonCatalog::installed($addon); if ('activate' === $operation) { if (!$installed['file']) wp_die(esc_html__('The addon is not installed.', '3d-print-showcase')); $result = activate_plugin($installed['file']); if (is_wp_error($result)) wp_die(esc_html($result->get_error_message())); $this->redirect('ds82-3dps-integrations', 'addon-activated'); } if (!in_array($operation, ['install','update'], true)) wp_die(esc_html__('Unsupported addon action.', '3d-print-showcase')); if (empty($addon['downloadable']) || empty($addon['download'])) wp_die(esc_html__('This addon is not currently available for download.', '3d-print-showcase')); if ('update' === $operation && !$installed['file']) wp_die(esc_html__('The addon is not installed.', '3d-print-showcase')); require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; require_once ABSPATH . 'wp-admin/includes/file.php'; require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; $skin = new \Automatic_Upgrader_Skin(); $upgrader = new \Plugin_Upgrader($skin); $result = $upgrader->install((string)$addon['download'], ['overwrite_package' => 'update' === $operation]); if (is_wp_error($result)) wp_die(esc_html($result->get_error_message())); if (!$result) wp_die(esc_html__('WordPress could not install the addon package.', '3d-print-showcase')); AddonCatalog::clear_cache(); delete_site_transient('update_plugins'); $this->redirect('ds82-3dps-integrations', 'update' === $operation ? 'addon-updated' : 'addon-installed'); } /** * @param array> $installedPlugins * @param array $exactFiles * @param array $nameFragments */ private function find_installed_plugin(array $installedPlugins, array $exactFiles, array $nameFragments): string { foreach ($exactFiles as $pluginFile) { if (isset($installedPlugins[$pluginFile])) { return $pluginFile; } } foreach ($installedPlugins as $pluginFile => $data) { $pluginName = (string) ($data['Name'] ?? ''); foreach ($nameFragments as $fragment) { if ('' !== $fragment && false !== stripos($pluginName, $fragment)) { return (string) $pluginFile; } } } return ''; } public function materials_page(): void { $this->guard_page(); $provider = $this->materials->provider(); $isInternal = $provider instanceof InternalMaterialProvider; $materials = $this->materials->cached(); $editSlug = sanitize_key((string) filter_input(INPUT_GET, 'edit_material', FILTER_UNSAFE_RAW)); $editing = $isInternal && $editSlug !== '' ? $provider->raw($editSlug) : []; $editExternalRaw = filter_input(INPUT_GET, 'edit_external', FILTER_UNSAFE_RAW); $editExternal = is_string($editExternalRaw) ? rawurldecode($editExternalRaw) : ''; $editingExternalMaterial = (!$isInternal && '' !== $editExternal) ? $provider->get_material($editExternal) : null; $editingExternal = $editingExternalMaterial instanceof Material ? $this->overrides->editing_values($editingExternalMaterial) : []; $settings = Settings::all(); $this->header(__('Materials', '3d-print-showcase')); $sources = $this->providers->definitions(); $activeSourceId = $this->materials->provider()->get_id(); ?>

$definition) : ?>









>>>>>>

render_external_material_editor($editingExternalMaterial, $editingExternal); ?>

strnatcasecmp($a->display_id(), $b->display_id())); // Optional stock data is supplied by Filament Storage. The base plugin stays // independent, so any compatible add-on can provide the same normalized map. $stockMap = apply_filters('ds82_3dps_material_stock_map', []); $stockMap = is_array($stockMap) ? $stockMap : []; $showStock = !empty($stockMap) || has_filter('ds82_3dps_material_stock_map'); ?>
$material) : ?>display_id()] ?? null; $grams = is_array($stock) ? max(0.0, (float) ($stock['remaining_weight_g'] ?? 0)) : 0.0; $spoolCount = is_array($stock) ? max(0, (int) ($stock['spool_count'] ?? 0)) : 0; $stockClass = $grams <= 0 ? 'is-empty' : ($grams < 200 ? 'is-low' : 'is-available'); ?>
display_id()); ?>previewImageUrl) : ?>name); ?>color_mode_label((string) $material->colorMode)); ?>color_layout_label((string) $material->colorModeType)); ?>color_preview($material); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>source, ['remote_rest', 'custom_rest'], true) && !empty($material->sourceData['remote_material_variant'])) ? (string) $material->sourceData['remote_material_variant'] : $this->appearance_label((string) $material->appearanceType); echo esc_html($surfaceDisplay); ?>materialType); ?>manufacturer); ?> 0) : ?>
|
footer(); } private function render_external_material_editor(Material $material, array $editing): void { $colors = array_values(array_filter(array_map( static fn (string $color): string => sanitize_hex_color($color) ?: '', $material->colors() ))); if (!$colors) { $colors = ['#b7b7b7']; } $colorCount = max(1, min(4, count($colors))); $colorMode = (string) ($material->colorMode ?: match ($colorCount) { 2 => 'dual', 3 => 'triple', 4 => 'quad', default => 'single', }); $surfaceTypes = [ 'standard' => __('Standard', '3d-print-showcase'), 'matte' => __('Matte', '3d-print-showcase'), 'glossy' => __('Glossy', '3d-print-showcase'), 'silk' => __('Silk', '3d-print-showcase'), 'satin' => __('Satin', '3d-print-showcase'), 'metallic' => __('Metallic', '3d-print-showcase'), 'pearl' => __('Pearlescent', '3d-print-showcase'), 'transparent' => __('Transparent', '3d-print-showcase'), ]; $settings = Settings::all(); ?>

label())); ?>

[__('Clearcoat', '3d-print-showcase'), 0, 1, .01], 'sheen' => [__('Sheen', '3d-print-showcase'), 0, 1, .01], 'iridescence' => [__('Iridescence', '3d-print-showcase'), 0, 1, .01], 'transmission' => [__('Transmission', '3d-print-showcase'), 0, 1, .01], 'ior' => [__('Index of refraction (IOR)', '3d-print-showcase'), 1, 2.5, .01]] as $key => $cfg) : ?> [__('Key light', '3d-print-showcase'), 0, 3, .05, 1.55], 'light_fill' => [__('Fill light', '3d-print-showcase'), 0, 2, .05, .55], 'light_ambient' => [__('Ambient light', '3d-print-showcase'), 0, 2, .05, .85], 'exposure' => [__('Exposure', '3d-print-showcase'), .25, 2, .05, 1]] as $key => $cfg) : ?>

guard_page(); $provider = $this->materials->provider(); $status = method_exists($provider, 'status') ? (array) $provider->status() : []; $this->header(__('Diagnostics', '3d-print-showcase')); ?>

get_id()); ?>

WordPress:

PHP:

: providers->ids()) ?: '—'); ?>

footer(); } public function tools_page(): void { $this->guard_page(); $this->header(__('Tools', '3d-print-showcase')); ?>

footer(); } public function save_settings(): void { $this->guard_action('ds82_3dps_save_settings'); check_admin_referer('ds82_3dps_save_settings'); $postedSettings = filter_input(INPUT_POST, 'settings', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY); $input = is_array($postedSettings) ? $this->sanitize_input_array($postedSettings) : []; $redirectPageInput = filter_input(INPUT_POST, 'redirect_page', FILTER_UNSAFE_RAW); $redirectPage = sanitize_key(is_string($redirectPageInput) ? $redirectPageInput : 'ds82-3dps'); if (in_array($redirectPage, ['ds82-3dps-integrations', 'ds82-3dps-materials'], true)) { $input = array_merge(Settings::all(), $input); } $sanitized = Settings::sanitize($input, $this->providers->ids(), $this->integrations->ids_by_type('selection')); update_option(Settings::OPTION, $sanitized, false); $this->redirect($redirectPage, 'settings-saved'); } public function save_internal_material(): void { $this->guard_action('ds82_3dps_save_internal_material'); check_admin_referer('ds82_3dps_save_internal_material'); $provider = $this->providers->get('internal'); if (!$provider instanceof InternalMaterialProvider) { $this->redirect('ds82-3dps-materials', 'material-save-failed'); } $postedMaterial = filter_input(INPUT_POST, 'material', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY); $input = is_array($postedMaterial) ? $this->sanitize_input_array($postedMaterial) : []; $provider->save($input); $this->redirect('ds82-3dps-materials', 'material-saved'); } public function save_material_override(): void { $this->guard_action('ds82_3dps_save_material_override'); check_admin_referer('ds82_3dps_save_material_override'); $identifierRaw = filter_input(INPUT_POST, 'identifier', FILTER_UNSAFE_RAW); $identifier = is_string($identifierRaw) ? sanitize_text_field(wp_unslash($identifierRaw)) : ''; $provider = $this->materials->provider(); $material = $identifier ? $provider->get_material($identifier) : null; if (!$material instanceof Material || 'internal' === $material->source) { $this->redirect('ds82-3dps-materials','material-save-failed'); } $posted = filter_input(INPUT_POST, 'material', FILTER_DEFAULT, FILTER_REQUIRE_ARRAY); $input = is_array($posted) ? $this->sanitize_input_array($posted) : []; $this->overrides->save($material,$input); $this->redirect('ds82-3dps-materials','material-saved'); } public function reset_material_override(): void { $this->guard_action('ds82_3dps_reset_material_override'); check_admin_referer('ds82_3dps_reset_material_override'); $identifierRaw = filter_input(INPUT_GET, 'identifier', FILTER_UNSAFE_RAW); $identifier = is_string($identifierRaw) ? sanitize_text_field(wp_unslash($identifierRaw)) : ''; if ($identifier) $this->overrides->delete($identifier); $this->redirect('ds82-3dps-materials','settings-saved'); } public function delete_internal_material(): void { $this->guard_action('ds82_3dps_delete_internal_material'); check_admin_referer('ds82_3dps_delete_internal_material'); $provider = $this->providers->get('internal'); if ($provider instanceof InternalMaterialProvider) { $slug = filter_input(INPUT_GET, 'slug', FILTER_UNSAFE_RAW); $provider->delete(sanitize_key(is_string($slug) ? $slug : '')); } $this->redirect('ds82-3dps-materials', 'material-deleted'); } public function sync_materials(): void { $this->guard_action('ds82_3dps_sync_materials'); check_admin_referer('ds82_3dps_sync_materials'); $this->materials->all(true); $this->redirect('ds82-3dps-materials', 'materials-synchronized'); } public function test_provider(): void { $this->guard_action('ds82_3dps_test_provider'); check_admin_referer('ds82_3dps_test_provider'); $provider = $this->materials->provider(); if (method_exists($provider, 'test_connection')) { $provider->test_connection(); } $this->redirect('ds82-3dps-diagnostics', 'connection-tested'); } public function clear_cache(): void { $this->guard_action('ds82_3dps_clear_cache'); check_admin_referer('ds82_3dps_clear_cache'); $provider = $this->materials->provider(); if (method_exists($provider, 'clear_cache')) { $provider->clear_cache(); } $this->redirect('ds82-3dps-materials', 'cache-cleared'); } public function export_settings(): void { $this->guard_action('ds82_3dps_export_settings'); check_admin_referer('ds82_3dps_export_settings'); $payload = ['plugin' => '3d-print-showcase', 'version' => DS82_3DPS_VERSION, 'exported_at' => gmdate('c'), 'settings' => Settings::all()]; nocache_headers(); header('Content-Type: application/json; charset=utf-8'); header('Content-Disposition: attachment; filename="ds82-3dps-settings-' . gmdate('Y-m-d') . '.json"'); echo wp_json_encode($payload, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped exit; } public function import_settings(): void { $this->guard_action('ds82_3dps_import_settings'); check_admin_referer('ds82_3dps_import_settings'); $uploadedFile = isset($_FILES['settings_file']) && is_array($_FILES['settings_file']) ? $_FILES['settings_file'] : []; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $uploadError = isset($uploadedFile['error']) ? absint($uploadedFile['error']) : UPLOAD_ERR_NO_FILE; $temporaryName = isset($uploadedFile['tmp_name']) ? sanitize_text_field(wp_unslash((string) $uploadedFile['tmp_name'])) : ''; $originalName = isset($uploadedFile['name']) ? sanitize_file_name(wp_unslash((string) $uploadedFile['name'])) : ''; if (UPLOAD_ERR_OK !== $uploadError || '' === $temporaryName || !is_uploaded_file($temporaryName) || 'json' !== strtolower((string) pathinfo($originalName, PATHINFO_EXTENSION))) { $this->redirect('ds82-3dps-tools', 'import-failed'); } $raw = file_get_contents($temporaryName); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $decoded = json_decode((string) $raw, true); $input = is_array($decoded['settings'] ?? null) ? $decoded['settings'] : (is_array($decoded) ? $decoded : []); if (!$input) { $this->redirect('ds82-3dps-tools', 'import-failed'); } update_option(Settings::OPTION, Settings::sanitize($input, $this->providers->ids(), $this->integrations->ids_by_type('selection')), false); $this->redirect('ds82-3dps-tools', 'settings-imported'); } public function reset_settings(): void { $this->guard_action('ds82_3dps_reset_settings'); check_admin_referer('ds82_3dps_reset_settings'); update_option(Settings::OPTION, Settings::defaults(), false); $this->redirect('ds82-3dps-tools', 'settings-reset'); } private function header(string $title): void { $notice = sanitize_key((string) filter_input(INPUT_GET, 'ds82_3dps_notice', FILTER_UNSAFE_RAW)); ?>

notice_text($notice)); ?>

'; } private function tabs(): array { $coreTabs = [ 'ds82-3dps' => __('Info', '3d-print-showcase'), 'ds82-3dps-settings' => __('Settings', '3d-print-showcase'), 'ds82-3dps-materials' => __('Materials', '3d-print-showcase'), 'ds82-3dps-shortcode-builder' => __('Shortcode Builder', '3d-print-showcase'), 'ds82-3dps-diagnostics' => __('Diagnostics', '3d-print-showcase'), 'ds82-3dps-tools' => __('Tools', '3d-print-showcase'), 'ds82-3dps-documentation' => __('Documentation', '3d-print-showcase'), 'ds82-3dps-user-viewer' => __('User 3D Viewer', '3d-print-showcase'), ]; // Add-ons may append their own administration tabs through this filter. // The Addons catalog is removed from the filtered result and appended // afterwards so it always remains the final navigation item. $tabs = apply_filters('ds82_3dps_admin_tabs', $coreTabs); if (!is_array($tabs)) { $tabs = $coreTabs; } unset($tabs['ds82-3dps-integrations']); $tabs['ds82-3dps-integrations'] = __('Addons', '3d-print-showcase'); return $tabs; } private function color_preview(Material $material): string { $colors = $material->colors(); if (!$colors) { $colors = ['#b7b7b7']; } $type = strtolower((string) $material->colorModeType); $safeColors = array_values(array_filter(array_map('sanitize_hex_color', $colors))); if (1 === count($safeColors)) { $background = $safeColors[0]; } elseif (str_contains($type, 'layered') || str_contains($type, 'vrstven')) { $bandSize = min(50.0, max(0.25, (float) ($material->sourceData['layer_band_percent'] ?? 1))); $stops = []; $bandCount = (int) ceil(100 / $bandSize); for ($band = 0; $band < $bandCount; $band++) { $color = $safeColors[$band % count($safeColors)]; $start = min(100, round($band * $bandSize, 2)); $end = min(100, round(($band + 1) * $bandSize, 2)); $stops[] = $color . ' ' . $start . '%'; $stops[] = $color . ' ' . $end . '%'; } $background = 'linear-gradient(to top,' . implode(',', $stops) . ')'; } elseif (str_contains($type, 'duhov') || str_contains($type, 'rainbow')) { $background = 'linear-gradient(to top,' . implode(',', $safeColors) . ')'; } elseif (str_contains($type, 'přech') || str_contains($type, 'gradient')) { $background = 'linear-gradient(to right,' . implode(',', $safeColors) . ')'; } else { $background = 'conic-gradient(' . implode(',', $safeColors) . ')'; } return '' . esc_html(implode(' · ', $colors)) . ''; } private function color_mode_label(string $value): string { return match (sanitize_key($value)) { 'dual' => __('Two colors', '3d-print-showcase'), 'triple' => __('Three colors', '3d-print-showcase'), 'quad', 'multi' => __('Four colors', '3d-print-showcase'), default => __('One color', '3d-print-showcase'), }; } private function color_layout_label(string $value): string { return match (sanitize_key($value)) { 'gradient' => __('Gradient', '3d-print-showcase'), 'rainbow' => __('Rainbow', '3d-print-showcase'), 'layered' => __('Layered', '3d-print-showcase'), default => __('Standard', '3d-print-showcase'), }; } private function appearance_label(string $value): string { $labels = [ 'standard' => __('Standard', '3d-print-showcase'), 'matte' => __('Matte', '3d-print-showcase'), 'glossy' => __('Glossy', '3d-print-showcase'), 'silk' => __('Silk', '3d-print-showcase'), 'satin' => __('Satin', '3d-print-showcase'), 'glitter' => __('Glitter', '3d-print-showcase'), 'metallic' => __('Metallic', '3d-print-showcase'), 'pearl' => __('Pearlescent', '3d-print-showcase'), 'transparent' => __('Transparent', '3d-print-showcase'), 'carbon' => __('Carbon', '3d-print-showcase'), 'wood' => __('Wood', '3d-print-showcase'), ]; return $labels[sanitize_key($value)] ?? ($value !== '' ? $value : $labels['standard']); } private function action_url(string $action): string { return wp_nonce_url(admin_url('admin-post.php?action=' . $action), $action); } private function capability(): string { return class_exists('WooCommerce') ? 'manage_woocommerce' : 'manage_options'; } private function guard_page(): void { if (!current_user_can($this->capability())) { wp_die(esc_html__('You are not allowed to access this page.', '3d-print-showcase')); } } private function sanitize_input_array(array $input): array { $sanitized = []; foreach ($input as $key => $value) { $safeKey = sanitize_key((string) $key); if (is_array($value)) { $sanitized[$safeKey] = $this->sanitize_input_array($value); } else { $sanitized[$safeKey] = sanitize_text_field((string) $value); } } return $sanitized; } private function guard_action(string $action): void { $this->guard_page(); } private function redirect(string $page, string $notice): never { wp_safe_redirect(add_query_arg(['page' => $page, 'ds82_3dps_notice' => $notice], admin_url('admin.php'))); exit; } private function current_page(): string { $page = filter_input(INPUT_GET, 'page', FILTER_UNSAFE_RAW); return sanitize_key(is_string($page) && '' !== $page ? $page : 'ds82-3dps'); } private function notice_text(string $notice): string { return match ($notice) { 'settings-saved' => __('Settings saved.', '3d-print-showcase'), 'materials-synchronized' => __('Materials synchronized.', '3d-print-showcase'), 'material-saved' => __('Material saved.', '3d-print-showcase'), 'material-deleted' => __('Material deleted.', '3d-print-showcase'), 'material-save-failed' => __('Material could not be saved.', '3d-print-showcase'), 'connection-tested' => __('Connection test completed.', '3d-print-showcase'), 'cache-cleared' => __('Material cache cleared.', '3d-print-showcase'), 'settings-imported' => __('Settings imported.', '3d-print-showcase'), 'settings-reset' => __('Settings reset.', '3d-print-showcase'), 'import-failed' => __('Settings import failed.', '3d-print-showcase'), 'addons-refreshed' => __('Addon catalog refreshed.', '3d-print-showcase'), 'addon-installed' => __('Addon installed successfully.', '3d-print-showcase'), 'addon-activated' => __('Addon activated successfully.', '3d-print-showcase'), 'addon-updated' => __('Addon updated successfully.', '3d-print-showcase'), default => __('Operation completed.', '3d-print-showcase'), }; } }