WIP - started implementing quick search

This commit is contained in:
Anton Stubenbord
2023-01-23 02:24:01 +01:00
parent 9bfb6aa661
commit f6ecbae6e8
50 changed files with 824 additions and 409 deletions

7
lib/constants.dart Normal file
View File

@@ -0,0 +1,7 @@
import 'package:device_info_plus/device_info_plus.dart';
import 'package:package_info_plus/package_info_plus.dart';
// Globally accessible variables which are definitely initialized after main().
late final PackageInfo packageInfo;
late final AndroidDeviceInfo? androidInfo;
late final IosDeviceInfo? iosInfo;

View File

@@ -8,7 +8,7 @@ import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/document_status_cubit.dart';
import 'package:paperless_mobile/core/model/document_processing_status.dart';
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
import 'package:paperless_mobile/util.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:web_socket_channel/io.dart';
abstract class StatusService {

View File

@@ -1,77 +0,0 @@
import 'dart:convert';
import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart';
import 'package:flutter/foundation.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/features/login/model/authentication_information.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
abstract class LocalVault {
Future<void> storeAuthenticationInformation(AuthenticationInformation auth);
Future<AuthenticationInformation?> loadAuthenticationInformation();
Future<ClientCertificate?> loadCertificate();
Future<bool> storeApplicationSettings(ApplicationSettingsState settings);
Future<ApplicationSettingsState?> loadApplicationSettings();
Future<void> clear();
}
class LocalVaultImpl implements LocalVault {
static const applicationSettingsKey = "applicationSettings";
static const authenticationKey = "authentication";
final EncryptedSharedPreferences sharedPreferences;
LocalVaultImpl(this.sharedPreferences);
@override
Future<void> storeAuthenticationInformation(
AuthenticationInformation auth,
) async {
await sharedPreferences.setString(
authenticationKey,
jsonEncode(auth.toJson()),
);
}
@override
Future<AuthenticationInformation?> loadAuthenticationInformation() async {
if ((await sharedPreferences.getString(authenticationKey)).isEmpty) {
return null;
}
return AuthenticationInformation.fromJson(
jsonDecode(await sharedPreferences.getString(authenticationKey)),
);
}
@override
Future<ClientCertificate?> loadCertificate() async {
return loadAuthenticationInformation()
.then((value) => value?.clientCertificate);
}
@override
Future<bool> storeApplicationSettings(ApplicationSettingsState settings) {
return sharedPreferences.setString(
applicationSettingsKey,
jsonEncode(settings.toJson()),
);
}
@override
Future<ApplicationSettingsState?> loadApplicationSettings() async {
final settings = await sharedPreferences.getString(applicationSettingsKey);
if (settings.isEmpty) {
return null;
}
return compute(
ApplicationSettingsState.fromJson,
jsonDecode(settings) as JSON,
);
}
@override
Future<void> clear() {
return sharedPreferences.clear();
}
}

View File

@@ -0,0 +1,13 @@
import 'package:flutter/material.dart';
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
import 'package:paperless_mobile/generated/l10n.dart';
String translateColorSchemeOption(
BuildContext context, ColorSchemeOption option) {
switch (option) {
case ColorSchemeOption.classic:
return S.of(context).colorSchemeOptionClassic;
case ColorSchemeOption.dynamic:
return S.of(context).colorSchemeOptionDynamic;
}
}

View File

@@ -1,6 +1,6 @@
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_mobile/features/login/bloc/authentication_state.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
extension AddressableHydratedStorage on Storage {
ApplicationSettingsState get settings {

View File

@@ -1,8 +1,10 @@
import 'dart:developer';
import 'dart:io';
import 'dart:typed_data';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:url_launcher/url_launcher.dart';
@@ -49,14 +51,18 @@ class DocumentDetailsCubit extends Cubit<DocumentDetailsState> {
}
Future<ResultType> openDocumentInSystemViewer() async {
final downloadDir = await FileService.temporaryDirectory;
final cacheDir = await FileService.temporaryDirectory;
final metaData = await _api.getMetaData(state.document);
final docBytes = await _api.download(state.document);
File f = File('${downloadDir.path}/${metaData.mediaFilename}');
f.createSync(recursive: true);
f.writeAsBytesSync(docBytes);
return OpenFilex.open(f.path, type: "application/pdf")
.then((value) => value.type);
final bytes = await _api.download(state.document);
final file = File('${cacheDir.path}/${metaData.mediaFilename}')
..createSync(recursive: true)
..writeAsBytesSync(bytes);
return OpenFilex.open(file.path, type: "application/pdf").then(
(value) => value.type,
);
}
void replaceDocument(DocumentModel document) {

View File

@@ -26,6 +26,7 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_widget.d
import 'package:paperless_mobile/features/labels/view/widgets/label_text.dart';
import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/format_helpers.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
@@ -556,15 +557,6 @@ class _DocumentDetailsPageState extends State<DocumentDetailsPage> {
);
}
static String formatBytes(int bytes, int decimals) {
if (bytes <= 0) return "0 B";
const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
var i = (log(bytes) / log(1024)).floor();
return ((bytes / pow(1024, i)).toStringAsFixed(decimals)) +
' ' +
suffixes[i];
}
Widget _buildSimilarDocumentsView() {
return const SimilarDocumentsView();
}

View File

@@ -7,7 +7,7 @@ import 'package:paperless_mobile/features/documents/view/widgets/documents_empty
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
import 'package:paperless_mobile/features/similar_documents/cubit/similar_documents_cubit.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/util.dart';
import 'package:paperless_mobile/constants.dart';
class SimilarDocumentsView extends StatefulWidget {
const SimilarDocumentsView({super.key});

View File

@@ -6,7 +6,7 @@ import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/util.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:provider/provider.dart';
class DocumentDownloadButton extends StatefulWidget {
@@ -48,20 +48,24 @@ class _DocumentDownloadButtonState extends State<DocumentDownloadButton> {
return;
}
setState(() => _isDownloadPending = true);
final service = context.read<PaperlessDocumentsApi>();
try {
final bytes =
await context.read<PaperlessDocumentsApi>().download(document);
final bytes = await service.download(document);
final meta = await service.getMetaData(document);
final Directory dir = await FileService.downloadsDirectory;
String filePath = "${dir.path}/${document.originalFileName}";
//TODO: Add replacement mechanism here (ask user if file should be replaced if exists)
await File(filePath).writeAsBytes(bytes);
String filePath = "${dir.path}/${meta.mediaFilename}";
final createdFile = File(filePath);
createdFile.createSync(recursive: true);
createdFile.writeAsBytesSync(bytes);
showSnackBar(context, S.of(context).documentDownloadSuccessMessage);
} on PaperlessServerException catch (error, stackTrace) {
showErrorMessage(context, error, stackTrace);
} catch (error) {
showGenericError(context, error);
} finally {
setState(() => _isDownloadPending = false);
if (mounted) {
setState(() => _isDownloadPending = false);
}
}
}
}

View File

@@ -0,0 +1,38 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_api/src/modules/documents_api/paperless_documents_api.dart';
import 'package:paperless_mobile/features/paged_document_view/documents_paging_mixin.dart';
import 'document_search_state.dart';
class DocumentSearchCubit extends HydratedCubit<DocumentSearchState>
with DocumentsPagingMixin {
DocumentSearchCubit(this.api) : super(const DocumentSearchState());
@override
final PaperlessDocumentsApi api;
Future<void> updateResults(String query) async {
await updateFilter(
filter: state.filter.copyWith(query: TextQuery.titleAndContent(query)),
);
emit(state.copyWith(searchHistory: [query, ...state.searchHistory]));
}
Future<void> updateSuggestions(String query) async {
final suggestions = await api.autocomplete(query);
emit(state.copyWith(suggestions: suggestions));
}
@override
DocumentSearchState? fromJson(Map<String, dynamic> json) {
return DocumentSearchState.fromJson(json);
}
@override
Map<String, dynamic>? toJson(DocumentSearchState state) {
return state.toJson();
}
}

View File

@@ -0,0 +1,75 @@
import 'package:equatable/equatable.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/features/paged_document_view/model/documents_paged_state.dart';
part 'document_search_state.g.dart';
@JsonSerializable(ignoreUnannotated: true)
class DocumentSearchState extends DocumentsPagedState {
@JsonKey()
final List<String> searchHistory;
final List<String> suggestions;
const DocumentSearchState({
this.searchHistory = const [],
this.suggestions = const [],
super.filter,
super.hasLoaded,
super.isLoading,
super.value,
});
@override
List<Object> get props => [
hasLoaded,
isLoading,
filter,
value,
searchHistory,
suggestions,
];
@override
DocumentSearchState copyWithPaged({
bool? hasLoaded,
bool? isLoading,
List<PagedSearchResult<DocumentModel>>? value,
DocumentFilter? filter,
}) {
return copyWith(
hasLoaded: hasLoaded,
isLoading: isLoading,
filter: filter,
value: value,
);
}
DocumentSearchState copyWith({
List<String>? searchHistory,
bool? hasLoaded,
bool? isLoading,
List<PagedSearchResult<DocumentModel>>? value,
DocumentFilter? filter,
List<String>? suggestions,
}) {
return DocumentSearchState(
value: value ?? this.value,
filter: filter ?? this.filter,
hasLoaded: hasLoaded ?? this.hasLoaded,
isLoading: isLoading ?? this.isLoading,
searchHistory: searchHistory ?? this.searchHistory,
suggestions: suggestions ?? this.suggestions,
);
}
factory DocumentSearchState.fromJson(Map<String, dynamic> json) =>
_$DocumentSearchStateFromJson(json);
Map<String, dynamic> toJson() => _$DocumentSearchStateToJson(this);
}
class

View File

@@ -0,0 +1,106 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/repository/provider/label_repositories_provider.dart';
import 'package:paperless_mobile/core/widgets/documents_list_loading_widget.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/document_search/cubit/document_search_cubit.dart';
import 'package:paperless_mobile/features/document_search/cubit/document_search_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/list/document_list_item.dart';
import 'package:provider/provider.dart';
class DocumentSearchDelegate extends SearchDelegate<DocumentModel> {
DocumentSearchDelegate({
required String hintText,
required super.searchFieldStyle,
}) : super(
searchFieldLabel: hintText,
keyboardType: TextInputType.text,
textInputAction: TextInputAction.search,
);
@override
Widget buildLeading(BuildContext context) => const BackButton();
@override
Widget buildSuggestions(BuildContext context) {
BlocBuilder<DocumentSearchCubit, DocumentSearchState>(
builder: (context, state) {
if (!state.hasLoaded && state.isLoading) {
return const DocumentsListLoadingWidget();
}
return ListView.builder(itemBuilder: (context, index) => ListTile(
title: Text(snapshot.data![index]),
onTap: () {
query = snapshot.data![index];
super.showResults(context);
},
),);
},
)
return FutureBuilder(
future: context.read<PaperlessDocumentsApi>().autocomplete(query),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
return ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, index) => ListTile(
title: Text(snapshot.data![index]),
onTap: () {
query = snapshot.data![index];
super.showResults(context);
},
),
);
},
);
}
@override
Widget buildResults(BuildContext context) {
return FutureBuilder(
future: context
.read<PaperlessDocumentsApi>()
.findAll(DocumentFilter(query: TextQuery.titleAndContent(query))),
builder: (context, snapshot) {
if (!snapshot.hasData) {
return const Center(
child: CircularProgressIndicator(),
);
}
final documents = snapshot.data!.results;
return ListView.builder(
itemBuilder: (context, index) => DocumentListItem(
document: documents[index],
onTap: (document) {
Navigator.push<DocumentModel?>(
context,
MaterialPageRoute(
builder: (context) => BlocProvider(
create: (context) => DocumentDetailsCubit(
context.read<PaperlessDocumentsApi>(),
document,
),
child: const LabelRepositoriesProvider(
child: DocumentDetailsPage(
isLabelClickable: false,
),
),
),
),
);
},
),
);
},
);
}
@override
List<Widget> buildActions(BuildContext context) => <Widget>[];
}

View File

@@ -8,7 +8,6 @@ import 'package:paperless_mobile/core/repository/label_repository.dart';
import 'package:paperless_mobile/core/repository/state/impl/correspondent_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
part 'document_upload_state.dart';
@@ -24,7 +23,6 @@ class DocumentUploadCubit extends Cubit<DocumentUploadState> {
final List<StreamSubscription> _subs = [];
DocumentUploadCubit({
required LocalVault localVault,
required PaperlessDocumentsApi documentApi,
required LabelRepository<Tag, TagRepositoryState> tagRepository,
required LabelRepository<Correspondent, CorrespondentRepositoryState>

View File

@@ -20,7 +20,7 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_fie
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/util.dart';
import 'package:paperless_mobile/constants.dart';
class DocumentUploadPreparationPage extends StatefulWidget {
final Uint8List fileBytes;

View File

@@ -21,7 +21,7 @@ import 'package:paperless_mobile/features/labels/tags/view/widgets/tags_form_fie
import 'package:paperless_mobile/features/labels/view/widgets/label_form_field.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/util.dart';
import 'package:paperless_mobile/constants.dart';
class DocumentEditPage extends StatefulWidget {
final FieldSuggestions suggestions;

View File

@@ -8,6 +8,7 @@ import 'package:paperless_mobile/core/repository/provider/label_repositories_pro
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/document_details/bloc/document_details_cubit.dart';
import 'package:paperless_mobile/features/document_details/view/pages/document_details_page.dart';
import 'package:paperless_mobile/features/document_search/document_search_delegate.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_cubit.dart';
import 'package:paperless_mobile/features/documents/bloc/documents_state.dart';
import 'package:paperless_mobile/features/documents/view/widgets/documents_empty_state.dart';
@@ -22,12 +23,12 @@ import 'package:paperless_mobile/features/login/bloc/authentication_cubit.dart';
import 'package:paperless_mobile/features/saved_view/cubit/saved_view_cubit.dart';
import 'package:paperless_mobile/features/saved_view/view/saved_view_selection_widget.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/util.dart';
import 'package:paperless_mobile/constants.dart';
class DocumentFilterIntent {
final DocumentFilter? filter;
@@ -148,9 +149,42 @@ class _DocumentsPageState extends State<DocumentsPage> {
builder: (context, state) {
if (state.selection.isEmpty) {
return AppBar(
title: Text(
"${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})",
title: TextField(
onTap: () => showSearch(
context: context,
delegate: DocumentSearchDelegate(
searchFieldStyle:
Theme.of(context).textTheme.bodyLarge,
hintText: "Search your documents",
),
),
readOnly: true,
decoration: InputDecoration(
hintText: "Search your documents",
hintStyle: Theme.of(context)
.textTheme
.bodyLarge
?.copyWith(
color: Theme.of(context)
.colorScheme
.onSurfaceVariant),
filled: true,
fillColor: Theme.of(context).colorScheme.surface,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(32),
borderSide: BorderSide.none,
),
prefixIcon: IconButton(
icon: const Icon(Icons.menu),
onPressed: () {
Scaffold.of(context).openDrawer();
},
),
),
),
// title: Text(
// "${S.of(context).documentsPageTitle} (${_formatDocumentCount(state.count)})",
// ),
actions: [
const SortDocumentsButton(),
BlocBuilder<ApplicationSettingsCubit,
@@ -182,6 +216,7 @@ class _DocumentsPageState extends State<DocumentsPage> {
? const LinearProgressIndicator()
: const SizedBox(height: 4.0),
),
automaticallyImplyLeading: false,
);
} else {
return AppBar(

View File

@@ -10,7 +10,7 @@ import 'package:paperless_mobile/features/edit_label/cubit/edit_label_cubit.dart
import 'package:paperless_mobile/features/edit_label/view/label_form.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/util.dart';
import 'package:paperless_mobile/constants.dart';
class EditLabelPage<T extends Label> extends StatelessWidget {
final T label;

View File

@@ -7,7 +7,7 @@ import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/util.dart';
import 'package:paperless_mobile/constants.dart';
class SubmitButtonConfig<T extends Label> {
final Widget icon;

View File

@@ -109,7 +109,6 @@ class _HomePageState extends State<HomePage> {
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: DocumentUploadCubit(
localVault: context.read(),
documentApi: context.read(),
tagRepository: context.read(),
correspondentRepository: context.read(),

View File

@@ -4,11 +4,13 @@ class RouteDescription {
final String label;
final Icon icon;
final Icon selectedIcon;
final Widget Function(Widget icon)? badgeBuilder;
RouteDescription({
required this.label,
required this.icon,
required this.selectedIcon,
this.badgeBuilder,
});
NavigationDestination toNavigationDestination() {
@@ -30,8 +32,8 @@ class RouteDescription {
BottomNavigationBarItem toBottomNavigationBarItem() {
return BottomNavigationBarItem(
label: label,
icon: icon,
activeIcon: selectedIcon,
icon: badgeBuilder?.call(icon) ?? icon,
activeIcon: badgeBuilder?.call(selectedIcon) ?? selectedIcon,
);
}
}

View File

@@ -12,7 +12,6 @@ import 'package:paperless_mobile/core/repository/state/impl/correspondent_reposi
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/storage_path_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/inbox/bloc/inbox_cubit.dart';
import 'package:paperless_mobile/features/inbox/view/pages/inbox_page.dart';
@@ -21,7 +20,7 @@ import 'package:paperless_mobile/features/settings/bloc/application_settings_cub
import 'package:paperless_mobile/features/settings/view/settings_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/util.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:url_launcher/link.dart';
import 'package:url_launcher/url_launcher_string.dart';
@@ -43,12 +42,9 @@ class AppDrawer extends StatefulWidget {
// }
class _AppDrawerState extends State<AppDrawer> {
late final Future<PackageInfo> _packageInfo;
@override
void initState() {
super.initState();
_packageInfo = PackageInfo.fromPlatform();
}
@override
@@ -120,162 +116,150 @@ class _AppDrawerState extends State<AppDrawer> {
bottomRight: Radius.circular(16.0),
),
),
child: Theme(
data: Theme.of(context).copyWith(
listTileTheme: ListTileThemeData(
tileColor: Colors.transparent,
),
),
child: ListView(
children: [
DrawerHeader(
padding: const EdgeInsets.only(
top: 8,
left: 8,
bottom: 0,
right: 8,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Image.asset(
'assets/logos/paperless_logo_white.png',
height: 32,
width: 32,
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
).paddedOnly(right: 8.0),
Text(
S.of(context).appTitleText,
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
],
),
Align(
alignment: Alignment.bottomRight,
child: BlocBuilder<PaperlessServerInformationCubit,
PaperlessServerInformationState>(
builder: (context, state) {
if (!state.isLoaded) {
return Container();
}
final info = state.information!;
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
ListTile(
contentPadding: EdgeInsets.zero,
dense: true,
title: Text(
S
.of(context)
.appDrawerHeaderLoggedInAsText +
(info.username ?? '?'),
style:
Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end,
maxLines: 1,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
state.information!.host ?? '',
style: Theme.of(context)
.textTheme
.bodyMedium,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end,
maxLines: 1,
),
Text(
'${S.of(context).serverInformationPaperlessVersionText} ${info.version} (API v${info.apiVersion})',
style: Theme.of(context)
.textTheme
.bodySmall,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end,
maxLines: 1,
),
],
),
isThreeLine: true,
),
],
);
},
child: ListView(
children: [
DrawerHeader(
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.secondaryContainer,
),
padding: const EdgeInsets.only(
top: 8,
left: 8,
bottom: 0,
right: 8,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Image.asset(
'assets/logos/paperless_logo_white.png',
height: 32,
width: 32,
color:
Theme.of(context).colorScheme.onPrimaryContainer,
).paddedOnly(right: 8.0),
Text(
S.of(context).appTitleText,
style: Theme.of(context)
.textTheme
.headlineSmall
?.copyWith(
color: Theme.of(context)
.colorScheme
.onPrimaryContainer,
),
),
],
),
Align(
alignment: Alignment.bottomRight,
child: BlocBuilder<PaperlessServerInformationCubit,
PaperlessServerInformationState>(
builder: (context, state) {
if (!state.isLoaded) {
return Container();
}
final info = state.information!;
return Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
ListTile(
contentPadding: EdgeInsets.zero,
dense: true,
title: Text(
S.of(context).appDrawerHeaderLoggedInAsText +
(info.username ?? '?'),
style: Theme.of(context).textTheme.bodyMedium,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end,
maxLines: 1,
),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
state.information!.host ?? '',
style: Theme.of(context)
.textTheme
.bodyMedium,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end,
maxLines: 1,
),
Text(
'${S.of(context).serverInformationPaperlessVersionText} ${info.version} (API v${info.apiVersion})',
style:
Theme.of(context).textTheme.bodySmall,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.end,
maxLines: 1,
),
],
),
isThreeLine: true,
),
],
);
},
),
],
),
],
),
),
...[
ListTile(
title: Text(S.of(context).bottomNavInboxPageLabel),
leading: const Icon(Icons.inbox),
onTap: () => _onOpenInbox(),
shape: listtTileShape,
),
ListTile(
leading: const Icon(Icons.settings),
shape: listtTileShape,
title: Text(
S.of(context).appDrawerSettingsLabel,
),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primaryContainer,
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: context.read<ApplicationSettingsCubit>(),
child: const SettingsPage(),
),
),
),
),
...[
ListTile(
title: Text(S.of(context).bottomNavInboxPageLabel),
leading: const Icon(Icons.inbox),
onTap: () => _onOpenInbox(),
shape: listtTileShape,
),
ListTile(
leading: const Icon(Icons.settings),
shape: listtTileShape,
title: Text(
S.of(context).appDrawerSettingsLabel,
),
onTap: () => Navigator.of(context).push(
MaterialPageRoute(
builder: (context) => BlocProvider.value(
value: context.read<ApplicationSettingsCubit>(),
child: const SettingsPage(),
),
),
),
),
const Divider(
indent: 16,
endIndent: 16,
),
ListTile(
leading: const Icon(Icons.bug_report),
title: Text(S.of(context).appDrawerReportBugLabel),
onTap: () {
launchUrlString(
'https://github.com/astubenbord/paperless-mobile/issues/new');
},
shape: listtTileShape,
),
ListTile(
title: Text(S.of(context).appDrawerAboutLabel),
leading: Icon(Icons.info_outline_rounded),
onTap: _onShowAboutDialog,
shape: listtTileShape,
),
ListTile(
leading: const Icon(Icons.logout),
title: Text(S.of(context).appDrawerLogoutLabel),
shape: listtTileShape,
onTap: () {
_onLogout();
},
)
],
const Divider(
indent: 16,
endIndent: 16,
),
ListTile(
leading: const Icon(Icons.bug_report),
title: Text(S.of(context).appDrawerReportBugLabel),
onTap: () {
launchUrlString(
'https://github.com/astubenbord/paperless-mobile/issues/new');
},
shape: listtTileShape,
),
ListTile(
title: Text(S.of(context).appDrawerAboutLabel),
leading: Icon(Icons.info_outline_rounded),
onTap: _onShowAboutDialog,
shape: listtTileShape,
),
ListTile(
leading: const Icon(Icons.logout),
title: Text(S.of(context).appDrawerLogoutLabel),
shape: listtTileShape,
onTap: () {
_onLogout();
},
)
],
),
],
),
),
),
@@ -285,7 +269,6 @@ class _AppDrawerState extends State<AppDrawer> {
void _onLogout() async {
try {
await context.read<AuthenticationCubit>().logout();
await context.read<LocalVault>().clear();
await context.read<ApplicationSettingsCubit>().clear();
await context.read<LabelRepository<Tag, TagRepositoryState>>().clear();
await context
@@ -354,15 +337,14 @@ class _AppDrawerState extends State<AppDrawer> {
);
}
Future<void> _onShowAboutDialog() async {
final snapshot = await _packageInfo;
void _onShowAboutDialog() {
showAboutDialog(
context: context,
applicationIcon: const ImageIcon(
AssetImage('assets/logos/paperless_logo_green.png'),
),
applicationName: 'Paperless Mobile',
applicationVersion: snapshot.version + '+' + snapshot.buildNumber,
applicationVersion: packageInfo.version + '+' + packageInfo.buildNumber,
children: [
Text(S.of(context).aboutDialogDevelopedByText('Anton Stubenbord')),
Link(

View File

@@ -14,7 +14,7 @@ import 'package:paperless_mobile/features/inbox/view/widgets/inbox_empty_widget.
import 'package:paperless_mobile/features/inbox/view/widgets/inbox_item.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/util.dart';
import 'package:paperless_mobile/constants.dart';
class InboxPage extends StatefulWidget {
const InboxPage({super.key});

View File

@@ -1,12 +1,9 @@
import 'package:local_auth/local_auth.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
class LocalAuthenticationService {
final LocalVault localStore;
final LocalAuthentication localAuthentication;
LocalAuthenticationService(
this.localStore,
this.localAuthentication,
);

View File

@@ -10,7 +10,7 @@ import 'package:paperless_mobile/features/login/view/widgets/form_fields/user_cr
import 'package:paperless_mobile/features/login/view/widgets/login_pages/server_connection_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/util.dart';
import 'package:paperless_mobile/constants.dart';
import 'widgets/never_scrollable_scroll_behavior.dart';
import 'widgets/login_pages/server_login_page.dart';

View File

@@ -6,7 +6,7 @@ import 'package:flutter_form_builder/flutter_form_builder.dart';
import 'package:paperless_mobile/extensions/flutter_extensions.dart';
import 'package:paperless_mobile/features/login/model/client_certificate.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/util.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:permission_handler/permission_handler.dart';
import 'obscured_input_text_form_field.dart';

View File

@@ -13,7 +13,7 @@ import 'package:paperless_mobile/features/saved_view/cubit/saved_view_state.dart
import 'package:paperless_mobile/features/saved_view/view/add_saved_view_page.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:paperless_mobile/util.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:shimmer/shimmer.dart';
class SavedViewSelectionWidget extends StatelessWidget {

View File

@@ -16,7 +16,6 @@ import 'package:paperless_mobile/core/repository/state/impl/correspondent_reposi
import 'package:paperless_mobile/core/repository/state/impl/document_type_repository_state.dart';
import 'package:paperless_mobile/core/repository/state/impl/tag_repository_state.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
import 'package:paperless_mobile/core/widgets/offline_banner.dart';
import 'package:paperless_mobile/features/document_upload/cubit/document_upload_cubit.dart';
import 'package:paperless_mobile/features/document_upload/view/document_upload_preparation_page.dart';
@@ -148,7 +147,6 @@ class _ScannerPageState extends State<ScannerPage>
builder: (_) => LabelRepositoriesProvider(
child: BlocProvider(
create: (context) => DocumentUploadCubit(
localVault: context.read<LocalVault>(),
documentApi: context.read<PaperlessDocumentsApi>(),
correspondentRepository: context.read<
LabelRepository<Correspondent,
@@ -266,7 +264,6 @@ class _ScannerPageState extends State<ScannerPage>
builder: (_) => LabelRepositoriesProvider(
child: BlocProvider(
create: (context) => DocumentUploadCubit(
localVault: context.read<LocalVault>(),
documentApi: context.read<PaperlessDocumentsApi>(),
correspondentRepository: context.read<
LabelRepository<Correspondent,

View File

@@ -1,7 +1,8 @@
import 'package:flutter/material.dart';
import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
class ApplicationSettingsCubit extends HydratedCubit<ApplicationSettingsState> {
@@ -27,17 +28,23 @@ class ApplicationSettingsCubit extends HydratedCubit<ApplicationSettingsState> {
}
}
Future<void> setThemeMode(ThemeMode? selectedMode) async {
void setThemeMode(ThemeMode? selectedMode) {
final updatedSettings = state.copyWith(preferredThemeMode: selectedMode);
_updateSettings(updatedSettings);
}
Future<void> setViewType(ViewType viewType) async {
void setViewType(ViewType viewType) {
final updatedSettings = state.copyWith(preferredViewType: viewType);
_updateSettings(updatedSettings);
}
Future<void> _updateSettings(ApplicationSettingsState settings) async {
void setColorSchemeOption(ColorSchemeOption schemeOption) {
final updatedSettings =
state.copyWith(preferredColorSchemeOption: schemeOption);
_updateSettings(updatedSettings);
}
void _updateSettings(ApplicationSettingsState settings) async {
emit(settings);
}

View File

@@ -2,7 +2,7 @@ import 'dart:io';
import 'package:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';
import 'package:paperless_mobile/core/type/types.dart';
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
import 'package:paperless_mobile/features/settings/model/view_type.dart';
part 'application_settings_state.g.dart';
@@ -13,22 +13,21 @@ part 'application_settings_state.g.dart';
@JsonSerializable()
class ApplicationSettingsState {
static final defaultSettings = ApplicationSettingsState(
isLocalAuthenticationEnabled: false,
preferredLocaleSubtag: Platform.localeName.split('_').first,
preferredThemeMode: ThemeMode.system,
preferredViewType: ViewType.list,
);
final bool isLocalAuthenticationEnabled;
final String preferredLocaleSubtag;
final ThemeMode preferredThemeMode;
final ViewType preferredViewType;
final ColorSchemeOption preferredColorSchemeOption;
ApplicationSettingsState({
required this.preferredLocaleSubtag,
required this.preferredThemeMode,
required this.isLocalAuthenticationEnabled,
required this.preferredViewType,
this.preferredThemeMode = ThemeMode.system,
this.isLocalAuthenticationEnabled = false,
this.preferredViewType = ViewType.list,
this.preferredColorSchemeOption = ColorSchemeOption.dynamic,
});
Map<String, dynamic> toJson() => _$ApplicationSettingsStateToJson(this);
@@ -40,6 +39,7 @@ class ApplicationSettingsState {
String? preferredLocaleSubtag,
ThemeMode? preferredThemeMode,
ViewType? preferredViewType,
ColorSchemeOption? preferredColorSchemeOption,
}) {
return ApplicationSettingsState(
isLocalAuthenticationEnabled:
@@ -48,6 +48,8 @@ class ApplicationSettingsState {
preferredLocaleSubtag ?? this.preferredLocaleSubtag,
preferredThemeMode: preferredThemeMode ?? this.preferredThemeMode,
preferredViewType: preferredViewType ?? this.preferredViewType,
preferredColorSchemeOption:
preferredColorSchemeOption ?? this.preferredColorSchemeOption,
);
}
}

View File

@@ -11,11 +11,16 @@ ApplicationSettingsState _$ApplicationSettingsStateFromJson(
ApplicationSettingsState(
preferredLocaleSubtag: json['preferredLocaleSubtag'] as String,
preferredThemeMode:
$enumDecode(_$ThemeModeEnumMap, json['preferredThemeMode']),
$enumDecodeNullable(_$ThemeModeEnumMap, json['preferredThemeMode']) ??
ThemeMode.system,
isLocalAuthenticationEnabled:
json['isLocalAuthenticationEnabled'] as bool,
json['isLocalAuthenticationEnabled'] as bool? ?? false,
preferredViewType:
$enumDecode(_$ViewTypeEnumMap, json['preferredViewType']),
$enumDecodeNullable(_$ViewTypeEnumMap, json['preferredViewType']) ??
ViewType.list,
preferredColorSchemeOption: $enumDecodeNullable(
_$ColorSchemeOptionEnumMap, json['preferredColorSchemeOption']) ??
ColorSchemeOption.dynamic,
);
Map<String, dynamic> _$ApplicationSettingsStateToJson(
@@ -25,6 +30,8 @@ Map<String, dynamic> _$ApplicationSettingsStateToJson(
'preferredLocaleSubtag': instance.preferredLocaleSubtag,
'preferredThemeMode': _$ThemeModeEnumMap[instance.preferredThemeMode]!,
'preferredViewType': _$ViewTypeEnumMap[instance.preferredViewType]!,
'preferredColorSchemeOption':
_$ColorSchemeOptionEnumMap[instance.preferredColorSchemeOption]!,
};
const _$ThemeModeEnumMap = {
@@ -37,3 +44,8 @@ const _$ViewTypeEnumMap = {
ViewType.grid: 'grid',
ViewType.list: 'list',
};
const _$ColorSchemeOptionEnumMap = {
ColorSchemeOption.classic: 'classic',
ColorSchemeOption.dynamic: 'dynamic',
};

View File

@@ -0,0 +1,4 @@
enum ColorSchemeOption {
classic,
dynamic;
}

View File

@@ -1,7 +1,12 @@
import 'dart:developer';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:paperless_mobile/features/settings/view/widgets/color_scheme_option_setting.dart';
import 'package:paperless_mobile/features/settings/view/widgets/language_selection_setting.dart';
import 'package:paperless_mobile/features/settings/view/widgets/theme_mode_setting.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/constants.dart';
class ApplicationSettingsPage extends StatelessWidget {
const ApplicationSettingsPage({super.key});
@@ -16,6 +21,7 @@ class ApplicationSettingsPage extends StatelessWidget {
children: const [
LanguageSelectionSetting(),
ThemeModeSetting(),
ColorSchemeOptionSetting(),
],
),
);

View File

@@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:paperless_mobile/features/settings/view/widgets/clear_storage_setting.dart';
import 'package:paperless_mobile/features/settings/view/widgets/clear_storage_settings.dart';
import 'package:paperless_mobile/generated/l10n.dart';
class StorageSettingsPage extends StatelessWidget {
@@ -13,7 +13,7 @@ class StorageSettingsPage extends StatelessWidget {
),
body: ListView(
children: const [
ClearStorageSetting(),
ClearCacheSetting(),
],
),
);

View File

@@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:provider/provider.dart';

View File

@@ -1,21 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm;
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:provider/provider.dart';
class ClearStorageSetting extends StatelessWidget {
const ClearStorageSetting({super.key});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text("Clear data"),
subtitle:
Text("Remove downloaded files, scans and clear the cache's content"),
onTap: () {
context.read<cm.CacheManager>().emptyCache();
FileService.clearUserData();
},
);
}
}

View File

@@ -0,0 +1,70 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_cache_manager/flutter_cache_manager.dart' as cm;
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/helpers/format_helpers.dart';
import 'package:paperless_mobile/helpers/message_helpers.dart';
import 'package:provider/provider.dart';
class ClearCacheSetting extends StatelessWidget {
const ClearCacheSetting({super.key});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text("Clear downloaded files"), //TODO: INTL
subtitle:
Text("Deletes all files downloaded from this app."), //TODO: INTL
onTap: () async {
final dir = await FileService.downloadsDirectory;
final deletedSize = _dirSize(dir);
await dir.delete(recursive: true);
// await context.read<cm.CacheManager>().emptyCache();
showSnackBar(
context,
"Downloads successfully cleared, removed $deletedSize.",
);
},
);
}
}
class ClearDownloadsSetting extends StatelessWidget {
const ClearDownloadsSetting({super.key});
@override
Widget build(BuildContext context) {
return ListTile(
title: Text("Clear downloads"), //TODO: INTL
subtitle: Text(
"Remove downloaded files, scans and clear the cache's content"), //TODO: INTL
onTap: () {
FileService.documentsDirectory;
FileService.downloadsDirectory;
context.read<cm.CacheManager>().emptyCache();
FileService.clearUserData();
//TODO: Show notification about clearing (include size?)
},
);
}
}
String _dirSize(Directory dir) {
int totalSize = 0;
try {
if (dir.existsSync()) {
dir
.listSync(recursive: true, followLinks: false)
.forEach((FileSystemEntity entity) {
if (entity is File) {
totalSize += entity.lengthSync();
}
});
}
} catch (e) {
print(e.toString());
}
return formatBytes(totalSize, 2);
}

View File

@@ -0,0 +1,86 @@
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/core/translation/color_scheme_option_localization_mapper.dart';
import 'package:paperless_mobile/core/widgets/hint_card.dart';
import 'package:paperless_mobile/features/login/services/authentication_service.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:provider/provider.dart';
class ColorSchemeOptionSetting extends StatelessWidget {
const ColorSchemeOptionSetting({super.key});
@override
Widget build(BuildContext context) {
return BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
builder: (context, settings) {
return ListTile(
title: Text(S.of(context).settingsPageColorSchemeSettingLabel),
subtitle: Text(
translateColorSchemeOption(
context,
settings.preferredColorSchemeOption,
),
),
onTap: () => showDialog(
context: context,
builder: (_) => RadioSettingsDialog<ColorSchemeOption>(
titleText: S.of(context).settingsPageColorSchemeSettingLabel,
descriptionText:
S.of(context).settingsPageColorSchemeSettingDialogDescription,
options: [
RadioOption(
value: ColorSchemeOption.classic,
label: translateColorSchemeOption(
context, ColorSchemeOption.classic),
),
RadioOption(
value: ColorSchemeOption.dynamic,
label: translateColorSchemeOption(
context,
ColorSchemeOption.dynamic,
),
),
],
footer: _isBelowAndroid12()
? HintCard(
hintText: S
.of(context)
.settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning,
hintIcon: Icons.warning_amber,
)
: null,
initialValue: context
.read<ApplicationSettingsCubit>()
.state
.preferredColorSchemeOption,
),
).then(
(value) {
if (value != null) {
context
.read<ApplicationSettingsCubit>()
.setColorSchemeOption(value);
}
},
),
);
},
);
}
bool _isBelowAndroid12() {
if (Platform.isAndroid) {
final int version =
int.tryParse(androidInfo!.version.release ?? '0') ?? 0;
return version < 12;
}
return false;
}
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
import 'package:paperless_mobile/generated/l10n.dart';
@@ -30,7 +30,7 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
onTap: () => showDialog(
context: context,
builder: (_) => RadioSettingsDialog<String>(
title: Text(S.of(context).settingsPageLanguageSettingLabel),
titleText: S.of(context).settingsPageLanguageSettingLabel,
options: [
RadioOption(
value: 'en',
@@ -54,8 +54,11 @@ class _LanguageSelectionSettingState extends State<LanguageSelectionSetting> {
.state
.preferredLocaleSubtag,
),
).then((value) =>
context.read<ApplicationSettingsCubit>().setLocale(value)),
).then((value) {
if (value != null) {
context.read<ApplicationSettingsCubit>().setLocale(value);
}
}),
);
},
);

View File

@@ -4,7 +4,9 @@ import 'package:paperless_mobile/generated/l10n.dart';
class RadioSettingsDialog<T> extends StatefulWidget {
final List<RadioOption<T>> options;
final T initialValue;
final Widget? title;
final String? titleText;
final String? descriptionText;
final Widget? footer;
final Widget? confirmButton;
final Widget? cancelButton;
@@ -12,9 +14,11 @@ class RadioSettingsDialog<T> extends StatefulWidget {
super.key,
required this.options,
required this.initialValue,
this.title,
this.titleText,
this.confirmButton,
this.cancelButton,
this.descriptionText,
this.footer,
});
@override
@@ -43,10 +47,16 @@ class _RadioSettingsDialogState<T> extends State<RadioSettingsDialog<T>> {
onPressed: () => Navigator.pop(context, _groupValue),
child: Text(S.of(context).genericActionOkLabel)),
],
title: widget.title,
title: widget.titleText != null ? Text(widget.titleText!) : null,
content: Column(
mainAxisSize: MainAxisSize.min,
children: widget.options.map(_buildOptionListTile).toList(),
children: [
if (widget.descriptionText != null)
Text(widget.descriptionText!,
style: Theme.of(context).textTheme.bodySmall),
...widget.options.map(_buildOptionListTile),
if (widget.footer != null) widget.footer!,
],
),
);
}

View File

@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/view/widgets/radio_settings_dialog.dart';
import 'package:paperless_mobile/generated/l10n.dart';
@@ -19,6 +19,11 @@ class ThemeModeSetting extends StatelessWidget {
onTap: () => showDialog<ThemeMode>(
context: context,
builder: (_) => RadioSettingsDialog<ThemeMode>(
titleText: S.of(context).settingsPageAppearanceSettingTitle,
initialValue: context
.read<ApplicationSettingsCubit>()
.state
.preferredThemeMode,
options: [
RadioOption(
value: ThemeMode.system,
@@ -38,14 +43,11 @@ class ThemeModeSetting extends StatelessWidget {
S.of(context).settingsPageAppearanceSettingDarkThemeLabel,
)
],
initialValue: context
.read<ApplicationSettingsCubit>()
.state
.preferredThemeMode,
title: Text(S.of(context).settingsPageAppearanceSettingTitle),
),
).then((value) {
return context.read<ApplicationSettingsCubit>().setThemeMode(value);
if (value != null) {
context.read<ApplicationSettingsCubit>().setThemeMode(value);
}
}),
);
},

View File

@@ -1,6 +1,15 @@
import 'dart:math';
String formatMaxCount(int? count, [int maxCount = 99]) {
if ((count ?? 0) > maxCount) {
return "$maxCount+";
}
return (count ?? 0).toString().padLeft(maxCount.toString().length);
}
String formatBytes(int bytes, int decimals) {
if (bytes <= 0) return "0 B";
const suffixes = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
var i = (log(bytes) / log(1024)).floor();
return ((bytes / pow(1024, i)).toStringAsFixed(decimals)) + ' ' + suffixes[i];
}

View File

@@ -611,5 +611,10 @@
"verifyIdentityPageTitle": "Ověř svou identitu",
"@verifyIdentityPageTitle": {},
"verifyIdentityPageVerifyIdentityButtonLabel": "Ověřit identitu",
"@verifyIdentityPageVerifyIdentityButtonLabel": {}
"@verifyIdentityPageVerifyIdentityButtonLabel": {},
"settingsPageColorSchemeSettingLabel": "Colors",
"colorSchemeOptionClassic": "Classic",
"colorSchemeOptionDznamic": "Dynamic",
"settingsPageColorSchemeSettingDialogDescription": "Choose between the classic color scheme in Paperless inspired green or use the dynamic color scheme based on your system theme.",
"settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation."
}

View File

@@ -611,5 +611,10 @@
"verifyIdentityPageTitle": "Verifiziere deine Identität",
"@verifyIdentityPageTitle": {},
"verifyIdentityPageVerifyIdentityButtonLabel": "Identität verifizieren",
"@verifyIdentityPageVerifyIdentityButtonLabel": {}
"@verifyIdentityPageVerifyIdentityButtonLabel": {},
"settingsPageColorSchemeSettingLabel": "Colors",
"colorSchemeOptionClassic": "Classic",
"colorSchemeOptionDznamic": "Dynamic",
"settingsPageColorSchemeSettingDialogDescription": "Choose between the classic color scheme in Paperless inspired green or use the dynamic color scheme based on your system theme.",
"settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation."
}

View File

@@ -611,5 +611,10 @@
"verifyIdentityPageTitle": "Verify your identity",
"@verifyIdentityPageTitle": {},
"verifyIdentityPageVerifyIdentityButtonLabel": "Verify Identity",
"@verifyIdentityPageVerifyIdentityButtonLabel": {}
"@verifyIdentityPageVerifyIdentityButtonLabel": {},
"settingsPageColorSchemeSettingLabel": "Colors",
"colorSchemeOptionClassic": "Classic",
"colorSchemeOptionDynamic": "Dynamic",
"settingsPageColorSchemeSettingDialogDescription": "Choose between a classic color scheme inspired by a traditional Paperless green or use the dynamic color scheme based on your system theme.",
"settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Selecting the 'Dynamic' option might not have any effect depending on your OS implementation."
}

View File

@@ -611,5 +611,10 @@
"verifyIdentityPageTitle": "Kimliğinizi doğrulayın",
"@verifyIdentityPageTitle": {},
"verifyIdentityPageVerifyIdentityButtonLabel": "Kimliği Doğrula",
"@verifyIdentityPageVerifyIdentityButtonLabel": {}
"@verifyIdentityPageVerifyIdentityButtonLabel": {},
"settingsPageColorSchemeSettingLabel": "Colors",
"colorSchemeOptionClassic": "Classic",
"colorSchemeOptionDznamic": "Dynamic",
"settingsPageColorSchemeSettingDialogDescription": "Choose between the classic color scheme in Paperless inspired green or use the dynamic color scheme based on your system theme.",
"settingsPageColorSchemeSettingDynamicThemeingVersionMismatchWarning": "Dynamic theming is only supported for devices running Android 12 and above. Using the 'dynamic' option might not have any effect depending on your OS implementation."
}

View File

@@ -1,6 +1,8 @@
import 'dart:developer';
import 'dart:io';
import 'package:connectivity_plus/connectivity_plus.dart';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:encrypted_shared_preferences/encrypted_shared_preferences.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
@@ -12,6 +14,7 @@ import 'package:hydrated_bloc/hydrated_bloc.dart';
import 'package:intl/date_symbol_data_local.dart';
import 'package:intl/intl_standalone.dart';
import 'package:local_auth/local_auth.dart';
import 'package:package_info_plus/package_info_plus.dart';
import 'package:paperless_api/paperless_api.dart';
import 'package:paperless_mobile/core/bloc/bloc_changes_observer.dart';
import 'package:paperless_mobile/core/bloc/connectivity_cubit.dart';
@@ -33,7 +36,6 @@ import 'package:paperless_mobile/core/security/session_manager.dart';
import 'package:paperless_mobile/core/service/connectivity_status_service.dart';
import 'package:paperless_mobile/core/service/dio_file_service.dart';
import 'package:paperless_mobile/core/service/file_service.dart';
import 'package:paperless_mobile/core/store/local_vault.dart';
import 'package:paperless_mobile/features/app_intro/application_intro_slideshow.dart';
import 'package:paperless_mobile/features/home/view/home_page.dart';
import 'package:paperless_mobile/features/home/view/widget/verify_identity_page.dart';
@@ -43,29 +45,36 @@ import 'package:paperless_mobile/features/login/services/authentication_service.
import 'package:paperless_mobile/features/login/view/login_page.dart';
import 'package:paperless_mobile/features/notifications/services/local_notification_service.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_cubit.dart';
import 'package:paperless_mobile/features/settings/model/application_settings_state.dart';
import 'package:paperless_mobile/features/settings/bloc/application_settings_state.dart';
import 'package:paperless_mobile/features/sharing/share_intent_queue.dart';
import 'package:paperless_mobile/features/tasks/cubit/task_status_cubit.dart';
import 'package:paperless_mobile/generated/l10n.dart';
import 'package:paperless_mobile/theme.dart';
import 'package:paperless_mobile/constants.dart';
import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart';
import 'package:receive_sharing_intent/receive_sharing_intent.dart';
import 'package:dynamic_color/dynamic_color.dart';
void main() async {
Bloc.observer = BlocChangesObserver();
final widgetsBinding = WidgetsFlutterBinding.ensureInitialized();
await findSystemLocale();
packageInfo = await PackageInfo.fromPlatform();
if (Platform.isAndroid) {
androidInfo = await DeviceInfoPlugin().androidInfo;
}
if (Platform.isIOS) {
iosInfo = await DeviceInfoPlugin().iosInfo;
}
// Initialize External dependencies
final connectivity = Connectivity();
final encryptedSharedPreferences = EncryptedSharedPreferences();
final localAuthentication = LocalAuthentication();
// Initialize other utility classes
final connectivityStatusService = ConnectivityStatusServiceImpl(connectivity);
final localVault = LocalVaultImpl(encryptedSharedPreferences);
final localAuthService =
LocalAuthenticationService(localVault, localAuthentication);
final localAuthService = LocalAuthenticationService(localAuthentication);
final hiveDir = await getApplicationDocumentsDirectory();
HydratedBloc.storage = await HydratedStorage.build(
@@ -152,7 +161,6 @@ void main() async {
),
),
),
Provider<LocalVault>.value(value: localVault),
Provider<ConnectivityStatusService>.value(
value: connectivityStatusService,
),
@@ -207,22 +215,6 @@ class PaperlessMobileEntrypoint extends StatefulWidget {
}
class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
final _lightTheme = ThemeData.from(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.lightGreen,
brightness: Brightness.light,
),
useMaterial3: true,
);
final _darkTheme = ThemeData.from(
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.lightGreen,
brightness: Brightness.dark,
),
useMaterial3: true,
);
@override
Widget build(BuildContext context) {
return MultiBlocProvider(
@@ -235,52 +227,36 @@ class _PaperlessMobileEntrypointState extends State<PaperlessMobileEntrypoint> {
],
child: BlocBuilder<ApplicationSettingsCubit, ApplicationSettingsState>(
builder: (context, settings) {
return MaterialApp(
debugShowCheckedModeBanner: true,
title: "Paperless Mobile",
theme: _lightTheme.copyWith(
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
return DynamicColorBuilder(
builder: (lightDynamic, darkDynamic) {
return MaterialApp(
debugShowCheckedModeBanner: true,
title: "Paperless Mobile",
theme: buildTheme(
brightness: Brightness.light,
dynamicScheme: lightDynamic,
preferredColorScheme: settings.preferredColorSchemeOption,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 16.0,
darkTheme: buildTheme(
brightness: Brightness.dark,
dynamicScheme: darkDynamic,
preferredColorScheme: settings.preferredColorSchemeOption,
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
listTileTheme: const ListTileThemeData(
tileColor: Colors.transparent,
),
),
darkTheme: _darkTheme.copyWith(
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
themeMode: settings.preferredThemeMode,
supportedLocales: S.delegate.supportedLocales,
locale: Locale.fromSubtags(
languageCode: settings.preferredLocaleSubtag,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 16.0,
),
),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
listTileTheme: const ListTileThemeData(
tileColor: Colors.transparent,
),
),
themeMode: settings.preferredThemeMode,
supportedLocales: S.delegate.supportedLocales,
locale: Locale.fromSubtags(
languageCode: settings.preferredLocaleSubtag,
),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
FormBuilderLocalizations.delegate,
],
home: const AuthenticationWrapper(),
localizationsDelegates: const [
S.delegate,
GlobalMaterialLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
FormBuilderLocalizations.delegate,
],
home: const AuthenticationWrapper(),
);
},
);
},
),

47
lib/theme.dart Normal file
View File

@@ -0,0 +1,47 @@
import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter/material.dart';
import 'package:paperless_mobile/features/settings/model/color_scheme_option.dart';
const _classicThemeColorSeed = Colors.lightGreen;
const _defaultListTileTheme = ListTileThemeData(
tileColor: Colors.transparent,
);
final _defaultInputDecorationTheme = InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(16),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16.0,
vertical: 16.0,
),
);
ThemeData buildTheme({
required Brightness brightness,
required ColorSchemeOption preferredColorScheme,
ColorScheme? dynamicScheme,
}) {
final classicScheme = ColorScheme.fromSeed(
seedColor: _classicThemeColorSeed,
brightness: brightness,
);
late ColorScheme colorScheme;
switch (preferredColorScheme) {
case ColorSchemeOption.classic:
colorScheme = classicScheme;
break;
case ColorSchemeOption.dynamic:
colorScheme = dynamicScheme ?? classicScheme;
break;
}
return ThemeData.from(
colorScheme: colorScheme.harmonized(),
useMaterial3: true,
).copyWith(
inputDecorationTheme: _defaultInputDecorationTheme,
listTileTheme: _defaultListTileTheme,
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
);
}

View File

@@ -1 +0,0 @@

View File

@@ -433,6 +433,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.1.0"
dynamic_color:
dependency: "direct main"
description:
name: dynamic_color
sha256: "37a15576f5a0bfd5555b613cf20ea3bd379607cf88d457374a16032f4e942174"
url: "https://pub.dev"
source: hosted
version: "1.5.4"
edge_detection:
dependency: "direct main"
description:

View File

@@ -87,6 +87,7 @@ dependencies:
flutter_staggered_grid_view: ^0.6.2
responsive_builder: ^0.4.3
open_filex: ^4.3.2
dynamic_color: ^1.5.4
dev_dependencies:
integration_test: