diff --git a/lib/app.dart b/lib/app.dart index 897b9220..ead54f14 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -75,7 +75,13 @@ class MyApp extends StatelessWidget { themeMode: themeMode, theme: light, darkTheme: tMode < 3 ? dark : _getAmoledTheme(dark), - home: const HomePage(), + home: Stores.setting.fullScreen.fetch() + ? OrientationBuilder( + builder: (_, ori) { + return HomePage(landscape: ori == Orientation.landscape); + }, + ) + : const HomePage(), ); } } diff --git a/lib/data/store/setting.dart b/lib/data/store/setting.dart index 711a89e4..32b73cd9 100644 --- a/lib/data/store/setting.dart +++ b/lib/data/store/setting.dart @@ -111,15 +111,15 @@ class SettingStore extends PersistentStore { Defaults.editorDarkTheme, ); - // late final fullScreen = property( - // 'fullScreen', - // false, - // ); + late final fullScreen = property( + 'fullScreen', + false, + ); - // late final fullScreenJitter = property( - // 'fullScreenJitter', - // true, - // ); + late final fullScreenJitter = property( + 'fullScreenJitter', + true, + ); // late final fullScreenRotateQuarter = property( // 'fullScreenRotateQuarter', diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 7810a3ae..16945ffb 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -109,6 +109,7 @@ "fullScreen": "Vollbildmodus", "fullScreenJitter": "Jitter im Vollbildmodus", "fullScreenJitterHelp": "Einbrennen des Bildschirms verhindern", + "fullScreenTip": "Soll der Vollbildmodus aktiviert werden, wenn das Gerät in den Quermodus gedreht wird? Diese Option gilt nur für die Server-Registerkarte.", "getPushTokenFailed": "Push-Token kann nicht abgerufen werden", "gettingToken": "Getting token...", "goBackQ": "Zurückkommen?", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 51fad05e..961d963d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -109,6 +109,7 @@ "fullScreen": "Full screen mode", "fullScreenJitter": "Full screen jitter", "fullScreenJitterHelp": "To avoid screen burn-in", + "fullScreenTip": "Should full-screen mode be enabled when the device is rotated to landscape mode? This option only applies to the server tab.", "getPushTokenFailed": "Can't fetch push token", "gettingToken": "Getting token...", "goBackQ": "Go back?", diff --git a/lib/l10n/app_es.arb b/lib/l10n/app_es.arb index 55886692..c368d1fb 100644 --- a/lib/l10n/app_es.arb +++ b/lib/l10n/app_es.arb @@ -109,6 +109,7 @@ "fullScreen": "Modo pantalla completa", "fullScreenJitter": "Temblores en modo pantalla completa", "fullScreenJitterHelp": "Prevención de quemaduras de pantalla", + "fullScreenTip": "¿Debe habilitarse el modo de pantalla completa cuando el dispositivo se rote al modo horizontal? Esta opción solo se aplica a la pestaña del servidor.", "getPushTokenFailed": "No se pudo obtener el token de notificación", "gettingToken": "Obteniendo Token...", "goBackQ": "¿Regresar?", diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index a477a9cd..49876469 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -109,6 +109,7 @@ "fullScreen": "Mode plein écran", "fullScreenJitter": "Tremblement plein écran", "fullScreenJitterHelp": "Pour éviter la rémanence d'écran", + "fullScreenTip": "Le mode plein écran doit-il être activé lorsque l'appareil est basculé en mode paysage ? Cette option s'applique uniquement à l'onglet du serveur.", "getPushTokenFailed": "Impossible de récupérer le jeton d'identification", "gettingToken": "Récupération du jeton...", "goBackQ": "Revenir en arrière ?", diff --git a/lib/l10n/app_id.arb b/lib/l10n/app_id.arb index 68f2bb2c..1f584ea3 100644 --- a/lib/l10n/app_id.arb +++ b/lib/l10n/app_id.arb @@ -109,6 +109,7 @@ "fullScreen": "Mode Layar Penuh", "fullScreenJitter": "Jitter layar penuh", "fullScreenJitterHelp": "Untuk menghindari pembakaran layar", + "fullScreenTip": "Apakah mode layar penuh diaktifkan ketika perangkat diputar ke modus lanskap? Opsi ini hanya berlaku untuk tab server.", "getPushTokenFailed": "Tidak bisa mengambil token dorong", "gettingToken": "Mendapatkan token ...", "goBackQ": "Datang kembali?", diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 9e75e87e..724363ad 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -109,6 +109,7 @@ "fullScreen": "フルスクリーンモード", "fullScreenJitter": "フルスクリーンモードのジッター", "fullScreenJitterHelp": "焼き付き防止", + "fullScreenTip": "デバイスが横向きに回転したときにフルスクリーンモードを有効にしますか?このオプションはサーバータブにのみ適用されます。", "getPushTokenFailed": "プッシュトークンを取得できませんでした", "gettingToken": "トークンを取得しています...", "goBackQ": "戻りますか?", diff --git a/lib/l10n/app_pt.arb b/lib/l10n/app_pt.arb index 87c0efdb..21ab2c12 100644 --- a/lib/l10n/app_pt.arb +++ b/lib/l10n/app_pt.arb @@ -109,6 +109,7 @@ "fullScreen": "Modo tela cheia", "fullScreenJitter": "Tremulação em tela cheia", "fullScreenJitterHelp": "Prevenir burn-in de tela", + "fullScreenTip": "Deve ser ativado o modo de tela cheia quando o dispositivo é girado para o modo paisagem? Esta opção aplica-se apenas à aba do servidor.", "getPushTokenFailed": "Não foi possível obter token de notificação", "gettingToken": "Obtendo Token...", "goBackQ": "Voltar?", diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 04ff9630..1ca60cf7 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -109,6 +109,7 @@ "fullScreen": "полноэкранный режим", "fullScreenJitter": "дрожание в полноэкранном режиме", "fullScreenJitterHelp": "предотвращение выгорания экрана", + "fullScreenTip": "Следует ли включить полноэкранный режим, когда устройство поворачивается в альбомный режим? Эта опция применяется только к вкладке сервера.", "getPushTokenFailed": "Не удалось получить токен уведомлений", "gettingToken": "Получение токена...", "goBackQ": "Вернуться?", diff --git a/lib/l10n/app_zh.arb b/lib/l10n/app_zh.arb index 08928169..9dede8df 100644 --- a/lib/l10n/app_zh.arb +++ b/lib/l10n/app_zh.arb @@ -109,6 +109,7 @@ "fullScreen": "全屏模式", "fullScreenJitter": "全屏模式抖动", "fullScreenJitterHelp": "防止烧屏", + "fullScreenTip": "当设备旋转为横屏时,是否开启全屏模式。此选项仅作用于服务器Tab页。", "getPushTokenFailed": "未能获取到推送token", "gettingToken": "正在获取Token...", "goBackQ": "返回?", diff --git a/lib/l10n/app_zh_tw.arb b/lib/l10n/app_zh_tw.arb index 4dc5d937..fcc899e1 100644 --- a/lib/l10n/app_zh_tw.arb +++ b/lib/l10n/app_zh_tw.arb @@ -109,6 +109,7 @@ "fullScreen": "全屏模式", "fullScreenJitter": "全屏模式抖動", "fullScreenJitterHelp": "防止燒屏", + "fullScreenTip": "當設備旋轉為橫屏時,是否開啟全屏模式?此選項僅適用於伺服器選項卡。", "getPushTokenFailed": "未能獲取到推送token", "gettingToken": "正在獲取Token...", "goBackQ": "返回?", diff --git a/lib/view/page/home/home.dart b/lib/view/page/home/home.dart index 716c8604..232189aa 100644 --- a/lib/view/page/home/home.dart +++ b/lib/view/page/home/home.dart @@ -29,13 +29,14 @@ import 'package:toolbox/data/res/ui.dart'; import 'package:toolbox/data/res/url.dart'; import 'package:toolbox/view/widget/appbar.dart'; import 'package:toolbox/view/widget/cardx.dart'; -import 'package:toolbox/view/widget/count_down_btn.dart'; import 'package:toolbox/view/widget/markdown.dart'; part 'appbar.dart'; class HomePage extends StatefulWidget { - const HomePage({super.key}); + final bool landscape; + + const HomePage({super.key, this.landscape = false}); @override State createState() => _HomePageState(); @@ -119,18 +120,20 @@ class _HomePageState extends State return Scaffold( drawer: _buildDrawer(), - appBar: _AppBar( - selectIndex: _selectIndex, - centerTitle: false, - title: const Text(BuildData.name), - actions: [ - IconButton( - icon: const Icon(Icons.developer_mode, size: 23), - tooltip: l10n.debug, - onPressed: () => AppRoute.debug().go(context), - ), - ], - ), + appBar: widget.landscape + ? null + : _AppBar( + selectIndex: _selectIndex, + centerTitle: false, + title: const Text(BuildData.name), + actions: [ + IconButton( + icon: const Icon(Icons.developer_mode, size: 23), + tooltip: l10n.debug, + onPressed: () => AppRoute.debug().go(context), + ), + ], + ), body: PageView.builder( controller: _pageController, itemCount: AppTab.values.length, @@ -141,10 +144,12 @@ class _HomePageState extends State } }, ), - bottomNavigationBar: ListenableBuilder( - listenable: _selectIndex, - builder: (_, __) => _buildBottomBar(), - ), + bottomNavigationBar: widget.landscape + ? null + : ListenableBuilder( + listenable: _selectIndex, + builder: (_, __) => _buildBottomBar(), + ), ); } @@ -321,7 +326,6 @@ ${GithubIds.participants.map((e) => '[$e](${e.url})').join(' ')} BioAuth.go(); _reqNotiPerm(); - context.showRoundDialog(child: CountDownBtn(onTap: context.pop)); if (Stores.setting.autoCheckAppUpdate.fetch()) { doUpdate(context); diff --git a/lib/view/page/server/tab.dart b/lib/view/page/server/tab.dart index de8fd960..f1e47890 100644 --- a/lib/view/page/server/tab.dart +++ b/lib/view/page/server/tab.dart @@ -1,3 +1,6 @@ +import 'dart:async'; +import 'dart:math' as math; + import 'package:after_layout/after_layout.dart'; import 'package:flutter/material.dart'; import 'package:get_it/get_it.dart'; @@ -5,6 +8,7 @@ import 'package:provider/provider.dart'; import 'package:toolbox/core/extension/context/common.dart'; import 'package:toolbox/core/extension/context/dialog.dart'; import 'package:toolbox/core/extension/context/locale.dart'; +import 'package:toolbox/core/extension/listx.dart'; import 'package:toolbox/core/extension/media_queryx.dart'; import 'package:toolbox/core/extension/numx.dart'; import 'package:toolbox/core/extension/ssh_client.dart'; @@ -38,17 +42,36 @@ class _ServerPageState extends State late MediaQueryData _media; late double _textFactorDouble; + double _offset = 1; late TextScaler _textFactor; final _cardsStatus = {}; + Timer? _timer; + String? _tag; bool _useDoubleColumn = false; + @override + void initState() { + super.initState(); + if (!Stores.setting.fullScreenJitter.fetch()) return; + _timer = Timer.periodic(const Duration(seconds: 30), (_) { + if (mounted) { + _updateOffset(); + setState(() {}); + } else { + _timer?.cancel(); + } + }); + } + @override void didChangeDependencies() { super.didChangeDependencies(); _media = MediaQuery.of(context); + _updateOffset(); + _updateTextScaler(); _useDoubleColumn = _media.useDoubleColumn && Stores.setting.doubleColumnServersPage.fetch(); } @@ -56,13 +79,22 @@ class _ServerPageState extends State @override Widget build(BuildContext context) { super.build(context); + return OrientationBuilder(builder: (_, orientation) { + if (orientation == Orientation.landscape) { + final useFullScreen = Stores.setting.fullScreen.fetch(); + if (useFullScreen) return _buildLandscape(); + } + return _buildPortrait(); + }); + } + + Widget _buildPortrait() { return Scaffold( appBar: _buildTagsSwitcher(Pros.server), body: ListenableBuilder( listenable: Stores.setting.textFactor.listenable(), builder: (_, __) { - _textFactorDouble = Stores.setting.textFactor.fetch(); - _textFactor = TextScaler.linear(_textFactorDouble); + _updateTextScaler(); return _buildBody(); }, ), @@ -74,6 +106,75 @@ class _ServerPageState extends State ); } + Widget _buildLandscape() { + final offset = Offset(_offset, _offset); + return Padding( + // Avoid display cutout + padding: EdgeInsets.all(_offset.abs()), + child: Transform.translate( + offset: offset, + child: Stack( + children: [ + _buildLandscapeBody(), + Positioned( + top: 0, + left: 0, + child: IconButton( + onPressed: () => AppRoute.settings().go(context), + icon: const Icon(Icons.settings, color: Colors.grey), + ), + ), + ], + ), + ), + ); + } + + Widget _buildLandscapeBody() { + return Consumer(builder: (_, pro, __) { + if (pro.serverOrder.isEmpty) { + return Center( + child: Text( + l10n.serverTabEmpty, + textAlign: TextAlign.center, + ), + ); + } + + return PageView.builder( + itemCount: pro.serverOrder.length, + itemBuilder: (_, idx) { + final id = pro.serverOrder[idx]; + final srv = pro.pick(id: id); + if (srv == null) return UIs.placeholder; + + final title = _buildServerCardTitle(srv); + final List children = [ + title, + ..._buildNormalCard(srv.status, srv.spi).joinWith(SizedBox( + height: _media.size.height / 10, + )) + ]; + + return Padding( + padding: _media.padding, + child: ListenableBuilder( + listenable: _getCardNoti(id), + builder: (_, __) { + return Column( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.center, + children: children, + ); + }, + ), + ); + }, + ); + }); + } + Widget _buildBody() { final child = Consumer( builder: (_, pro, __) { @@ -560,6 +661,19 @@ class _ServerPageState extends State id, () => _CardNotifier(const _CardStatus()), ); + + void _updateOffset() { + if (!Stores.setting.fullScreenJitter.fetch()) return; + final x = _media.size.height * 0.03; + final r = math.Random().nextDouble(); + final n = math.Random().nextBool() ? 1 : -1; + _offset = x * r * n; + } + + void _updateTextScaler() { + _textFactorDouble = Stores.setting.textFactor.fetch(); + _textFactor = TextScaler.linear(_textFactorDouble); + } } typedef _CardNotifier = ValueNotifier<_CardStatus>; diff --git a/lib/view/page/setting/entry.dart b/lib/view/page/setting/entry.dart index e23e5769..bb6e497d 100644 --- a/lib/view/page/setting/entry.dart +++ b/lib/view/page/setting/entry.dart @@ -91,9 +91,9 @@ class _SettingPageState extends State { _buildEditor(), /// Fullscreen Mode is designed for old mobile phone which can be - /// used as a status screen, so it's only available on mobile phone. - // if (!isDesktop) _buildTitle(l10n.fullScreen), - // if (!isDesktop) _buildFullScreen(), + /// used as a status screen. + if (isMobile) _buildTitle(l10n.fullScreen), + if (isMobile) _buildFullScreen(), const SizedBox(height: 37), ], ), @@ -131,15 +131,15 @@ class _SettingPageState extends State { ); } - // Widget _buildFullScreen() { - // return Column( - // children: [ - // _buildFullScreenSwitch(), - // _buildFullScreenJitter(), - // _buildFulScreenRotateQuarter(), - // ].map((e) => CardX(child: e)).toList(), - // ); - // } + Widget _buildFullScreen() { + return Column( + children: [ + _buildFullScreenSwitch(), + _buildFullScreenJitter(), + // _buildFulScreenRotateQuarter(), + ].map((e) => CardX(child: e)).toList(), + ); + } Widget _buildServer() { return Column( @@ -599,23 +599,29 @@ class _SettingPageState extends State { ); } - // Widget _buildFullScreenSwitch() { - // return ListTile( - // title: Text(l10n.fullScreen), - // trailing: StoreSwitch( - // prop: _setting.fullScreen, - // callback: (_) => RebuildNodes.app.rebuild(), - // ), - // ); - // } + Widget _buildFullScreenSwitch() { + return ListTile( + title: Text(l10n.fullScreen), + subtitle: Text(l10n.fullScreenTip, style: UIs.textGrey), + trailing: StoreSwitch( + prop: _setting.fullScreen, + callback: (_) => RebuildNodes.app.rebuild(), + ), + ); + } - // Widget _buildFullScreenJitter() { - // return ListTile( - // title: Text(l10n.fullScreenJitter), - // subtitle: Text(l10n.fullScreenJitterHelp, style: UIs.textGrey), - // trailing: StoreSwitch(prop: _setting.fullScreenJitter), - // ); - // } + Widget _buildFullScreenJitter() { + return ListTile( + title: Text(l10n.fullScreenJitter), + subtitle: Text(l10n.fullScreenJitterHelp, style: UIs.textGrey), + trailing: StoreSwitch( + prop: _setting.fullScreenJitter, + callback: (_) { + context.showSnackBar(l10n.needRestart); + }, + ), + ); + } // Widget _buildFulScreenRotateQuarter() { // final degrees = List.generate(4, (idx) => '${idx * 90}°').toList();