Skip to content

Commit

Permalink
feat(filename): Add possibility to customize the file name of the loc…
Browse files Browse the repository at this point in the history
…al todo file while initialization of the app #35
  • Loading branch information
tmaegel committed Mar 29, 2024
1 parent b8cd03e commit 86193dc
Show file tree
Hide file tree
Showing 12 changed files with 300 additions and 141 deletions.
4 changes: 2 additions & 2 deletions README.md
Expand Up @@ -32,7 +32,7 @@ This application is under active development and will continue to be modified an
- Completely customizable filters
- Ordering
- Filter by project, context, priorities and completion
- Custom path of todo file on device
- Custom path and filename of todo file on device

## Planned features

Expand All @@ -43,7 +43,7 @@ This application is under active development and will continue to be modified an
- [ ] Build and publish to Microsoft Store (Windows)
- [ ] Add language localization (e.g. english, german)
- [ ] Custom path of todo file on remote side
- [ ] Custom todo filename on device and remote side
- [ ] Custom todo filename remote side
- [ ] Import existing todos from file
- [ ] Export todos to file
- [ ] Archiving of completed todos (done.txt)
Expand Down
13 changes: 7 additions & 6 deletions integration_test/login/login_integration_test.dart
Expand Up @@ -61,13 +61,13 @@ void main() async {
await tester.pumpAndSettle();
await tester.tap(find.text('Settings'));
await tester.pumpAndSettle();
await tester.scrollUntilVisible(find.text('Reset and logout'), 500);
await tester.tap(find.text('Reset and logout'));
await tester.scrollUntilVisible(find.text('Reinitialization'), 500);
await tester.tap(find.text('Reinitialization'));
await tester.pumpAndSettle();
await tester.tap(
find.descendant(
of: find.byType(AlertDialog),
matching: find.text('Logout'),
matching: find.text('Reninitialize'),
),
);
await tester.pumpAndSettle(const Duration(milliseconds: 5000));
Expand Down Expand Up @@ -143,13 +143,14 @@ void main() async {
await tester.pumpAndSettle();
await tester.tap(find.text('Settings'));
await tester.pumpAndSettle();
await tester.scrollUntilVisible(find.text('Reset and logout'), 500);
await tester.tap(find.text('Reset and logout'));

await tester.scrollUntilVisible(find.text('Reinitialization'), 500);
await tester.tap(find.text('Reinitialization'));
await tester.pumpAndSettle();
await tester.tap(
find.descendant(
of: find.byType(AlertDialog),
matching: find.text('Logout'),
matching: find.text('Reninitialize'),
),
);
await tester.pumpAndSettle(const Duration(milliseconds: 5000));
Expand Down
4 changes: 2 additions & 2 deletions lib/config/theme/theme.dart
Expand Up @@ -33,7 +33,7 @@ final ThemeData lightTheme = light.copyWith(
listTileTheme: light.listTileTheme.copyWith(
selectedColor: light.textTheme.bodySmall?.color,
selectedTileColor: light.hoverColor,
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 0.0),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
Expand Down Expand Up @@ -87,7 +87,7 @@ final ThemeData darkTheme = dark.copyWith(
listTileTheme: dark.listTileTheme.copyWith(
selectedColor: dark.textTheme.bodySmall?.color,
selectedTileColor: dark.hoverColor,
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0, vertical: 0.0),
contentPadding: const EdgeInsets.symmetric(horizontal: 12.0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
Expand Down
3 changes: 3 additions & 0 deletions lib/constants/app.dart
Expand Up @@ -4,3 +4,6 @@ const String version = '0.7.1';

/// https://m3.material.io/foundations/layout/applying-layout/window-size-classes
const int maxScreenWidthCompact = 600;

const String defaultTodoFilename = 'todo.txt';
const String defaultDoneFilename = 'done.txt';
2 changes: 1 addition & 1 deletion lib/main.dart
Expand Up @@ -304,7 +304,7 @@ class CoreApp extends StatelessWidget {
LoginState loginState, TodoFileState todoFileState) {
late TodoListApi api;
File todoFile = File(
'${todoFileState.localPath}${Platform.pathSeparator}${todoFileState.todoFilename}');
'${todoFileState.localPath}${Platform.pathSeparator}${todoFileState.localFilename}');
log.info('Use todo file ${todoFile.path}');
switch (loginState) {
case LoginLocal():
Expand Down
1 change: 0 additions & 1 deletion lib/presentation/app_info/pages/app_details_page.dart
Expand Up @@ -28,7 +28,6 @@ class AppInfoView extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 8.0),
children: [
ListTile(
// Overwrite the default in themeData
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
leading: const Icon(Icons.update),
title: const Text('Version'),
Expand Down
2 changes: 1 addition & 1 deletion lib/presentation/licenses/pages/licenses_page.dart
Expand Up @@ -20,7 +20,7 @@ class LicenseListView extends StatelessWidget {
@override
Widget build(BuildContext context) {
return ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
padding: const EdgeInsets.symmetric(horizontal: 8.0),
itemCount: ossLicenses.length,
itemBuilder: (BuildContext context, int index) {
Package package = ossLicenses[index];
Expand Down
167 changes: 120 additions & 47 deletions lib/presentation/login/pages/login_page.dart
Expand Up @@ -3,12 +3,13 @@ import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:ntodotxt/common_widgets/app_bar.dart';
import 'package:ntodotxt/common_widgets/info_dialog.dart';
import 'package:ntodotxt/constants/app.dart';
import 'package:ntodotxt/misc.dart';
import 'package:ntodotxt/presentation/login/states/login_cubit.dart';
import 'package:ntodotxt/presentation/todo_file/todo_file_cubit.dart';
import 'package:ntodotxt/presentation/todo_file/todo_file_state.dart';
import 'package:path_provider/path_provider.dart';
import 'package:permission_handler/permission_handler.dart';

class LocalLoginView extends StatefulWidget {
Expand All @@ -26,34 +27,41 @@ class _LocalLoginViewState extends State<LocalLoginView> {
return Stack(
children: [
Scaffold(
appBar: AppBar(
titleSpacing: 0.0,
title: const Text('Local'),
),
appBar: const MainAppBar(title: 'Local'),
body: ListView(
padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
children: const [
LocalPathInput(),
padding: const EdgeInsets.symmetric(horizontal: 8.0),
children: [
ListTile(
title: Text(
'Local storage',
style: Theme.of(context).textTheme.titleSmall,
),
),
const LocalFilenameInput(),
const LocalPathInput(),
],
),
floatingActionButton: BlocBuilder<TodoFileCubit, TodoFileState>(
builder: (BuildContext context, TodoFileState state) {
return FloatingActionButton.extended(
heroTag: 'localUsage',
icon: const Icon(Icons.done),
label: const Text('Apply'),
tooltip: 'Apply',
onPressed: () async {
try {
setState(() => loading = true);
await context.read<LoginCubit>().loginLocal(
todoFile: File(
'${state.localPath}${Platform.pathSeparator}${state.todoFilename}'),
);
} finally {
setState(() => loading = false);
}
},
return Visibility(
visible: state is! TodoFileLoading,
child: FloatingActionButton.extended(
heroTag: 'localUsage',
icon: const Icon(Icons.done),
label: const Text('Apply'),
tooltip: 'Apply',
onPressed: () async {
try {
setState(() => loading = true);
await context.read<LoginCubit>().loginLocal(
todoFile: File(
'${state.localPath}${Platform.pathSeparator}${state.localFilename}'),
);
} finally {
setState(() => loading = false);
}
},
),
);
},
),
Expand Down Expand Up @@ -130,20 +138,19 @@ class _WebDAVLoginViewState extends State<WebDAVLoginView> {
child: Stack(
children: [
Scaffold(
appBar: AppBar(
titleSpacing: 0.0,
title: const Text('WebDAV'),
),
appBar: const MainAppBar(title: 'WebDAV'),
body: Form(
key: formKey,
child: ListView(
padding:
const EdgeInsets.symmetric(horizontal: 8.0, vertical: 8.0),
padding: const EdgeInsets.symmetric(horizontal: 8.0),
children: [
const LocalPathInput(),
const Divider(),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
title: Text(
'Remote storage',
style: Theme.of(context).textTheme.titleSmall,
),
),
ListTile(
leading: const Icon(Icons.dns),
title: TextFormField(
controller: serverTextFieldController,
Expand Down Expand Up @@ -175,7 +182,6 @@ class _WebDAVLoginViewState extends State<WebDAVLoginView> {
),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
leading: const Icon(Icons.http),
title: TextFormField(
controller: baseUrlTextFieldController,
Expand All @@ -198,7 +204,6 @@ class _WebDAVLoginViewState extends State<WebDAVLoginView> {
),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
leading: const Icon(Icons.person),
title: TextFormField(
controller: usernameTextFieldController,
Expand All @@ -221,7 +226,6 @@ class _WebDAVLoginViewState extends State<WebDAVLoginView> {
),
),
ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
leading: const Icon(Icons.password),
title: TextFormField(
controller: passwordTextFieldController,
Expand All @@ -244,6 +248,15 @@ class _WebDAVLoginViewState extends State<WebDAVLoginView> {
},
),
),
const Divider(),
ListTile(
title: Text(
'Local storage',
style: Theme.of(context).textTheme.titleSmall,
),
),
const LocalFilenameInput(),
const LocalPathInput(),
],
),
),
Expand All @@ -262,7 +275,7 @@ class _WebDAVLoginViewState extends State<WebDAVLoginView> {
setState(() => loading = true);
await context.read<LoginCubit>().loginWebDAV(
todoFile: File(
'${state.localPath}${Platform.pathSeparator}${state.todoFilename}'),
'${state.localPath}${Platform.pathSeparator}${state.localFilename}'),
server: serverAddr,
baseUrl: baseUrl,
username: username,
Expand Down Expand Up @@ -302,15 +315,12 @@ class LocalPathInput extends StatelessWidget {
previousState.localPath != state.localPath,
builder: (BuildContext context, TodoFileState state) {
return ListTile(
contentPadding: const EdgeInsets.symmetric(horizontal: 8.0),
leading: const Icon(Icons.folder),
title: Text(
'Local path',
style: state.localPath == null
? null
: Theme.of(context).textTheme.bodySmall,
style: Theme.of(context).textTheme.bodySmall,
),
subtitle: state.localPath != null ? Text(state.localPath!) : null,
subtitle: Text(state.localPath),
trailing: IconButton(
icon: const Icon(Icons.help_outline),
onPressed: () => InfoDialog.dialog(
Expand All @@ -324,15 +334,13 @@ Use this option if it's important to you where your todos are stored on your dev
onTap: () async {
if (!PlatformInfo.isAppOS ||
await Permission.manageExternalStorage.request().isGranted) {
String fallbackDirectory =
(await getApplicationCacheDirectory()).path;
String? selectedDirectory =
await FilePicker.platform.getDirectoryPath();
if (context.mounted) {
// If user canceled the directory picker use app cache directory as fallback.
await context.read<TodoFileCubit>().updateLocalPath(
selectedDirectory ??
(state.localPath ?? fallbackDirectory));
await context
.read<TodoFileCubit>()
.saveLocalPath(selectedDirectory ?? state.localPath);
}
}
},
Expand All @@ -341,3 +349,68 @@ Use this option if it's important to you where your todos are stored on your dev
);
}
}

class LocalFilenameInput extends StatefulWidget {
const LocalFilenameInput({super.key});

@override
State<LocalFilenameInput> createState() => _LocalFilenameInputState();
}

class _LocalFilenameInputState extends State<LocalFilenameInput> {
late TextEditingController controller;

@override
void initState() {
super.initState();
controller = TextEditingController();
}

@override
void dispose() {
controller.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return BlocBuilder<TodoFileCubit, TodoFileState>(
builder: (BuildContext context, TodoFileState state) {
controller.text = state.localFilename;
return ListTile(
leading: const Icon(Icons.description),
title: TextFormField(
controller: controller,
style: Theme.of(context).textTheme.bodyMedium,
decoration: const InputDecoration(
labelText: 'Local filename',
hintText: defaultTodoFilename,
),
onChanged: (String value) =>
context.read<TodoFileCubit>().updateLocalFilename(value),
),
trailing: state is! TodoFileLoading
? null
: IconButton(
icon: const Icon(Icons.save),
onPressed: () async {
if (controller.text.isEmpty) {
SnackBarHandler.info(
context,
'Empty local filename is not allowed. Using default one.',
);
await context
.read<TodoFileCubit>()
.saveLocalFilename(defaultTodoFilename);
} else {
await context
.read<TodoFileCubit>()
.saveLocalFilename(controller.text);
}
},
),
);
},
);
}
}

0 comments on commit 86193dc

Please sign in to comment.