Initial commit

This commit is contained in:
Anton Stubenbord
2022-10-30 14:15:37 +01:00
commit cb797df7d2
272 changed files with 16278 additions and 0 deletions

View File

@@ -0,0 +1,15 @@
import 'package:flutter/material.dart';
class ComingSoon extends StatelessWidget {
const ComingSoon({super.key});
@override
Widget build(BuildContext context) {
return Center(
child: Text(
"Coming Soon\u2122",
style: Theme.of(context).textTheme.titleLarge,
),
);
}
}

View File

@@ -0,0 +1,70 @@
import 'dart:math' as math;
import 'dart:ui';
import 'package:flutter/material.dart';
class ElevatedConfirmationButton extends StatefulWidget {
factory ElevatedConfirmationButton.icon(BuildContext context,
{required void Function() onPressed, required Icon icon, required Widget label}) {
final double scale = MediaQuery.maybeOf(context)?.textScaleFactor ?? 1;
final double gap = scale <= 1 ? 8 : lerpDouble(8, 4, math.min(scale - 1, 1))!;
return ElevatedConfirmationButton(
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[icon, SizedBox(width: gap), Flexible(child: label)],
),
onPressed: onPressed,
);
}
const ElevatedConfirmationButton({
Key? key,
this.color,
required this.onPressed,
required this.child,
this.confirmWidget = const Text("Confirm?"),
}) : super(key: key);
final Color? color;
final void Function()? onPressed;
final Widget child;
final Widget confirmWidget;
@override
State<ElevatedConfirmationButton> createState() => _ElevatedConfirmationButtonState();
}
class _ElevatedConfirmationButtonState extends State<ElevatedConfirmationButton> {
bool _clickedOnce = false;
double? _originalWidth;
final GlobalKey _originalWidgetKey = GlobalKey();
@override
Widget build(BuildContext context) {
if (!_clickedOnce) {
return ElevatedButton(
key: _originalWidgetKey,
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(widget.color),
),
onPressed: () {
_originalWidth =
(_originalWidgetKey.currentContext?.findRenderObject() as RenderBox).size.width;
setState(() => _clickedOnce = true);
},
child: widget.child,
);
} else {
return Builder(builder: (context) {
return SizedBox(
width: _originalWidth,
child: ElevatedButton(
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(widget.color),
),
onPressed: widget.onPressed,
child: widget.confirmWidget,
),
);
});
}
}
}

View File

@@ -0,0 +1,86 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
class DocumentsListLoadingWidget extends StatelessWidget {
static const tags = [" ", " ", " "];
static const titleLengths = <double>[double.infinity, 150.0, 200.0];
static const correspondentLengths = <double>[200.0, 300.0, 150.0];
static const fontSize = 16.0;
const DocumentsListLoadingWidget({super.key});
@override
Widget build(BuildContext context) {
return SizedBox(
height: MediaQuery.of(context).size.height,
width: double.infinity,
child: Column(
mainAxisSize: MainAxisSize.max,
children: <Widget>[
Expanded(
child: Shimmer.fromColors(
baseColor: Theme.of(context).brightness == Brightness.light
? Colors.grey[300]!
: Colors.grey[900]!,
highlightColor: Theme.of(context).brightness == Brightness.light
? Colors.grey[100]!
: Colors.grey[600]!,
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
itemBuilder: (context, index) {
final r = Random(index);
final tagCount = r.nextInt(tags.length + 1);
final correspondentLength = correspondentLengths[
r.nextInt(correspondentLengths.length - 1)];
final titleLength =
titleLengths[r.nextInt(titleLengths.length - 1)];
return ListTile(
isThreeLine: true,
leading: Container(
color: Colors.white,
height: 50,
width: 50,
),
title: Container(
padding: const EdgeInsets.symmetric(vertical: 2.0),
width: correspondentLength,
height: fontSize,
color: Colors.white,
),
subtitle: Padding(
padding: const EdgeInsets.symmetric(vertical: 2.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Container(
padding: const EdgeInsets.symmetric(vertical: 2.0),
height: fontSize,
width: titleLength,
color: Colors.white,
),
Wrap(
spacing: 2.0,
children: List.generate(
tagCount,
(index) => Chip(
label: Text(tags[r.nextInt(tags.length)]),
),
),
),
],
),
),
);
},
itemCount: 25,
),
),
),
],
),
);
}
}

View File

@@ -0,0 +1,45 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class EmptyState extends StatelessWidget {
final String title;
final String subtitle;
final Widget? bottomChild;
const EmptyState({
Key? key,
required this.title,
required this.subtitle,
this.bottomChild,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final size = MediaQuery.of(context).size;
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: size.height / 3,
width: size.width / 3,
child: SvgPicture.asset("assets/images/empty-state.svg"),
),
Column(
children: [
Text(
title,
style: Theme.of(context).textTheme.titleLarge?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
subtitle,
style: Theme.of(context).textTheme.titleMedium,
),
],
),
if (bottomChild != null) ...[bottomChild!] else ...[]
],
);
}
}

View File

@@ -0,0 +1,215 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
@immutable
class ExpandableFloatingActionButton extends StatefulWidget {
const ExpandableFloatingActionButton({
super.key,
this.initialOpen,
required this.distance,
required this.children,
});
final bool? initialOpen;
final double distance;
final List<Widget> children;
@override
State<ExpandableFloatingActionButton> createState() =>
_ExpandableFloatingActionButtonState();
}
class _ExpandableFloatingActionButtonState
extends State<ExpandableFloatingActionButton>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _expandAnimation;
bool _open = false;
@override
void initState() {
super.initState();
_open = widget.initialOpen ?? false;
_controller = AnimationController(
value: _open ? 1.0 : 0.0,
duration: const Duration(milliseconds: 250),
vsync: this,
);
_expandAnimation = CurvedAnimation(
curve: Curves.fastOutSlowIn,
reverseCurve: Curves.easeOutQuad,
parent: _controller,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _toggle() {
setState(() {
_open = !_open;
if (_open) {
_controller.forward();
} else {
_controller.reverse();
}
});
}
@override
Widget build(BuildContext context) {
return SizedBox.expand(
child: Stack(
alignment: Alignment.bottomRight,
clipBehavior: Clip.none,
children: [
_buildTapToCloseFab(),
..._buildExpandingActionButtons(),
_buildTapToOpenFab(),
],
),
);
}
Widget _buildTapToCloseFab() {
return SizedBox(
width: 56.0,
height: 56.0,
child: Center(
child: Material(
shape: const CircleBorder(),
clipBehavior: Clip.antiAlias,
elevation: 4.0,
child: InkWell(
onTap: _toggle,
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Icon(
Icons.close,
color: Theme.of(context).primaryColor,
),
),
),
),
),
);
}
List<Widget> _buildExpandingActionButtons() {
final children = <Widget>[];
final count = widget.children.length;
final step = 90.0 / (count - 1);
for (var i = 0, angleInDegrees = 0.0;
i < count;
i++, angleInDegrees += step) {
children.add(
_ExpandingActionButton(
directionInDegrees: angleInDegrees,
maxDistance: widget.distance,
progress: _expandAnimation,
child: widget.children[i],
),
);
}
return children;
}
Widget _buildTapToOpenFab() {
return IgnorePointer(
ignoring: _open,
child: AnimatedContainer(
transformAlignment: Alignment.center,
transform: Matrix4.diagonal3Values(
_open ? 0.7 : 1.0,
_open ? 0.7 : 1.0,
1.0,
),
duration: const Duration(milliseconds: 250),
curve: const Interval(0.0, 0.5, curve: Curves.easeOut),
child: AnimatedOpacity(
opacity: _open ? 0.0 : 1.0,
curve: const Interval(0.25, 1.0, curve: Curves.easeInOut),
duration: const Duration(milliseconds: 250),
child: FloatingActionButton(
onPressed: _toggle,
child: const Icon(Icons.create),
),
),
),
);
}
}
@immutable
class _ExpandingActionButton extends StatelessWidget {
const _ExpandingActionButton({
required this.directionInDegrees,
required this.maxDistance,
required this.progress,
required this.child,
});
final double directionInDegrees;
final double maxDistance;
final Animation<double> progress;
final Widget child;
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: progress,
builder: (context, child) {
final offset = Offset.fromDirection(
directionInDegrees * (math.pi / 180.0),
progress.value * maxDistance,
);
return Positioned(
right: 4.0 + offset.dx,
bottom: 4.0 + offset.dy,
child: Transform.rotate(
angle: (1.0 - progress.value) * math.pi / 2,
child: child!,
),
);
},
child: FadeTransition(
opacity: progress,
child: child,
),
);
}
}
@immutable
class ExpandableActionButton extends StatelessWidget {
const ExpandableActionButton({
super.key,
this.color,
this.onPressed,
required this.icon,
});
final VoidCallback? onPressed;
final Widget icon;
final Color? color;
@override
Widget build(BuildContext context) {
return SizedBox(
height: 48,
width: 48,
child: ElevatedButton(
onPressed: onPressed,
child: icon,
style: ButtonStyle(
padding: MaterialStateProperty.all(EdgeInsets.zero),
backgroundColor: MaterialStateProperty.all(color),
),
),
);
}
}

View File

@@ -0,0 +1,125 @@
import 'dart:math';
import 'package:flutter/material.dart';
class HighlightedText extends StatelessWidget {
final String text;
final List<String> highlights;
final Color? color;
final TextStyle? style;
final bool caseSensitive;
final TextAlign textAlign;
final TextDirection? textDirection;
final TextOverflow overflow;
final double textScaleFactor;
final int? maxLines;
final StrutStyle? strutStyle;
final TextWidthBasis textWidthBasis;
final TextHeightBehavior? textHeightBehavior;
const HighlightedText({
super.key,
required this.text,
required this.highlights,
this.style,
this.color = Colors.yellowAccent,
this.caseSensitive = true,
this.textAlign = TextAlign.start,
this.textDirection = TextDirection.ltr,
this.overflow = TextOverflow.clip,
this.textScaleFactor = 1.0,
this.maxLines,
this.strutStyle,
this.textWidthBasis = TextWidthBasis.parent,
this.textHeightBehavior,
});
@override
Widget build(BuildContext context) {
if (text.isEmpty || highlights.isEmpty || highlights.contains('')) {
return SelectableText.rich(
_normalSpan(text, context),
key: key,
textAlign: textAlign,
textDirection: textDirection,
textScaleFactor: textScaleFactor,
maxLines: maxLines,
strutStyle: strutStyle,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
style: TextStyle(overflow: overflow),
);
}
return SelectableText.rich(
TextSpan(children: _buildChildren(context)),
key: key,
textAlign: textAlign,
textDirection: textDirection,
textScaleFactor: textScaleFactor,
maxLines: maxLines,
strutStyle: strutStyle,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
style: TextStyle(overflow: overflow),
);
}
List<TextSpan> _buildChildren(BuildContext context) {
List<TextSpan> _spans = [];
int _start = 0;
String _text = caseSensitive ? text : text.toLowerCase();
List<String> _highlights =
caseSensitive ? highlights : highlights.map((e) => e.toLowerCase()).toList();
while (true) {
Map<int, String> _highlightsMap = {}; //key (index), value (highlight).
for (final h in _highlights) {
final idx = _text.indexOf(h, _start);
if (idx >= 0) {
_highlightsMap.putIfAbsent(_text.indexOf(h, _start), () => h);
}
}
if (_highlightsMap.isNotEmpty) {
int _currentIndex = _highlightsMap.keys.reduce(min);
String _currentHighlight = text.substring(
_currentIndex,
_currentIndex + _highlightsMap[_currentIndex]!.length,
);
if (_currentIndex == _start) {
_spans.add(_highlightSpan(_currentHighlight));
_start += _currentHighlight.length;
} else {
_spans.add(_normalSpan(text.substring(_start, _currentIndex), context));
_spans.add(_highlightSpan(_currentHighlight));
_start = _currentIndex + _currentHighlight.length;
}
} else {
_spans.add(_normalSpan(text.substring(_start, text.length), context));
break;
}
}
return _spans;
}
TextSpan _highlightSpan(String value) {
return TextSpan(
text: value,
style: style?.copyWith(
backgroundColor: color,
),
);
}
TextSpan _normalSpan(String value, BuildContext context) {
return TextSpan(
text: value,
style: style ?? Theme.of(context).textTheme.bodyText2,
);
}
}

View File

@@ -0,0 +1,30 @@
import 'package:flutter/material.dart';
import 'package:flutter_paperless_mobile/generated/l10n.dart';
class OfflineBanner extends StatelessWidget with PreferredSizeWidget {
const OfflineBanner({super.key});
@override
Widget build(BuildContext context) {
return Container(
color: Theme.of(context).disabledColor,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Icon(
Icons.cloud_off,
size: 24,
),
),
Text(S.of(context).genericMessageOfflineText),
],
),
);
}
@override
Size get preferredSize => const Size.fromHeight(24);
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter_paperless_mobile/generated/l10n.dart';
class OfflineWidget extends StatelessWidget {
const OfflineWidget({super.key});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.mood_bad, size: (Theme.of(context).iconTheme.size ?? 24) * 3),
Text(
S.of(context).offlineWidgetText,
textAlign: TextAlign.center,
),
],
),
);
}
}

View File

@@ -0,0 +1,23 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
class PaperlessLogo extends StatelessWidget {
final double? height;
final double? width;
const PaperlessLogo({Key? key, this.height, this.width}) : super(key: key);
@override
Widget build(BuildContext context) {
return Container(
constraints: BoxConstraints(
maxHeight: height ?? Theme.of(context).iconTheme.size ?? 32,
maxWidth: width ?? Theme.of(context).iconTheme.size ?? 32,
),
padding: const EdgeInsets.only(right: 8),
child: SvgPicture.asset(
"assets/logo/paperless_ng_logo_light.svg",
color: Theme.of(context).primaryColor,
),
);
}
}