*/ private static $settings_overlay = array(); /** * When set, get() returns this map instead of Instance / option storage. * * @var array|null */ private static $forced_settings = null; /** * Settings page tabs. * * @return array slug => label */ public static function tabs() { return array( 'general' => __( 'Connection', 'dadan' ), 'cta' => __( 'Record button', 'dadan' ), 'studio' => __( 'Recording', 'dadan' ), 'theme' => __( 'Studio theme', 'dadan' ), 'advanced' => __( 'Access & advanced', 'dadan' ), ); } /** * Run a callback with an instance context for get()/field_key(). * * @param int $instance_id Instance post ID. * @param callable $callback Callback. * @return mixed */ public static function with_instance( $instance_id, $callback ) { $prev = self::$context_id; self::$context_id = max( 0, (int) $instance_id ); try { return call_user_func( $callback ); } finally { self::$context_id = $prev; } } /** * Run a callback with per-placement settings overrides (block / shortcode). * * @param array $overlay Partial settings to merge on top of get(). * @param callable $callback Callback. * @return mixed */ public static function with_settings_overlay( array $overlay, $callback ) { $prev = self::$settings_overlay; self::$settings_overlay = array_merge( $prev, $overlay ); try { return call_user_func( $callback ); } finally { self::$settings_overlay = $prev; } } /** * Whether privileged placement credentials may be taken from stored block/widget attrs. * * Used at render time (NEW-1 / DADAN-03 residual): only trust App ID / folder / team / * access overrides when the post author is an administrator. Non-admin-authored content * always falls back to Instance credentials. * * @param int $post_id Post or Elementor document ID (0 = current queried post). * @return bool */ public static function placement_author_can_set_credentials( $post_id = 0 ) { $post_id = absint( $post_id ); if ( $post_id <= 0 ) { $post_id = absint( get_the_ID() ); } if ( $post_id <= 0 ) { return current_user_can( 'manage_options' ); } $post = get_post( $post_id ); if ( ! $post ) { return false; } return user_can( (int) $post->post_author, 'manage_options' ); } /** * Run a callback with fully standalone settings (no Instance / option read). * * @param array $settings Complete sanitized settings map. * @param callable $callback Callback. * @return mixed */ public static function with_forced_settings( array $settings, $callback ) { $prev = self::$forced_settings; self::$forced_settings = $settings; try { return call_user_func( $callback ); } finally { self::$forced_settings = $prev; } } /** * Sanitize a settings array without syncing the Default instance. * * @param array $input Raw settings. * @return array */ public static function sanitize_array( array $input ) { $GLOBALS['dadan_skip_default_sync'] = true; try { $obj = new self(); return $obj->sanitize( $input ); } finally { unset( $GLOBALS['dadan_skip_default_sync'] ); } } /** * Current form context instance ID. * * @return int */ public static function context_id() { return (int) self::$context_id; } /** * Form field name prefix (instance meta vs legacy option). * * @return string */ public static function field_key() { if ( self::$context_id > 0 ) { return Dadan_Instance::FORM_FIELD_KEY; } return self::OPTION_KEY; } /** * Register hooks. */ public function register() { // DADAN-24: no legacy register_setting — Instances own persistence; avoids double sanitize. add_action( 'admin_menu', array( $this, 'register_menu' ), 20 ); add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_settings_assets' ) ); } /** * Defaults. * * @return array */ public static function defaults() { return array( 'public_app_id' => '', 'video_title' => 'WordPress recording', 'recording_quality' => '1080p', 'default_mode' => 'both', 'folder_id' => '', 'team_id' => '', 'mount' => 'light', 'locale' => 'en', 'countdown_seconds' => 3, 'insert_button_text' => 'Done', // Studio feature flags (createInstance config.features). 'feature_modes' => array( 'both', 'camera', 'screen' ), 'feature_mode_picker' => true, 'feature_camera_picker' => true, 'feature_mic_picker' => true, 'feature_system_audio' => true, 'feature_backdrop' => true, 'feature_deck_grip' => true, 'feature_close_button' => true, // Off by default (DADAN-12): unfinished takes in IndexedDB on shared devices. 'feature_restore_pending' => false, // Theme (config.theme). 'theme_primary' => self::DADAN_BG, 'theme_appearance' => 'light', 'theme_font_family' => '', 'theme_border_radius' => '', 'theme_logo_url' => '', 'theme_surface' => '', 'theme_surface_muted' => '', 'theme_ink' => '', 'theme_ink_soft' => '', 'theme_ink_muted' => '', 'theme_border' => '', 'theme_border_strong' => '', 'theme_danger' => '', 'theme_success' => '', 'theme_shadow' => '', // Screen capture — one checkbox group → surfaces + preferredSurface (first checked). 'screen_surfaces' => array( 'monitor', 'window', 'browser' ), 'screen_preferred' => 'monitor', 'screen_surface_picker' => false, 'screen_prefer_current_tab' => false, 'screen_self_browser' => '', 'screen_surface_switching' => '', 'screen_monitor_type' => '', // Developer: dispatch window CustomEvents for contract events. 'dispatch_browser_events' => true, // Who may use the public Record button on the front end. 'front_record_access' => 'anyone', // CTA builder (app-like). Preset "dadan" is the default / fallback. 'cta_preset' => 'dadan', 'cta_variant' => 'icon', 'cta_label' => '', 'cta_bg' => self::DADAN_BG, 'cta_fg' => self::DADAN_FG, 'cta_shape' => 'rounded', 'cta_size' => 'md', 'cta_image_id' => 0, 'cta_hide_on_phones' => true, 'cta_custom_class' => '', 'cta_custom_id' => '', // Legacy (migrated on read). 'trigger_style' => 'icon', 'trigger_label' => '', ); } /** * Sanitize a space-separated list of CSS class names. * * @param string $raw Raw input. * @return string */ public static function sanitize_css_classes( $raw ) { $parts = preg_split( '/\s+/', trim( (string) $raw ) ); if ( ! is_array( $parts ) ) { return ''; } $clean = array(); foreach ( $parts as $part ) { $part = preg_replace( '/[^A-Za-z0-9_-]/', '', (string) $part ); if ( '' !== $part && preg_match( '/^[A-Za-z_]/', $part ) ) { $clean[] = $part; } } return implode( ' ', array_unique( $clean ) ); } /** * Sanitize an HTML element id. * * @param string $raw Raw input. * @return string */ public static function sanitize_html_id( $raw ) { $id = preg_replace( '/[^A-Za-z0-9_-]/', '', trim( (string) $raw ) ); if ( '' === $id || ! preg_match( '/^[A-Za-z]/', $id ) ) { return ''; } return $id; } /** * Optional custom CSS classes from settings. * * @param int $instance_id Optional instance ID. * @return string */ public static function cta_custom_class( $instance_id = 0 ) { $s = $instance_id ? self::get( $instance_id ) : self::get(); return self::sanitize_css_classes( isset( $s['cta_custom_class'] ) ? (string) $s['cta_custom_class'] : '' ); } /** * Optional custom element id from settings. * * @param int $instance_id Optional instance ID. * @return string */ public static function cta_custom_id( $instance_id = 0 ) { $s = $instance_id ? self::get( $instance_id ) : self::get(); return self::sanitize_html_id( isset( $s['cta_custom_id'] ) ? (string) $s['cta_custom_id'] : '' ); } /** * Whether the public record CTA should be hidden on phones. * * @param int $instance_id Optional instance ID. * @return bool */ public static function cta_hide_on_phones( $instance_id = 0 ) { $s = $instance_id ? self::get( $instance_id ) : self::get(); if ( ! array_key_exists( 'cta_hide_on_phones', $s ) ) { return true; } return (bool) $s['cta_hide_on_phones']; } /** * Get merged settings for an instance (or default / legacy option). * * @param int|null $instance_id Instance post ID; null uses context or default. * @return array */ public static function get( $instance_id = null ) { if ( is_array( self::$forced_settings ) ) { $merged = self::$forced_settings; if ( ! empty( self::$settings_overlay ) ) { $merged = array_merge( $merged, self::$settings_overlay ); } return $merged; } if ( null === $instance_id ) { $instance_id = self::$context_id; } $instance_id = (int) $instance_id; if ( $instance_id > 0 && class_exists( 'Dadan_Instance' ) ) { $merged = Dadan_Instance::get_settings( $instance_id ); } elseif ( class_exists( 'Dadan_Instance' ) ) { $default = (int) get_option( Dadan_Instance::DEFAULT_OPT, 0 ); if ( $default > 0 && get_post( $default ) && Dadan_Instance::POST_TYPE === get_post_type( $default ) ) { $merged = Dadan_Instance::get_settings( $default ); } else { $merged = null; } } else { $merged = null; } if ( null === $merged ) { $stored = get_option( self::OPTION_KEY, array() ); if ( ! is_array( $stored ) ) { $stored = array(); } // One-time logical migrate: old icon|text → CTA preset/variant. if ( ! isset( $stored['cta_preset'] ) && isset( $stored['trigger_style'] ) ) { if ( 'text' === $stored['trigger_style'] ) { $stored['cta_preset'] = 'custom'; $stored['cta_variant'] = 'button'; if ( ! empty( $stored['trigger_label'] ) ) { $stored['cta_label'] = $stored['trigger_label']; } } else { $stored['cta_preset'] = 'dadan'; } } $merged = array_merge( self::defaults(), $stored ); } if ( ! empty( self::$settings_overlay ) ) { $merged = array_merge( $merged, self::$settings_overlay ); } return $merged; } /** * Public app id only (trimmed). * * @param int $instance_id Optional instance ID. * @return string */ public static function public_app_id( $instance_id = 0 ) { $settings = $instance_id ? self::get( $instance_id ) : self::get(); return isset( $settings['public_app_id'] ) ? trim( (string) $settings['public_app_id'] ) : ''; } /** * Whether a value is a browser-safe Dadan public App ID (app_…). * * @param string $id Candidate ID. * @return bool */ public static function is_valid_public_app_id( $id ) { $id = trim( (string) $id ); return (bool) preg_match( '/^app_[A-Za-z0-9_-]+$/', $id ); } /** * Sanitize a public App ID; empty string if invalid. Optionally registers a settings error. * * @param mixed $raw Raw input. * @param string $error_group Settings error code group (empty = no admin notice). * @return string */ public static function sanitize_public_app_id( $raw, $error_group = '' ) { $id = trim( sanitize_text_field( (string) $raw ) ); if ( '' === $id ) { return ''; } if ( self::is_valid_public_app_id( $id ) ) { return $id; } if ( '' !== $error_group ) { if ( preg_match( '/^(dadan_|dk_|dok_)/i', $id ) ) { add_settings_error( $error_group, 'dadan_bad_key', __( 'That value looks like a secret API key. Use a public App ID (app_…), not an API key.', 'dadan' ), 'error' ); } else { add_settings_error( $error_group, 'dadan_bad_app_id', __( 'Public App ID must start with app_ and use only letters, numbers, underscores, or hyphens.', 'dadan' ), 'error' ); } } return ''; } /** * Front-end who-can-record policy for an instance (0 = current/default context). * * @param int $instance_id Optional instance ID. * @return string anyone|logged_in|edit_posts */ public static function front_record_access( $instance_id = 0 ) { $s = $instance_id ? self::get( $instance_id ) : self::get(); $v = isset( $s['front_record_access'] ) ? (string) $s['front_record_access'] : 'anyone'; return in_array( $v, array( 'anyone', 'logged_in', 'edit_posts' ), true ) ? $v : 'anyone'; } /** * Whether the current request may use a public Record button. * * @param int $instance_id Optional instance ID (0 = current/forced context). * @return bool */ public static function visitor_can_record( $instance_id = 0 ) { $access = self::front_record_access( $instance_id ); if ( 'anyone' === $access ) { return true; } if ( 'logged_in' === $access ) { return is_user_logged_in(); } return is_user_logged_in() && current_user_can( 'edit_posts' ); } /** * Whether recording is configured for an instance (0 = default). * * @param int $instance_id Optional instance ID. * @return bool */ public static function can_record( $instance_id = 0 ) { return self::is_valid_public_app_id( self::public_app_id( $instance_id ) ); } /** * Resolved CTA config for rendering (Dadan preset wins as default/fallback). * * @param int $instance_id Optional instance ID. * @return array{ * preset:string,variant:string,label:string,bg:string,fg:string, * shape:string,size:string,image_id:int,image_url:string,icon_url:string * } */ public static function cta_config( $instance_id = 0 ) { $s = $instance_id ? self::get( $instance_id ) : self::get(); $preset = isset( $s['cta_preset'] ) ? (string) $s['cta_preset'] : 'dadan'; if ( ! in_array( $preset, array( 'dadan', 'custom' ), true ) ) { $preset = 'dadan'; } $variant = isset( $s['cta_variant'] ) ? (string) $s['cta_variant'] : 'icon'; if ( ! in_array( $variant, array( 'icon', 'button', 'image' ), true ) ) { $variant = 'icon'; } $label = isset( $s['cta_label'] ) ? trim( (string) $s['cta_label'] ) : ''; if ( '' === $label ) { $label = __( 'Open recorder', 'dadan' ); } $bg = self::sanitize_hex( isset( $s['cta_bg'] ) ? (string) $s['cta_bg'] : self::DADAN_BG, self::DADAN_BG ); $fg = self::sanitize_hex( isset( $s['cta_fg'] ) ? (string) $s['cta_fg'] : self::DADAN_FG, self::DADAN_FG ); $shape = isset( $s['cta_shape'] ) ? (string) $s['cta_shape'] : 'rounded'; if ( ! in_array( $shape, array( 'rounded', 'pill', 'square' ), true ) ) { $shape = 'rounded'; } $size = isset( $s['cta_size'] ) ? (string) $s['cta_size'] : 'md'; if ( ! in_array( $size, array( 'sm', 'md', 'lg' ), true ) ) { $size = 'md'; } $image_id = isset( $s['cta_image_id'] ) ? absint( $s['cta_image_id'] ) : 0; $image_url = $image_id ? (string) wp_get_attachment_image_url( $image_id, 'medium' ) : ''; $dadan = self::dadan_icon_url(); // Dadan preset always uses brand mark. if ( 'dadan' === $preset ) { return array( 'preset' => 'dadan', 'variant' => 'icon', 'label' => $label, 'bg' => self::DADAN_BG, 'fg' => self::DADAN_FG, 'shape' => 'rounded', 'size' => 'md', 'image_id' => 0, 'image_url' => '', 'icon_url' => $dadan, ); } // Custom image variant without an upload → fall back to Dadan. if ( 'image' === $variant && '' === $image_url ) { $variant = 'icon'; $image_url = ''; } $icon_url = $image_url ? $image_url : $dadan; return array( 'preset' => 'custom', 'variant' => $variant, 'label' => $label, 'bg' => $bg, 'fg' => $fg, 'shape' => $shape, 'size' => $size, 'image_id' => $image_id, 'image_url' => $image_url, 'icon_url' => $icon_url, ); } /** * Bundled Dadan mark URL. * * @return string */ public static function dadan_icon_url() { return DADAN_VIDEO_URL . 'assets/dadan-icon.svg'; } /** * @deprecated Use cta_config() / dadan_icon_url(). * @return string */ public static function trigger_icon_url() { $cta = self::cta_config(); return $cta['icon_url']; } /** * @deprecated Use cta_config(). * @return string */ public static function trigger_style() { $cta = self::cta_config(); if ( 'dadan' === $cta['preset'] ) { return 'icon'; } return 'button' === $cta['variant'] ? 'text' : 'icon'; } /** * @deprecated Use cta_config(). * @return string */ public static function trigger_label() { return self::cta_config()['label']; } /** * Sanitize a CSS hex color. * * @param string $value Raw. * @param string $fallback Fallback. * @return string */ public static function sanitize_hex( $value, $fallback ) { $normalized = self::normalize_css_color( $value ); return '' !== $normalized ? $normalized : $fallback; } /** * Normalize CSS colors from Elementor (alpha / globals) and Gutenberg to #RRGGBB. * Accepts #RGB, #RRGGBB, #RRGGBBAA, rgb(), rgba(). Returns '' if unusable. * * @param mixed $value Raw color. * @return string Uppercase #RRGGBB or empty. */ public static function normalize_css_color( $value ) { $value = trim( (string) $value ); if ( '' === $value || 0 === strpos( $value, 'globals/' ) || 0 === strpos( $value, 'var(' ) ) { return ''; } if ( preg_match( '/^#([A-Fa-f0-9]{3}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/', $value, $m ) ) { $hex = $m[1]; if ( 3 === strlen( $hex ) ) { $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; } elseif ( 8 === strlen( $hex ) ) { $hex = substr( $hex, 0, 6 ); // Drop alpha — studio CSS vars are opaque. } return '#' . strtoupper( $hex ); } if ( preg_match( '/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i', $value, $m ) ) { $r = max( 0, min( 255, (int) round( (float) $m[1] ) ) ); $g = max( 0, min( 255, (int) round( (float) $m[2] ) ) ); $b = max( 0, min( 255, (int) round( (float) $m[3] ) ) ); return sprintf( '#%02X%02X%02X', $r, $g, $b ); } return ''; } /** * CSS border-radius for CTA shape. * * @param string $shape Shape key. * @return string */ public static function cta_radius( $shape ) { if ( 'pill' === $shape ) { return '999px'; } if ( 'square' === $shape ) { return '4px'; } return '12px'; } /** * Pixel size for icon/image variants. * * @param string $size sm|md|lg. * @return int */ public static function cta_box_size( $size ) { $map = array( 'sm' => 36, 'md' => 48, 'lg' => 64, ); return isset( $map[ $size ] ) ? $map[ $size ] : 48; } /** * Padding / font for text CTA sizes (inline so themes cannot override). * * @param string $size sm|md|lg. * @return array{pad:string,font:string} */ public static function cta_button_metrics( $size ) { $map = array( 'sm' => array( 'pad' => '8px 14px', 'font' => '13px' ), 'md' => array( 'pad' => '10px 18px', 'font' => '14px' ), 'lg' => array( 'pad' => '14px 22px', 'font' => '16px' ), ); return isset( $map[ $size ] ) ? $map[ $size ] : $map['md']; } /** * Markup for the record open control. * * Layout uses inline !important styles so theme button/img rules cannot * break vertical alignment or image padding (matches Settings preview). * * @param array $args id, class, label override, instance_id. * @return string HTML button. */ public static function render_trigger_button( $args = array() ) { $args = wp_parse_args( $args, array( 'id' => '', 'class' => 'dadan-record-trigger', 'label' => '', 'instance_id' => 0, ) ); $instance_id = absint( $args['instance_id'] ); if ( $instance_id > 0 && $instance_id !== self::$context_id ) { return self::with_instance( $instance_id, static function () use ( $args ) { $args['instance_id'] = 0; return self::render_trigger_button( $args ); } ); } $cta = self::cta_config(); $label = trim( (string) $args['label'] ); if ( '' === $label ) { $label = $cta['label']; } $variant = $cta['variant']; $extra = self::cta_custom_class(); $classes = trim( (string) $args['class'] . ' dadan-record-trigger--' . $variant . ' dadan-record-trigger--' . $cta['preset'] . ' dadan-record-trigger--size-' . $cta['size'] . ' dadan-record-trigger--shape-' . $cta['shape'] . ( self::cta_hide_on_phones() ? ' dadan-hide-on-phone' : '' ) . ( '' !== $extra ? ' ' . $extra : '' ) ); $btn_id = trim( (string) $args['id'] ); if ( '' === $btn_id ) { $btn_id = self::cta_custom_id(); } $id_attr = '' !== $btn_id ? ' id="' . esc_attr( $btn_id ) . '"' : ''; $radius = self::cta_radius( $cta['shape'] ); $box = self::cta_box_size( $cta['size'] ); $base = 'display:inline-flex !important;align-items:center !important;justify-content:center !important;box-sizing:border-box !important;margin:0 !important;border:0 !important;cursor:pointer;vertical-align:middle;text-decoration:none;appearance:none;-webkit-appearance:none;'; if ( 'button' === $variant ) { $m = self::cta_button_metrics( $cta['size'] ); $style = sprintf( '%1$sbackground:%2$s !important;color:%3$s !important;border-radius:%4$s !important;padding:%5$s !important;font-size:%6$s !important;font-weight:600 !important;line-height:1 !important;min-height:0 !important;height:auto !important;width:auto !important;', $base, esc_attr( $cta['bg'] ), esc_attr( $cta['fg'] ), esc_attr( $radius ), esc_attr( $m['pad'] ), esc_attr( $m['font'] ) ); $label_style = 'display:inline-block !important;line-height:1.2 !important;vertical-align:middle !important;margin:0 !important;padding:0 !important;'; return sprintf( '', esc_attr( $classes ), $id_attr, $style, $label_style, esc_html( $label ) ); } $is_dadan_mark = ( 'dadan' === $cta['preset'] ) || ( 'icon' === $variant && '' === $cta['image_url'] ); $img_src = ( 'image' === $variant && $cta['image_url'] ) ? $cta['image_url'] : $cta['icon_url']; /* * Do not rely on button { padding } — themes often zero it. * Dadan mark: full-bleed. Custom image/icon: smaller inner frame centered * on the colored chip so edge-to-edge photos still show inset (like preview). */ if ( $is_dadan_mark && 'image' !== $variant ) { $style = sprintf( '%1$swidth:%2$dpx !important;height:%2$dpx !important;min-width:%2$dpx !important;min-height:%2$dpx !important;border-radius:%3$s !important;background:transparent !important;padding:0 !important;overflow:hidden !important;line-height:0 !important;', $base, (int) $box, esc_attr( $radius ) ); return sprintf( '', esc_attr( $classes ), $id_attr, $style, esc_attr( $label ), esc_url( $img_src ), (int) $box ); } // ~18% inset per side so full-bleed photos match Settings preview. $inner = (int) max( 16, round( $box * 0.64 ) ); $style = sprintf( '%1$swidth:%2$dpx !important;height:%2$dpx !important;min-width:%2$dpx !important;min-height:%2$dpx !important;border-radius:%3$s !important;background:%4$s !important;padding:0 !important;overflow:hidden !important;line-height:0 !important;box-shadow:0 4px 14px rgba(74,60,250,0.28);', $base, (int) $box, esc_attr( $radius ), esc_attr( $cta['bg'] ) ); $frame_style = sprintf( 'display:block !important;width:%1$dpx !important;height:%1$dpx !important;max-width:%1$dpx !important;max-height:%1$dpx !important;margin:0 !important;padding:0 !important;border:0 !important;flex:0 0 %1$dpx !important;overflow:hidden !important;line-height:0 !important;', $inner ); $img_style = sprintf( 'display:block !important;width:%1$dpx !important;height:%1$dpx !important;max-width:%1$dpx !important;max-height:%1$dpx !important;margin:0 !important;padding:0 !important;border:0 !important;object-fit:contain !important;', $inner ); return sprintf( '', esc_attr( $classes ), $id_attr, $style, esc_attr( $label ), $frame_style, esc_url( $img_src ), $inner, $img_style ); } /** * KSES allowlist for render_trigger_button() output (PC-1). * * @return array> */ public static function kses_trigger_button() { $attr = array( 'type' => true, 'class' => true, 'id' => true, 'style' => true, 'aria-label' => true, 'title' => true, 'aria-busy' => true, 'disabled' => true, ); return array( 'button' => $attr, 'span' => array( 'class' => true, 'style' => true, ), 'img' => array( 'class' => true, 'src' => true, 'alt' => true, 'width' => true, 'height' => true, 'decoding' => true, 'style' => true, ), ); } /** * Modes offered in Settings / studio picker (audio-only not exposed). * * @return string[] */ public static function all_modes() { return array( 'both', 'camera', 'screen' ); } /** * Locales the built-in studio actually supports (chrome strings). * Record SDK accepts a locale hint for BYO UIs; studio UI is English-only today. * * @return array code => label */ public static function supported_locales() { return array( 'en' => __( 'English', 'dadan' ), ); } /** * Allowed display surfaces. * * @return string[] */ public static function all_surfaces() { return array( 'monitor', 'window', 'browser' ); } /** * Normalize checkbox-group POST values (array, or a single string). * * @param mixed $raw Raw list. * @return string[] */ public static function normalize_list_input( $raw ) { if ( is_array( $raw ) ) { return $raw; } if ( is_string( $raw ) && '' !== $raw ) { return array( $raw ); } return array(); } /** * Sanitize a list of modes from settings input. * * @param mixed $raw Raw list. * @return string[] */ public static function sanitize_modes( $raw ) { $list = self::normalize_list_input( $raw ); $out = array(); foreach ( $list as $mode ) { $mode = sanitize_text_field( (string) $mode ); if ( in_array( $mode, self::all_modes(), true ) ) { $out[] = $mode; } } $out = array_values( array_unique( $out ) ); $out = array_values( array_filter( $out, function ( $m ) { return 'audio' !== $m; } ) ); return $out ? $out : array( 'both', 'camera', 'screen' ); } /** * Sanitize display surfaces list. * * @param mixed $raw Raw list. * @param string $fallback_one When empty and a preferred surface is known, keep that one. * @return string[] */ public static function sanitize_surfaces( $raw, $fallback_one = '' ) { $list = self::normalize_list_input( $raw ); $out = array(); foreach ( $list as $surface ) { $surface = sanitize_text_field( (string) $surface ); if ( in_array( $surface, self::all_surfaces(), true ) ) { $out[] = $surface; } } $out = array_values( array_unique( $out ) ); if ( $out ) { return $out; } if ( $fallback_one && in_array( $fallback_one, self::all_surfaces(), true ) ) { return array( $fallback_one ); } return array( 'monitor', 'window', 'browser' ); } /** * Build theme object for the SDK (omit empty optional keys). * * @param array $s Settings. * @return array */ public static function theme_for_sdk( $s ) { $appearance = ( isset( $s['theme_appearance'] ) && 'dark' === $s['theme_appearance'] ) ? 'dark' : 'light'; $theme = array( 'primary' => self::sanitize_hex( isset( $s['theme_primary'] ) ? (string) $s['theme_primary'] : self::DADAN_BG, self::DADAN_BG ), 'appearance' => $appearance, ); $map = array( 'fontFamily' => 'theme_font_family', 'borderRadius' => 'theme_border_radius', 'logoUrl' => 'theme_logo_url', 'surface' => 'theme_surface', 'surfaceMuted' => 'theme_surface_muted', 'ink' => 'theme_ink', 'inkSoft' => 'theme_ink_soft', 'inkMuted' => 'theme_ink_muted', 'border' => 'theme_border', 'borderStrong' => 'theme_border_strong', 'danger' => 'theme_danger', 'success' => 'theme_success', 'shadow' => 'theme_shadow', ); $color_js_keys = array( 'surface', 'surfaceMuted', 'ink', 'inkSoft', 'inkMuted', 'border', 'borderStrong', 'danger', 'success', ); foreach ( $map as $js_key => $php_key ) { $val = isset( $s[ $php_key ] ) ? trim( (string) $s[ $php_key ] ) : ''; if ( '' !== $val ) { if ( 'logoUrl' === $js_key ) { $val = esc_url_raw( $val ); if ( '' === $val ) { continue; } } elseif ( in_array( $js_key, $color_js_keys, true ) ) { // Keep non-hex shadows out of this branch; colors get normalized. $as_color = self::normalize_css_color( $val ); if ( '' !== $as_color ) { $val = $as_color; } elseif ( preg_match( '/^rgba?\(/i', $val ) || '#' === substr( $val, 0, 1 ) ) { continue; // Unusable color — omit rather than pass garbage. } } $theme[ $js_key ] = $val; } } /* * Stock recorder CSS defines light tokens on :root/:host and has no * [data-theme=dark] rules. When Appearance is Dark and the partner * left extended tokens empty, fill surfaces/ink so the deck actually * darkens (SDK applyThemeToElement reads these keys). */ if ( 'dark' === $appearance ) { $dark_defaults = array( 'surface' => '#1a1625', 'surfaceMuted' => '#241f33', 'ink' => '#f5f3ff', 'inkSoft' => '#b8b0cc', 'inkMuted' => '#9a92b0', 'border' => 'rgba(255,255,255,0.12)', 'borderStrong' => 'rgba(255,255,255,0.2)', 'shadow' => '0 12px 32px rgba(0,0,0,0.45), 0 2px 8px rgba(0,0,0,0.35)', ); foreach ( $dark_defaults as $key => $val ) { if ( empty( $theme[ $key ] ) ) { $theme[ $key ] = $val; } } } return $theme; } /** * Screen capture config for the SDK. * * @param array $s Settings. * @return array */ public static function screen_capture_for_sdk( $s ) { $surfaces = self::sanitize_surfaces( isset( $s['screen_surfaces'] ) ? $s['screen_surfaces'] : array(), 'monitor' ); $preferred = $surfaces[0]; /* * One checkbox group → SDK config (Vue-style): * surfaces = checked; preferredSurface = first checked; no toolbar picker. */ $cfg = array( 'surfaces' => $surfaces, 'preferredSurface' => $preferred, 'surfacePicker' => false, ); if ( ! empty( $s['screen_prefer_current_tab'] ) || 'browser' === $preferred ) { $cfg['preferCurrentTab'] = true; } foreach ( array( 'selfBrowserSurface' => 'screen_self_browser', 'surfaceSwitching' => 'screen_surface_switching', 'monitorTypeSurfaces' => 'screen_monitor_type', ) as $js_key => $php_key ) { $val = isset( $s[ $php_key ] ) ? sanitize_text_field( (string) $s[ $php_key ] ) : ''; if ( in_array( $val, array( 'include', 'exclude' ), true ) ) { $cfg[ $js_key ] = $val; } } if ( 'monitor' !== $preferred && ! isset( $cfg['monitorTypeSurfaces'] ) ) { $cfg['monitorTypeSurfaces'] = 'exclude'; } return $cfg; } /** * Config passed to the browser recorder. * * @param int $instance_id Optional instance ID (0 = default / context). * @return array */ public static function recorder_config( $instance_id = 0 ) { if ( $instance_id > 0 ) { return self::with_instance( $instance_id, static function () { return self::recorder_config( 0 ); } ); } $s = self::get(); $cta = self::cta_config(); $modes = self::sanitize_modes( isset( $s['feature_modes'] ) ? $s['feature_modes'] : array() ); $default_mode = isset( $s['default_mode'] ) ? (string) $s['default_mode'] : 'both'; if ( 'audio' === $default_mode || ! in_array( $default_mode, self::all_modes(), true ) ) { $default_mode = 'both'; } if ( ! in_array( $default_mode, $modes, true ) ) { $default_mode = $modes[0]; } $countdown = isset( $s['countdown_seconds'] ) ? (int) $s['countdown_seconds'] : 3; if ( $countdown < 0 ) { $countdown = 0; } if ( $countdown > 10 ) { $countdown = 10; } $mount = ( isset( $s['mount'] ) && 'shadow' === $s['mount'] ) ? 'shadow' : 'light'; $app_id = self::public_app_id(); if ( ! self::is_valid_public_app_id( $app_id ) ) { $app_id = ''; } $cfg = array( 'publicAppId' => $app_id, 'videoTitle' => isset( $s['video_title'] ) && '' !== trim( (string) $s['video_title'] ) ? sanitize_text_field( $s['video_title'] ) : 'WordPress recording', 'recordingQuality' => in_array( $s['recording_quality'], array( '720p', '1080p', '2k' ), true ) ? $s['recording_quality'] : '1080p', 'defaultMode' => $default_mode, 'mount' => $mount, 'locale' => ( isset( $s['locale'] ) && array_key_exists( (string) $s['locale'], self::supported_locales() ) ) ? (string) $s['locale'] : 'en', 'countdownSeconds' => $countdown, 'insertButtonText' => isset( $s['insert_button_text'] ) && '' !== trim( (string) $s['insert_button_text'] ) ? sanitize_text_field( $s['insert_button_text'] ) : 'Done', 'features' => array( 'modes' => $modes, 'modePicker' => ! empty( $s['feature_mode_picker'] ), 'cameraPicker' => ! empty( $s['feature_camera_picker'] ), 'micPicker' => ! empty( $s['feature_mic_picker'] ), 'systemAudio' => ! empty( $s['feature_system_audio'] ), 'backdrop' => ! empty( $s['feature_backdrop'] ), 'deckGrip' => ! empty( $s['feature_deck_grip'] ), 'closeButton' => ! empty( $s['feature_close_button'] ), 'restorePendingTake' => ! array_key_exists( 'feature_restore_pending', $s ) || ! empty( $s['feature_restore_pending'] ), ), 'theme' => self::theme_for_sdk( $s ), 'screenCapture' => self::screen_capture_for_sdk( $s ), 'dispatchBrowserEvents' => ! array_key_exists( 'dispatch_browser_events', $s ) || ! empty( $s['dispatch_browser_events'] ), 'cta' => $cta, 'ctaHideOnPhones' => self::cta_hide_on_phones(), 'ctaCustomClass' => self::cta_custom_class(), 'ctaCustomId' => self::cta_custom_id(), // Back-compat keys for editor JS. 'triggerStyle' => 'button' === $cta['variant'] ? 'text' : 'icon', 'triggerLabel' => $cta['label'], 'triggerIconUrl' => $cta['icon_url'], 'apiBase' => DADAN_API_BASE, ); $folder = isset( $s['folder_id'] ) ? trim( (string) $s['folder_id'] ) : ''; if ( '' !== $folder ) { $cfg['folderId'] = $folder; } $team = isset( $s['team_id'] ) ? trim( (string) $s['team_id'] ) : ''; if ( '' !== $team ) { $cfg['teamId'] = $team; } /** * Filter the Record SDK config localized to the browser. * * @param array $cfg SDK-shaped config + CTA helpers. * @param int $instance_id Instance post ID (0 = default context). */ return apply_filters( 'dadan_recorder_config', $cfg, self::$context_id ); } /** * Assets on the settings screen (media modal + CTA preview). * * @param string $hook Admin hook. */ public function enqueue_settings_assets( $hook ) { // Match by page slug — hook suffix varies with menu titles/locale. $page = isset( $_GET['page'] ) ? sanitize_text_field( wp_unslash( $_GET['page'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended if ( 'dadan-video-settings' !== $page && false === strpos( (string) $hook, 'dadan-video-settings' ) ) { return; } wp_enqueue_media(); wp_enqueue_style( 'dadan-video-public-record', DADAN_VIDEO_URL . 'assets/public-record.css', array(), DADAN_VIDEO_VERSION ); wp_enqueue_style( 'dadan-video-admin', DADAN_VIDEO_URL . 'assets/admin.css', array(), DADAN_VIDEO_VERSION ); wp_enqueue_script( 'dadan-video-admin-cta', DADAN_VIDEO_URL . 'assets/admin-cta.js', array( 'jquery', 'media-editor', 'media-views' ), DADAN_VIDEO_VERSION, true ); wp_localize_script( 'dadan-video-admin-cta', 'dadanCtaAdmin', array( 'fieldKey' => self::field_key(), 'dadanIconUrl' => self::dadan_icon_url(), 'i18nChoose' => __( 'Choose CTA image', 'dadan' ), 'i18nUse' => __( 'Use image', 'dadan' ), 'i18nMediaMissing' => __( 'WordPress media library failed to load. Refresh the page and try again.', 'dadan' ), ) ); } /** * Legacy Settings slug → Instances list (bookmarks / docs). */ public function register_menu() { // DADAN-23: empty parent avoids PHP 8.1 null deprecation; page only redirects. $hook = add_submenu_page( '', __( 'Dadan Settings', 'dadan' ), __( 'Settings', 'dadan' ), 'manage_options', 'dadan-video-settings', array( $this, 'render_page' ) ); if ( $hook ) { add_action( 'load-' . $hook, array( $this, 'redirect_legacy_settings' ) ); } } /** * Redirect legacy Settings slug before headers are sent (DADAN-23). */ public function redirect_legacy_settings() { if ( ! current_user_can( 'manage_options' ) ) { return; } wp_safe_redirect( admin_url( 'edit.php?post_type=' . Dadan_Instance::POST_TYPE ) ); exit; } /** * Sanitize options. * * @param mixed $input Raw input. * @return array */ public function sanitize( $input ) { $out = self::defaults(); if ( ! is_array( $input ) ) { return $out; } $out['public_app_id'] = self::sanitize_public_app_id( isset( $input['public_app_id'] ) ? $input['public_app_id'] : '', 'dadan_video' ); $out['video_title'] = isset( $input['video_title'] ) ? sanitize_text_field( $input['video_title'] ) : 'WordPress recording'; if ( '' === trim( $out['video_title'] ) ) { $out['video_title'] = 'WordPress recording'; } $quality = isset( $input['recording_quality'] ) ? sanitize_text_field( $input['recording_quality'] ) : '1080p'; $out['recording_quality'] = in_array( $quality, array( '720p', '1080p', '2k' ), true ) ? $quality : '1080p'; $mode = isset( $input['default_mode'] ) ? sanitize_text_field( $input['default_mode'] ) : 'both'; if ( 'audio' === $mode ) { $mode = 'both'; } $out['default_mode'] = in_array( $mode, self::all_modes(), true ) ? $mode : 'both'; $out['folder_id'] = isset( $input['folder_id'] ) ? sanitize_text_field( $input['folder_id'] ) : ''; $out['team_id'] = isset( $input['team_id'] ) ? sanitize_text_field( $input['team_id'] ) : ''; $mount = isset( $input['mount'] ) ? sanitize_text_field( $input['mount'] ) : 'light'; $out['mount'] = in_array( $mount, array( 'light', 'shadow' ), true ) ? $mount : 'light'; $locale_in = isset( $input['locale'] ) ? sanitize_text_field( (string) $input['locale'] ) : 'en'; $out['locale'] = array_key_exists( $locale_in, self::supported_locales() ) ? $locale_in : 'en'; $countdown = isset( $input['countdown_seconds'] ) ? (int) $input['countdown_seconds'] : 3; if ( $countdown < 0 ) { $countdown = 0; } if ( $countdown > 10 ) { $countdown = 10; } $out['countdown_seconds'] = $countdown; $out['insert_button_text'] = isset( $input['insert_button_text'] ) ? sanitize_text_field( $input['insert_button_text'] ) : 'Done'; if ( '' === trim( $out['insert_button_text'] ) ) { $out['insert_button_text'] = 'Done'; } $out['feature_modes'] = self::sanitize_modes( isset( $input['feature_modes'] ) ? $input['feature_modes'] : array() ); if ( ! in_array( $out['default_mode'], $out['feature_modes'], true ) ) { $out['default_mode'] = $out['feature_modes'][0]; } $bool_keys = array( 'feature_mode_picker', 'feature_camera_picker', 'feature_mic_picker', 'feature_system_audio', 'feature_backdrop', 'feature_deck_grip', 'feature_close_button', 'feature_restore_pending', 'screen_prefer_current_tab', 'dispatch_browser_events', 'cta_hide_on_phones', ); foreach ( $bool_keys as $bk ) { $out[ $bk ] = ! empty( $input[ $bk ] ); } $access = isset( $input['front_record_access'] ) ? sanitize_text_field( (string) $input['front_record_access'] ) : 'anyone'; $out['front_record_access'] = in_array( $access, array( 'anyone', 'logged_in', 'edit_posts' ), true ) ? $access : 'anyone'; // Studio toolbar never shows the surface dropdown. $out['screen_surface_picker'] = false; $out['theme_primary'] = self::sanitize_hex( isset( $input['theme_primary'] ) ? (string) $input['theme_primary'] : self::DADAN_BG, self::DADAN_BG ); $appearance = isset( $input['theme_appearance'] ) ? sanitize_text_field( $input['theme_appearance'] ) : 'light'; $out['theme_appearance'] = in_array( $appearance, array( 'light', 'dark' ), true ) ? $appearance : 'light'; $text_theme = array( 'theme_font_family', 'theme_border_radius', 'theme_surface', 'theme_surface_muted', 'theme_ink', 'theme_ink_soft', 'theme_ink_muted', 'theme_border', 'theme_border_strong', 'theme_danger', 'theme_success', 'theme_shadow', ); foreach ( $text_theme as $tk ) { $out[ $tk ] = isset( $input[ $tk ] ) ? sanitize_text_field( $input[ $tk ] ) : ''; } $out['theme_logo_url'] = isset( $input['theme_logo_url'] ) ? esc_url_raw( (string) $input['theme_logo_url'] ) : ''; // Sentinel so we know the checkbox group was submitted. if ( ! empty( $input['screen_surfaces_set'] ) ) { $out['screen_surfaces'] = self::sanitize_surfaces( isset( $input['screen_surfaces'] ) ? $input['screen_surfaces'] : array(), 'monitor' ); } else { // DADAN-24: read the instance being saved (context), not always Default. $prev_id = self::$context_id > 0 ? self::$context_id : null; $prev = self::get( $prev_id ); $out['screen_surfaces'] = self::sanitize_surfaces( isset( $prev['screen_surfaces'] ) ? $prev['screen_surfaces'] : array(), 'monitor' ); } $out['screen_preferred'] = $out['screen_surfaces'][0]; foreach ( array( 'screen_self_browser', 'screen_surface_switching', 'screen_monitor_type' ) as $sk ) { $val = isset( $input[ $sk ] ) ? sanitize_text_field( $input[ $sk ] ) : ''; $out[ $sk ] = in_array( $val, array( '', 'include', 'exclude' ), true ) ? $val : ''; } $preset = isset( $input['cta_preset'] ) ? sanitize_text_field( $input['cta_preset'] ) : 'dadan'; $out['cta_preset'] = in_array( $preset, array( 'dadan', 'custom' ), true ) ? $preset : 'dadan'; $variant = isset( $input['cta_variant'] ) ? sanitize_text_field( $input['cta_variant'] ) : 'icon'; $out['cta_variant'] = in_array( $variant, array( 'icon', 'button', 'image' ), true ) ? $variant : 'icon'; $out['cta_label'] = isset( $input['cta_label'] ) ? sanitize_text_field( $input['cta_label'] ) : ''; $out['cta_bg'] = self::sanitize_hex( isset( $input['cta_bg'] ) ? (string) $input['cta_bg'] : self::DADAN_BG, self::DADAN_BG ); $out['cta_fg'] = self::sanitize_hex( isset( $input['cta_fg'] ) ? (string) $input['cta_fg'] : self::DADAN_FG, self::DADAN_FG ); $shape = isset( $input['cta_shape'] ) ? sanitize_text_field( $input['cta_shape'] ) : 'rounded'; $out['cta_shape'] = in_array( $shape, array( 'rounded', 'pill', 'square' ), true ) ? $shape : 'rounded'; $size = isset( $input['cta_size'] ) ? sanitize_text_field( $input['cta_size'] ) : 'md'; $out['cta_size'] = in_array( $size, array( 'sm', 'md', 'lg' ), true ) ? $size : 'md'; $out['cta_image_id'] = isset( $input['cta_image_id'] ) ? absint( $input['cta_image_id'] ) : 0; $out['cta_custom_class'] = isset( $input['cta_custom_class'] ) ? self::sanitize_css_classes( $input['cta_custom_class'] ) : ''; $out['cta_custom_id'] = isset( $input['cta_custom_id'] ) ? self::sanitize_html_id( $input['cta_custom_id'] ) : ''; // Keep legacy fields in sync for older readers. $out['trigger_style'] = ( 'custom' === $out['cta_preset'] && 'button' === $out['cta_variant'] ) ? 'text' : 'icon'; $out['trigger_label'] = $out['cta_label']; return $out; } /** * Admin UI: open a settings card. * * @param string $title Card title. * @param string $desc Optional one-line description. * @param string $icon Optional dashicon slug (without "dashicons-"). */ private static function ui_card_open( $title, $desc = '', $icon = '' ) { echo '
'; if ( '' !== $icon ) { echo ''; } echo '

' . esc_html( $title ) . '

'; if ( '' !== $desc ) { echo '

' . esc_html( $desc ) . '

'; } echo '
'; } /** * Admin UI: close a settings card. */ private static function ui_card_close() { echo '
'; } /** * Admin UI: open a labelled field row (label + hint left, control right). * * @param string $label Field label. * @param string $for Control id for the '; } echo ''; } /** * Admin UI: switch-style checkbox. * * @param string $name Settings key (or "key][]" style suffix). * @param bool $checked Checked state. * @param string $label Visible label. * @param string $hint Optional helper text. * @param string $value Submitted value. */ private static function ui_toggle( $name, $checked, $label, $hint = '', $value = '1' ) { $key = self::field_key(); $id = 'dadan-' . sanitize_html_class( str_replace( array( '_', '[', ']' ), '-', $name . '-' . $value ) ); echo ''; } /** * Admin UI: required color — native picker (submitted) + hex mirror. * * @param string $name Settings key. * @param string $value Hex value. * @param string $id Picker id. */ private static function ui_color( $name, $value, $id ) { $key = self::field_key(); echo ''; echo ''; echo ''; echo ''; } /** * Header shown above the instance tabs: name, status, save + shortcode. * * @param int $instance_id Instance post ID. */ private function render_instance_header( $instance_id ) { $s = self::get(); $ready = self::is_valid_public_app_id( isset( $s['public_app_id'] ) ? $s['public_app_id'] : '' ); $is_def = class_exists( 'Dadan_Instance' ) && (int) $instance_id === Dadan_Instance::default_id(); $code = class_exists( 'Dadan_Instance' ) ? Dadan_Instance::shortcode_for( $instance_id ) : ''; $is_draft = 'auto-draft' === get_post_status( $instance_id ); ?>

> __( 'Icon', 'dadan' ), 'button' => __( 'Text button', 'dadan' ), 'image' => __( 'Image', 'dadan' ), ), __( 'Button type', 'dadan' ), array( 'icon' => 'video-alt2', 'button' => 'editor-textcolor', 'image' => 'format-image', ) ); self::ui_field_close(); ?>
__( 'Rounded', 'dadan' ), 'pill' => __( 'Pill', 'dadan' ), 'square' => __( 'Square', 'dadan' ), ), __( 'Shape', 'dadan' ) ); self::ui_field_close(); self::ui_field_open( __( 'Size', 'dadan' ) ); self::ui_segmented( 'cta_size', $size, array( 'sm' => __( 'Small', 'dadan' ), 'md' => __( 'Medium', 'dadan' ), 'lg' => __( 'Large', 'dadan' ), ), __( 'Size', 'dadan' ) ); self::ui_field_close(); ?>
redirect_legacy_settings(); } /** * Tabbed settings form for a Dadan Instance edit screen. * * @param int $instance_id Instance post ID. */ public function render_instance_form( $instance_id ) { $instance_id = (int) $instance_id; self::with_instance( $instance_id, function () use ( $instance_id ) { $tabs = self::tabs(); $icons = self::tab_icons(); $current_tab = self::current_tab(); $base_url = get_edit_post_link( $instance_id, 'raw' ); if ( ! $base_url ) { $base_url = admin_url( 'post.php?post=' . $instance_id . '&action=edit' ); } wp_nonce_field( 'dadan_instance_save_' . $instance_id, 'dadan_instance_nonce' ); ?>
render_instance_header( $instance_id ); ?>
> render_panel_general(); } elseif ( 'cta' === $slug ) { $this->render_panel_cta(); } elseif ( 'studio' === $slug ) { $this->render_panel_studio(); } elseif ( 'theme' === $slug ) { $this->render_panel_theme(); } else { $this->render_panel_advanced(); } ?>
*/ private static function tab_icons() { return array( 'general' => 'admin-links', 'cta' => 'marker', 'studio' => 'video-alt3', 'theme' => 'art', 'advanced' => 'admin-tools', ); } /** * General tab: account connection + upload destination. */ private function render_panel_general() { $s = self::get(); $key = self::field_key(); $app = isset( $s['public_app_id'] ) ? (string) $s['public_app_id'] : ''; $valid = self::is_valid_public_app_id( $app ); self::ui_card_open( __( 'Connect your Dadan app', 'dadan' ), __( 'Recordings upload straight to your Dadan workspace.', 'dadan' ), 'admin-links' ); ?>
  1. ' . esc_html( untrailingslashit( home_url() ) ) . '' ); ?>

render_cta_builder(); } /** * Recording settings tab (slug: studio). */ private function render_panel_studio() { $s = self::get(); $key = self::field_key(); $modes = self::sanitize_modes( isset( $s['feature_modes'] ) ? $s['feature_modes'] : array() ); $mode_labels = array( 'both' => __( 'Screen + camera', 'dadan' ), 'camera' => __( 'Camera only', 'dadan' ), 'screen' => __( 'Screen only', 'dadan' ), ); $mode_icons = array( 'both' => 'welcome-view-site', 'camera' => 'format-video', 'screen' => 'desktop', ); $flags = array( 'feature_mode_picker' => array( __( 'Mode picker', 'dadan' ), __( 'Let people switch between the allowed modes.', 'dadan' ) ), 'feature_camera_picker' => array( __( 'Camera picker', 'dadan' ), __( 'Choose between connected cameras.', 'dadan' ) ), 'feature_mic_picker' => array( __( 'Microphone picker', 'dadan' ), __( 'Choose between connected microphones.', 'dadan' ) ), 'feature_system_audio' => array( __( 'System / tab audio', 'dadan' ), __( 'Capture sound from the shared screen or tab.', 'dadan' ) ), 'feature_backdrop' => array( __( 'Backdrop picker', 'dadan' ), __( 'Background behind the camera bubble.', 'dadan' ) ), 'feature_deck_grip' => array( __( 'Drag handle', 'dadan' ), __( 'Move the control deck around the page.', 'dadan' ) ), 'feature_close_button' => array( __( 'Close button', 'dadan' ), __( 'Show an X to leave the studio.', 'dadan' ) ), 'feature_restore_pending' => array( __( 'Recover unsent takes', 'dadan' ), __( 'Offer to upload a take that was interrupted by a refresh.', 'dadan' ) ), ); self::ui_card_open( __( 'Capture', 'dadan' ), __( 'What people can record and how it starts.', 'dadan' ), 'video-alt3' ); self::ui_field_open( __( 'Allowed modes', 'dadan' ), '', __( 'At least one. The default must be allowed.', 'dadan' ) ); ?>
$label ) : ?>
'720p', '1080p' => '1080p', '2k' => '2K', ), __( 'Recording quality', 'dadan' ) ); self::ui_field_close(); self::ui_field_open( __( 'Countdown', 'dadan' ), 'dadan-countdown', __( 'Seconds before recording starts. 0 starts immediately.', 'dadan' ) ); ?> '; foreach ( $flags as $fname => $meta ) { self::ui_toggle( $fname, ! empty( $s[ $fname ] ), $meta[0], $meta[1] ); } echo ''; self::ui_card_close(); } /** * Theme tab. */ private function render_panel_theme() { $s = self::get(); $key = self::field_key(); $ext_colors = array( 'theme_surface' => __( 'Surface', 'dadan' ), 'theme_surface_muted' => __( 'Surface muted', 'dadan' ), 'theme_ink' => __( 'Text', 'dadan' ), 'theme_ink_soft' => __( 'Text soft', 'dadan' ), 'theme_ink_muted' => __( 'Text muted', 'dadan' ), 'theme_border' => __( 'Border', 'dadan' ), 'theme_border_strong' => __( 'Border strong', 'dadan' ), 'theme_danger' => __( 'Danger', 'dadan' ), 'theme_success' => __( 'Success', 'dadan' ), ); self::ui_card_open( __( 'Studio brand', 'dadan' ), __( 'Styles the recording studio people see after clicking the button. The button itself is styled under Record button.', 'dadan' ), 'art' ); self::ui_field_open( __( 'Accent color', 'dadan' ), 'dadan-theme-primary', __( 'Record button, highlights and focus rings.', 'dadan' ) ); self::ui_color( 'theme_primary', (string) $s['theme_primary'], 'dadan-theme-primary' ); self::ui_field_close(); self::ui_field_open( __( 'Appearance', 'dadan' ) ); self::ui_segmented( 'theme_appearance', $s['theme_appearance'], array( 'light' => __( 'Light', 'dadan' ), 'dark' => __( 'Dark', 'dadan' ), ), __( 'Appearance', 'dadan' ), array( 'light' => 'lightbulb', 'dark' => 'star-filled', ) ); self::ui_field_close(); self::ui_field_open( __( 'Font family', 'dadan' ), 'dadan-theme-font', __( 'Any CSS font stack. Empty uses the studio default.', 'dadan' ) ); ?> '; foreach ( $ext_colors as $fname => $flabel ) { $val = isset( $s[ $fname ] ) ? trim( (string) $s[ $fname ] ) : ''; $is_hex = (bool) preg_match( '/^#[0-9A-Fa-f]{6}$/', $val ); $picker = $is_hex ? $val : '#ffffff'; ?>
aria-label="" />
'; self::ui_field_open( __( 'Deck shadow', 'dadan' ), 'dadan-theme_shadow', __( 'Any CSS box-shadow.', 'dadan' ) ); ?> __( 'Entire screen', 'dadan' ), 'window' => __( 'Window', 'dadan' ), 'browser' => __( 'Browser tab', 'dadan' ), ); $surface_icons = array( 'monitor' => 'desktop', 'window' => 'welcome-widgets-menus', 'browser' => 'admin-site-alt3', ); $include_opts = array( '' => __( 'Browser default', 'dadan' ), 'include' => __( 'Include', 'dadan' ), 'exclude' => __( 'Exclude', 'dadan' ), ); $access = self::front_record_access(); $access_opts = array( 'anyone' => array( __( 'Anyone', 'dadan' ), __( 'Every visitor sees the record button.', 'dadan' ) ), 'logged_in' => array( __( 'Logged-in users', 'dadan' ), __( 'Members and customers who are signed in.', 'dadan' ) ), 'edit_posts' => array( __( 'Editors and above', 'dadan' ), __( 'People who can edit posts (edit_posts).', 'dadan' ) ), ); self::ui_card_open( __( 'Who can record', 'dadan' ), __( 'Others don\'t see the button and never receive the App ID. Pair this with origin allowlisting in Dadan.', 'dadan' ), 'groups' ); ?>
$meta ) : ?>
$label ) : ?>
'; self::ui_toggle( 'screen_prefer_current_tab', ! empty( $s['screen_prefer_current_tab'] ), __( 'Prefer the current tab', 'dadan' ), __( 'Chrome and Edge suggest this tab first. Always on when Browser tab is listed first.', 'dadan' ) ); echo ''; ?>
array( 'dadan-self-browser', __( 'This tab in the list', 'dadan' ) ), 'screen_surface_switching' => array( 'dadan-surface-switching', __( 'Switch source while sharing', 'dadan' ) ), 'screen_monitor_type' => array( 'dadan-monitor-type', __( 'Whole screens in the list', 'dadan' ) ), ); foreach ( $selects as $fname => $meta ) : ?>
__( 'Inherit theme CSS', 'dadan' ), 'shadow' => __( 'Isolated', 'dadan' ), ), __( 'Mount mode', 'dadan' ) ); self::ui_field_close(); self::ui_card_close(); self::ui_card_open( __( 'Developers', 'dadan' ), '', 'editor-code' ); echo '
'; self::ui_toggle( 'dispatch_browser_events', ! array_key_exists( 'dispatch_browser_events', $s ) || ! empty( $s['dispatch_browser_events'] ), __( 'Browser events', 'dadan' ), __( 'Fire window events such as dadan:recording.completed for analytics or custom code.', 'dadan' ) ); echo '
'; ?>

DadanWPRecorder.open({ mode: 'both' });
DadanWPRecorder.close();
DadanWPRecorder.on('recording.completed', (payload) => { /* … */ });
window.addEventListener('dadan:recording.completed', (e) => console.log(e.detail));

const support = await DadanWPRecorder.isSupported();
if (!support.supported) throw new Error(support.error);

const rec = await DadanWPRecorder.createHeadless({
  mode: 'both',
  hooks: {
    beforeStart: async () => true,
    beforeStop: async () => confirm('Finish?'),
  },
});
rec.on('preview.ready', ({ stream }) => {
  document.querySelector('#my-preview').srcObject = stream;
});
await rec.start();
await rec.stop();
await rec.destroy();

add_filter( 'dadan_recorder_config', function ( $cfg ) {
    $cfg['videoTitle'] = 'Site recording';
    return $cfg;
} );