PluginProbe
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin / 1.1.9
OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin v1.1.9
1.1.9 1.1.8 1.1.7 1.1.6 1.1.5 1.1.4 1.1.3 1.1.2 1.1.1 1.1.0 1.0.1 1.0.0 0.9.8 0.9.7 0.9.6 0.9.4 0.9.5 0.9.3 0.9.2 0.9.1 0.9.0 0.8.9 0.8.8 0.8.7 0.8.6 All 33 releases
desktop-mode / includes / mobile.php

mobile.php in OpenStation: Desktop Windows, Dock & Virtual Desktops for WP Admin 1.1.9, at includes/mobile.php

282 lines 10.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * OpenStation — responsive mode: the server's half.
4 *
5 * The shell renders one of three experiences — `desktop`, `tablet`
6 * (reported, rendered as desktop for now) or `mobile`, the phone
7 * layer. The mode is a pure function of the viewport width and the
8 * user's `mobileLayout` preference, computed in `src/mode/index.ts`.
9 * PHP cannot see the viewport, so its job is three things:
10 *
11 * 1. Ship the inputs — the preference (filterable), the breakpoints
12 * (filterable) and the default tab-bar pins (filterable) — in
13 * the shell config.
14 * 2. Print a head stamp: a few bytes of inline script that write
15 * `data-os-mode` on `<html>` from the same rule, before the
16 * body parses, so the first paint on a phone is already the
17 * phone layer and never a flash of desktop.
18 * 3. Widen the admin viewport meta so the phone layer can paint
19 * under the notch and resize for the keyboard.
20 *
21 * @package OpenStation
22 */
23
24 defined( 'ABSPATH' ) || exit;
25
26 /** Widest viewport (CSS px, inclusive) that is a phone. */
27 const OPENSTATION_MODE_MOBILE_MAX_WIDTH = 767;
28
29 /** Widest viewport (CSS px, inclusive) that is a tablet. */
30 const OPENSTATION_MODE_TABLET_MAX_WIDTH = 1024;
31
32 /**
33 * The user's mode preference: `auto`, `desktop` or `mobile`.
34 *
35 * @param int|null $user_id User id; defaults to the current user.
36 * @return string One of `OPENSTATION_OS_SETTINGS_MOBILE_LAYOUTS`.
37 */
38 function openstation_mode_preference( $user_id = null ) {
39 $user_id = null === $user_id ? get_current_user_id() : (int) $user_id;
40 $settings = openstation_get_os_settings( $user_id );
41 $saved = isset( $settings['mobileLayout'] ) ? (string) $settings['mobileLayout'] : 'auto';
42
43 /**
44 * Filters the user's mode preference before it reaches the shell.
45 *
46 * Lets a plugin force the phone layer for a role, or keep a kiosk
47 * user on the desktop whatever their device. Returning anything
48 * other than `auto`, `desktop` or `mobile` falls back to the
49 * saved value.
50 *
51 * @param string $preference The saved preference.
52 * @param int $user_id The user it applies to.
53 */
54 $preference = apply_filters( 'openstation_mode_preference', $saved, $user_id );
55
56 return in_array( $preference, OPENSTATION_OS_SETTINGS_MOBILE_LAYOUTS, true )
57 ? $preference
58 : $saved;
59 }
60
61 /**
62 * The breakpoints the mode is resolved against.
63 *
64 * The invariant `0 < mobile < tablet` is enforced after the filter so
65 * the three bands stay disjoint whatever a plugin returned.
66 *
67 * @return array { mobile: int, tablet: int } — the widest viewport of each band.
68 */
69 function openstation_mode_breakpoints() {
70 $defaults = array(
71 'mobile' => OPENSTATION_MODE_MOBILE_MAX_WIDTH,
72 'tablet' => OPENSTATION_MODE_TABLET_MAX_WIDTH,
73 );
74
75 /**
76 * Filters the responsive breakpoints, in CSS pixels.
77 *
78 * `mobile` is the widest viewport that gets the phone layer;
79 * `tablet` the widest that reports as a tablet. Both inclusive.
80 *
81 * @param array $breakpoints { mobile: int, tablet: int }.
82 */
83 $raw = apply_filters( 'openstation_mode_breakpoints', $defaults );
84 $raw = is_array( $raw ) ? $raw : array();
85
86 $mobile = isset( $raw['mobile'] ) && is_numeric( $raw['mobile'] ) && (int) $raw['mobile'] > 0
87 ? (int) $raw['mobile']
88 : $defaults['mobile'];
89 $tablet = isset( $raw['tablet'] ) && is_numeric( $raw['tablet'] ) && (int) $raw['tablet'] > 0
90 ? (int) $raw['tablet']
91 : $defaults['tablet'];
92
93 return array(
94 'mobile' => $mobile,
95 'tablet' => max( $mobile + 1, $tablet ),
96 );
97 }
98
99 /**
100 * The navigation ids pinned to the phone tab bar by default.
101 *
102 * The bar has five slots: Home, up to three pins, and the app
103 * switcher. A user's own pins (`mobileTabs` in OpenStation
104 * Preferences) override this list when set; this is what a user who
105 * never chose gets.
106 *
107 * @return string[] Nav item ids, at most three.
108 */
109 function openstation_mobile_tab_bar() {
110 $defaults = array( 'menu-posts', 'menu-media', 'menu-comments' );
111
112 /**
113 * Filters the default phone tab-bar pins.
114 *
115 * Ids are navigation item ids — the same ids `navOrder` uses: an
116 * admin menu's hook name (`menu-posts`, `menu-media`,
117 * `toplevel_page_woocommerce`) or a native window id
118 * (`desktop-mode-recycle-bin`). Each passes through
119 * `sanitize_key()`. Unknown ids are skipped by the shell, so a
120 * pin for a plugin that is not installed is harmless. Only the
121 * first three survive.
122 *
123 * @param string[] $ids Default pins.
124 */
125 $ids = apply_filters( 'openstation_mobile_tab_bar', $defaults );
126 if ( ! is_array( $ids ) ) {
127 return $defaults;
128 }
129
130 $out = array();
131 $seen = array();
132 foreach ( $ids as $id ) {
133 if ( ! is_string( $id ) || '' === $id ) {
134 continue;
135 }
136 $slug = sanitize_key( openstation_canonical_nav_id( $id ) );
137 if ( '' === $slug || isset( $seen[ $slug ] ) ) {
138 continue;
139 }
140 $seen[ $slug ] = true;
141 $out[] = $slug;
142 if ( count( $out ) >= OPENSTATION_OS_SETTINGS_MOBILE_TABS_MAX ) {
143 break;
144 }
145 }
146 return $out;
147 }
148
149 /**
150 * The `mode` entry of the shell config blob.
151 *
152 * @param int|null $user_id User id; defaults to the current user.
153 * @return array { preference: string, breakpoints: array, tabBar: string[] }
154 */
155 function openstation_mode_config( $user_id = null ) {
156 return array(
157 'preference' => openstation_mode_preference( $user_id ),
158 'breakpoints' => openstation_mode_breakpoints(),
159 'tabBar' => openstation_mobile_tab_bar(),
160 );
161 }
162
163 /**
164 * Whether the server would GUESS this request is a phone.
165 *
166 * A hint, never a decision: it gates a `prefetch` link for the phone
167 * bundle. The real mode is resolved from the viewport by the head
168 * stamp and the shell. `wp_is_mobile()` is a user-agent sniff that
169 * also matches tablets, which is fine for a low-priority prefetch.
170 *
171 * @param int|null $user_id User id; defaults to the current user.
172 * @return bool
173 */
174 function openstation_mode_hint_is_mobile( $user_id = null ) {
175 $preference = openstation_mode_preference( $user_id );
176 if ( 'mobile' === $preference ) {
177 return true;
178 }
179 if ( 'desktop' === $preference ) {
180 return false;
181 }
182 return function_exists( 'wp_is_mobile' ) && wp_is_mobile();
183 }
184
185 /**
186 * The inline script that stamps `data-os-mode` and `data-os-display`
187 * on `<html>`.
188 *
189 * Mirrors `resolveMode()` and `resolveDisplay()` in
190 * `src/mode/index.ts`: a forced preference wins; otherwise the width
191 * is compared with the two breakpoints. The display is `standalone`
192 * when the document runs as an installed app — the `display-mode`
193 * media query matches, or Safari's `navigator.standalone` says the
194 * page was launched from the home screen — and `browser` otherwise.
195 * Kept to one statement with no dependencies so it can run before
196 * anything else in the document.
197 *
198 * @param string $preference `auto`, `desktop` or `mobile`.
199 * @param array $breakpoints { mobile: int, tablet: int }.
200 * @return string JavaScript source.
201 */
202 function openstation_mode_stamp_script( $preference, $breakpoints ) {
203 $preference = in_array( $preference, OPENSTATION_OS_SETTINGS_MOBILE_LAYOUTS, true )
204 ? $preference
205 : 'auto';
206 $mobile = (int) $breakpoints['mobile'];
207 $tablet = (int) $breakpoints['tablet'];
208
209 return '(function(){var p=' . wp_json_encode( $preference ) . ','
210 . 'w=window.innerWidth||0,'
211 . 'm=p==="mobile"?"mobile":p==="desktop"?"desktop":'
212 . 'w<=' . $mobile . '?"mobile":w<=' . $tablet . '?"tablet":"desktop",'
213 . 'd=(window.matchMedia&&window.matchMedia("(display-mode: standalone)").matches)'
214 . '||navigator.standalone===true?"standalone":"browser",'
215 . 'h=document.documentElement;'
216 . 'h.setAttribute("data-os-mode",m);h.setAttribute("data-os-display",d);})();';
217 }
218
219 /**
220 * Prints the head stamp on shell requests.
221 *
222 * Hooked early on `admin_head` so it precedes every other head
223 * script. `<html>` exists by the time `<head>` is being parsed, so
224 * the attribute lands before the body — and therefore before the
225 * first paint — whatever the stylesheet order.
226 */
227 function openstation_print_mode_stamp() {
228 if ( ! function_exists( 'openstation_is_shell_request' ) || ! openstation_is_shell_request() ) {
229 return;
230 }
231 $script = openstation_mode_stamp_script(
232 openstation_mode_preference(),
233 openstation_mode_breakpoints()
234 );
235 if ( function_exists( 'wp_print_inline_script_tag' ) ) {
236 wp_print_inline_script_tag( $script, array( 'id' => 'os-mode-stamp' ) );
237 return;
238 }
239 // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedScript -- a two-line head stamp on pre-5.7 hosts; there is no enqueue slot that runs before the body parses.
240 echo '<script id="os-mode-stamp">' . $script . '</script>' . "\n"; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- built by openstation_mode_stamp_script() from integers and a wp_json_encode()d enum.
241 }
242 add_action( 'admin_head', 'openstation_print_mode_stamp', 0 );
243
244 /**
245 * Widens the admin viewport meta on shell requests.
246 *
247 * `viewport-fit=cover` lets the phone layer paint under the notch
248 * and read `env(safe-area-inset-*)`; `interactive-widget=
249 * resizes-content` makes the on-screen keyboard shrink the layout
250 * rather than overlay it (Chrome for Android; ignored elsewhere).
251 * `maximum-scale=1,user-scalable=no` turns page zoom off: the shell
252 * is an application surface, not a document — a pinch or a focus
253 * zoom leaves the dock, the tab bar and every window half off
254 * screen with no way back. Mobile Safari honours the pair for the
255 * focus zoom and, in a home-screen app, for the pinch; where it
256 * does not, `src/mode/zoom-guard.ts` cancels the gesture itself.
257 * Desktop browsers ignore both and keep their own zoom.
258 *
259 * @param string $meta The viewport meta content.
260 * @return string
261 */
262 function openstation_mode_viewport_meta( $meta ) {
263 if ( ! function_exists( 'openstation_is_shell_request' ) || ! openstation_is_shell_request() ) {
264 return $meta;
265 }
266 $meta = (string) $meta;
267 if ( false === strpos( $meta, 'viewport-fit' ) ) {
268 $meta .= ',viewport-fit=cover';
269 }
270 if ( false === strpos( $meta, 'interactive-widget' ) ) {
271 $meta .= ',interactive-widget=resizes-content';
272 }
273 if ( false === strpos( $meta, 'maximum-scale' ) ) {
274 $meta .= ',maximum-scale=1';
275 }
276 if ( false === strpos( $meta, 'user-scalable' ) ) {
277 $meta .= ',user-scalable=no';
278 }
279 return $meta;
280 }
281 add_filter( 'admin_viewport_meta', 'openstation_mode_viewport_meta' );
282