Merge branch 'focus-mode' into 'main'

Focus mode

See merge request mysocialportal/relatica!60
This commit is contained in:
HankG 2024-08-19 19:25:45 +00:00
commit 8b67f35cb1
16 changed files with 973 additions and 271 deletions

View file

@ -22,8 +22,12 @@ linter:
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
analyzer:
plugins:
- custom_lint

View file

@ -0,0 +1,201 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:wheel_chooser/wheel_chooser.dart';
import '../models/focus_mode_data.dart';
import '../riverpod_controllers/focus_mode.dart';
import '../routes.dart';
import 'padding.dart';
const foreverDuration = Duration(days: 10000);
class FocusModeMenuItem extends ConsumerWidget {
const FocusModeMenuItem({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) {
final focusMode = ref.watch(focusModeProvider);
final title =
focusMode.enabled ? 'Disable Focus Mode' : 'Enable Focus Mode';
return Padding(
padding: const EdgeInsets.all(8.0),
child: ListTile(
title: Text(title),
onTap: () async {
if (focusMode.enabled) {
context.pop();
context.push(ScreenPaths.focusModeDisable);
} else {
final duration = await _chooseDuration(context);
if (duration == null) {
return;
}
final disableTime = duration == foreverDuration
? null
: DateTime.now().add(duration);
final update = FocusModeData(true, disableTime: disableTime);
ref.read(focusModeProvider.notifier).setMode(update);
if (context.mounted) {
context.pop();
context.go(ScreenPaths.timelines);
}
}
},
),
);
}
}
Future<Duration?> _chooseDuration(
BuildContext context,
) {
var hours = 0;
var minutes = 30;
return showDialog<Duration?>(
context: context,
barrierDismissible: true,
builder: (BuildContext context) {
return AlertDialog(
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Choose Focus Duration',
style: Theme.of(context)
.textTheme
.bodyLarge!
.copyWith(fontWeight: FontWeight.bold),
),
const VerticalPadding(),
Wrap(
runSpacing: 10.0,
spacing: 10.0,
children: [
ElevatedButton(
child: const Text('15 minutes'),
onPressed: () {
Navigator.pop(
context,
const Duration(
minutes: 15,
),
); // showDialog() returns true
},
),
ElevatedButton(
child: const Text('30 minutes'),
onPressed: () {
Navigator.pop(
context,
const Duration(
minutes: 30,
),
); // showDialog() returns true
},
),
ElevatedButton(
child: const Text('1 hour'),
onPressed: () {
Navigator.pop(
context,
const Duration(
hours: 1,
),
); // showDialog() returns true
},
),
ElevatedButton(
child: const Text('6 hours'),
onPressed: () {
Navigator.pop(
context,
const Duration(
hours: 6,
),
); // showDialog() returns true
},
),
ElevatedButton(
child: const Text('12 hours'),
onPressed: () {
Navigator.pop(
context,
const Duration(
hours: 12,
),
); // showDialog() returns true
},
),
ElevatedButton(
child: const Text('1 day'),
onPressed: () {
Navigator.pop(
context,
const Duration(
days: 1,
),
); // showDialog() returns true
},
),
ElevatedButton(
child: const Text('Forever'),
onPressed: () {
Navigator.pop(
context,
foreverDuration,
); // showDialog() returns true
},
),
],
),
const VerticalPadding(),
SizedBox(
height: 100,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Flexible(
child: WheelChooser.integer(
initValue: hours,
onValueChanged: (v) => hours = v,
maxValue: 24,
minValue: 0,
unSelectTextStyle: const TextStyle(color: Colors.grey),
),
),
const Text('hours'),
Flexible(
child: WheelChooser.integer(
initValue: minutes,
onValueChanged: (v) => minutes = v,
maxValue: 59,
minValue: 0,
unSelectTextStyle: const TextStyle(color: Colors.grey),
),
),
const Text('minutes'),
],
),
)
],
),
actions: [
ElevatedButton(
child: const Text('Select'),
onPressed: () {
Navigator.pop(
context,
Duration(
hours: hours,
minutes: minutes,
),
); // showDialog() returns true
},
),
]);
},
);
}

View file

@ -0,0 +1,55 @@
import 'dart:async';
import 'package:flutter/material.dart';
import '../utils/dateutils.dart';
class FocusModeStatusHeadline extends StatefulWidget {
final DateTime? disableTime;
const FocusModeStatusHeadline({super.key, required this.disableTime});
@override
State<FocusModeStatusHeadline> createState() =>
_FocusModeStatusHeadlineState();
}
class _FocusModeStatusHeadlineState extends State<FocusModeStatusHeadline> {
Timer? updateTimer;
Duration? timeUntil;
@override
void initState() {
super.initState();
_updateTimeUntil();
updateTimer = Timer.periodic(Duration(seconds: 1), (_) {
setState(() {
_updateTimeUntil();
});
});
}
@override
void dispose() {
print('Disposing');
updateTimer?.cancel();
super.dispose();
}
void _updateTimeUntil() {
timeUntil = widget.disableTime?.difference(DateTime.now());
}
@override
Widget build(BuildContext context) {
final title = timeUntil == null
? 'Focus Mode for Forever'
: 'Focus Mode for ${timeUntil!.simpleLabel}';
return Text(
title,
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
fontWeight: FontWeight.bold,
),
);
}
}

View file

@ -5,6 +5,7 @@ import 'package:logging/logging.dart';
import '../globals.dart';
import '../routes.dart';
import '../services/auth_service.dart';
import 'focus_mode_menu_item.dart';
import 'login_aware_cached_network_image.dart';
class StandardAppDrawer extends StatelessWidget {
@ -62,6 +63,7 @@ class StandardAppDrawer extends StatelessWidget {
'Manage Profiles',
() => context.pushNamed(ScreenPaths.manageProfiles),
),
const FocusModeMenuItem(),
const Divider(),
buildMenuButton(
context,

View file

@ -1,4 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart' as fr;
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import 'package:media_kit/media_kit.dart';
import 'package:multi_trigger_autocomplete/multi_trigger_autocomplete.dart';
@ -8,6 +10,7 @@ import 'package:provider/provider.dart';
import 'app_theme.dart';
import 'di_initialization.dart';
import 'globals.dart';
import 'riverpod_controllers/focus_mode.dart';
import 'routes.dart';
import 'services/auth_service.dart';
import 'services/blocks_manager.dart';
@ -50,7 +53,7 @@ void main() async {
// enabled: !kReleaseMode && enablePreview,
// builder: (context) => const App(),
// ));
runApp(const App());
runApp(const fr.ProviderScope(child: App()));
}
Future<void> setupPackageInfoAndUserAgent() async {
@ -59,13 +62,45 @@ Future<void> setupPackageInfoAndUserAgent() async {
userAgent = 'Relatica/$appVersion';
}
class App extends StatelessWidget {
class App extends fr.ConsumerStatefulWidget {
const App({super.key});
// This widget is the root of your application.
@override
fr.ConsumerState<App> createState() => _AppState();
}
class _AppState extends fr.ConsumerState<App> {
@override
Widget build(BuildContext context) {
final settingsService = getIt<SettingsService>();
final authService = getIt<AccountsService>();
final appRouter = GoRouter(
initialLocation: ScreenPaths.timelines,
debugLogDiagnostics: true,
refreshListenable: authService,
redirect: (context, state) async {
final loggedIn = authService.loggedIn;
final focusMode = ref.read(focusModeProvider);
print('Focus mode? $focusMode');
if (!loggedIn && authService.initializing) {
return ScreenPaths.splash;
}
if (!loggedIn && !allowedLoggedOut.contains(state.uri.toString())) {
return ScreenPaths.signin;
}
if (loggedIn && allowedLoggedOut.contains(state.uri.toString())) {
return ScreenPaths.timelines;
}
return null;
},
routes: routes,
);
return AnimatedBuilder(
builder: (context, child) {
Logger.root.level = settingsService.logLevel;

View file

@ -0,0 +1,18 @@
class FocusModeData {
final DateTime? disableTime;
final bool enabled;
const FocusModeData(this.enabled, {this.disableTime});
factory FocusModeData.disabled() => const FocusModeData(false);
factory FocusModeData.fromJson(Map<String, dynamic> json) => FocusModeData(
json['enabled'],
disableTime: DateTime.tryParse(json['disableTime'] ?? ''),
);
Map<String, dynamic> toJson() => {
'enabled': enabled,
if (disableTime != null) 'disableTime': disableTime!.toIso8601String(),
};
}

View file

@ -54,6 +54,9 @@ class TimelineIdentifiers {
factory TimelineIdentifiers.home() =>
const TimelineIdentifiers(timeline: TimelineType.home);
factory TimelineIdentifiers.myPosts() =>
const TimelineIdentifiers(timeline: TimelineType.self);
factory TimelineIdentifiers.profile(String profileId) => TimelineIdentifiers(
timeline: TimelineType.profile,
auxData: profileId,

View file

@ -0,0 +1,38 @@
import 'dart:async';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import '../globals.dart';
import '../models/focus_mode_data.dart';
import '../services/setting_service.dart';
part 'focus_mode.g.dart';
@Riverpod(keepAlive: true)
class FocusMode extends _$FocusMode {
Timer? _disableTimer;
void setMode(FocusModeData newMode) {
_disableTimer?.cancel();
var updatedState = newMode;
if (newMode.enabled && newMode.disableTime != null) {
final timeDifference = newMode.disableTime!.difference(DateTime.now());
if (timeDifference.isNegative || timeDifference.inMicroseconds == 0) {
updatedState = FocusModeData.disabled();
}
_disableTimer = Timer(timeDifference, () {
state = FocusModeData.disabled();
});
}
getIt<SettingsService>().focusModeData = updatedState;
state = updatedState;
}
@override
FocusModeData build() {
final storedFocusMode = getIt<SettingsService>().focusModeData;
setMode(storedFocusMode);
return state;
}
}

View file

@ -0,0 +1,24 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'focus_mode.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$focusModeHash() => r'93028f8514cecda8ce68506ec242f4d26d63b4b2';
/// See also [FocusMode].
@ProviderFor(FocusMode)
final focusModeProvider = NotifierProvider<FocusMode, FocusModeData>.internal(
FocusMode.new,
name: r'focusModeProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') ? null : _$focusModeHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$FocusMode = Notifier<FocusModeData>;
// ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -1,6 +1,5 @@
import 'package:go_router/go_router.dart';
import 'globals.dart';
import 'models/interaction_type_enum.dart';
import 'screens/blocks_screen.dart';
import 'screens/circle_add_users_screen.dart';
@ -8,6 +7,7 @@ import 'screens/circle_create_screen.dart';
import 'screens/circle_editor_screen.dart';
import 'screens/circle_management_screen.dart';
import 'screens/contacts_screen.dart';
import 'screens/disable_focus_mode_screen.dart';
import 'screens/editor.dart';
import 'screens/filter_editor_screen.dart';
import 'screens/filters_screen.dart';
@ -29,7 +29,6 @@ import 'screens/sign_in.dart';
import 'screens/splash.dart';
import 'screens/user_posts_screen.dart';
import 'screens/user_profile_screen.dart';
import 'services/auth_service.dart';
class ScreenPaths {
static String blocks = '/blocks';
@ -37,6 +36,7 @@ class ScreenPaths {
static String thread = '/thread';
static String connectHandle = '/connect';
static String contacts = '/contacts';
static String focusModeDisable = '/focus_mode_disable';
static String splash = '/splash';
static String settings = '/settings';
static String messages = '/messages';
@ -56,271 +56,254 @@ class ScreenPaths {
}
bool needAuthChangeInitialized = true;
final _authService = getIt<AccountsService>();
final allowedLoggedOut = [
ScreenPaths.splash,
ScreenPaths.signin,
ScreenPaths.signup
];
final appRouter = GoRouter(
initialLocation: ScreenPaths.timelines,
debugLogDiagnostics: true,
refreshListenable: _authService,
redirect: (context, state) async {
final loggedIn = _authService.loggedIn;
if (!loggedIn && _authService.initializing) {
return ScreenPaths.splash;
}
if (!loggedIn && !allowedLoggedOut.contains(state.uri.toString())) {
return ScreenPaths.signin;
}
if (loggedIn && allowedLoggedOut.contains(state.uri.toString())) {
return ScreenPaths.timelines;
}
return null;
},
final routes = [
GoRoute(
path: ScreenPaths.blocks,
name: ScreenPaths.blocks,
builder: (context, state) => const BlocksScreen(),
),
GoRoute(
path: ScreenPaths.focusModeDisable,
name: ScreenPaths.focusModeDisable,
builder: (context, state) => const DisableFocusModeScreen(),
),
GoRoute(
path: ScreenPaths.filters,
name: ScreenPaths.filters,
builder: (context, state) => const FiltersScreen(),
routes: [
GoRoute(
path: ScreenPaths.blocks,
name: ScreenPaths.blocks,
builder: (context, state) => const BlocksScreen(),
path: 'new',
pageBuilder: (context, state) => const NoTransitionPage(
child: FilterEditorScreen(id: ''),
),
),
GoRoute(
path: ScreenPaths.filters,
name: ScreenPaths.filters,
builder: (context, state) => const FiltersScreen(),
routes: [
GoRoute(
path: 'new',
pageBuilder: (context, state) => const NoTransitionPage(
child: FilterEditorScreen(id: ''),
),
),
GoRoute(
path: 'edit/:id',
pageBuilder: (context, state) => NoTransitionPage(
child: FilterEditorScreen(id: state.pathParameters['id']!)),
)
],
),
GoRoute(
path: ScreenPaths.signin,
name: ScreenPaths.signin,
builder: (context, state) => const SignInScreen(),
),
GoRoute(
path: ScreenPaths.manageProfiles,
name: ScreenPaths.manageProfiles,
builder: (context, state) => const SignInScreen(),
),
GoRoute(
path: ScreenPaths.contacts,
name: ScreenPaths.contacts,
path: 'edit/:id',
pageBuilder: (context, state) => NoTransitionPage(
name: ScreenPaths.contacts,
child: const ContactsScreen(),
),
),
child: FilterEditorScreen(id: state.pathParameters['id']!)),
)
],
),
GoRoute(
path: ScreenPaths.signin,
name: ScreenPaths.signin,
builder: (context, state) => const SignInScreen(),
),
GoRoute(
path: ScreenPaths.manageProfiles,
name: ScreenPaths.manageProfiles,
builder: (context, state) => const SignInScreen(),
),
GoRoute(
path: ScreenPaths.contacts,
name: ScreenPaths.contacts,
pageBuilder: (context, state) => NoTransitionPage(
name: ScreenPaths.contacts,
child: const ContactsScreen(),
),
),
GoRoute(
path: '/connect/:id',
name: ScreenPaths.connectHandle,
builder: (context, state) =>
FollowRequestAdjudicationScreen(userId: state.pathParameters['id']!),
),
GoRoute(
path: ScreenPaths.timelines,
name: ScreenPaths.timelines,
pageBuilder: (context, state) => NoTransitionPage(
name: ScreenPaths.timelines,
child: const HomeScreen(),
),
),
GoRoute(
path: ScreenPaths.messages,
name: ScreenPaths.messages,
pageBuilder: (context, state) => const NoTransitionPage(
child: MessagesScreen(),
),
routes: [
GoRoute(
path: '/connect/:id',
name: ScreenPaths.connectHandle,
builder: (context, state) => FollowRequestAdjudicationScreen(
userId: state.pathParameters['id']!),
),
GoRoute(
path: ScreenPaths.timelines,
name: ScreenPaths.timelines,
path: 'new_thread',
pageBuilder: (context, state) => NoTransitionPage(
name: ScreenPaths.timelines,
child: const HomeScreen(),
child: MessagesNewThread(),
),
),
],
),
GoRoute(
name: ScreenPaths.thread,
path: ScreenPaths.thread,
builder: (context, state) =>
MessageThreadScreen(parentThreadId: state.uri.queryParameters['uri']!),
),
GoRoute(
name: ScreenPaths.circleManagement,
path: ScreenPaths.circleManagement,
builder: (context, state) => const CircleManagementScreen(),
routes: [
GoRoute(
path: 'show/:id',
builder: (context, state) => CircleEditorScreen(
circleId: state.pathParameters['id']!,
),
),
GoRoute(
path: ScreenPaths.messages,
name: ScreenPaths.messages,
pageBuilder: (context, state) => const NoTransitionPage(
child: MessagesScreen(),
),
routes: [
GoRoute(
path: 'new_thread',
pageBuilder: (context, state) => NoTransitionPage(
child: MessagesNewThread(),
),
),
],
path: 'new',
builder: (context, state) => const CircleCreateScreen(),
),
GoRoute(
name: ScreenPaths.thread,
path: ScreenPaths.thread,
builder: (context, state) => MessageThreadScreen(
parentThreadId: state.uri.queryParameters['uri']!),
),
GoRoute(
name: ScreenPaths.circleManagement,
path: ScreenPaths.circleManagement,
builder: (context, state) => const CircleManagementScreen(),
routes: [
GoRoute(
path: 'show/:id',
builder: (context, state) => CircleEditorScreen(
circleId: state.pathParameters['id']!,
),
),
GoRoute(
path: 'new',
builder: (context, state) => const CircleCreateScreen(),
),
GoRoute(
path: 'add_users/:id',
builder: (context, state) =>
CircleAddUsersScreen(circleId: state.pathParameters['id']!),
),
],
),
GoRoute(
path: ScreenPaths.settings,
name: ScreenPaths.settings,
pageBuilder: (context, state) => const NoTransitionPage(
child: SettingsScreen(),
),
),
GoRoute(
path: ScreenPaths.gallery,
name: ScreenPaths.gallery,
pageBuilder: (context, state) => const NoTransitionPage(
child: GalleryBrowsersScreen(),
),
routes: [
GoRoute(
path: 'show',
builder: (context, state) => GalleryScreen(
galleryName: state.extra!.toString(),
),
),
GoRoute(
path: 'edit/:name/image/:id',
builder: (context, state) => ImageEditorScreen(
galleryName: state.pathParameters['name']!,
imageId: state.pathParameters['id']!,
),
),
],
),
GoRoute(
path: ScreenPaths.notifications,
name: ScreenPaths.notifications,
pageBuilder: (context, state) => const NoTransitionPage(
child: NotificationsScreen(),
),
),
GoRoute(
path: ScreenPaths.splash,
name: ScreenPaths.splash,
builder: (context, state) => const SplashScreen(),
),
GoRoute(
path: '/post',
redirect: (context, state) {
if (state.uri.toString() == '/post') {
return '/post/new';
}
return null;
},
routes: [
GoRoute(
path: 'new',
builder: (context, state) => const EditorScreen(
forEditing: false,
),
),
GoRoute(
path: 'edit/:id',
builder: (context, state) => EditorScreen(
id: state.pathParameters['id'] ?? 'Not Found',
forEditing: true,
),
),
GoRoute(
path: 'view/:id/:goto_id',
builder: (context, state) => PostScreen(
id: state.pathParameters['id'] ?? 'Not Found',
goToId: state.pathParameters['goto_id'] ?? 'Not Found',
),
),
]),
GoRoute(
path: '/comment',
redirect: (context, state) {
if (state.uri.toString() == '/comment') {
return '/comment/new';
}
return null;
},
routes: [
GoRoute(
path: 'new',
builder: (context, state) => EditorScreen(
parentId: state.uri.queryParameters['parent_id'] ?? '',
forEditing: false,
),
),
GoRoute(
path: 'edit/:id',
builder: (context, state) => EditorScreen(
id: state.pathParameters['id'] ?? 'Not Found',
forEditing: true,
),
),
]),
GoRoute(
path: '/user_posts/:id',
name: ScreenPaths.userPosts,
path: 'add_users/:id',
builder: (context, state) =>
UserPostsScreen(userId: state.pathParameters['id']!),
CircleAddUsersScreen(circleId: state.pathParameters['id']!),
),
],
),
GoRoute(
path: ScreenPaths.settings,
name: ScreenPaths.settings,
pageBuilder: (context, state) => const NoTransitionPage(
child: SettingsScreen(),
),
),
GoRoute(
path: ScreenPaths.gallery,
name: ScreenPaths.gallery,
pageBuilder: (context, state) => const NoTransitionPage(
child: GalleryBrowsersScreen(),
),
routes: [
GoRoute(
path: '/likes/:id',
name: ScreenPaths.likes,
builder: (context, state) => InteractionsViewerScreen(
statusId: state.pathParameters['id']!,
type: InteractionType.like,
path: 'show',
builder: (context, state) => GalleryScreen(
galleryName: state.extra!.toString(),
),
),
GoRoute(
path: '/reshares/:id',
name: ScreenPaths.reshares,
builder: (context, state) => InteractionsViewerScreen(
statusId: state.pathParameters['id']!,
type: InteractionType.reshare,
path: 'edit/:name/image/:id',
builder: (context, state) => ImageEditorScreen(
galleryName: state.pathParameters['name']!,
imageId: state.pathParameters['id']!,
),
),
GoRoute(
path: '/user_profile/:id',
name: ScreenPaths.userProfile,
builder: (context, state) =>
UserProfileScreen(userId: state.pathParameters['id']!),
),
GoRoute(
path: ScreenPaths.search,
name: ScreenPaths.search,
pageBuilder: (context, state) => NoTransitionPage(
name: ScreenPaths.search,
child: const SearchScreen(),
],
),
GoRoute(
path: ScreenPaths.notifications,
name: ScreenPaths.notifications,
pageBuilder: (context, state) => const NoTransitionPage(
child: NotificationsScreen(),
),
),
GoRoute(
path: ScreenPaths.splash,
name: ScreenPaths.splash,
builder: (context, state) => const SplashScreen(),
),
GoRoute(
path: '/post',
redirect: (context, state) {
if (state.uri.toString() == '/post') {
return '/post/new';
}
return null;
},
routes: [
GoRoute(
path: 'new',
builder: (context, state) => const EditorScreen(
forEditing: false,
),
),
),
GoRoute(
path: ScreenPaths.logViewer,
name: ScreenPaths.logViewer,
pageBuilder: (context, state) => const NoTransitionPage(
child: LogViewerScreen(),
GoRoute(
path: 'edit/:id',
builder: (context, state) => EditorScreen(
id: state.pathParameters['id'] ?? 'Not Found',
forEditing: true,
),
),
),
]);
GoRoute(
path: 'view/:id/:goto_id',
builder: (context, state) => PostScreen(
id: state.pathParameters['id'] ?? 'Not Found',
goToId: state.pathParameters['goto_id'] ?? 'Not Found',
),
),
]),
GoRoute(
path: '/comment',
redirect: (context, state) {
if (state.uri.toString() == '/comment') {
return '/comment/new';
}
return null;
},
routes: [
GoRoute(
path: 'new',
builder: (context, state) => EditorScreen(
parentId: state.uri.queryParameters['parent_id'] ?? '',
forEditing: false,
),
),
GoRoute(
path: 'edit/:id',
builder: (context, state) => EditorScreen(
id: state.pathParameters['id'] ?? 'Not Found',
forEditing: true,
),
),
]),
GoRoute(
path: '/user_posts/:id',
name: ScreenPaths.userPosts,
builder: (context, state) =>
UserPostsScreen(userId: state.pathParameters['id']!),
),
GoRoute(
path: '/likes/:id',
name: ScreenPaths.likes,
builder: (context, state) => InteractionsViewerScreen(
statusId: state.pathParameters['id']!,
type: InteractionType.like,
),
),
GoRoute(
path: '/reshares/:id',
name: ScreenPaths.reshares,
builder: (context, state) => InteractionsViewerScreen(
statusId: state.pathParameters['id']!,
type: InteractionType.reshare,
),
),
GoRoute(
path: '/user_profile/:id',
name: ScreenPaths.userProfile,
builder: (context, state) =>
UserProfileScreen(userId: state.pathParameters['id']!),
),
GoRoute(
path: ScreenPaths.search,
name: ScreenPaths.search,
pageBuilder: (context, state) => NoTransitionPage(
name: ScreenPaths.search,
child: const SearchScreen(),
),
),
GoRoute(
path: ScreenPaths.logViewer,
name: ScreenPaths.logViewer,
pageBuilder: (context, state) => const NoTransitionPage(
child: LogViewerScreen(),
),
),
];

View file

@ -0,0 +1,137 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import '../controls/focus_mode_status_headline.dart';
import '../controls/padding.dart';
import '../models/focus_mode_data.dart';
import '../riverpod_controllers/focus_mode.dart';
import '../routes.dart';
import '../utils/snackbar_builder.dart';
class GameState {
final int maxNumber;
final int number;
final int? lastGuess;
String get hint {
if (lastGuess == null) {
return 'Guess a number between 0 and $maxNumber';
}
if (lastGuess! < number) {
return '$lastGuess is too low. Guess a higher number';
}
if (lastGuess! > number) {
return '$lastGuess is too high. Guess a lower number';
}
return 'You got it!';
}
bool get found => number == lastGuess;
const GameState({
required this.number,
required this.maxNumber,
this.lastGuess,
});
GameState update(int lastGuess) => GameState(
number: number,
maxNumber: maxNumber,
lastGuess: lastGuess,
);
factory GameState.newGame(int maxNumber) =>
GameState(number: Random().nextInt(maxNumber), maxNumber: maxNumber);
}
const _maxNumber = 100;
const introMessage =
"If you guess the number I've picked from 0 to $_maxNumber you may disable focus mode...";
class DisableFocusModeScreen extends ConsumerStatefulWidget {
const DisableFocusModeScreen({super.key});
@override
ConsumerState<DisableFocusModeScreen> createState() =>
_DisableFocusModeScreenState();
}
class _DisableFocusModeScreenState
extends ConsumerState<DisableFocusModeScreen> {
final formKey = GlobalKey<FormState>();
final guessController = TextEditingController();
var game = GameState.newGame(_maxNumber);
var message = introMessage;
@override
Widget build(BuildContext context) {
final focusMode = ref.watch(focusModeProvider);
if (!focusMode.enabled) {
context.go(ScreenPaths.timelines);
}
return Scaffold(
appBar: AppBar(
title: const Text('Disable Focus Mode?'),
),
body: Center(
child: Form(
key: formKey,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
FocusModeStatusHeadline(disableTime: focusMode.disableTime),
Text(message),
const VerticalPadding(),
TextFormField(
controller: guessController,
keyboardType: TextInputType.number,
autovalidateMode: AutovalidateMode.onUserInteraction,
validator: (value) => int.tryParse(value!) == null
? 'Please enter a number'
: null,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: const BorderSide(),
borderRadius: BorderRadius.circular(5.0),
),
),
),
const VerticalPadding(),
ElevatedButton(
onPressed: () {
final valid = formKey.currentState?.validate() ?? false;
if (!valid) {
buildSnackbar(context,
'Please enter an integer between 0 and $_maxNumber');
return;
}
final guess = int.parse(guessController.text);
game = game.update(guess);
if (game.found) {
ref
.read(focusModeProvider.notifier)
.setMode(const FocusModeData(false));
context.go(ScreenPaths.timelines);
} else {
setState(() {
message = game.hint;
});
}
},
child: const Text('Guess'))
],
),
),
),
),
);
}
}

View file

@ -1,7 +1,10 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
import 'package:logging/logging.dart';
import 'package:provider/provider.dart';
import 'package:relatica/controls/focus_mode_status_headline.dart';
import 'package:relatica/riverpod_controllers/focus_mode.dart';
import '../controls/app_bottom_nav_bar.dart';
import '../controls/linear_status_indicator.dart';
@ -16,14 +19,14 @@ import '../services/network_status_service.dart';
import '../services/timeline_manager.dart';
import '../utils/active_profile_selector.dart';
class HomeScreen extends StatefulWidget {
class HomeScreen extends ConsumerStatefulWidget {
const HomeScreen({super.key});
@override
State<HomeScreen> createState() => _HomeScreenState();
ConsumerState<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
class _HomeScreenState extends ConsumerState<HomeScreen> {
final _logger = Logger('$HomeScreen');
TimelineIdentifiers currentTimeline = TimelineIdentifiers.home();
@ -49,8 +52,12 @@ class _HomeScreenState extends State<HomeScreen> {
_logger.finest('Build');
final accountService = getIt<AccountsService>();
final nss = getIt<NetworkStatusService>();
final focusMode = ref.watch(focusModeProvider);
final timeline = TimelinePanel(timeline: currentTimeline);
final timeline = TimelinePanel(
timeline: focusMode.enabled
? TimelineIdentifiers.myPosts()
: currentTimeline);
return Scaffold(
appBar: AppBar(
@ -65,7 +72,9 @@ class _HomeScreenState extends State<HomeScreen> {
})
: null,
backgroundColor: Theme.of(context).canvasColor,
title: buildTimelineSelector(context),
title: focusMode.enabled
? FocusModeStatusHeadline(disableTime: focusMode.disableTime)
: buildTimelineSelector(context),
),
body: Center(
child: Column(
@ -78,10 +87,12 @@ class _HomeScreenState extends State<HomeScreen> {
),
),
drawer: const StandardAppDrawer(),
bottomNavigationBar: AppBottomNavBar(
currentButton: NavBarButtons.timelines,
onHomeButtonReclick: () => timeline.scrollToTop(),
),
bottomNavigationBar: focusMode.enabled
? null
: AppBottomNavBar(
currentButton: NavBarButtons.timelines,
onHomeButtonReclick: () => timeline.scrollToTop(),
),
floatingActionButton: FloatingActionButton.small(
onPressed: () {
context.push('/post/new');

View file

@ -5,9 +5,19 @@ import 'package:flutter/material.dart';
import 'package:logging/logging.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../models/focus_mode_data.dart';
import '../models/settings/network_capabilities_settings.dart';
import '../utils/theme_mode_extensions.dart';
const _lowBandwidthModeKey = 'LowBandwidthMode';
const _themeModeKey = 'ThemeMode';
const _colorBlindnessTestingModeKey = 'ColorBlindnessTestingMode';
const _logLevelKey = 'LogLevel';
const _networkCapabilitiesKey = 'NetworkCapabilities';
const _notificationGroupingKey = 'NotificationGrouping';
const _spoilerHidingEnabledKey = 'SpoilerHidingEnabled';
const _focusModeKey = 'FocusMode';
class SettingsService extends ChangeNotifier {
late final SharedPreferences _prefs;
var _initialized = false;
@ -86,6 +96,18 @@ class SettingsService extends ChangeNotifier {
notifyListeners();
}
FocusModeData _focusModeData = FocusModeData.disabled();
FocusModeData get focusModeData => _focusModeData;
set focusModeData(FocusModeData updatedData) {
_focusModeData = updatedData;
final jsonData = _focusModeData.toJson();
final jsonString = jsonEncode(jsonData);
_prefs.setString(_focusModeKey, jsonString);
notifyListeners();
}
Future<void> initialize() async {
if (_initialized) {
return;
@ -98,18 +120,11 @@ class SettingsService extends ChangeNotifier {
_colorBlindnessType = _colorBlindnessTypeFromPrefs(_prefs);
_logLevel = _levelFromPrefs(_prefs);
_networkCapabilities = _networkCapabilitiesFromPrefs(_prefs);
_focusModeData = _focusModeDataFromPrefs(_prefs);
_initialized = true;
}
}
const _lowBandwidthModeKey = 'LowBandwidthMode';
const _themeModeKey = 'ThemeMode';
const _colorBlindnessTestingModeKey = 'ColorBlindnessTestingMode';
const _logLevelKey = 'LogLevel';
const _networkCapabilitiesKey = 'NetworkCapabilities';
const _notificationGroupingKey = 'NotificationGrouping';
const _spoilerHidingEnabledKey = 'SpoilerHidingEnabled';
ColorBlindnessType _colorBlindnessTypeFromPrefs(SharedPreferences prefs) {
final cbString = prefs.getString(_colorBlindnessTestingModeKey);
if (cbString?.isEmpty ?? true) {
@ -149,3 +164,14 @@ Level _levelFromPrefs(SharedPreferences prefs) {
_ => Level.OFF,
};
}
FocusModeData _focusModeDataFromPrefs(SharedPreferences prefs) {
final fmString = prefs.getString(_focusModeKey);
if (fmString?.isEmpty ?? true) {
return FocusModeData.disabled();
}
final Map<String, dynamic> json = jsonDecode(fmString!);
final fm = FocusModeData.fromJson(json);
return fm;
}

View file

@ -78,3 +78,35 @@ const _separator = '_';
extension DateTimeExtensions on DateTime {
String toFileNameString() => '$year$month$day$_separator$hour$minute$second';
}
extension DurationExtensions on Duration {
String get simpleLabel {
final days = inHours / 24.0;
if (days >= 1) {
return days.round() == 1 ? '1 day' : '${days.round()} days';
}
final hours = inMinutes / 60.0;
if (hours >= 1) {
return hours.round() == 1 ? '1 hour' : '${hours.round()} hours';
}
final minutes = inSeconds / 60.0;
if (minutes >= 1) {
return minutes.round() == 1 ? '1 minute' : '${minutes.round()} minutes';
}
final seconds = inMilliseconds / 1000.0;
if (seconds >= 1) {
return seconds.round() == 1 ? '1 second' : '${seconds.round()} seconds';
}
if (inMilliseconds != 0) {
return inMilliseconds == 1 ? '1 millisecond' : '$inSeconds milliseconds';
}
return inMicroseconds == 1
? '1 microsecond'
: '$inMicroseconds microseconds';
}
}

View file

@ -17,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.4.1"
analyzer_plugin:
dependency: transitive
description:
name: analyzer_plugin
sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161"
url: "https://pub.dev"
source: hosted
version: "0.11.3"
archive:
dependency: transitive
description:
@ -85,10 +93,10 @@ packages:
dependency: "direct dev"
description:
name: build_runner
sha256: "1414d6d733a85d8ad2f1dfcb3ea7945759e35a123cb99ccfac75d0758f75edfa"
sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7"
url: "https://pub.dev"
source: hosted
version: "2.4.10"
version: "2.4.11"
build_runner_core:
dependency: transitive
description:
@ -161,6 +169,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.3"
ci:
dependency: transitive
description:
name: ci
sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13"
url: "https://pub.dev"
source: hosted
version: "0.1.0"
cli_util:
dependency: transitive
description:
name: cli_util
sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19
url: "https://pub.dev"
source: hosted
version: "0.4.1"
clock:
dependency: transitive
description:
@ -241,6 +265,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
custom_lint:
dependency: "direct dev"
description:
name: custom_lint
sha256: "7c0aec12df22f9082146c354692056677f1e70bc43471644d1fdb36c6fdda799"
url: "https://pub.dev"
source: hosted
version: "0.6.4"
custom_lint_builder:
dependency: transitive
description:
name: custom_lint_builder
sha256: d7dc41e709dde223806660268678be7993559e523eb3164e2a1425fd6f7615a9
url: "https://pub.dev"
source: hosted
version: "0.6.4"
custom_lint_core:
dependency: transitive
description:
name: custom_lint_core
sha256: a85e8f78f4c52f6c63cdaf8c872eb573db0231dcdf3c3a5906d493c1f8bc20e6
url: "https://pub.dev"
source: hosted
version: "0.6.3"
dart_style:
dependency: transitive
description:
@ -414,6 +462,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.4"
flutter_riverpod:
dependency: "direct main"
description:
name: flutter_riverpod
sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d"
url: "https://pub.dev"
source: hosted
version: "2.5.1"
flutter_secure_storage:
dependency: "direct main"
description:
@ -504,6 +560,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.15.1"
freezed_annotation:
dependency: transitive
description:
name: freezed_annotation
sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2
url: "https://pub.dev"
source: hosted
version: "2.4.4"
frontend_server_client:
dependency: transitive
description:
@ -560,6 +624,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.1"
hotreloader:
dependency: transitive
description:
name: hotreloader
sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e
url: "https://pub.dev"
source: hosted
version: "4.2.0"
html:
dependency: "direct main"
description:
@ -1056,6 +1128,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.2"
riverpod:
dependency: transitive
description:
name: riverpod
sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d
url: "https://pub.dev"
source: hosted
version: "2.5.1"
riverpod_analyzer_utils:
dependency: transitive
description:
name: riverpod_analyzer_utils
sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f"
url: "https://pub.dev"
source: hosted
version: "0.5.1"
riverpod_annotation:
dependency: "direct main"
description:
name: riverpod_annotation
sha256: e5e796c0eba4030c704e9dae1b834a6541814963292839dcf9638d53eba84f5c
url: "https://pub.dev"
source: hosted
version: "2.3.5"
riverpod_generator:
dependency: "direct dev"
description:
name: riverpod_generator
sha256: d451608bf17a372025fc36058863737636625dfdb7e3cbf6142e0dfeb366ab22
url: "https://pub.dev"
source: hosted
version: "2.4.0"
riverpod_lint:
dependency: "direct dev"
description:
name: riverpod_lint
sha256: "3c67c14ccd16f0c9d53e35ef70d06cd9d072e2fb14557326886bbde903b230a5"
url: "https://pub.dev"
source: hosted
version: "2.3.10"
rxdart:
dependency: transitive
description:
@ -1261,6 +1373,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.11.1"
state_notifier:
dependency: transitive
description:
name: state_notifier
sha256: b8677376aa54f2d7c58280d5a007f9e8774f1968d1fb1c096adcb4792fba29bb
url: "https://pub.dev"
source: hosted
version: "1.0.0"
stream_channel:
dependency: transitive
description:
@ -1565,6 +1685,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.0.0"
wheel_chooser:
dependency: "direct main"
description:
name: wheel_chooser
sha256: "3fee36f081f321c58a0b7b4afcdd92599f2ca520b3a1420084774e6b19cca1d8"
url: "https://pub.dev"
source: hosted
version: "1.1.2"
win32:
dependency: transitive
description:

View file

@ -20,14 +20,19 @@ dependencies:
file_picker: ^8.0.6
flutter_dotenv: ^5.1.0
flutter_file_dialog: ^3.0.2
flutter_riverpod: ^2.5.1
flutter_secure_storage: ^9.2.2
flutter_svg: ^2.0.10+1
flutter_web_auth_2: ^3.1.2
flutter_widget_from_html_core: ^0.15.1
get_it: ^7.7.0
get_it_mixin: ^4.2.2
go_router: ^14.1.2
html: ^0.15.4
http: any
http_parser: any
image: ^4.2.0
image_gallery_saver: ^2.0.3
image_picker: ^1.1.2
logging: ^1.2.0
markdown: ^7.2.2
@ -43,6 +48,7 @@ dependencies:
path_provider: ^2.1.3
provider: ^6.1.2
result_monad: ^2.3.2
riverpod_annotation: ^2.3.5
scrollable_positioned_list: ^0.3.8
shared_preferences: ^2.2.3
sqlite3: ^2.4.3
@ -52,18 +58,17 @@ dependencies:
url_launcher: ^6.3.0
uuid: ^4.4.2
video_player: ^2.9.1
flutter_svg: ^2.0.10+1
image_gallery_saver: ^2.0.3
wheel_chooser: ^1.1.2
http: any
http_parser: any
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^4.0.0
build_runner: ^2.4.10
build_runner: ^2.4.11
objectbox_generator: ^4.0.1
riverpod_generator: ^2.4.0
custom_lint: ^0.6.4
riverpod_lint: ^2.3.10
flutter:
uses-material-design: true