Skip to content

Commit

Permalink
feat(*): remembering window size and position on desktop platforms
Browse files Browse the repository at this point in the history
  • Loading branch information
realth000 committed Oct 4, 2024
1 parent 5921446 commit 52da87a
Show file tree
Hide file tree
Showing 13 changed files with 254 additions and 55 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
- app:现在在头像加载失败时会使用本地默认的头像,避免头像一直空白。
- app:现在加载失败的头像更容易触发重新加载。
- app:桌面平台上设置窗口标题。
- app:桌面平台上支持记住窗口大小和位置。
- 默认开启,可在设置 -> 窗口中关闭。
- app: 桌面平台支持窗口居中。
- 默认关闭,可在设置 -> 窗口中开启。
- 登录:在登录界面显示注册账户的跳转链接。
- 历史:新增帖子浏览记录。
- 记录帖子名称、浏览的用户,帖子所在分区以及浏览时间。
Expand Down
78 changes: 74 additions & 4 deletions lib/app.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
Expand All @@ -16,12 +18,14 @@ import 'package:tsdm_client/features/upgrade/repository/upgrade_repository.dart'
import 'package:tsdm_client/i18n/strings.g.dart';
import 'package:tsdm_client/instance.dart';
import 'package:tsdm_client/routes/app_routes.dart';
import 'package:tsdm_client/shared/models/models.dart';
import 'package:tsdm_client/shared/providers/net_client_provider/net_client_provider.dart';
import 'package:tsdm_client/shared/providers/providers.dart';
import 'package:tsdm_client/shared/providers/storage_provider/storage_provider.dart';
import 'package:tsdm_client/shared/repositories/forum_home_repository/forum_home_repository.dart';
import 'package:tsdm_client/shared/repositories/fragments_repository/fragments_repository.dart';
import 'package:tsdm_client/themes/app_themes.dart';
import 'package:tsdm_client/utils/platform.dart';
import 'package:window_manager/window_manager.dart';

/// Main app for tsdm_client.
Expand All @@ -40,6 +44,66 @@ class App extends StatefulWidget {
}

class _AppState extends State<App> with WindowListener {
/// Duration used to debounce the frequency to save window attributes into
/// storage.
///
/// Only save the latest value to storage if in recent duration no more attr
/// changes triggered.
static const _syncDebounceDuration = Duration(milliseconds: 80);

/// Temporary store of current window position value.
var _windowPosition = Offset.zero;

/// Temporary store of current window size value.
var _windowSize = Size.zero;

/// Timer to debounce the saving progress of window position.
///
/// Save [_windowPosition] to storage when timer timeout.
Timer? windowPositionTimer;

/// Timer to debounce the saving progress of window size.
///
/// Save [_windowSize] to storage when timer timeout.
Timer? windowSizeTimer;

void setupWindowPositionTimer() {
if (windowPositionTimer?.isActive ?? false) {
windowPositionTimer!.cancel();
}
windowPositionTimer = Timer(_syncDebounceDuration, () async {
talker.debug('save window position to $_windowPosition');
final settings = getIt.get<SettingsRepository>().currentSettings;
if (!settings.windowRememberPosition || settings.windowInCenter) {
// Do nothing if not remembering window position, or window forced in
// center.
return;
}
// FIXME: Access provider in top-level components is anti-pattern.
await getIt
.get<StorageProvider>()
.saveOffset(SettingsKeys.windowPosition.name, _windowPosition);
});
}

void setupWindowSizeTimer() {
if (windowSizeTimer?.isActive ?? false) {
windowSizeTimer!.cancel();
}
windowSizeTimer = Timer(_syncDebounceDuration, () async {
talker.debug('save window size to $_windowPosition');
final settings = getIt.get<SettingsRepository>().currentSettings;
if (!settings.windowRememberSize) {
// Do nothing if not remembering window size.
return;
}
// FIXME: Access provider in top-level components is anti-pattern.
await getIt
.get<StorageProvider>()
.saveSize(SettingsKeys.windowSize.name, _windowSize);
});
}

@override
void initState() {
super.initState();
Expand All @@ -49,6 +113,8 @@ class _AppState extends State<App> with WindowListener {
@override
void dispose() {
windowManager.removeListener(this);
windowPositionTimer?.cancel();
windowSizeTimer?.cancel();
super.dispose();
}

Expand Down Expand Up @@ -136,14 +202,18 @@ class _AppState extends State<App> with WindowListener {
@override
Future<void> onWindowMove() async {
super.onWindowMove();
final x = await windowManager.getPosition();
debugPrint('>>> window moved to $x');
if (isDesktop) {
_windowPosition = await windowManager.getPosition();
setupWindowPositionTimer();
}
}

@override
Future<void> onWindowResize() async {
super.onWindowResize();
final x = await windowManager.getSize();
debugPrint('>>> window size is $x');
if (isDesktop) {
_windowSize = await windowManager.getSize();
setupWindowSizeTimer();
}
}
}
6 changes: 6 additions & 0 deletions lib/constants/layout.dart
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,12 @@ const edgeInsetsL24R24 = EdgeInsets.only(left: 24, right: 24);
/// An [EdgeInsets] with 24 at left and right, 12 at bottom.
const edgeInsetsL24R24B12 = EdgeInsets.only(left: 24, right: 24, bottom: 12);

/// An [EdgeInsets] with 24 at left and right, 16 at top and bottom.
const edgeInsetsL24T12R24B12 = EdgeInsets.symmetric(
horizontal: 24,
vertical: 12,
);

/// An [EdgeInsets] with 12 at left, 4 at top and 4 at bottom.
const edgeInsetsL12T4R4B4 =
EdgeInsets.only(left: 12, top: 4, right: 4, bottom: 4);
Expand Down
8 changes: 4 additions & 4 deletions lib/features/settings/repositories/settings_repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -80,10 +80,10 @@ final class SettingsRepository with LoggerMixin {
netClientAcceptEncoding: s.extract(_SK.netClientAcceptEncoding),
netClientAcceptLanguage: s.extract(_SK.netClientAcceptLanguage),
netClientUserAgent: s.extract(_SK.netClientUserAgent),
windowWidth: s.extract(_SK.windowWidth),
windowHeight: s.extract(_SK.windowHeight),
windowPositionDx: s.extract(_SK.windowPositionDx),
windowPositionDy: s.extract(_SK.windowPositionDy),
windowRememberSize: s.extract(_SK.windowRememberSize),
windowSize: s.extract(_SK.windowSize),
windowRememberPosition: s.extract(_SK.windowRememberPosition),
windowPosition: s.extract(_SK.windowPosition),
windowInCenter: s.extract(_SK.windowInCenter),
loginUsername: s.extract(_SK.loginUsername),
loginUid: s.extract(_SK.loginUid),
Expand Down
47 changes: 47 additions & 0 deletions lib/features/settings/view/settings_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import 'package:tsdm_client/widgets/color_palette.dart';
import 'package:tsdm_client/widgets/section_list_tile.dart';
import 'package:tsdm_client/widgets/section_switch_list_tile.dart';
import 'package:tsdm_client/widgets/section_title_text.dart';
import 'package:tsdm_client/widgets/tips.dart';

/// Settings page of the app.
class SettingsPage extends StatefulWidget {
Expand Down Expand Up @@ -303,6 +304,51 @@ class _SettingsPageState extends State<SettingsPage> {
];
}

/// App window related settings.
///
/// Only available on desktop platforms.
List<Widget> _buildWindowSection(
BuildContext context,
SettingsState state,
) {
final tr = context.t.settingsPage.windowSection;
final windowRememberSize = state.settingsMap.windowRememberSize;
final windowRememberPosition = state.settingsMap.windowRememberPosition;
final windowInCenter = state.settingsMap.windowInCenter;

return [
SectionTitleText(tr.title),
SectionSwitchListTile(
secondary: const Icon(Icons.settings_overscan_outlined),
title: Text(tr.windowRememberSize.title),
subtitle: Text(tr.windowRememberSize.detail),
value: windowRememberSize,
onChanged: (v) => context
.read<SettingsBloc>()
.add(SettingsValueChanged(SettingsKeys.windowRememberSize, v)),
),
SectionSwitchListTile(
secondary: const Icon(Icons.open_with_outlined),
title: Text(tr.windowRememberPosition.title),
subtitle: Text(tr.windowRememberPosition.detail),
value: windowRememberPosition,
onChanged: (v) => context
.read<SettingsBloc>()
.add(SettingsValueChanged(SettingsKeys.windowRememberPosition, v)),
),
SectionSwitchListTile(
secondary: const Icon(Icons.filter_center_focus_outlined),
title: Text(tr.windowInCenter.title),
subtitle: Text(tr.windowInCenter.detail),
value: windowInCenter,
onChanged: (v) => context
.read<SettingsBloc>()
.add(SettingsValueChanged(SettingsKeys.windowInCenter, v)),
),
Tips(tr.disableHint),
];
}

List<Widget> _buildBehaviorSection(
BuildContext context,
SettingsState state,
Expand Down Expand Up @@ -567,6 +613,7 @@ class _SettingsPageState extends State<SettingsPage> {
controller: scrollController,
children: [
..._buildAppearanceSection(context, state),
if (isDesktop) ..._buildWindowSection(context, state),
..._buildBehaviorSection(context, state),
..._buildCheckinSection(context, state),
..._buildStorageSection(context, state),
Expand Down
16 changes: 16 additions & 0 deletions lib/i18n/strings.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,22 @@
}
}
},
"windowSection": {
"title": "Window",
"disableHint": "Add cmdline args \"--no-window-configs\" will disable this settings section, which is useful when window is missing after startup.",
"windowRememberSize": {
"title": "Remember window size",
"detail": "Save window size and apply every time app starts"
},
"windowRememberPosition": {
"title": "Remember window position",
"detail": "Save window position and apply every time app starts"
},
"windowInCenter": {
"title": "Window in center",
"detail": "Force the window in the center of screen when app starts"
}
},
"behaviorSection": {
"title": "Behavior",
"doublePressExit": {
Expand Down
16 changes: 16 additions & 0 deletions lib/i18n/strings_zh-CN.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,22 @@
}
}
},
"windowSection": {
"title": "窗口",
"disableHint": "在启动时增加参数\"--no-window-configs\"将会禁用该部分设置。如果软件开启后看不到窗口,可以尝试增加此参数。",
"windowRememberSize": {
"title": "记住窗口大小",
"detail": "启动时还原上次的窗口大小"
},
"windowRememberPosition": {
"title": "记住窗口位置",
"detail": "启动时还原上次的窗口位置"
},
"windowInCenter": {
"title": "窗口居中",
"detail": "启动时强制窗口居中"
}
},
"behaviorSection": {
"title": "行为",
"doublePressExit": {
Expand Down
16 changes: 16 additions & 0 deletions lib/i18n/strings_zh-TW.i18n.json
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,22 @@
}
}
},
"windowSection": {
"title": "視窗",
"disableHint": "在啟動時增加參數\"--no-window-configs\"將會停用該部分設置,如果軟體開啟後看不到窗口,可以嘗試增加此參數",
"windowRememberSize": {
"title": "記住視窗大小",
"detail": "記錄視窗大小並在每次啟動時套用"
},
"windowRememberPosition": {
"title": "記住視窗位置",
"detail": "記住視窗位置並在每次啟動時套用"
},
"windowInCenter": {
"title": "視窗居中",
"detail": "強制視窗劇中,並在每次啟動時套用"
}
},
"behaviorSection": {
"title": "行為",
"doublePressExit": {
Expand Down
27 changes: 17 additions & 10 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import 'package:tsdm_client/constants/layout.dart';
import 'package:tsdm_client/features/settings/repositories/settings_repository.dart';
import 'package:tsdm_client/i18n/strings.g.dart';
import 'package:tsdm_client/instance.dart';
import 'package:tsdm_client/shared/models/models.dart';
import 'package:tsdm_client/shared/providers/providers.dart';
import 'package:tsdm_client/utils/platform.dart';
import 'package:tsdm_client/utils/window_configs.dart';
Expand All @@ -21,9 +20,9 @@ Future<void> main(List<String> args) async {
WidgetsFlutterBinding.ensureInitialized();
await initProviders();

final settings = getIt.get<SettingsRepository>();
final settings = getIt.get<SettingsRepository>().currentSettings;

final settingsLocale = await settings.getValue<String>(SettingsKeys.locale);
final settingsLocale = settings.locale;
final locale =
AppLocale.values.firstWhereOrNull((v) => v.languageTag == settingsLocale);
if (locale == null) {
Expand All @@ -32,27 +31,35 @@ Future<void> main(List<String> args) async {
LocaleSettings.setLocale(locale);
}

if (isDesktop) {
await windowManager.ensureInitialized();
await windowManager.ensureInitialized();

if (isDesktop && !cmdArgs.noWindowConfigs) {
await desktopUpdateWindowTitle();
await windowManager.center();
if (settings.windowInCenter) {
await windowManager.center();
} else if (settings.windowRememberPosition &&
settings.windowPosition != Offset.zero) {
await windowManager.setPosition(settings.windowPosition);
}
if (settings.windowRememberSize && settings.windowSize != Size.zero) {
await windowManager.setSize(settings.windowSize);
}
}

// System color.
// Use this color when following system color settings turned on.
//
// A not empty value represents currently is using system color and the color
// value is inside it.
final useSystemTheme =
await settings.getValue<bool>(SettingsKeys.accentColorFollowSystem);
final useSystemTheme = settings.accentColorFollowSystem;

final color = switch (useSystemTheme) {
true => await SystemTheme.accentColor
.load()
.then((_) => SystemTheme.accentColor.accent.value),
false => await settings.getValue<int>(SettingsKeys.accentColor),
false => settings.accentColor,
};
final themeModeIndex = await settings.getValue<int>(SettingsKeys.themeMode);
final themeModeIndex = settings.themeMode;

runApp(
TranslationProvider(
Expand Down
Loading

0 comments on commit 52da87a

Please sign in to comment.