CH-J SM Mobilespráva serverů přes SSH
Menu
Vydané zdroje

CH-J SM Mobile

Procházejte adresáře a soubory konkrétního vydání aplikace.

Stáhnout zdroje ZIP
CH-J / SM Mobile 175 souborů
sm-mobile-20261016T185235Z-8e93ecc1b59a Přidáno: · Sestaveno:
adaptive.dart Přidáno: Datum není evidováno 10,8 KB · 307 řádků
Kód Stáhnout soubor
1import 'dart:math' as math;
2import 'dart:ui' show DisplayFeature, DisplayFeatureState, DisplayFeatureType;
3import 'package:flutter/material.dart';
5/// Width classes in logical pixels. Layout never depends on a device model,
6/// physical resolution or devicePixelRatio, only on the space the OS gives us.
7enum WindowClass {
8 compact,
9 medium,
10 expanded,
11 large,
12 extraLarge;
14 static const mediumMin = 600.0,
15 expandedMin = 840.0,
16 largeMin = 1200.0,
17 extraLargeMin = 1600.0;
19 static WindowClass ofWidth(double width) => width >= extraLargeMin
20 ? extraLarge
21 : width >= largeMin
22 ? large
23 : width >= expandedMin
24 ? expanded
25 : width >= mediumMin
26 ? medium
27 : compact;
29 bool operator >=(WindowClass other) => index >= other.index;
32/// Below this window size the app keeps this logical canvas and scrolls it,
33/// instead of squeezing controls until they overlap or overflow.
34const minimumCanvas = Size(240, 320);
36/// A fold or hinge that splits the window into separately usable regions:
37/// a physical hinge with area, or a fold that is currently half opened.
38/// A flat fold (for example a fully open Galaxy/Pixel Fold) does not split.
39bool isSeparating(DisplayFeature f) =>
40 f.type != DisplayFeatureType.cutout &&
41 (f.bounds.shortestSide > 0 ||
42 f.state == DisplayFeatureState.postureHalfOpened);
44class AdaptiveLayout {
45 final Size size;
46 final EdgeInsets padding;
47 final List<DisplayFeature> features;
48 const AdaptiveLayout(
49 this.size, {
50 this.padding = EdgeInsets.zero,
51 this.features = const [],
52 });
54 factory AdaptiveLayout.of(BuildContext context) => AdaptiveLayout(
55 MediaQuery.sizeOf(context),
56 padding: MediaQuery.viewPaddingOf(context),
57 features: MediaQuery.displayFeaturesOf(context),
58 );
60 WindowClass get windowClass => WindowClass.ofWidth(size.width);
61 bool get landscape => size.width > size.height;
63 /// System bars placed on a side edge (landscape phones, or displays whose
64 /// status area sits beside the content) favour side navigation.
65 bool get sideSystemBars {
66 final side = math.max(padding.left, padding.right);
67 return side > 0 && side > padding.top;
68 }
70 /// The first separating feature that crosses the whole window, if any.
71 DisplayFeature? get separatingFeature {
72 for (final f in features) {
73 if (!isSeparating(f)) continue;
74 final b = f.bounds;
75 final vertical = b.height >= size.height - 1 && b.width < size.width;
76 final horizontal = b.width >= size.width - 1 && b.height < size.height;
77 if (vertical || horizontal) return f;
78 }
79 return null;
80 }
82 /// Rail instead of a bottom bar: enough width, or a short landscape window
83 /// where a bottom bar would eat a large part of the height.
84 /// A tabletop posture: the fold runs across the window.
85 bool get horizontalFold {
86 final f = separatingFeature;
87 return f != null && f.bounds.width >= size.width - 1;
88 }
90 /// Rail instead of a bottom bar: enough width, or a short landscape window
91 /// where a bottom bar would eat a large part of the height. In a tabletop
92 /// posture the bar stays in the lower half, away from the fold.
93 bool get useRail =>
94 !horizontalFold &&
95 (size.width >= WindowClass.mediumMin ||
96 (size.width >= 480 && (landscape || sideSystemBars)));
98 /// List and detail side by side.
99 bool get twoPane =>
100 windowClass >= WindowClass.expanded || separatingFeature != null;
103/// Lays [primary] and an optional [secondary] pane out. A separating fold or
104/// hinge always gets a pane on each side and nothing under it; otherwise the
105/// secondary pane takes [secondaryWidth] at the start.
106class FoldAwareSplit extends StatefulWidget {
107 final Widget primary;
108 final Widget? secondary;
109 final double secondaryWidth;
110 const FoldAwareSplit({
111 required this.primary,
112 this.secondary,
113 this.secondaryWidth = 360,
114 super.key,
115 });
116 @override
117 State<FoldAwareSplit> createState() => _FoldAwareSplitState();
120class _FoldAwareSplitState extends State<FoldAwareSplit> {
121 // Display features are in window coordinates; this is where we are in it.
122 Rect? global;
124 void _measure(_) {
125 if (!mounted) return;
126 final box = context.findRenderObject() as RenderBox?;
127 if (box == null || !box.hasSize) return;
128 final next = box.localToGlobal(Offset.zero) & box.size;
129 if (next != global) setState(() => global = next);
130 }
132 @override
133 Widget build(BuildContext context) {
134 final features = MediaQuery.displayFeaturesOf(context);
135 final fold = features.where(isSeparating).firstOrNull;
136 if (fold != null) WidgetsBinding.instance.addPostFrameCallback(_measure);
137 return LayoutBuilder(
138 builder: (context, constraints) {
139 final size = constraints.biggest;
140 final secondary = widget.secondary;
141 Rect? local;
142 if (fold != null && global != null && global!.size == size) {
143 local = fold.bounds.shift(-global!.topLeft);
144 }
145 if (local != null) {
146 final vertical = local.height >= size.height - 1;
147 final spans = vertical
148 ? local.top <= 1 && local.bottom >= size.height - 1
149 : local.left <= 1 && local.right >= size.width - 1;
150 final a = vertical ? local.left : local.top;
151 final b = vertical
152 ? size.width - local.right
153 : size.height - local.bottom;
154 // A fold outside this box, or at its very edge, needs no split.
155 if (spans && a >= 1 && b >= 1) {
156 final gap = vertical ? local.width : local.height;
157 // Vertical (book) fold: list at the start, detail at the end.
158 // Horizontal (tabletop) fold: detail above, list below.
159 final first = vertical ? secondary : widget.primary;
160 final second = vertical ? widget.primary : secondary;
161 // Insets on the fold side belong to the other pane.
162 Widget pane(Widget? child, {required bool leading}) =>
163 MediaQuery.removePadding(
164 context: context,
165 removeLeft: vertical && !leading,
166 removeRight: vertical && leading,
167 removeTop: !vertical && !leading,
168 removeBottom: !vertical && leading,
169 child: child ?? const SizedBox.shrink(),
170 );
171 return Flex(
172 direction: vertical ? Axis.horizontal : Axis.vertical,
173 crossAxisAlignment: CrossAxisAlignment.stretch,
174 children: [
175 SizedBox(
176 width: vertical ? a : null,
177 height: vertical ? null : a,
178 child: pane(first, leading: true),
179 ),
180 SizedBox(
181 width: vertical ? gap : null,
182 height: vertical ? null : gap,
183 ),
184 Expanded(child: pane(second, leading: false)),
185 ],
186 );
187 }
188 }
189 if (secondary == null) return widget.primary;
190 return Row(
191 crossAxisAlignment: CrossAxisAlignment.stretch,
192 children: [
193 SizedBox(
194 width: math.min(widget.secondaryWidth, size.width / 2),
195 child: secondary,
196 ),
197 const VerticalDivider(width: 1),
198 Expanded(child: widget.primary),
199 ],
200 );
201 },
202 );
203 }
206/// Centers scrolling content at a readable width while the scroll area itself
207/// (and its scrollbar) still spans the full width.
208EdgeInsets readablePadding(
209 BoxConstraints constraints, {
210 double maxWidth = 760,
211 double gutter = 20,
212}) {
213 // Narrow panes (split screen, one side of a fold) keep more room for content.
214 if (constraints.maxWidth < 360) gutter = math.min(gutter, 8);
215 final side = math.max(gutter, (constraints.maxWidth - maxWidth) / 2);
216 return EdgeInsets.symmetric(horizontal: side, vertical: gutter);
219/// Gives [child] at least [minHeight]; when the space is shorter (software
220/// keyboard in a landscape phone, a tiny split-screen window) the content
221/// scrolls instead of overflowing.
222class MinHeight extends StatelessWidget {
223 final double minHeight;
224 final Widget child;
225 const MinHeight({required this.minHeight, required this.child, super.key});
226 @override
227 Widget build(BuildContext context) => LayoutBuilder(
228 builder: (context, constraints) => constraints.maxHeight >= minHeight
229 ? child
230 : SingleChildScrollView(
231 child: SizedBox(height: minHeight, child: child),
232 ),
233 );
236/// The software keyboard leaves so little height that app chrome should step
237/// aside for the content being typed into.
238bool keyboardSqueezed(BuildContext context) {
239 final keyboard = MediaQuery.viewInsetsOf(context).bottom;
240 return keyboard > 0 && MediaQuery.sizeOf(context).height - keyboard < 360;
243/// Places equal-width tiles into as many columns as fit [minTileWidth].
244class AdaptiveGrid extends StatelessWidget {
245 final List<Widget> children;
246 final double minTileWidth, spacing;
247 final int maxColumns;
248 const AdaptiveGrid({
249 required this.children,
250 this.minTileWidth = 260,
251 this.spacing = 10,
252 this.maxColumns = 4,
253 super.key,
254 });
255 @override
256 Widget build(BuildContext context) => LayoutBuilder(
257 builder: (context, constraints) {
258 final width = constraints.maxWidth;
259 final columns = ((width + spacing) / (minTileWidth + spacing))
260 .floor()
261 .clamp(1, maxColumns);
262 final tile = (width - spacing * (columns - 1)) / columns;
263 return Wrap(
264 spacing: spacing,
265 children: [
266 for (final child in children) SizedBox(width: tile, child: child),
267 ],
268 );
269 },
270 );
273/// Keeps a minimum logical canvas for windows smaller than [minimumCanvas]
274/// (tiny split-screen or freeform windows) and lets the user scroll it.
275class MinimumCanvas extends StatelessWidget {
276 final Widget child;
277 const MinimumCanvas({required this.child, super.key});
278 @override
279 Widget build(BuildContext context) {
280 final media = MediaQuery.of(context);
281 final size = media.size;
282 if (size.width >= minimumCanvas.width &&
283 size.height >= minimumCanvas.height) {
284 return child;
285 }
286 final canvas = Size(
287 math.max(size.width, minimumCanvas.width),
288 math.max(size.height, minimumCanvas.height),
289 );
290 return Material(
291 child: SingleChildScrollView(
292 scrollDirection: Axis.horizontal,
293 child: SingleChildScrollView(
294 child: MediaQuery(
295 data: media.copyWith(size: canvas),
296 child: SizedBox.fromSize(size: canvas, child: child),
297 ),
298 ),
299 ),
300 );
301 }
304/// Navigation labels follow the system text size up to a limit, so that six
305/// destinations stay reachable; their semantics labels are never truncated.
306Widget clampNavigationText(Widget child) =>
307 MediaQuery.withClampedTextScaling(maxScaleFactor: 1.5, child: child);
SHA-25629e557013bea9abc69dfbfdec66e38670adfd3e8584394efbec085da6fbfa015

CH-J Proprietary Software License 1.14 · Zdroje jsou zveřejněny pod CH-J Proprietary Software License 1.14. Jejich dostupnost nemění licenční podmínky ani neposkytuje další oprávnění.

SHA-256 archivu46e427a43e87af091004f36145590bda8fe90086dfb0f631afeb5029fb0b5655
Soubory