mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: feature flag in settings page (#4833)
This commit is contained in:
parent
f484675008
commit
a1823fa4c0
@ -58,4 +58,10 @@ class KVKeys {
|
|||||||
/// The value range is from 0.8 to 1.0. If it's greater than 1.0, it will cause
|
/// The value range is from 0.8 to 1.0. If it's greater than 1.0, it will cause
|
||||||
/// the text to be too large and not aligned with the icon
|
/// the text to be too large and not aligned with the icon
|
||||||
static const String textScaleFactor = 'textScaleFactor';
|
static const String textScaleFactor = 'textScaleFactor';
|
||||||
|
|
||||||
|
/// The key for saving the feature flags
|
||||||
|
///
|
||||||
|
/// The value is a json string with the following format:
|
||||||
|
/// {'feature_flag_1': true, 'feature_flag_2': false}
|
||||||
|
static const String featureFlag = 'featureFlag';
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import 'package:appflowy/shared/feature_flags.dart';
|
||||||
import 'package:appflowy_backend/log.dart';
|
import 'package:appflowy_backend/log.dart';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
|
|
||||||
@ -59,3 +60,15 @@ class TemporaryDirectoryCache implements ICache {
|
|||||||
await tmpDir.delete(recursive: true);
|
await tmpDir.delete(recursive: true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class FeatureFlagCache implements ICache {
|
||||||
|
@override
|
||||||
|
Future<int> cacheSize() async {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> clearAll() async {
|
||||||
|
await FeatureFlag.clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,10 +1,17 @@
|
|||||||
|
import 'dart:collection';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:appflowy/core/config/kv.dart';
|
||||||
|
import 'package:appflowy/core/config/kv_keys.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
|
||||||
|
typedef FeatureFlagMap = Map<FeatureFlag, bool>;
|
||||||
|
|
||||||
/// The [FeatureFlag] is used to control the front-end features of the app.
|
/// The [FeatureFlag] is used to control the front-end features of the app.
|
||||||
///
|
///
|
||||||
/// For example, if your feature is still under development,
|
/// For example, if your feature is still under development,
|
||||||
/// you can set the value to `false` to hide the feature.
|
/// you can set the value to `false` to hide the feature.
|
||||||
enum FeatureFlag {
|
enum FeatureFlag {
|
||||||
// Feature flags
|
|
||||||
|
|
||||||
// used to control the visibility of the collaborative workspace feature
|
// used to control the visibility of the collaborative workspace feature
|
||||||
// if it's on, you can see the workspace list and the workspace settings
|
// if it's on, you can see the workspace list and the workspace settings
|
||||||
// in the top-left corner of the app
|
// in the top-left corner of the app
|
||||||
@ -14,7 +21,56 @@ enum FeatureFlag {
|
|||||||
// if it's on, you can see the members settings in the settings page
|
// if it's on, you can see the members settings in the settings page
|
||||||
membersSettings;
|
membersSettings;
|
||||||
|
|
||||||
|
static Future<void> initialize() async {
|
||||||
|
final values = await getIt<KeyValueStorage>().getWithFormat<FeatureFlagMap>(
|
||||||
|
KVKeys.featureFlag,
|
||||||
|
(value) => Map.from(jsonDecode(value)).map(
|
||||||
|
(key, value) => MapEntry(
|
||||||
|
FeatureFlag.values.firstWhere((e) => e.name == key),
|
||||||
|
value as bool,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
) ??
|
||||||
|
{};
|
||||||
|
|
||||||
|
_values = {
|
||||||
|
...{for (final flag in FeatureFlag.values) flag: false},
|
||||||
|
...values,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static UnmodifiableMapView<FeatureFlag, bool> get data =>
|
||||||
|
UnmodifiableMapView(_values);
|
||||||
|
|
||||||
|
Future<void> turnOn() async {
|
||||||
|
await update(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> turnOff() async {
|
||||||
|
await update(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> update(bool value) async {
|
||||||
|
_values[this] = value;
|
||||||
|
|
||||||
|
await getIt<KeyValueStorage>().set(
|
||||||
|
KVKeys.featureFlag,
|
||||||
|
jsonEncode(
|
||||||
|
_values.map((key, value) => MapEntry(key.name, value)),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
static Future<void> clear() async {
|
||||||
|
_values = {};
|
||||||
|
await getIt<KeyValueStorage>().remove(KVKeys.featureFlag);
|
||||||
|
}
|
||||||
|
|
||||||
bool get isOn {
|
bool get isOn {
|
||||||
|
if (_values.containsKey(this)) {
|
||||||
|
return _values[this]!;
|
||||||
|
}
|
||||||
|
|
||||||
switch (this) {
|
switch (this) {
|
||||||
case FeatureFlag.collaborativeWorkspace:
|
case FeatureFlag.collaborativeWorkspace:
|
||||||
return false;
|
return false;
|
||||||
@ -22,4 +78,17 @@ enum FeatureFlag {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String get description {
|
||||||
|
switch (this) {
|
||||||
|
case FeatureFlag.collaborativeWorkspace:
|
||||||
|
return 'if it\'s on, you can see the workspace list and the workspace settings in the top-left corner of the app';
|
||||||
|
case FeatureFlag.membersSettings:
|
||||||
|
return 'if it\'s on, you can see the members settings in the settings page';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String get key => 'appflowy_feature_flag_${toString()}';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
FeatureFlagMap _values = {};
|
||||||
|
@ -134,7 +134,8 @@ void _resolveCommonService(
|
|||||||
getIt.registerFactory<FlowyCacheManager>(
|
getIt.registerFactory<FlowyCacheManager>(
|
||||||
() => FlowyCacheManager()
|
() => FlowyCacheManager()
|
||||||
..registerCache(TemporaryDirectoryCache())
|
..registerCache(TemporaryDirectoryCache())
|
||||||
..registerCache(CustomImageCacheManager()),
|
..registerCache(CustomImageCacheManager())
|
||||||
|
..registerCache(FeatureFlagCache()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import 'dart:async';
|
|||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:appflowy/env/cloud_env.dart';
|
import 'package:appflowy/env/cloud_env.dart';
|
||||||
|
import 'package:appflowy/startup/tasks/feature_flag_task.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
import 'package:appflowy/workspace/application/settings/prelude.dart';
|
||||||
import 'package:appflowy_backend/appflowy_backend.dart';
|
import 'package:appflowy_backend/appflowy_backend.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
@ -113,6 +114,7 @@ class FlowyRunner {
|
|||||||
// there's a flag named _enable in memory_leak_detector.dart. If it's false, the task will be ignored.
|
// there's a flag named _enable in memory_leak_detector.dart. If it's false, the task will be ignored.
|
||||||
MemoryLeakDetectorTask(),
|
MemoryLeakDetectorTask(),
|
||||||
const DebugTask(),
|
const DebugTask(),
|
||||||
|
const FeatureFlagTask(),
|
||||||
|
|
||||||
// localization
|
// localization
|
||||||
const InitLocalizationTask(),
|
const InitLocalizationTask(),
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import 'package:appflowy/shared/feature_flags.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
|
||||||
|
import '../startup.dart';
|
||||||
|
|
||||||
|
class FeatureFlagTask extends LaunchTask {
|
||||||
|
const FeatureFlagTask();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> initialize(LaunchContext context) async {
|
||||||
|
// the hotkey manager is not supported on mobile
|
||||||
|
if (!kDebugMode) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await FeatureFlag.initialize();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> dispose() async {}
|
||||||
|
}
|
@ -17,6 +17,7 @@ enum SettingsPage {
|
|||||||
cloud,
|
cloud,
|
||||||
shortcuts,
|
shortcuts,
|
||||||
member,
|
member,
|
||||||
|
featureFlags,
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsDialogBloc
|
class SettingsDialogBloc
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import 'package:appflowy/generated/locale_keys.g.dart';
|
import 'package:appflowy/generated/locale_keys.g.dart';
|
||||||
import 'package:appflowy/startup/startup.dart';
|
import 'package:appflowy/startup/startup.dart';
|
||||||
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dart';
|
||||||
|
import 'package:appflowy/workspace/presentation/settings/widgets/feature_flags/feature_flag_page.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/members/workspace_member_page.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/settings_appearance_view.dart';
|
||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/settings_customize_shortcuts_view.dart';
|
||||||
@ -114,6 +115,8 @@ class SettingsDialog extends StatelessWidget {
|
|||||||
return const SettingsCustomizeShortcutsWrapper();
|
return const SettingsCustomizeShortcutsWrapper();
|
||||||
case SettingsPage.member:
|
case SettingsPage.member:
|
||||||
return WorkspaceMembersPage(userProfile: user);
|
return WorkspaceMembersPage(userProfile: user);
|
||||||
|
case SettingsPage.featureFlags:
|
||||||
|
return const FeatureFlagsPage();
|
||||||
default:
|
default:
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,70 @@
|
|||||||
|
import 'package:appflowy/shared/feature_flags.dart';
|
||||||
|
import 'package:appflowy/startup/startup.dart';
|
||||||
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class FeatureFlagsPage extends StatelessWidget {
|
||||||
|
const FeatureFlagsPage({
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return SingleChildScrollView(
|
||||||
|
child: SeparatedColumn(
|
||||||
|
children: [
|
||||||
|
...FeatureFlag.data.entries.map(
|
||||||
|
(e) => _FeatureFlagItem(featureFlag: e.key),
|
||||||
|
),
|
||||||
|
FlowyTextButton(
|
||||||
|
'Restart the app to apply changes',
|
||||||
|
fontSize: 16.0,
|
||||||
|
fontColor: Colors.red,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 16.0,
|
||||||
|
vertical: 12.0,
|
||||||
|
),
|
||||||
|
onPressed: () async {
|
||||||
|
await runAppFlowy();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FeatureFlagItem extends StatefulWidget {
|
||||||
|
const _FeatureFlagItem({
|
||||||
|
required this.featureFlag,
|
||||||
|
});
|
||||||
|
|
||||||
|
final FeatureFlag featureFlag;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<_FeatureFlagItem> createState() => _FeatureFlagItemState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FeatureFlagItemState extends State<_FeatureFlagItem> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return ListTile(
|
||||||
|
title: FlowyText(
|
||||||
|
widget.featureFlag.name,
|
||||||
|
fontSize: 16.0,
|
||||||
|
),
|
||||||
|
subtitle: FlowyText.small(
|
||||||
|
widget.featureFlag.description,
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
trailing: Switch(
|
||||||
|
value: widget.featureFlag.isOn,
|
||||||
|
onChanged: (value) {
|
||||||
|
setState(() {
|
||||||
|
widget.featureFlag.update(value);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -4,6 +4,7 @@ import 'package:appflowy/workspace/application/settings/settings_dialog_bloc.dar
|
|||||||
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
import 'package:appflowy/workspace/presentation/settings/widgets/settings_menu_element.dart';
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
import 'package:flowy_infra_ui/flowy_infra_ui.dart';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SettingsMenu extends StatelessWidget {
|
class SettingsMenu extends StatelessWidget {
|
||||||
@ -79,6 +80,15 @@ class SettingsMenu extends StatelessWidget {
|
|||||||
icon: Icons.people,
|
icon: Icons.people,
|
||||||
changeSelectedPage: changeSelectedPage,
|
changeSelectedPage: changeSelectedPage,
|
||||||
),
|
),
|
||||||
|
if (kDebugMode)
|
||||||
|
SettingsMenuElement(
|
||||||
|
// no need to translate this page
|
||||||
|
page: SettingsPage.featureFlags,
|
||||||
|
selectedPage: currentPage,
|
||||||
|
label: 'Feature Flags',
|
||||||
|
icon: Icons.flag,
|
||||||
|
changeSelectedPage: changeSelectedPage,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
@ -2,6 +2,9 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
typedef SeparatorBuilder = Widget Function();
|
typedef SeparatorBuilder = Widget Function();
|
||||||
|
|
||||||
|
Widget _defaultColumnSeparatorBuilder() => const Divider();
|
||||||
|
Widget _defaultRowSeparatorBuilder() => const VerticalDivider();
|
||||||
|
|
||||||
class SeparatedColumn extends Column {
|
class SeparatedColumn extends Column {
|
||||||
SeparatedColumn({
|
SeparatedColumn({
|
||||||
super.key,
|
super.key,
|
||||||
@ -11,7 +14,7 @@ class SeparatedColumn extends Column {
|
|||||||
super.textBaseline,
|
super.textBaseline,
|
||||||
super.textDirection,
|
super.textDirection,
|
||||||
super.verticalDirection,
|
super.verticalDirection,
|
||||||
required SeparatorBuilder separatorBuilder,
|
SeparatorBuilder separatorBuilder = _defaultColumnSeparatorBuilder,
|
||||||
required List<Widget> children,
|
required List<Widget> children,
|
||||||
}) : super(children: _insertSeparators(children, separatorBuilder));
|
}) : super(children: _insertSeparators(children, separatorBuilder));
|
||||||
}
|
}
|
||||||
@ -25,7 +28,7 @@ class SeparatedRow extends Row {
|
|||||||
super.textBaseline,
|
super.textBaseline,
|
||||||
super.textDirection,
|
super.textDirection,
|
||||||
super.verticalDirection,
|
super.verticalDirection,
|
||||||
required SeparatorBuilder separatorBuilder,
|
SeparatorBuilder separatorBuilder = _defaultRowSeparatorBuilder,
|
||||||
required List<Widget> children,
|
required List<Widget> children,
|
||||||
}) : super(children: _insertSeparators(children, separatorBuilder));
|
}) : super(children: _insertSeparators(children, separatorBuilder));
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user