', 45 * $i ); } return '' . $teeth . ''; } /** * Refuse an action that changes SITE truth to anyone who may not. * The app itself is open to every shell user; these actions are not. * * @param Os $os Host handle. * @return void * @throws \RuntimeException When the acting user lacks `manage_options`. */ function require_admin( Os $os ) { if ( ! $os->can( 'manage_options' ) ) { throw new \RuntimeException( esc_html__( 'You are not allowed to change site-wide options.', 'desktop-mode' ) ); } } /** * The server facts the client view paints from — the ones that can * change while the window is open. The `focus` lifecycle action * recomputes them whenever the window regains focus, which is how the * AI toggle un-gates after the user connects a provider elsewhere. * * @param State $state Unused — nothing here depends on the page. * @param Os $os Host handle. * @return array */ function data( State $state, Os $os ) { $admin = $os->can( 'manage_options' ); return array( 'isAdmin' => $admin, 'canUpload' => $os->can( 'upload_files' ), 'canManageDesktopThemes' => function_exists( 'openstation_desktop_theme_upload_capability' ) && $os->can( openstation_desktop_theme_upload_capability() ), 'extendedOptions' => $admin && function_exists( 'openstation_get_extended_options' ) ? openstation_get_extended_options() : null, 'commentsAi' => $admin && function_exists( 'openstation_comments_ai_is_enabled' ) ? array( 'enabled' => openstation_comments_ai_is_enabled(), 'providerConfigured' => openstation_comments_ai_provider_configured(), ) : null, 'aiAssistant' => function_exists( 'openstation_ai_assistant_config' ) ? openstation_ai_assistant_config( $os->auth->user_id() ) : null, ); } /** * The comments-AI toggle: a site option, the same write the * `POST desktop-mode/v1/comments/ai-settings` route makes. * * @param State $state Unused. * @param Os $os Host handle. * @param array $args `enabled` (bool). * @return void */ function comments_ai_action( State $state, Os $os, array $args ) { require_admin( $os ); if ( ! defined( 'OPENSTATION_COMMENTS_AI_OPTION' ) ) { return; } $enabled = ! empty( $args['enabled'] ); update_option( OPENSTATION_COMMENTS_AI_OPTION, $enabled, false ); /** This action is documented in apps/comments/parts/ai-moderation.php */ do_action( 'openstation_comments_ai_toggled', $enabled ); } /** * Extended Options: merged over the stored set, then a menu refresh — * every option gates a server-side registration (`games` decides * whether the games module loads at all), and the shell only learns * what the server registers from a fresh payload. * * @param State $state Unused. * @param Os $os Host handle. * @param array $args `options` (array of bools). * @return void */ function extended_action( State $state, Os $os, array $args ) { require_admin( $os ); if ( function_exists( 'openstation_save_extended_options' ) ) { openstation_save_extended_options( isset( $args['options'] ) && is_array( $args['options'] ) ? $args['options'] : array() ); } $os->refresh_menu(); } /** * "Delete folder sharing data" — drops every shares table, the same * destructive cleanup the files REST route performs. * * @param State $state Unused. * @param Os $os Host handle. * @return void */ function purge_shares_action( State $state, Os $os ) { require_admin( $os ); if ( ! function_exists( 'openstation_files_rest_purge_sharing_tables' ) ) { $os->toast( __( 'Files REST endpoint is not available.', 'desktop-mode' ) ); return; } $response = openstation_files_rest_purge_sharing_tables(); $dropped = $response instanceof \WP_REST_Response ? (array) $response->get_data()['dropped'] : array(); $os->toast( sprintf( /* translators: %d: number of tables dropped. */ __( 'Folder sharing data deleted. (%d tables)', 'desktop-mode' ), count( $dropped ) ) ); } return App::define( ID ) // Not translated: the product's own name, as the legacy window had it. ->title( 'OpenStation Preferences' ) ->icon( gear_svg() ) ->size( 820, 720 ) ->min_size( 560, 480 ) // No launcher of its own: the System tile's flyout row opens it // and answers for it on the rail (`NavItem.answersFor`), exactly // as before. `wp.os.openOsSettings()` is the portable opener. ->placement( 'none' ) ->capabilities( 'read' ) // `data()` is a handful of capability checks and options, so it // ships with the window and the pages paint the moment the window // opens — as the legacy panel did — instead of behind a spinner for // the length of the `mount` round trip. ->prefetch() // The page is the whole declared state; the settings live in the // shell's store and reach the view through `wp.os.getOsSettings()`. ->state( array( 'tab' => 'appearance' ) ) // `wp.os.openOsSettings( { tabId } )` lands on the page it named. ->mount( static function ( State $state, Os $os ) { $tab = sanitize_key( (string) $os->param( 'tab', '' ) ); if ( '' !== $tab ) { $state->set( 'tab', $tab ); } } ) ->action( 'extended', __NAMESPACE__ . '\extended_action' ) ->action( 'comments-ai', __NAMESPACE__ . '\comments_ai_action' ) ->action( 'reset-intros', static function ( State $state, Os $os ) { if ( function_exists( 'openstation_clear_seen_intros' ) ) { openstation_clear_seen_intros( $os->auth->user_id() ); } } ) ->action( 'purge-shares', __NAMESPACE__ . '\purge_shares_action' ) // Regaining focus re-probes the server facts: nothing to run, the // recomputed `data()` is the point. ->action( 'focus', static function () {} ) ->data( __NAMESPACE__ . '\data' ) // The static facts, shipped once with the window config. ->config( array( 'mediaUrl' => esc_url_raw( rest_url( 'wp/v2/media' ) ), 'desktopThemesUrl' => esc_url_raw( rest_url( 'desktop-mode/v1/desktop-themes' ) ), 'aboutFeedUrl' => esc_url_raw( add_query_arg( array( 'action' => 'openstation_about_feed', 'nonce' => wp_create_nonce( 'openstation_about_feed' ), ), admin_url( 'admin-ajax.php' ) ) ), 'pluginUrl' => esc_url_raw( untrailingslashit( OPENSTATION_URL ) ), 'pluginVersion' => OPENSTATION_VERSION, ) );