Switch to Inter, round the corners softly and make the nav switch tabs

Inter replaces General Sans (Inter Display for titles and big numbers).
Corners come back from before the squaring commit, scaled to a soft set
(16 cards, 14 buttons, 12 rows, 8 chips) and applied to the screens built
since. The nav is now a cluster of round frosted buttons that switches
Today, Routines, Goals, Groups and Progress in place inside Home: no
pushed pages, no back arrows, and Android back returns to Today.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
This commit is contained in:
alvocool
2026-11-09 07:08:46 +03:00
parent add94f621c
commit d3431ca5de
54 changed files with 736 additions and 518 deletions

View File

@@ -111,7 +111,8 @@ status field. That's what makes honest history and excuse analysis possible.
See `design.md`. In short: sage-to-cream ambient backdrop, frosted glass See `design.md`. In short: sage-to-cream ambient backdrop, frosted glass
controls, painted `SceneArt` instead of photos, image-led cards, two-tone controls, painted `SceneArt` instead of photos, image-led cards, two-tone
greetings, square night buttons with a lime spark, no rounded corners. greetings, night buttons with a lime spark, softly rounded corners (never
pill-soft) and a floating round-button nav that switches tabs in place.
- Screens are built from `Sheet` (`designs/Shell.dart`); Home, Routines and - Screens are built from `Sheet` (`designs/Shell.dart`); Home, Routines and
the routine player compose `AmbientBackground` + glass directly. the routine player compose `AmbientBackground` + glass directly.
@@ -121,7 +122,7 @@ greetings, square night buttons with a lime spark, no rounded corners.
- Standing owns a colour used consistently everywhere (chrome, chip, meter). - Standing owns a colour used consistently everywhere (chrome, chip, meter).
- One primary button per screen; everything else outlined or text. - One primary button per screen; everything else outlined or text.
Font: **General Sans** (Fontshare, FFL — see `fonts/LICENSE-GeneralSans.txt`), Font: **Inter** (SIL OFL — see `fonts/LICENSE-Inter.txt`; Light is Inter Display),
wrapped as `TextType.{Bold,Medium,Regular,Light}`. wrapped as `TextType.{Bold,Medium,Regular,Light}`.
--- ---

View File

@@ -30,7 +30,11 @@ build their own layouts from the same pieces.
The sections below describe the original record-style type scale, which still The sections below describe the original record-style type scale, which still
applies to labels and metrics. applies to labels and metrics.
## Type — General Sans ## Type — Inter
`TextType.Light` is Inter Display Regular (the display cut, for titles and
big numbers); Regular, Medium and Bold are Inter Regular, Medium and
SemiBold.
| Role | Family | Size | Use | | Role | Family | Size | Use |
|---|---|---|---| |---|---|---|---|
@@ -45,7 +49,8 @@ directly above a large dark value. Used everywhere numbers appear.
## Geometry ## Geometry
- No rounded corners anywhere: sheets, cards, chips, buttons, fields, tiles and portraits are all square (`BorderRadius.zero`). Only small status dots, glows, the sun, progress rings and the breathing circle stay round. - Softly rounded, never pill-soft: large cards and image cards `16`, buttons `14`, rows, tiles and fields `12`, small controls `10`, chips and badges `8`, bars `4`. Sheets round their top at `20`.
- Portraits, the nav buttons, status dots, rings and the breathing circle are full circles.
- Screen padding `20` horizontal. Card padding `16`. Section gap `28`. - Screen padding `20` horizontal. Card padding `16`. Section gap `28`.
- Hairlines at `colorDivider` (black 7%), borders at `colorBorder` (black 8%). - Hairlines at `colorDivider` (black 7%), borders at `colorBorder` (black 8%).
- One soft shadow only: `0 4 18 rgba(0,0,0,0.03)`. No coloured glows. - One soft shadow only: `0 4 18 rgba(0,0,0,0.03)`. No coloured glows.
@@ -66,6 +71,14 @@ Near-black ink (`colorPrimaryDark #141414`) on warm off-white
The tier colour is used consistently — header, chip, meter, queue accent — so The tier colour is used consistently — header, chip, meter, queue accent — so
the standing is legible without reading the word. the standing is legible without reading the word.
## Navigation
The nav is a floating cluster of round frosted buttons, icons only; the
open tab is a raised white disc. It switches tabs inside Home (Today,
Routines, Goals, Groups, Progress) in place: nothing is pushed, a tab has
no back arrow (`tab: true`), and Android back returns to Today. Tabs are
built on first open and then kept.
## Structure ## Structure
Every screen is: black `headerBar` (back, title, action) → white sheet with a Every screen is: black `headerBar` (back, title, action) → white sheet with a

View File

@@ -1,57 +0,0 @@
Fontshare EULA
---—---------------------------------—------------------------------
Free Font - End User License Agreement (FF EULA)
---—---------------------------------—------------------------------
Notice to User
Indian Type Foundry designs, produces and distributes font software as digital fonts to end users worldwide. In addition to commercial fonts that are available for a fee, ITF also offers several fonts which can be used free of charge. The free fonts are distributed through a dedicated platform called www.fontshare.com (“Fontshare”) to end users worldwide. These free fonts are subject to this legally binding EULA between the Indian Type Foundry (“Indian Type Foundry” or “Licensor”) and you (“Licensee”). 
You acknowledge that the Font Software and designs embodied therein are protected by the copyright, other intellectual property rights and industrial property rights and by international treaties. They are and remain at all times the intellectual property of the Indian Type Foundry.
In addition to direct download, Fontshare also offers these free fonts via Fonthsare API using a code. In this case, the Font Software is delivered directly from the servers used by Indian Type Foundry to the Licensee's website, without the Licensee having to download the Font Software.
By downloading, accessing the API, installing, storing, copying or using one of any Font Software, you agree to the following terms. 
Definitions
“Font Software” refers to the set of computer files or programs released under this license that instructs your computer to display and/or print each letters, characters, typographic designs, ornament and so forth. Font Software includes all bitmap and vector representations of fonts and typographic representations and embellishments created by or derived from the Font Software. 
“Original Version” refers to the Font Software as distributed by the Indian Type Foundry as the copyright holder. 
“Derivative Work” refers to the pictorial representation of the font created by the Font Software, including typographic characters such as letters, numerals, ornaments, symbols, or punctuation and special characters.
01. Grant of License
You are hereby granted a non-exclusive, non-assignable, non-transferrable, terminable license to access, download and use the Font Software for your personal or commercial use for an unlimited period of time for free of charge. 
You may use the font Software in any media (including Print, Web, Mobile, Digital, Apps, ePub, Broadcasting and OEM) at any scale, at any location worldwide. 
You may use the Font Software to create logos and other graphic elements, images on any surface, vector files or other scalable drawings and static images. 
You may use the Font Software on any number of devices (computer, tablet, phone). The number of output devices (Printers) is not restricted. 
You may make only such reasonable number of back-up copies suitable to your permitted use. 
You may but are not required to identify Indian Type Foundry Fonts in your work credits. 
02. Limitations of usage
You may not modify, edit, adapt, translate, reverse engineer, decompile or disassemble, alter or otherwise copy the Font Software or the designs embodied therein in whole or in part, without the prior written consent of the Licensor. 
The Fonts may not - beyond the permitted copies and the uses defined herein - be distributed, duplicated, loaned, resold or licensed in any way, whether by lending, donating or give otherwise to a person or entity. This includes the distribution of the Fonts by e-mail, on USB sticks, CD-ROMs, or other media, uploading them in a public server or making the fonts available on peer-to-peer networks. A passing on to external designers or service providers (design agencies, repro studios, printers, etc.) is also not permitted. 
You are not allowed to transmit the Font Software over the Internet in font serving or for font replacement by means of technologies such as but not limited to EOT, Cufon, sIFR or similar technologies that may be developed in the future without the prior written consent of the Licensor. 
03. Embedding
You may embed the Font Software in PDF and other digital documents provided that is done in a secured, read-only mode. It must be ensured beyond doubt that the recipient cannot use the Font Software to edit or to create new documents. The design data (PDFs) created in this way and under these created design data (PDFs) may be distributed in any number. 
The extraction of the Font Software in whole or in part is prohibited. 
04. Third party use, Commercial print service provider
You may include the Font Software in a non-editable electronic document solely for printing and display purposes and provide that electronic document to the commercial print service provider for the purpose of printing. If the print service needs to install the fonts, they too need to download the Font Software from the Licensor's website.
05. Derivative Work
You are allowed to make derivative works as far as you use them for your personal or commercial use. However, you cannot modify, make changes or reverse engineer the original font software provided to you. Any derivative works are the exclusive property of the Licensor and shall be subject to the terms and conditions of this EULA. Derivative works may not be sub-licensed, sold, leased, rented, loaned, or given away without the express written permission of the Licensor. 
06. Warranty and Liability
BECAUSE THIS SOFTWARE IS LICENSED FREE OF CHARGE, INDIAN TYPE FOUNDRY MAKES NO WARRANTIES, EXPRESS OR IMPLIED AS TO THE MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR OTHERWISE. THE FONT SOFTWARE WAS NOT MANUFACTURED FOR USE IN MANUFACTURING CONTROL DEVICES OR NAVIGATION DEVICES OR IN CIRCUMSTANCES THAT COULD RESULT IN ENVIRONMENTAL DAMAGE OR PERSONAL INJURY. WITHOUT LIMITING THE FOREGOING, INDIAN TYPE FOUNDRY SHALL IN NO EVENT BE LIABLE TO THE LICENSED USER OR ANY OTHER THIRD PARTY FOR ANY DIRECT, CONSEQUENTIAL OR INCIDENTAL DAMAGES, INCLUDING DAMAGES FROM LOSS OF BUSINESS PROFITS, BUSINESS INTERRUPTION, LOSS OF BUSINESS INFORMATION NOR FOR LOST PROFITS OR SAVINGS ARISING OUT OF THE USE OR INABILITY TO USE THE PRODUCT EVEN IF NOTIFIED IN ADVANCE, UNDER NO CIRCUMSTANCES SHALL INDIAN TYPE FOUNDRY’S LIABILITY EXCEED THE REPLACEMENT COST OF THE SOFTWARE. 
IF LICENSEE CHOOSES TO ACCESS THE FONT SOFTWARE THROUGH A CODE (API), IT MAY HAVE A DIRECT IMPACT ON LICENSEE'S WEBSITE OR APPLICATIONS. INDIAN TYPE FOUNDRY IS NOT RESPONSIBLE OR LIABLE FOR ANY INTERRUPTION, MALFUNCTION, DOWNTIME OR OTHER FAILURE OF THE WEBSITE OR ITS API.
07. Updates, Maintenance and Support Services
Licensor will not provide you with any support services for the Software under this Agreement.
08. Termination 
Any breach of the terms of this agreement shall be a cause for termination, provided that such breach is notified in writing to the Licensee by the Licensor and the Licensee failed to rectify the breach within 30 days of the receipt of such notification. 
In the event of termination and without limitation of any remedies under law or equity, you must delete the Font Software and all copies thereof. Proof of this must be provided upon request of the Licensor.  
We reserve the right to claim damages for the violation of the conditions. 
09. Final Provisions
If individual provisions of this agreement are or become invalid, the validity of the remaining provisions shall remain unaffected. Invalid provisions shall be replaced by mutual agreement by such provisions that are suitable to achieve the desired economic purpose, taking into account the interests of both parties. The same shall apply mutatis mutandis to the filling of any gaps which may arise in this agreement.
This contract is subject to laws of the Republic of India. Place of performance and exclusive place of jurisdiction for all disputes between the parties arising out of or in connection with this contract is, as far as legally permissible, Ahmedabad, India.
- 
Last Updated on 22 March 2021
Copyright 2021 Indian Type Foundry. All rights reserved. 

View File

@@ -0,0 +1,92 @@
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
This Font Software is licensed under the SIL Open Font License, Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font creation
efforts of academic and linguistic communities, and to provide a free and
open framework in which fonts may be shared and improved in partnership
with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply
to any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software components as
distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to, deleting,
or substituting -- in part or in whole -- any of the components of the
Original Version, by changing formats or by porting the Font Software to a
new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION AND CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed, modify,
redistribute, and sell modified and unmodified copies of the Font
Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components,
in Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the corresponding
Copyright Holder. This restriction only applies to the primary font name as
presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created
using the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -48,7 +48,7 @@ class TickBox extends StatelessWidget {
height: size, height: size,
decoration: BoxDecoration( decoration: BoxDecoration(
color: checked ? tone : Colors.transparent, color: checked ? tone : Colors.transparent,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(6),
border: Border.all( border: Border.all(
color: checked ? tone : colorGrey.withValues(alpha: 0.55), color: checked ? tone : colorGrey.withValues(alpha: 0.55),
width: 1.6, width: 1.6,
@@ -202,7 +202,7 @@ Widget addTaskAffordance(
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: tone.withValues(alpha: 0.12), color: tone.withValues(alpha: 0.12),
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(12),
border: Border.all(color: tone.withValues(alpha: 0.25), width: 1), border: Border.all(color: tone.withValues(alpha: 0.25), width: 1),
), ),
child: Icon(Icons.add_rounded, size: 22, color: tone), child: Icon(Icons.add_rounded, size: 22, color: tone),

View File

@@ -5,7 +5,7 @@ import '../utils/Colors.dart';
import 'text/Text.dart'; import 'text/Text.dart';
/// The font families, wrapped so no screen ever names a family directly. /// The font families, wrapped so no screen ever names a family directly.
/// General Sans — see fonts/LICENSE-GeneralSans.txt. /// Inter (SIL OFL) — see fonts/LICENSE-Inter.txt.
String getTextType(TextType type) { String getTextType(TextType type) {
switch (type) { switch (type) {
case TextType.Bold: case TextType.Bold:
@@ -32,7 +32,7 @@ Widget pill(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
decoration: BoxDecoration( decoration: BoxDecoration(
color: background, color: background,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
border: Border.all(color: foreground.withValues(alpha: 0.20), width: 1), border: Border.all(color: foreground.withValues(alpha: 0.20), width: 1),
), ),
child: Row( child: Row(
@@ -61,7 +61,7 @@ Widget card({
EdgeInsets margin = EdgeInsets.zero, EdgeInsets margin = EdgeInsets.zero,
Color? background, Color? background,
Color? borderColor, Color? borderColor,
double radius = 22, double radius = 16,
VoidCallback? onTap, VoidCallback? onTap,
}) { }) {
final Widget body = Container( final Widget body = Container(
@@ -70,7 +70,7 @@ Widget card({
margin: margin, margin: margin,
decoration: BoxDecoration( decoration: BoxDecoration(
color: background ?? colorCard, color: background ?? colorCard,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(radius),
border: Border.all(color: borderColor ?? colorGlassBorder, width: 1), border: Border.all(color: borderColor ?? colorGlassBorder, width: 1),
boxShadow: const [ boxShadow: const [
BoxShadow( BoxShadow(
@@ -159,6 +159,7 @@ Widget emptyState(
height: 76, height: 76,
decoration: BoxDecoration( decoration: BoxDecoration(
color: tone.withValues(alpha: 0.08), color: tone.withValues(alpha: 0.08),
shape: BoxShape.circle,
), ),
child: Icon(icon, size: 32, color: tone), child: Icon(icon, size: 32, color: tone),
), ),
@@ -190,7 +191,7 @@ Widget meter(
: fraction; : fraction;
return ClipRRect( return ClipRRect(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
child: LinearProgressIndicator( child: LinearProgressIndicator(
value: clamped, value: clamped,
minHeight: height, minHeight: height,

View File

@@ -1,125 +1,100 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import '../about/internal/application/TextType.dart';
import '../utils/Colors.dart'; import '../utils/Colors.dart';
import 'GlassSurface.dart'; import 'GlassSurface.dart';
import 'text/Text.dart';
typedef NavEntry = ({IconData icon, String label, VoidCallback onTap}); typedef NavEntry = ({IconData icon, String label, VoidCallback onTap});
/// The floating tab bar: the selected tab is a filled night block, the rest /// The floating tab bar: a cluster of round frosted buttons, icons only.
/// sit quietly on white. An optional dark action button can sit beside it. /// The open tab rises out of the row as a solid white disc. It switches tabs
/// in place; nothing here pushes a page.
class FloatingNav extends StatelessWidget { class FloatingNav extends StatelessWidget {
final List<NavEntry> entries; final List<NavEntry> entries;
final int selected; final int selected;
final IconData? actionIcon;
final VoidCallback? onAction;
const FloatingNav({ const FloatingNav({
super.key, super.key,
required this.entries, required this.entries,
required this.selected, required this.selected,
this.actionIcon,
this.onAction,
}); });
static const double _size = 58;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return SafeArea( return SafeArea(
top: false, top: false,
child: Padding( child: Padding(
padding: const EdgeInsets.fromLTRB(16, 0, 16, 14), padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
child: Row( child: Center(
children: [ child: Container(
Expanded( decoration: const BoxDecoration(
child: Container( borderRadius: BorderRadius.all(Radius.circular(_size)),
decoration: BoxDecoration( boxShadow: [
borderRadius: BorderRadius.zero, BoxShadow(
boxShadow: const [ color: Color(0x1F1A1F1C),
BoxShadow( blurRadius: 30,
color: Color(0x1A1A1F1C), offset: Offset(0, 12),
blurRadius: 30, ),
offset: Offset(0, 10), ],
), ),
child: GlassSurface(
radius: const BorderRadius.all(Radius.circular(_size)),
tint: colorGlass,
padding: const EdgeInsets.all(5),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
for (int index = 0; index < entries.length; index++) ...[
if (index > 0) const SizedBox(width: 4),
_button(entries[index], index == selected),
], ],
), ],
child: GlassSurface(
radius: BorderRadius.zero,
tint: colorGlassStrong,
padding: const EdgeInsets.all(6),
child: Row(
children: [
for (int index = 0; index < entries.length; index++)
Expanded(child: _tab(entries[index], index == selected)),
],
),
),
), ),
), ),
if (onAction != null) ...[ ),
const SizedBox(width: 10),
GestureDetector(
onTap: onAction,
child: Container(
width: 72,
height: 72,
decoration: BoxDecoration(
gradient: const RadialGradient(
center: Alignment(-0.3, -0.4),
colors: <Color>[colorNightGlow, colorNight],
),
border: Border.all(color: colorWhite, width: 3),
boxShadow: const [
BoxShadow(
color: Color(0x331A1F1C),
blurRadius: 24,
offset: Offset(0, 10),
),
],
),
child: Icon(actionIcon, size: 24, color: colorLime),
),
),
],
],
), ),
), ),
); );
} }
Widget _tab(NavEntry entry, bool active) { Widget _button(NavEntry entry, bool active) {
return GestureDetector( return Semantics(
onTap: entry.onTap, button: true,
behavior: HitTestBehavior.opaque, selected: active,
child: AnimatedContainer( label: entry.label,
duration: const Duration(milliseconds: 220), child: Tooltip(
curve: Curves.easeOutCubic, message: entry.label,
padding: const EdgeInsets.symmetric(vertical: 10), child: GestureDetector(
decoration: BoxDecoration( onTap: entry.onTap,
color: active ? colorHeaderBottom : colorWhite.withValues(alpha: 0.0), behavior: HitTestBehavior.opaque,
borderRadius: BorderRadius.zero, child: AnimatedContainer(
boxShadow: active duration: const Duration(milliseconds: 220),
? const [ curve: Curves.easeOutCubic,
BoxShadow( width: _size,
color: Color(0x141A1F1C), height: _size,
blurRadius: 12, alignment: Alignment.center,
offset: Offset(0, 4), decoration: BoxDecoration(
), shape: BoxShape.circle,
] color: active ? colorWhite : colorWhite.withValues(alpha: 0.35),
: const [], border: Border.all(
), color: colorWhite.withValues(alpha: active ? 1 : 0.6),
child: Column( width: 1,
mainAxisSize: MainAxisSize.min, ),
children: [ boxShadow: active
Icon(entry.icon, ? const [
size: 23, color: active ? colorWhite : colorInkSoft), BoxShadow(
const SizedBox(height: 4), color: Color(0x261A1F1C),
text(entry.label, 10, active ? TextType.Medium : TextType.Regular, blurRadius: 16,
color: active ? colorWhite : colorInkSoft, maxLines: 1), offset: Offset(0, 6),
], ),
]
: const [],
),
child: Icon(entry.icon,
size: 22, color: active ? colorInk : colorInkSoft),
),
), ),
), ),
); );

View File

@@ -23,7 +23,7 @@ class GlassSurface extends StatelessWidget {
const GlassSurface({ const GlassSurface({
super.key, super.key,
required this.child, required this.child,
this.radius = const BorderRadius.all(Radius.zero), this.radius = const BorderRadius.all(Radius.circular(14)),
this.padding = EdgeInsets.zero, this.padding = EdgeInsets.zero,
this.tint, this.tint,
this.onDark = false, this.onDark = false,
@@ -73,7 +73,7 @@ Widget glassCircle(
children: [ children: [
GlassSurface( GlassSurface(
onDark: onDark, onDark: onDark,
radius: BorderRadius.zero, radius: BorderRadius.circular(size),
child: SizedBox( child: SizedBox(
width: size, width: size,
height: size, height: size,

View File

@@ -32,8 +32,8 @@ Future<Goal?> showGoalPicker(
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: colorSheetBackground, color: colorSheetBackground,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.zero, topLeft: Radius.circular(16),
topRight: Radius.zero, topRight: Radius.circular(16),
), ),
), ),
child: ListView( child: ListView(
@@ -93,7 +93,7 @@ Widget _goalOption(
padding: const EdgeInsets.fromLTRB(14, 13, 14, 13), padding: const EdgeInsets.fromLTRB(14, 13, 14, 13),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorWhite, color: colorWhite,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: selected ? accent.withValues(alpha: 0.5) : colorBorder, color: selected ? accent.withValues(alpha: 0.5) : colorBorder,
width: selected ? 1.5 : 1, width: selected ? 1.5 : 1,

View File

@@ -6,7 +6,7 @@ import '../utils/CommonUtils.dart';
import 'scene/SceneArt.dart'; import 'scene/SceneArt.dart';
import 'text/Text.dart'; import 'text/Text.dart';
/// A member's square portrait: a landscape picked from their name with the /// A member's round portrait: a landscape picked from their name with the
/// initial on it, so everyone is recognisable without a photo. /// initial on it, so everyone is recognisable without a photo.
Widget memberAvatar(String name, double size, {Color? border}) { Widget memberAvatar(String name, double size, {Color? border}) {
const List<List<Color>> palettes = <List<Color>>[ const List<List<Color>> palettes = <List<Color>>[
@@ -21,16 +21,21 @@ Widget memberAvatar(String name, double size, {Color? border}) {
return Container( return Container(
width: size, width: size,
height: size, height: size,
decoration: decoration: BoxDecoration(
BoxDecoration(border: Border.all(color: border ?? colorWhite, width: 2)), shape: BoxShape.circle,
child: Stack( border: Border.all(color: border ?? colorWhite, width: 2),
fit: StackFit.expand, ),
children: [ child: ClipOval(
SceneArt(palette: palettes[seed % palettes.length], seed: seed), child: Stack(
Center( fit: StackFit.expand,
child: text(initial, size * 0.42, TextType.Medium, color: colorWhite), children: [
), SceneArt(palette: palettes[seed % palettes.length], seed: seed),
], Center(
child:
text(initial, size * 0.42, TextType.Medium, color: colorWhite),
),
],
),
), ),
); );
} }
@@ -61,6 +66,7 @@ Widget avatarStack(List<String> names, double size,
height: size, height: size,
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle,
color: colorNight, color: colorNight,
border: Border.all(color: border ?? colorWhite, width: 2), border: Border.all(color: border ?? colorWhite, width: 2),
), ),

View File

@@ -27,6 +27,9 @@ class Sheet extends StatelessWidget {
/// Optional trailing control in the chrome. /// Optional trailing control in the chrome.
final Widget? action; final Widget? action;
/// Space under the content; tabs leave room for the floating nav.
final double bottomSpace;
/// Stage colour. Leaving it null (with no banner) gives the ambient tone. /// Stage colour. Leaving it null (with no banner) gives the ambient tone.
final Color? chrome; final Color? chrome;
@@ -53,6 +56,7 @@ class Sheet extends StatelessWidget {
this.scrollable = true, this.scrollable = true,
this.scene, this.scene,
this.sceneSeed = 0, this.sceneSeed = 0,
this.bottomSpace = 40,
}); });
bool get _light => chrome == null && banner == null && scene == null; bool get _light => chrome == null && banner == null && scene == null;
@@ -62,8 +66,7 @@ class Sheet extends StatelessWidget {
final bool light = _light; final bool light = _light;
final Color stage = chrome ?? colorPrimaryDark; final Color stage = chrome ?? colorPrimaryDark;
final Color ink = light ? colorInk : colorWhite; final Color ink = light ? colorInk : colorWhite;
final Color soft = final Color soft = light ? colorInkSoft : colorWhite.withValues(alpha: 0.5);
light ? colorInkSoft : colorWhite.withValues(alpha: 0.5);
final Widget header = SafeArea( final Widget header = SafeArea(
bottom: false, bottom: false,
@@ -80,8 +83,7 @@ class Sheet extends StatelessWidget {
? null ? null
: Align( : Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: glassCircle( child: glassCircle(CupertinoIcons.arrow_left, onBack!,
CupertinoIcons.arrow_left, onBack!,
onDark: !light), onDark: !light),
), ),
), ),
@@ -136,14 +138,14 @@ class Sheet extends StatelessWidget {
decoration: BoxDecoration( decoration: BoxDecoration(
color: light ? colorFrost : colorPrimaryLight, color: light ? colorFrost : colorPrimaryLight,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.zero, topLeft: Radius.circular(16),
topRight: Radius.zero, topRight: Radius.circular(16),
), ),
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
child: scrollable child: scrollable
? SingleChildScrollView( ? SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 28, 20, 40), padding: EdgeInsets.fromLTRB(20, 28, 20, bottomSpace),
child: child, child: child,
) )
: Padding( : Padding(

View File

@@ -13,7 +13,7 @@ Widget roundedCornerButton(
Color? foreground, Color? foreground,
IconData? icon, IconData? icon,
bool enabled = true, bool enabled = true,
double radius = 99, double radius = 14,
double verticalPadding = 16, double verticalPadding = 16,
}) { }) {
final Color bg = enabled ? (background ?? colorNight) : colorGrey; final Color bg = enabled ? (background ?? colorNight) : colorGrey;
@@ -27,7 +27,7 @@ Widget roundedCornerButton(
disabledBackgroundColor: colorGrey.withValues(alpha: 0.4), disabledBackgroundColor: colorGrey.withValues(alpha: 0.4),
padding: EdgeInsets.symmetric(vertical: verticalPadding), padding: EdgeInsets.symmetric(vertical: verticalPadding),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(radius),
), ),
), ),
child: Row( child: Row(
@@ -55,7 +55,7 @@ Widget outlinedActionButton(
Color? foreground, Color? foreground,
IconData? icon, IconData? icon,
bool enabled = true, bool enabled = true,
double radius = 99, double radius = 14,
}) { }) {
final Color fg = enabled ? (foreground ?? colorPrimaryDark) : colorGrey; final Color fg = enabled ? (foreground ?? colorPrimaryDark) : colorGrey;
@@ -65,7 +65,7 @@ Widget outlinedActionButton(
side: BorderSide(color: fg.withValues(alpha: 0.35), width: 1), side: BorderSide(color: fg.withValues(alpha: 0.35), width: 1),
padding: const EdgeInsets.symmetric(vertical: 16), padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(radius),
), ),
), ),
child: Row( child: Row(
@@ -112,7 +112,7 @@ Widget iconButton(
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: background ?? Colors.transparent, color: background ?? Colors.transparent,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(radius),
border: bordered border: bordered
? Border.all(color: borderColor ?? colorBorder, width: 1) ? Border.all(color: borderColor ?? colorBorder, width: 1)
: null, : null,
@@ -161,7 +161,7 @@ Widget segmentedSelector<T>({
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: isSelected ? active : colorWhite, color: isSelected ? active : colorWhite,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(10),
border: Border.all( border: Border.all(
color: isSelected ? active : colorBorder, color: isSelected ? active : colorBorder,
width: 1, width: 1,

View File

@@ -17,7 +17,7 @@ Widget countStepper(
padding: const EdgeInsets.fromLTRB(14, 10, 8, 10), padding: const EdgeInsets.fromLTRB(14, 10, 8, 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorWhite, color: colorWhite,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(10),
border: Border.all(color: colorBorder, width: 1), border: Border.all(color: colorBorder, width: 1),
), ),
child: Row( child: Row(
@@ -54,7 +54,7 @@ Widget _stepButton(IconData icon, VoidCallback? onTap) {
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: enabled ? colorSecondary : colorInset, color: enabled ? colorSecondary : colorInset,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(10),
), ),
child: Icon(icon, child: Icon(icon,
size: 14, color: enabled ? colorPrimaryDark : colorGrey), size: 14, color: enabled ? colorPrimaryDark : colorGrey),

View File

@@ -54,19 +54,19 @@ Widget inputField(
contentPadding: contentPadding:
const EdgeInsets.symmetric(horizontal: 14, vertical: 14), const EdgeInsets.symmetric(horizontal: 14, vertical: 14),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: colorBorder, width: 1), borderSide: BorderSide(color: colorBorder, width: 1),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: colorPrimaryDark, width: 1.4), borderSide: BorderSide(color: colorPrimaryDark, width: 1.4),
), ),
errorBorder: OutlineInputBorder( errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: colorNegative, width: 1), borderSide: BorderSide(color: colorNegative, width: 1),
), ),
focusedErrorBorder: OutlineInputBorder( focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: colorNegative, width: 1.4), borderSide: BorderSide(color: colorNegative, width: 1.4),
), ),
errorStyle: TextStyle( errorStyle: TextStyle(
@@ -163,19 +163,19 @@ Widget excuseField(
fillColor: colorWhite, fillColor: colorWhite,
contentPadding: const EdgeInsets.all(14), contentPadding: const EdgeInsets.all(14),
enabledBorder: OutlineInputBorder( enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: colorBorder, width: 1), borderSide: BorderSide(color: colorBorder, width: 1),
), ),
focusedBorder: OutlineInputBorder( focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: colorPrimaryDark, width: 1.4), borderSide: BorderSide(color: colorPrimaryDark, width: 1.4),
), ),
errorBorder: OutlineInputBorder( errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: colorNegative, width: 1), borderSide: BorderSide(color: colorNegative, width: 1),
), ),
focusedErrorBorder: OutlineInputBorder( focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(10),
borderSide: BorderSide(color: colorNegative, width: 1.4), borderSide: BorderSide(color: colorNegative, width: 1.4),
), ),
errorStyle: TextStyle( errorStyle: TextStyle(
@@ -212,7 +212,7 @@ Widget selectField(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 16), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorWhite, color: colorWhite,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(10),
border: Border.all(color: colorBorder, width: 1), border: Border.all(color: colorBorder, width: 1),
), ),
child: Row( child: Row(

View File

@@ -443,8 +443,8 @@ class NewCommitmentState extends State<NewCommitment>
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorSheetBackground, color: colorSheetBackground,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.zero, topLeft: Radius.circular(16),
topRight: Radius.zero, topRight: Radius.circular(16),
), ),
), ),
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -460,7 +460,7 @@ class NewCommitmentState extends State<NewCommitment>
margin: const EdgeInsets.only(bottom: 20), margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorGrey.withValues(alpha: 0.35), color: colorGrey.withValues(alpha: 0.35),
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
), ),
), ),
), ),

View File

@@ -142,7 +142,7 @@ class ExcuseReportState extends State<ExcuseReport>
padding: const EdgeInsets.all(14), padding: const EdgeInsets.all(14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorInset, color: colorInset,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(10),
border: Border.all(color: colorBorder, width: 1), border: Border.all(color: colorBorder, width: 1),
), ),
child: Row( child: Row(

View File

@@ -242,7 +242,7 @@ class GoalDetailState extends State<GoalDetail>
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorWhite.withValues(alpha: 0.08), color: colorWhite.withValues(alpha: 0.08),
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(12),
), ),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -405,7 +405,7 @@ class GoalDetailState extends State<GoalDetail>
width: 58, width: 58,
height: 58, height: 58,
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(12),
child: SceneArt( child: SceneArt(
palette: scenePalette(routine.kind), palette: scenePalette(routine.kind),
seed: sceneSeed(routine.title), seed: sceneSeed(routine.title),
@@ -454,6 +454,7 @@ class GoalDetailState extends State<GoalDetail>
color: routine.kind == RoutineKind.MustDo color: routine.kind == RoutineKind.MustDo
? colorPrimaryDark ? colorPrimaryDark
: tone, : tone,
shape: BoxShape.circle,
), ),
child: Icon(CupertinoIcons.play_fill, child: Icon(CupertinoIcons.play_fill,
size: 15, color: colorWhite), size: 15, color: colorWhite),

View File

@@ -282,7 +282,7 @@ class GoalEditorState extends State<GoalEditor> implements ConnectGoalEditor {
return Container( return Container(
padding: const EdgeInsets.all(18), padding: const EdgeInsets.all(18),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
@@ -338,6 +338,7 @@ class GoalEditorState extends State<GoalEditor> implements ConnectGoalEditor {
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colour, color: colour,
shape: BoxShape.circle,
border: Border.all( border: Border.all(
color: selected ? colorWhite : Colors.transparent, color: selected ? colorWhite : Colors.transparent,
width: 3, width: 3,

View File

@@ -3,7 +3,11 @@ import 'package:flutter/material.dart';
import 'GoalsState.dart'; import 'GoalsState.dart';
class Goals extends StatefulWidget { class Goals extends StatefulWidget {
const Goals({super.key}); /// Shown as a tab inside Home: no back arrow, and room at the bottom
/// for the nav.
final bool tab;
const Goals({super.key, this.tab = false});
@override @override
State<Goals> createState() => GoalsState(); State<Goals> createState() => GoalsState();

View File

@@ -74,7 +74,8 @@ class GoalsState extends State<Goals> implements ConnectGoals {
return Sheet( return Sheet(
eyebrow: "Grounded", eyebrow: "Grounded",
title: "Goals", title: "Goals",
onBack: _onBack, onBack: widget.tab ? null : _onBack,
bottomSpace: widget.tab ? 130 : 40,
action: chromeAction(CupertinoIcons.add, _onNewGoal), action: chromeAction(CupertinoIcons.add, _onNewGoal),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -120,7 +121,7 @@ class GoalsState extends State<Goals> implements ConnectGoals {
height: 260, height: 260,
margin: const EdgeInsets.only(bottom: 14), margin: const EdgeInsets.only(bottom: 14),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(16),
boxShadow: const [ boxShadow: const [
BoxShadow( BoxShadow(
color: Color(0x262A3A30), color: Color(0x262A3A30),
@@ -130,7 +131,7 @@ class GoalsState extends State<Goals> implements ConnectGoals {
], ],
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(16),
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
@@ -151,7 +152,7 @@ class GoalsState extends State<Goals> implements ConnectGoals {
children: [ children: [
GlassSurface( GlassSurface(
onDark: true, onDark: true,
radius: BorderRadius.zero, radius: BorderRadius.circular(14),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5), horizontal: 10, vertical: 5),
child: text( child: text(

View File

@@ -119,8 +119,8 @@ class GroupChatState extends State<GroupChat> implements ConnectGroupChat {
final GroupMessage? older = index + 1 < newestFirst.length final GroupMessage? older = index + 1 < newestFirst.length
? newestFirst[index + 1] ? newestFirst[index + 1]
: null; : null;
final bool newDay = older == null || final bool newDay =
!_sameDay(older.sentAt, message.sentAt); older == null || !_sameDay(older.sentAt, message.sentAt);
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -145,6 +145,7 @@ class GroupChatState extends State<GroupChat> implements ConnectGroupChat {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
@@ -198,9 +199,12 @@ class GroupChatState extends State<GroupChat> implements ConnectGroupChat {
child: Center( child: Center(
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 5),
color: colorWhite.withValues(alpha: 0.7), decoration: BoxDecoration(
child: text(_dayName(moment), 11, TextType.Medium, color: colorWhite.withValues(alpha: 0.7),
color: colorInkSoft), borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child:
text(_dayName(moment), 11, TextType.Medium, color: colorInkSoft),
), ),
), ),
); );
@@ -241,7 +245,10 @@ class GroupChatState extends State<GroupChat> implements ConnectGroupChat {
alignment: Alignment.centerRight, alignment: Alignment.centerRight,
child: Container( child: Container(
padding: const EdgeInsets.fromLTRB(14, 11, 14, 10), padding: const EdgeInsets.fromLTRB(14, 11, 14, 10),
color: colorNight, decoration: BoxDecoration(
color: colorNight,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -273,6 +280,7 @@ class GroupChatState extends State<GroupChat> implements ConnectGroupChat {
child: Container( child: Container(
padding: const EdgeInsets.fromLTRB(14, 10, 14, 10), padding: const EdgeInsets.fromLTRB(14, 10, 14, 10),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
color: colorWhite, color: colorWhite,
border: Border.all(color: colorGlassBorder), border: Border.all(color: colorGlassBorder),
), ),
@@ -308,6 +316,7 @@ class GroupChatState extends State<GroupChat> implements ConnectGroupChat {
child: Container( child: Container(
padding: const EdgeInsets.fromLTRB(12, 12, 14, 12), padding: const EdgeInsets.fromLTRB(12, 12, 14, 12),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
color: checkIn ? colorStandingGoodBg : colorOverdueWarmEnd, color: checkIn ? colorStandingGoodBg : colorOverdueWarmEnd,
border: Border( border: Border(
left: BorderSide( left: BorderSide(
@@ -357,7 +366,10 @@ class GroupChatState extends State<GroupChat> implements ConnectGroupChat {
Widget _composer() { Widget _composer() {
return Container( return Container(
color: colorWhite, decoration: BoxDecoration(
color: colorWhite,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
padding: EdgeInsets.fromLTRB( padding: EdgeInsets.fromLTRB(
0, 10, 0, MediaQuery.of(context).padding.bottom + 10), 0, 10, 0, MediaQuery.of(context).padding.bottom + 10),
child: Column( child: Column(
@@ -375,7 +387,10 @@ class GroupChatState extends State<GroupChat> implements ConnectGroupChat {
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14), padding: const EdgeInsets.symmetric(horizontal: 14),
alignment: Alignment.center, alignment: Alignment.center,
color: colorAmbientMid, decoration: BoxDecoration(
color: colorAmbientMid,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: text(_quickReplies[index], 13, TextType.Regular, child: text(_quickReplies[index], 13, TextType.Regular,
color: colorInk), color: colorInk),
), ),
@@ -396,7 +411,10 @@ class GroupChatState extends State<GroupChat> implements ConnectGroupChat {
child: Container( child: Container(
width: 50, width: 50,
height: 50, height: 50,
color: colorNight, decoration: BoxDecoration(
color: colorNight,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Icon(CupertinoIcons.paperplane_fill, child: Icon(CupertinoIcons.paperplane_fill,
size: 20, color: colorLime), size: 20, color: colorLime),
), ),

View File

@@ -137,8 +137,8 @@ class GroupDetailState extends State<GroupDetail>
_amount.text = formatAmount(value).replaceAll(",", ""); _amount.text = formatAmount(value).replaceAll(",", "");
} }
void _onSubmitLog(BuildContext sheetContext, GroupGoal goal, void _onSubmitLog(
GroupObjective objective) { BuildContext sheetContext, GroupGoal goal, GroupObjective objective) {
final double amount = double.tryParse(_amount.text.trim()) ?? 0; final double amount = double.tryParse(_amount.text.trim()) ?? 0;
if (amount <= 0) { if (amount <= 0) {
return; return;
@@ -258,7 +258,10 @@ class GroupDetailState extends State<GroupDetail>
behavior: HitTestBehavior.opaque, behavior: HitTestBehavior.opaque,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
color: colorAmbientMid, decoration: BoxDecoration(
color: colorAmbientMid,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -292,6 +295,7 @@ class GroupDetailState extends State<GroupDetail>
child: Container( child: Container(
padding: const EdgeInsets.fromLTRB(20, 18, 16, 18), padding: const EdgeInsets.fromLTRB(20, 18, 16, 18),
decoration: const BoxDecoration( decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(12)),
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
@@ -317,16 +321,18 @@ class GroupDetailState extends State<GroupDetail>
Icon(CupertinoIcons.chat_bubble_2_fill, Icon(CupertinoIcons.chat_bubble_2_fill,
size: 16, color: colorLime), size: 16, color: colorLime),
const SizedBox(width: 8), const SizedBox(width: 8),
text("Group chat", 14, TextType.Medium, text("Group chat", 14, TextType.Medium, color: colorLime),
color: colorLime),
if (_group.unread > 0) ...[ if (_group.unread > 0) ...[
const SizedBox(width: 10), const SizedBox(width: 10),
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 7, vertical: 3), horizontal: 7, vertical: 3),
color: colorLime, decoration: BoxDecoration(
child: text("${_group.unread} new", 10, color: colorLime,
TextType.Medium, borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: text(
"${_group.unread} new", 10, TextType.Medium,
color: colorNight), color: colorNight),
), ),
], ],
@@ -351,9 +357,12 @@ class GroupDetailState extends State<GroupDetail>
Container( Container(
width: 46, width: 46,
height: 46, height: 46,
color: colorWhite.withValues(alpha: 0.12), decoration: BoxDecoration(
child: Icon(CupertinoIcons.arrow_right, color: colorWhite.withValues(alpha: 0.12),
size: 20, color: colorWhite), borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child:
Icon(CupertinoIcons.arrow_right, size: 20, color: colorWhite),
), ),
], ],
), ),
@@ -409,7 +418,9 @@ class GroupDetailState extends State<GroupDetail>
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
meter(goal.progress, meter(goal.progress,
fill: tone, track: tone.withValues(alpha: 0.14), height: 6), fill: tone,
track: tone.withValues(alpha: 0.14),
height: 6),
const SizedBox(height: 8), const SizedBox(height: 8),
], ],
), ),
@@ -426,6 +437,7 @@ class GroupDetailState extends State<GroupDetail>
return Container( return Container(
padding: const EdgeInsets.fromLTRB(20, 14, 14, 16), padding: const EdgeInsets.fromLTRB(20, 14, 14, 16),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
border: Border(top: BorderSide(color: colorBorder)), border: Border(top: BorderSide(color: colorBorder)),
), ),
child: Row( child: Row(
@@ -472,7 +484,10 @@ class GroupDetailState extends State<GroupDetail>
onTap: () => _onLog(goal, objective), onTap: () => _onLog(goal, objective),
child: Container( child: Container(
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
color: colorNight, decoration: BoxDecoration(
color: colorNight,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -560,7 +575,10 @@ class GroupDetailState extends State<GroupDetail>
child: Container( child: Container(
width: 44, width: 44,
height: 44, height: 44,
color: nudged ? colorAmbientMid : colorNight, decoration: BoxDecoration(
color: nudged ? colorAmbientMid : colorNight,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Icon( child: Icon(
nudged nudged
? CupertinoIcons.checkmark ? CupertinoIcons.checkmark
@@ -579,13 +597,13 @@ class GroupDetailState extends State<GroupDetail>
/// Log progress: an amount (with one-tap presets that suit the unit) and /// Log progress: an amount (with one-tap presets that suit the unit) and
/// an optional note that rides along into the chat. /// an optional note that rides along into the chat.
Widget _logSheet(BuildContext sheetContext, GroupGoal goal, Widget _logSheet(
GroupObjective objective) { BuildContext sheetContext, GroupGoal goal, GroupObjective objective) {
final List<double> presets = _presets(objective.unit); final List<double> presets = _presets(objective.unit);
return Padding( return Padding(
padding: EdgeInsets.fromLTRB(20, 16, 20, padding: EdgeInsets.fromLTRB(
MediaQuery.of(sheetContext).viewInsets.bottom + 28), 20, 16, 20, MediaQuery.of(sheetContext).viewInsets.bottom + 28),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -612,9 +630,12 @@ class GroupDetailState extends State<GroupDetail>
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 14), padding: const EdgeInsets.symmetric(vertical: 14),
alignment: Alignment.center, alignment: Alignment.center,
color: colorAmbientMid, decoration: BoxDecoration(
child: text("+${formatAmount(preset)}", 15, color: colorAmbientMid,
TextType.Medium, borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: text(
"+${formatAmount(preset)}", 15, TextType.Medium,
color: colorInk), color: colorInk),
), ),
), ),
@@ -636,8 +657,8 @@ class GroupDetailState extends State<GroupDetail>
const SizedBox(height: 14), const SizedBox(height: 14),
inputField("Note", _note, hint: "Optional: posted with the check-in"), inputField("Note", _note, hint: "Optional: posted with the check-in"),
const SizedBox(height: 22), const SizedBox(height: 22),
roundedCornerButton("Log it", roundedCornerButton(
() => _onSubmitLog(sheetContext, goal, objective), "Log it", () => _onSubmitLog(sheetContext, goal, objective),
icon: CupertinoIcons.checkmark), icon: CupertinoIcons.checkmark),
], ],
), ),

View File

@@ -99,7 +99,8 @@ class GroupEditorState extends State<GroupEditor>
} }
if (_joining) { if (_joining) {
_model?.join(JoinGroupRequest(inviteCode: _code.text.trim().toUpperCase())); _model
?.join(JoinGroupRequest(inviteCode: _code.text.trim().toUpperCase()));
} else { } else {
_model?.create(GroupRequest( _model?.create(GroupRequest(
name: _name.text.trim(), name: _name.text.trim(),
@@ -130,9 +131,8 @@ class GroupEditorState extends State<GroupEditor>
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
displayTitle(_joining displayTitle(
? "Got a code?" _joining ? "Got a code?" : "Who's keeping\nyou honest?"),
: "Who's keeping\nyou honest?"),
const SizedBox(height: 22), const SizedBox(height: 22),
segmentedSelector<String>( segmentedSelector<String>(
options: const <String>["Create", "Join"], options: const <String>["Create", "Join"],
@@ -221,6 +221,7 @@ class GroupEditorState extends State<GroupEditor>
return Container( return Container(
padding: const EdgeInsets.all(20), padding: const EdgeInsets.all(20),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
@@ -237,8 +238,7 @@ class GroupEditorState extends State<GroupEditor>
size: 16, color: colorWhite.withValues(alpha: 0.7)), size: 16, color: colorWhite.withValues(alpha: 0.7)),
const SizedBox(width: 8), const SizedBox(width: 8),
text("1 MEMBER", 9, TextType.Bold, text("1 MEMBER", 9, TextType.Bold,
color: colorWhite.withValues(alpha: 0.6), color: colorWhite.withValues(alpha: 0.6), letterSpacing: 1.2),
letterSpacing: 1.2),
], ],
), ),
const SizedBox(height: 14), const SizedBox(height: 14),
@@ -266,6 +266,7 @@ class GroupEditorState extends State<GroupEditor>
height: 40, height: 40,
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(12)),
color: colour, color: colour,
border: Border.all( border: Border.all(
color: selected ? colorWhite : Colors.transparent, color: selected ? colorWhite : Colors.transparent,

View File

@@ -195,8 +195,7 @@ class GroupGoalEditorState extends State<GroupGoalEditor>
maxLines: 2, maxLines: 2,
), ),
const SizedBox(height: 28), const SizedBox(height: 28),
sectionBreak("Objectives", sectionBreak("Objectives", caption: "what everyone logs toward"),
caption: "what everyone logs toward"),
text( text(
"Break the goal into numbers people can add to: kilometres, sessions, pages, hours. Each member logs their share and the totals add up.", "Break the goal into numbers people can add to: kilometres, sessions, pages, hours. Each member logs their share and the totals add up.",
12, 12,
@@ -288,7 +287,8 @@ class GroupGoalEditorState extends State<GroupGoalEditor>
keyboard: const TextInputType.numberWithOptions( keyboard: const TextInputType.numberWithOptions(
decimal: true), decimal: true),
formatters: <TextInputFormatter>[ formatters: <TextInputFormatter>[
FilteringTextInputFormatter.allow(RegExp(r"[0-9.]")), FilteringTextInputFormatter.allow(
RegExp(r"[0-9.]")),
], ],
), ),
), ),

View File

@@ -3,7 +3,11 @@ import 'package:flutter/material.dart';
import 'GroupsState.dart'; import 'GroupsState.dart';
class Groups extends StatefulWidget { class Groups extends StatefulWidget {
const Groups({super.key}); /// Shown as a tab inside Home: no back arrow, and room at the bottom
/// for the nav.
final bool tab;
const Groups({super.key, this.tab = false});
@override @override
State<Groups> createState() => GroupsState(); State<Groups> createState() => GroupsState();

View File

@@ -86,7 +86,8 @@ class GroupsState extends State<Groups> implements ConnectGroups {
return Sheet( return Sheet(
eyebrow: "Grounded", eyebrow: "Grounded",
title: "Groups", title: "Groups",
onBack: _onBack, onBack: widget.tab ? null : _onBack,
bottomSpace: widget.tab ? 130 : 40,
action: chromeAction(CupertinoIcons.add, _onNewGroup), action: chromeAction(CupertinoIcons.add, _onNewGroup),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -133,6 +134,7 @@ class GroupsState extends State<Groups> implements ConnectGroups {
height: 250, height: 250,
margin: const EdgeInsets.only(bottom: 14), margin: const EdgeInsets.only(bottom: 14),
decoration: const BoxDecoration( decoration: const BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(16)),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Color(0x262A3A30), color: Color(0x262A3A30),
@@ -141,6 +143,7 @@ class GroupsState extends State<Groups> implements ConnectGroups {
), ),
], ],
), ),
clipBehavior: Clip.antiAlias,
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
@@ -175,7 +178,10 @@ class GroupsState extends State<Groups> implements ConnectGroups {
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6), horizontal: 10, vertical: 6),
color: colorLime, decoration: BoxDecoration(
color: colorLime,
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [

View File

@@ -23,7 +23,8 @@ class ViewGroupChat extends ParentViewModel {
connection.onMessagesLoaded(getGroupMessageList(response.data)); connection.onMessagesLoaded(getGroupMessageList(response.data));
} catch (e) { } catch (e) {
handleError(e, () => loadMessages(groupId), () => dismissError(), "Retry"); handleError(
e, () => loadMessages(groupId), () => dismissError(), "Retry");
} }
} }

View File

@@ -42,8 +42,8 @@ class ViewGroupDetail extends ParentViewModel {
connection.onProgressLogged(Group.fromJson(response.data), summary); connection.onProgressLogged(Group.fromJson(response.data), summary);
} catch (e) { } catch (e) {
handleError(e, () => logProgress(request, summary), handleError(e, () => logProgress(request, summary), () => dismissError(),
() => dismissError(), "Retry"); "Retry");
} }
} }

View File

@@ -81,6 +81,10 @@ class HomeState extends State<Home> implements ConnectHome {
bool _weekly = false; bool _weekly = false;
/// The open tab, and the tabs built so far.
int _tab = 0;
final Set<int> _opened = <int>{0};
Standing _standing = Standing.Good; Standing _standing = Standing.Good;
@@ -183,28 +187,14 @@ class HomeState extends State<Home> implements ConnectHome {
_model?.tick(item); _model?.tick(item);
} }
void _onOpenProgress() {
GroundedNavigation().navigateToPageWithData(const Progress(), context);
}
void _onOpenReportCard() { void _onOpenReportCard() {
GroundedNavigation().navigateToPage( GroundedNavigation().navigateToPage(
NavigatorType.justOpen, const ReportCardScreen(), context); NavigatorType.justOpen, const ReportCardScreen(), context);
} }
void _onOpenGoals() async { void _onOpenGoals() => _onNavGoals();
final result = await GroundedNavigation()
.navigateToPageWithData(const Goals(), context);
if (result == true) { void _onOpenGroups() => _onNavGroups();
_model?.loadPlan();
}
}
void _onOpenGroups() async {
await GroundedNavigation().navigateToPageWithData(const Groups(), context);
_model?.loadGroups();
}
void _onOpenGroupChat(Group group) async { void _onOpenGroupChat(Group group) async {
await GroundedNavigation() await GroundedNavigation()
@@ -224,11 +214,7 @@ class HomeState extends State<Home> implements ConnectHome {
.navigateToPage(NavigatorType.justOpen, const Training(), context); .navigateToPage(NavigatorType.justOpen, const Training(), context);
} }
void _onOpenRoutines() async { void _onOpenRoutines() => _onNavRoutines();
await GroundedNavigation()
.navigateToPageWithData(const Routines(), context);
_model?.loadRoutines();
}
void _onOpenKind(RoutineKind kind) async { void _onOpenKind(RoutineKind kind) async {
await GroundedNavigation() await GroundedNavigation()
@@ -260,7 +246,39 @@ class HomeState extends State<Home> implements ConnectHome {
.navigateToPage(NavigatorType.justOpen, const ExcuseReport(), context); .navigateToPage(NavigatorType.justOpen, const ExcuseReport(), context);
} }
void _onNavToday() {} /// The nav switches tabs in place; nothing is pushed. A tab is built the
/// first time it is opened and then kept, so it holds its scroll and data.
void _onSelectTab(int index) {
if (index == _tab) {
return;
}
setState(() {
_tab = index;
_opened.add(index);
});
// Back on Today: pick up whatever changed in the other tabs.
if (index == 0) {
_model?.loadRoutines();
_model?.loadGroups();
}
}
void _onNavToday() => _onSelectTab(0);
void _onNavRoutines() => _onSelectTab(1);
void _onNavGoals() => _onSelectTab(2);
void _onNavGroups() => _onSelectTab(3);
void _onNavProgress() => _onSelectTab(4);
void _onBackToToday(bool didPop, Object? result) {
if (!didPop) {
_onSelectTab(0);
}
}
void _onShowToday() { void _onShowToday() {
setState(() { setState(() {
@@ -288,9 +306,9 @@ class HomeState extends State<Home> implements ConnectHome {
return FadeTransition( return FadeTransition(
opacity: curve, opacity: curve,
child: SlideTransition( child: SlideTransition(
position: Tween<Offset>( position:
begin: const Offset(0, 0.12), end: Offset.zero) Tween<Offset>(begin: const Offset(0, 0.12), end: Offset.zero)
.animate(curve), .animate(curve),
child: child, child: child,
), ),
); );
@@ -389,85 +407,112 @@ class HomeState extends State<Home> implements ConnectHome {
final bool queueIsHome = final bool queueIsHome =
StandingEngine.showsOverdueQueueAsHome(_standing) && !_distressed; StandingEngine.showsOverdueQueueAsHome(_standing) && !_distressed;
return AnnotatedRegion<SystemUiOverlayStyle>( // Back on another tab returns to Today rather than leaving the app.
value: SystemUiOverlayStyle.light, return PopScope(
child: Scaffold( canPop: _tab == 0,
backgroundColor: colorAmbientMid, onPopInvokedWithResult: _onBackToToday,
body: AmbientBackground( child: AnnotatedRegion<SystemUiOverlayStyle>(
child: Stack( value: SystemUiOverlayStyle.light,
children: [ child: Scaffold(
SingleChildScrollView( backgroundColor: colorAmbientMid,
padding: const EdgeInsets.only(bottom: 130), body: AmbientBackground(
child: Column( child: Stack(
crossAxisAlignment: CrossAxisAlignment.stretch, children: [
mainAxisSize: MainAxisSize.min, IndexedStack(
children: [ index: _tab,
_header(), children: <Widget>[
Padding( _todayView(queueIsHome),
padding: const EdgeInsets.fromLTRB(20, 24, 20, 0), _opened.contains(1)
child: Column( ? const Routines(tab: true)
crossAxisAlignment: CrossAxisAlignment.stretch, : const SizedBox.shrink(),
mainAxisSize: MainAxisSize.min, _opened.contains(2)
children: [ ? const Goals(tab: true)
if (_distressed) : const SizedBox.shrink(),
_frosted(_distressBody()) _opened.contains(3)
else if (queueIsHome) ? const Groups(tab: true)
_frosted(_groundedBody()) : const SizedBox.shrink(),
else _opened.contains(4)
_planBody(), ? const Progress(tab: true)
], : const SizedBox.shrink(),
),
),
], ],
), ),
), Positioned(
Positioned( left: 0,
left: 0, right: 0,
right: 0, bottom: 0,
bottom: 0, child: FloatingNav(
child: FloatingNav( selected: _tab,
selected: 0, entries: <NavEntry>[
entries: <NavEntry>[ (
( icon: CupertinoIcons.house_fill,
icon: CupertinoIcons.house_fill, label: "Today",
label: "Today", onTap: _onNavToday
onTap: _onNavToday ),
), (
( icon: CupertinoIcons.square_stack_3d_up_fill,
icon: CupertinoIcons.square_stack_3d_up_fill, label: "Routines",
label: "Routines", onTap: _onNavRoutines
onTap: _onOpenRoutines ),
), (
( icon: CupertinoIcons.flag_fill,
icon: CupertinoIcons.flag_fill, label: "Goals",
label: "Goals", onTap: _onNavGoals
onTap: _onOpenGoals ),
), (
( icon: CupertinoIcons.person_3_fill,
icon: CupertinoIcons.person_3_fill, label: "Groups",
label: "Groups", onTap: _onNavGroups
onTap: _onOpenGroups ),
), (
( icon: CupertinoIcons.chart_bar_alt_fill,
icon: CupertinoIcons.chart_bar_alt_fill, label: "Progress",
label: "Progress", onTap: _onNavProgress
onTap: _onOpenProgress ),
), ],
], ).animate().fadeIn(duration: 400.ms, delay: 250.ms).slideY(
).animate().fadeIn(duration: 400.ms, delay: 250.ms).slideY( begin: 0.4,
begin: 0.4, end: 0,
end: 0, duration: 500.ms,
duration: 500.ms, delay: 250.ms,
delay: 250.ms, curve: Curves.easeOutCubic),
curve: Curves.easeOutCubic), ),
), ],
], ),
), ),
), ),
), ),
); );
} }
/// The Today tab: the forest header and the day under it.
Widget _todayView(bool queueIsHome) {
return SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 130),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
_header(),
Padding(
padding: const EdgeInsets.fromLTRB(20, 24, 20, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min,
children: [
if (_distressed)
_frosted(_distressBody())
else if (queueIsHome)
_frosted(_groundedBody())
else
_planBody(),
],
),
),
],
),
);
}
// ── Header ──────────────────────────────────────────────────────────────── // ── Header ────────────────────────────────────────────────────────────────
/// The deep forest block: who you are, the week as a streak, and the day /// The deep forest block: who you are, the week as a streak, and the day
@@ -475,6 +520,7 @@ class HomeState extends State<Home> implements ConnectHome {
Widget _header() { Widget _header() {
return Container( return Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(
borderRadius: BorderRadius.vertical(bottom: Radius.circular(20)),
gradient: LinearGradient( gradient: LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
@@ -568,6 +614,7 @@ class HomeState extends State<Home> implements ConnectHome {
return Container( return Container(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(12)),
color: colorHeaderPanel, color: colorHeaderPanel,
border: Border.all(color: colorHeaderLine), border: Border.all(color: colorHeaderLine),
), ),
@@ -589,7 +636,12 @@ class HomeState extends State<Home> implements ConnectHome {
curve: Curves.easeOutCubic, curve: Curves.easeOutCubic,
padding: const EdgeInsets.symmetric(vertical: 13), padding: const EdgeInsets.symmetric(vertical: 13),
alignment: Alignment.center, alignment: Alignment.center,
color: active ? colorHeaderSelected : colorHeaderSelected.withValues(alpha: 0), decoration: BoxDecoration(
color: active
? colorHeaderSelected
: colorHeaderSelected.withValues(alpha: 0),
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: text(label, 16, active ? TextType.Medium : TextType.Regular, child: text(label, 16, active ? TextType.Medium : TextType.Regular,
color: active ? colorWhite : colorHeaderMuted), color: active ? colorWhite : colorHeaderMuted),
), ),
@@ -603,12 +655,19 @@ class HomeState extends State<Home> implements ConnectHome {
final DateTime first = DateTime(now.year, now.month, now.day) final DateTime first = DateTime(now.year, now.month, now.day)
.subtract(const Duration(days: 6)); .subtract(const Duration(days: 6));
const List<String> days = <String>[ const List<String> days = <String>[
"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun" "Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat",
"Sun"
]; ];
return Container( return Container(
padding: const EdgeInsets.fromLTRB(18, 16, 18, 18), padding: const EdgeInsets.fromLTRB(18, 16, 18, 18),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(12)),
color: colorHeaderPanel, color: colorHeaderPanel,
border: Border.all(color: colorHeaderLine), border: Border.all(color: colorHeaderLine),
), ),
@@ -621,8 +680,7 @@ class HomeState extends State<Home> implements ConnectHome {
children: [ children: [
text("${_streak()}", 28, TextType.Medium, color: colorWhite), text("${_streak()}", 28, TextType.Medium, color: colorWhite),
const SizedBox(width: 8), const SizedBox(width: 8),
text("day streak", 16, TextType.Regular, text("day streak", 16, TextType.Regular, color: colorHeaderMuted),
color: colorHeaderMuted),
const Spacer(), const Spacer(),
Icon(Icons.eco, size: 32, color: colorLime), Icon(Icons.eco, size: 32, color: colorLime),
], ],
@@ -733,10 +791,10 @@ class HomeState extends State<Home> implements ConnectHome {
/// The week in numbers: the average kept, a bar per day, and the classes. /// The week in numbers: the average kept, a bar per day, and the classes.
Widget _weeklyNumbers() { Widget _weeklyNumbers() {
final Map<int, double> byDay = _week?.completionByWeekday ?? <int, double>{}; final Map<int, double> byDay =
final double average = byDay.isEmpty _week?.completionByWeekday ?? <int, double>{};
? 0 final double average =
: byDay.values.reduce((a, b) => a + b) / byDay.length; byDay.isEmpty ? 0 : byDay.values.reduce((a, b) => a + b) / byDay.length;
const List<String> letters = <String>["M", "T", "W", "T", "F", "S", "S"]; const List<String> letters = <String>["M", "T", "W", "T", "F", "S", "S"];
final DateTime now = DateTime.now(); final DateTime now = DateTime.now();
final List<int> order = <int>[ final List<int> order = <int>[
@@ -781,12 +839,17 @@ class HomeState extends State<Home> implements ConnectHome {
duration: const Duration(milliseconds: 400), duration: const Duration(milliseconds: 400),
curve: Curves.easeOutCubic, curve: Curves.easeOutCubic,
height: 90 * (byDay[day] ?? 0).clamp(0.04, 1.0), height: 90 * (byDay[day] ?? 0).clamp(0.04, 1.0),
color: (byDay[day] ?? 0) >= _keptFloor decoration: BoxDecoration(
? colorBarElective color: (byDay[day] ?? 0) >= _keptFloor
: colorHeaderLine, ? colorBarElective
: colorHeaderLine,
borderRadius: BorderRadius.all(Radius.circular(12)),
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
text(letters[day - 1], 13, text(
letters[day - 1],
13,
day == now.weekday day == now.weekday
? TextType.Medium ? TextType.Medium
: TextType.Regular, : TextType.Regular,
@@ -834,7 +897,9 @@ class HomeState extends State<Home> implements ConnectHome {
return Container( return Container(
width: 74, width: 74,
height: 74, height: 74,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: const BorderRadius.all(Radius.circular(16)),
border: Border.all(color: colorWhite, width: 2), border: Border.all(color: colorWhite, width: 2),
), ),
child: Opacity( child: Opacity(
@@ -899,7 +964,10 @@ class HomeState extends State<Home> implements ConnectHome {
const SizedBox(height: 9), const SizedBox(height: 9),
Container( Container(
height: 7, height: 7,
color: colorHeaderLine, decoration: BoxDecoration(
color: colorHeaderLine,
borderRadius: BorderRadius.all(Radius.circular(12)),
),
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: FractionallySizedBox( child: FractionallySizedBox(
widthFactor: fraction.clamp(0.0, 1.0), widthFactor: fraction.clamp(0.0, 1.0),
@@ -978,23 +1046,29 @@ class HomeState extends State<Home> implements ConnectHome {
child: Container( child: Container(
width: 54, width: 54,
height: 54, height: 54,
color: colorWhite, decoration: BoxDecoration(
color: colorWhite,
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: Icon(CupertinoIcons.xmark, size: 22, color: colorInk), child: Icon(CupertinoIcons.xmark, size: 22, color: colorInk),
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
Container( Container(
width: double.infinity, width: double.infinity,
color: colorWhite, decoration: BoxDecoration(
padding: EdgeInsets.fromLTRB( color: colorWhite,
18, 14, 18, MediaQuery.of(sheetContext).padding.bottom + 22), borderRadius: BorderRadius.all(Radius.circular(12)),
),
padding: EdgeInsets.fromLTRB(18, 14, 18,
MediaQuery.of(sheetContext).padding.bottom + 22),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Center( Center(
child: Container( child:
width: 40, height: 4, color: colorGrabber), Container(width: 40, height: 4, color: colorGrabber),
), ),
const SizedBox(height: 18), const SizedBox(height: 18),
text("Add to today", 24, TextType.Medium, text("Add to today", 24, TextType.Medium,
@@ -1023,12 +1097,12 @@ class HomeState extends State<Home> implements ConnectHome {
"Must-do", _onAddPickMustDo)), "Must-do", _onAddPickMustDo)),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: _addTile(CupertinoIcons.flag, child: _addTile(
"Goal", _onAddPickGoal)), CupertinoIcons.flag, "Goal", _onAddPickGoal)),
const SizedBox(width: 10), const SizedBox(width: 10),
Expanded( Expanded(
child: _addTile(CupertinoIcons.person_3, child: _addTile(CupertinoIcons.person_3, "Group",
"Group", _onAddPickGroup)), _onAddPickGroup)),
], ],
), ),
const SizedBox(height: 10), const SizedBox(height: 10),
@@ -1049,7 +1123,10 @@ class HomeState extends State<Home> implements ConnectHome {
onTap: onTap, onTap: onTap,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 22), padding: const EdgeInsets.symmetric(vertical: 22),
color: colorAmbientMid, decoration: BoxDecoration(
color: colorAmbientMid,
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
@@ -1067,7 +1144,10 @@ class HomeState extends State<Home> implements ConnectHome {
onTap: onTap, onTap: onTap,
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 22), padding: const EdgeInsets.symmetric(vertical: 22),
color: colorAmbientMid, decoration: BoxDecoration(
color: colorAmbientMid,
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
@@ -1093,7 +1173,9 @@ class HomeState extends State<Home> implements ConnectHome {
return Container( return Container(
width: size, width: size,
height: size, height: size,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: colorWhite, width: 2), border: Border.all(color: colorWhite, width: 2),
), ),
child: Stack( child: Stack(
@@ -1101,8 +1183,8 @@ class HomeState extends State<Home> implements ConnectHome {
children: [ children: [
SceneArt(palette: sceneWorkout, seed: sceneSeed(_user.name)), SceneArt(palette: sceneWorkout, seed: sceneSeed(_user.name)),
Center( Center(
child: text(initial, size * 0.4, TextType.Medium, child:
color: colorWhite), text(initial, size * 0.4, TextType.Medium, color: colorWhite),
), ),
], ],
), ),
@@ -1195,7 +1277,7 @@ class HomeState extends State<Home> implements ConnectHome {
padding: const EdgeInsets.fromLTRB(20, 24, 20, 24), padding: const EdgeInsets.fromLTRB(20, 24, 20, 24),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorFrost, color: colorFrost,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.all(Radius.circular(16)),
border: Border.all(color: colorGlassBorder), border: Border.all(color: colorGlassBorder),
), ),
child: child, child: child,
@@ -1300,12 +1382,12 @@ class HomeState extends State<Home> implements ConnectHome {
children: [ children: [
_category(sceneWorkout, kindIcon(RoutineKind.Workout), "Workout", 3, _category(sceneWorkout, kindIcon(RoutineKind.Workout), "Workout", 3,
() => _onOpenKind(RoutineKind.Workout)), () => _onOpenKind(RoutineKind.Workout)),
_category(sceneMeditation, kindIcon(RoutineKind.Meditation), _category(sceneMeditation, kindIcon(RoutineKind.Meditation), "Meditate",
"Meditate", 5, () => _onOpenKind(RoutineKind.Meditation)), 5, () => _onOpenKind(RoutineKind.Meditation)),
_category(sceneMustDo, kindIcon(RoutineKind.MustDo), "Must-do", 8, _category(sceneMustDo, kindIcon(RoutineKind.MustDo), "Must-do", 8,
() => _onOpenKind(RoutineKind.MustDo)), () => _onOpenKind(RoutineKind.MustDo)),
_category(goalScene(goalAccents[4]), CupertinoIcons.flag_fill, _category(goalScene(goalAccents[4]), CupertinoIcons.flag_fill, "Goals",
"Goals", 13, _onOpenGoals), 13, _onOpenGoals),
_category(goalScene(goalAccents[2]), CupertinoIcons.flame_fill, _category(goalScene(goalAccents[2]), CupertinoIcons.flame_fill,
"Training", 21, _onOpenTraining), "Training", 21, _onOpenTraining),
], ],
@@ -1324,6 +1406,7 @@ class HomeState extends State<Home> implements ConnectHome {
width: 74, width: 74,
height: 74, height: 74,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: colorWhite, width: 2.5), border: Border.all(color: colorWhite, width: 2.5),
boxShadow: const [ boxShadow: const [
BoxShadow( BoxShadow(
@@ -1333,7 +1416,7 @@ class HomeState extends State<Home> implements ConnectHome {
), ),
], ],
), ),
child: ClipRect( child: ClipOval(
child: SceneArt( child: SceneArt(
palette: palette, palette: palette,
seed: seed, seed: seed,
@@ -1376,7 +1459,7 @@ class HomeState extends State<Home> implements ConnectHome {
child: Container( child: Container(
width: 244, width: 244,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.all(Radius.circular(16)),
boxShadow: const [ boxShadow: const [
BoxShadow( BoxShadow(
color: Color(0x262A3A30), color: Color(0x262A3A30),
@@ -1386,7 +1469,7 @@ class HomeState extends State<Home> implements ConnectHome {
], ],
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.all(Radius.circular(16)),
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
@@ -1407,7 +1490,7 @@ class HomeState extends State<Home> implements ConnectHome {
children: [ children: [
GlassSurface( GlassSurface(
onDark: true, onDark: true,
radius: BorderRadius.zero, radius: BorderRadius.all(Radius.circular(14)),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5), horizontal: 10, vertical: 5),
child: text( child: text(
@@ -1515,7 +1598,9 @@ class HomeState extends State<Home> implements ConnectHome {
onTap: () => _onOpenGroupChat(group), onTap: () => _onOpenGroupChat(group),
child: Container( child: Container(
width: 250, width: 250,
clipBehavior: Clip.antiAlias,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(12)),
color: colorWhite, color: colorWhite,
boxShadow: const [ boxShadow: const [
BoxShadow( BoxShadow(
@@ -1550,9 +1635,13 @@ class HomeState extends State<Home> implements ConnectHome {
Container( Container(
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4), horizontal: 8, vertical: 4),
color: colorLime, decoration: BoxDecoration(
child: text("${group.unread} new", 10, color: colorLime,
TextType.Medium, borderRadius:
BorderRadius.all(Radius.circular(12)),
),
child: text(
"${group.unread} new", 10, TextType.Medium,
color: colorNight), color: colorNight),
), ),
const Spacer(), const Spacer(),
@@ -1618,9 +1707,8 @@ class HomeState extends State<Home> implements ConnectHome {
sectionBreak( sectionBreak(
"To-do", "To-do",
caption: "${open.length} open, no debt", caption: "${open.length} open, no debt",
trailing: done.isEmpty trailing:
? null done.isEmpty ? null : _viewAll("Clear done", _onClearDoneTodos),
: _viewAll("Clear done", _onClearDoneTodos),
), ),
GlassSurface( GlassSurface(
tint: colorGlassStrong, tint: colorGlassStrong,
@@ -1645,7 +1733,10 @@ class HomeState extends State<Home> implements ConnectHome {
child: Container( child: Container(
width: 48, width: 48,
height: 48, height: 48,
color: colorNight, decoration: BoxDecoration(
color: colorNight,
borderRadius: BorderRadius.all(Radius.circular(12)),
),
alignment: Alignment.center, alignment: Alignment.center,
child: Icon(CupertinoIcons.add, child: Icon(CupertinoIcons.add,
size: 22, color: colorLime), size: 22, color: colorLime),
@@ -1691,6 +1782,7 @@ class HomeState extends State<Home> implements ConnectHome {
width: 30, width: 30,
height: 30, height: 30,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(8)),
color: item.done ? colorPrimary : colorWhite, color: item.done ? colorPrimary : colorWhite,
border: Border.all( border: Border.all(
color: item.done color: item.done
@@ -1794,11 +1886,14 @@ class HomeState extends State<Home> implements ConnectHome {
"Your standing will recover as the debt decays.", "Your standing will recover as the debt decays.",
) )
else else
..._overdue.take(_standing == Standing.Lockdown ? 1 : _overdue.length) ..._overdue
.take(_standing == Standing.Lockdown ? 1 : _overdue.length)
.map(_commitmentRow), .map(_commitmentRow),
const SizedBox(height: 24), const SizedBox(height: 24),
roundedCornerButton( roundedCornerButton(
_standing == Standing.Lockdown ? "Deal with this one" : "Open the queue", _standing == Standing.Lockdown
? "Deal with this one"
: "Open the queue",
_onOpenOverdue, _onOpenOverdue,
background: standingColor(_standing), background: standingColor(_standing),
icon: CupertinoIcons.arrow_right, icon: CupertinoIcons.arrow_right,
@@ -1853,8 +1948,8 @@ class HomeState extends State<Home> implements ConnectHome {
const SizedBox(height: 24), const SizedBox(height: 24),
sectionBreak("Three things", caption: "that actually matter"), sectionBreak("Three things", caption: "that actually matter"),
..._plan ..._plan
.where((item) => .where(
item.commitmentClass == CommitmentClass.NonNegotiable) (item) => item.commitmentClass == CommitmentClass.NonNegotiable)
.take(3) .take(3)
.map(_commitmentRow), .map(_commitmentRow),
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -1871,7 +1966,7 @@ class HomeState extends State<Home> implements ConnectHome {
onTap: _onOpenOverdue, onTap: _onOpenOverdue,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.all(Radius.circular(16)),
gradient: const LinearGradient( gradient: const LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
@@ -1894,6 +1989,7 @@ class HomeState extends State<Home> implements ConnectHome {
height: 58, height: 58,
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle,
gradient: const RadialGradient( gradient: const RadialGradient(
center: Alignment(-0.3, -0.4), center: Alignment(-0.3, -0.4),
colors: <Color>[colorOverdueGlow, colorStandingGrounded], colors: <Color>[colorOverdueGlow, colorStandingGrounded],
@@ -1943,7 +2039,7 @@ class HomeState extends State<Home> implements ConnectHome {
child: Container( child: Container(
padding: const EdgeInsets.fromLTRB(22, 22, 22, 24), padding: const EdgeInsets.fromLTRB(22, 22, 22, 24),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.all(Radius.circular(16)),
gradient: const LinearGradient( gradient: const LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
@@ -1967,8 +2063,7 @@ class HomeState extends State<Home> implements ConnectHome {
const SizedBox(width: 6), const SizedBox(width: 6),
text("Pattern spotted", 12, TextType.Medium, color: colorLime), text("Pattern spotted", 12, TextType.Medium, color: colorLime),
const Spacer(), const Spacer(),
text("${cluster.occurrences}× this month", 11, text("${cluster.occurrences}× this month", 11, TextType.Regular,
TextType.Regular,
color: colorWhite.withValues(alpha: 0.5)), color: colorWhite.withValues(alpha: 0.5)),
], ],
), ),
@@ -2017,7 +2112,7 @@ class HomeState extends State<Home> implements ConnectHome {
width: 88, width: 88,
height: 88, height: 88,
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.all(Radius.circular(12)),
child: Opacity( child: Opacity(
opacity: ticked ? 0.45 : 1, opacity: ticked ? 0.45 : 1,
child: SceneArt( child: SceneArt(
@@ -2059,8 +2154,7 @@ class HomeState extends State<Home> implements ConnectHome {
const SizedBox(height: 5), const SizedBox(height: 5),
Row( Row(
children: [ children: [
Icon(CupertinoIcons.clock, size: 11, Icon(CupertinoIcons.clock, size: 11, color: colorInkSoft),
color: colorInkSoft),
const SizedBox(width: 4), const SizedBox(width: 4),
Flexible( Flexible(
child: text(formatWindow(item), 11, TextType.Regular, child: text(formatWindow(item), 11, TextType.Regular,
@@ -2069,11 +2163,9 @@ class HomeState extends State<Home> implements ConnectHome {
overflow: TextOverflow.ellipsis), overflow: TextOverflow.ellipsis),
), ),
const SizedBox(width: 10), const SizedBox(width: 10),
Icon(CupertinoIcons.timer, size: 11, Icon(CupertinoIcons.timer, size: 11, color: colorInkSoft),
color: colorInkSoft),
const SizedBox(width: 4), const SizedBox(width: 4),
text(formatMinutes(item.estMinutes), 11, text(formatMinutes(item.estMinutes), 11, TextType.Regular,
TextType.Regular,
color: colorInkSoft), color: colorInkSoft),
], ],
), ),
@@ -2101,6 +2193,7 @@ class HomeState extends State<Home> implements ConnectHome {
width: 44, width: 44,
height: 44, height: 44,
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle,
color: checked ? colorPrimary : colorWhite, color: checked ? colorPrimary : colorWhite,
border: Border.all( border: Border.all(
color: checked ? colorPrimary : accent.withValues(alpha: 0.35), color: checked ? colorPrimary : accent.withValues(alpha: 0.35),

View File

@@ -184,8 +184,8 @@ class LiveTaskState extends State<LiveTask>
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorSheetBackground, color: colorSheetBackground,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.zero, topLeft: Radius.circular(16),
topRight: Radius.zero, topRight: Radius.circular(16),
), ),
), ),
child: SingleChildScrollView( child: SingleChildScrollView(

View File

@@ -107,8 +107,8 @@ class LoginState extends State<Login> implements ConnectLogin {
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorPrimaryLight, color: colorPrimaryLight,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.zero, topLeft: Radius.circular(16),
topRight: Radius.zero, topRight: Radius.circular(16),
), ),
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,

View File

@@ -147,8 +147,8 @@ class OverdueQueueState extends State<OverdueQueue>
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorSheetBackground, color: colorSheetBackground,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.zero, topLeft: Radius.circular(16),
topRight: Radius.zero, topRight: Radius.circular(16),
), ),
), ),
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -166,7 +166,7 @@ class OverdueQueueState extends State<OverdueQueue>
margin: const EdgeInsets.only(bottom: 20), margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorGrey.withValues(alpha: 0.35), color: colorGrey.withValues(alpha: 0.35),
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
), ),
), ),
), ),
@@ -242,8 +242,8 @@ class OverdueQueueState extends State<OverdueQueue>
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorSheetBackground, color: colorSheetBackground,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.zero, topLeft: Radius.circular(16),
topRight: Radius.zero, topRight: Radius.circular(16),
), ),
), ),
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -261,7 +261,7 @@ class OverdueQueueState extends State<OverdueQueue>
margin: const EdgeInsets.only(bottom: 20), margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorGrey.withValues(alpha: 0.35), color: colorGrey.withValues(alpha: 0.35),
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
), ),
), ),
), ),
@@ -343,8 +343,8 @@ class OverdueQueueState extends State<OverdueQueue>
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorSheetBackground, color: colorSheetBackground,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.zero, topLeft: Radius.circular(16),
topRight: Radius.zero, topRight: Radius.circular(16),
), ),
), ),
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -360,7 +360,7 @@ class OverdueQueueState extends State<OverdueQueue>
margin: const EdgeInsets.only(bottom: 20), margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorGrey.withValues(alpha: 0.35), color: colorGrey.withValues(alpha: 0.35),
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
), ),
), ),
), ),
@@ -504,7 +504,7 @@ class OverdueQueueState extends State<OverdueQueue>
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorWhite.withValues(alpha: 0.10), color: colorWhite.withValues(alpha: 0.10),
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(12),
), ),
child: Row( child: Row(
children: [ children: [

View File

@@ -152,7 +152,7 @@ class ParentViewModel extends ChangeNotifier {
height: 64, height: 64,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorPrimaryDark, color: colorPrimaryDark,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
), ),
child: Icon(CupertinoIcons.wifi_slash, child: Icon(CupertinoIcons.wifi_slash,
size: 26, color: colorWhite), size: 26, color: colorWhite),
@@ -396,7 +396,7 @@ class ParentViewModel extends ChangeNotifier {
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorStandingGroundedBg, color: colorStandingGroundedBg,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(10),
), ),
child: Icon( child: Icon(
CupertinoIcons.exclamationmark_triangle_fill, CupertinoIcons.exclamationmark_triangle_fill,
@@ -544,8 +544,8 @@ class ParentViewModel extends ChangeNotifier {
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorSheetBackground, color: colorSheetBackground,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.zero, topLeft: Radius.circular(16),
topRight: Radius.zero, topRight: Radius.circular(16),
), ),
), ),
child: Column( child: Column(
@@ -559,7 +559,7 @@ class ParentViewModel extends ChangeNotifier {
margin: const EdgeInsets.only(top: 12, bottom: 8), margin: const EdgeInsets.only(top: 12, bottom: 8),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorGrey.withValues(alpha: 0.35), color: colorGrey.withValues(alpha: 0.35),
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
), ),
), ),
), ),
@@ -579,7 +579,7 @@ class ParentViewModel extends ChangeNotifier {
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: actionBgColor, color: actionBgColor,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(15),
), ),
child: Icon(actionIconData, child: Icon(actionIconData,
size: 24, color: actionColor), size: 24, color: actionColor),

View File

@@ -3,7 +3,11 @@ import 'package:flutter/material.dart';
import 'ProgressState.dart'; import 'ProgressState.dart';
class Progress extends StatefulWidget { class Progress extends StatefulWidget {
const Progress({super.key}); /// Shown as a tab inside Home: no back arrow, and room at the bottom
/// for the nav.
final bool tab;
const Progress({super.key, this.tab = false});
@override @override
State<Progress> createState() => ProgressState(); State<Progress> createState() => ProgressState();

View File

@@ -33,7 +33,13 @@ class ProgressState extends State<Progress> implements ConnectProgress {
bool _loaded = false; bool _loaded = false;
static const List<String> _weekLetters = <String>[ static const List<String> _weekLetters = <String>[
"M", "T", "W", "T", "F", "S", "S" "M",
"T",
"W",
"T",
"F",
"S",
"S"
]; ];
@override @override
@@ -130,7 +136,8 @@ class ProgressState extends State<Progress> implements ConnectProgress {
return Sheet( return Sheet(
eyebrow: "Over time", eyebrow: "Over time",
title: "Progress", title: "Progress",
onBack: _onBack, onBack: widget.tab ? null : _onBack,
bottomSpace: widget.tab ? 130 : 40,
action: chromeAction(CupertinoIcons.slider_horizontal_3, _onOpenTargets), action: chromeAction(CupertinoIcons.slider_horizontal_3, _onOpenTargets),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
@@ -175,7 +182,6 @@ class ProgressState extends State<Progress> implements ConnectProgress {
"Weekly report card", "Weekly report card",
_onOpenReportCard, _onOpenReportCard,
icon: CupertinoIcons.doc_text, icon: CupertinoIcons.doc_text,
radius: 0,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
@@ -190,7 +196,10 @@ class ProgressState extends State<Progress> implements ConnectProgress {
onTap: _onOpenTargets, onTap: _onOpenTargets,
child: Container( child: Container(
padding: const EdgeInsets.fromLTRB(18, 16, 14, 16), padding: const EdgeInsets.fromLTRB(18, 16, 14, 16),
color: colorNight, decoration: BoxDecoration(
color: colorNight,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Row( child: Row(
children: [ children: [
Icon(CupertinoIcons.scope, size: 22, color: colorLime), Icon(CupertinoIcons.scope, size: 22, color: colorLime),
@@ -226,8 +235,7 @@ class ProgressState extends State<Progress> implements ConnectProgress {
/// and the bars underneath. /// and the bars underneath.
Widget _headline() { Widget _headline() {
final int held = ProgressEngine.held(_current); final int held = ProgressEngine.held(_current);
final int target = final int target = ProgressEngine.routineTarget(_targets, _period, _start);
ProgressEngine.routineTarget(_targets, _period, _start);
final int delta = held - ProgressEngine.held(_previous); final int delta = held - ProgressEngine.held(_previous);
return card( return card(
@@ -270,14 +278,16 @@ class ProgressState extends State<Progress> implements ConnectProgress {
} }
Widget _deltaChip(int delta) { Widget _deltaChip(int delta) {
final Color tone = delta > 0 final Color tone =
? colorPositive delta > 0 ? colorPositive : (delta < 0 ? colorNegative : colorInkSoft);
: (delta < 0 ? colorNegative : colorInkSoft);
final String sign = delta > 0 ? "+" : ""; final String sign = delta > 0 ? "+" : "";
return Container( return Container(
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 5), padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 5),
color: tone.withValues(alpha: 0.12), decoration: BoxDecoration(
color: tone.withValues(alpha: 0.12),
borderRadius: const BorderRadius.all(Radius.circular(8)),
),
child: text("$sign$delta vs $_previousNoun", 11, TextType.Medium, child: text("$sign$delta vs $_previousNoun", 11, TextType.Medium,
color: tone), color: tone),
); );
@@ -319,9 +329,12 @@ class ProgressState extends State<Progress> implements ConnectProgress {
const SizedBox(height: 4), const SizedBox(height: 4),
Container( Container(
height: future ? 4 : 6 + 84 * fraction, height: future ? 4 : 6 + 84 * fraction,
color: isCurrent decoration: BoxDecoration(
? colorNight color: isCurrent
: (future ? colorMuted : colorChartGrid), ? colorNight
: (future ? colorMuted : colorChartGrid),
borderRadius: const BorderRadius.all(Radius.circular(4)),
),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
text( text(
@@ -343,8 +356,7 @@ class ProgressState extends State<Progress> implements ConnectProgress {
Widget _activeTile() { Widget _activeTile() {
final int minutes = ProgressEngine.minutes(_current); final int minutes = ProgressEngine.minutes(_current);
final int target = final int target = ProgressEngine.minuteTarget(_targets, _period, _start);
ProgressEngine.minuteTarget(_targets, _period, _start);
return _tile( return _tile(
CupertinoIcons.clock, CupertinoIcons.clock,
@@ -396,14 +408,20 @@ class ProgressState extends State<Progress> implements ConnectProgress {
return Container( return Container(
margin: const EdgeInsets.only(bottom: 10), margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.fromLTRB(14, 12, 16, 12), padding: const EdgeInsets.fromLTRB(14, 12, 16, 12),
color: colorCard, decoration: BoxDecoration(
color: colorCard,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Row( child: Row(
children: [ children: [
Container( Container(
width: 42, width: 42,
height: 42, height: 42,
alignment: Alignment.center, alignment: Alignment.center,
color: kindColor(kind).withValues(alpha: 0.14), decoration: BoxDecoration(
color: kindColor(kind).withValues(alpha: 0.14),
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Icon(kindIcon(kind), size: 20, color: kindColor(kind)), child: Icon(kindIcon(kind), size: 20, color: kindColor(kind)),
), ),
const SizedBox(width: 14), const SizedBox(width: 14),

View File

@@ -166,7 +166,6 @@ class TargetSetupState extends State<TargetSetup>
"Save targets", "Save targets",
_onSave, _onSave,
icon: CupertinoIcons.checkmark, icon: CupertinoIcons.checkmark,
radius: 0,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
], ],
@@ -225,8 +224,8 @@ class TargetSetupState extends State<TargetSetup>
if (_scheduled != _targets.routinesPerWeek) if (_scheduled != _targets.routinesPerWeek)
GestureDetector( GestureDetector(
onTap: _onMatchSchedule, onTap: _onMatchSchedule,
child: text("Match it", 12, TextType.Medium, child:
color: colorInk), text("Match it", 12, TextType.Medium, color: colorInk),
), ),
], ],
), ),
@@ -243,7 +242,10 @@ class TargetSetupState extends State<TargetSetup>
width: 48, width: 48,
height: 48, height: 48,
alignment: Alignment.center, alignment: Alignment.center,
color: enabled ? colorNight : colorMuted, decoration: BoxDecoration(
color: enabled ? colorNight : colorMuted,
borderRadius: const BorderRadius.all(Radius.circular(12)),
),
child: Icon(icon, size: 20, color: enabled ? colorLime : colorGrey), child: Icon(icon, size: 20, color: enabled ? colorLime : colorGrey),
), ),
); );

View File

@@ -17,11 +17,9 @@ class ViewProgress extends ParentViewModel {
/// The runs and targets are on the device, so there is no network guard /// The runs and targets are on the device, so there is no network guard
/// or spinner. Until a target is saved, the schedule stands in for it. /// or spinner. Until a target is saved, the schedule stands in for it.
void initialise() async { void initialise() async {
final List<RoutineRunRecord> records = final List<RoutineRunRecord> records = await getDataManager().getRunLog();
await getDataManager().getRunLog();
final ProgressTargets targets = final ProgressTargets targets = await getDataManager().getProgressTargets();
await getDataManager().getProgressTargets();
if (!targets.set) { if (!targets.set) {
try { try {
@@ -31,8 +29,8 @@ class ViewProgress extends ParentViewModel {
page: Pageable(0, 0, 50, 0), page: Pageable(0, 0, 50, 0),
), ),
)); ));
final int scheduled = ProgressEngine.scheduledPerWeek( final int scheduled =
getRoutineChainList(response.data)); ProgressEngine.scheduledPerWeek(getRoutineChainList(response.data));
if (scheduled > 0) { if (scheduled > 0) {
targets.routinesPerWeek = scheduled; targets.routinesPerWeek = scheduled;
} }

View File

@@ -20,8 +20,7 @@ class ViewTargetSetup extends ParentViewModel {
/// Targets are on the device; the routines are only needed to count the /// Targets are on the device; the routines are only needed to count the
/// schedule and to place reminders, so their failure is quiet. /// schedule and to place reminders, so their failure is quiet.
void initialise() async { void initialise() async {
final ProgressTargets targets = final ProgressTargets targets = await getDataManager().getProgressTargets();
await getDataManager().getProgressTargets();
try { try {
final response = await getDataManager().getMyRoutines(HistoryRequest( final response = await getDataManager().getMyRoutines(HistoryRequest(

View File

@@ -99,7 +99,7 @@ class ReportCardScreenState extends State<ReportCardScreen>
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorWhite.withValues(alpha: 0.08), color: colorWhite.withValues(alpha: 0.08),
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(12),
), ),
child: Row( child: Row(
children: [ children: [
@@ -128,7 +128,7 @@ class ReportCardScreenState extends State<ReportCardScreen>
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorWhite, color: colorWhite,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(12),
), ),
child: text(_report.grade, 26, TextType.Light, child: text(_report.grade, 26, TextType.Light,
color: colorPrimaryDark), color: colorPrimaryDark),

View File

@@ -402,7 +402,7 @@ class RoutineBuilderState extends State<RoutineBuilder>
height: 124, height: 124,
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(12),
gradient: selected gradient: selected
? LinearGradient( ? LinearGradient(
begin: Alignment.topLeft, begin: Alignment.topLeft,
@@ -424,6 +424,7 @@ class RoutineBuilderState extends State<RoutineBuilder>
color: selected color: selected
? colorWhite.withValues(alpha: 0.16) ? colorWhite.withValues(alpha: 0.16)
: kindBackground(kind), : kindBackground(kind),
shape: BoxShape.circle,
), ),
child: Icon(kindIcon(kind), child: Icon(kindIcon(kind),
size: 17, color: selected ? colorWhite : tone), size: 17, color: selected ? colorWhite : tone),
@@ -469,7 +470,7 @@ class RoutineBuilderState extends State<RoutineBuilder>
padding: const EdgeInsets.fromLTRB(14, 12, 14, 12), padding: const EdgeInsets.fromLTRB(14, 12, 14, 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: applied ? kindBackground(_routine.kind) : colorWhite, color: applied ? kindBackground(_routine.kind) : colorWhite,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: applied color: applied
? kindColor(_routine.kind).withValues(alpha: 0.4) ? kindColor(_routine.kind).withValues(alpha: 0.4)
@@ -514,6 +515,7 @@ class RoutineBuilderState extends State<RoutineBuilder>
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: on ? tone : colorWhite, color: on ? tone : colorWhite,
shape: BoxShape.circle,
border: Border.all(color: on ? tone : colorBorder), border: Border.all(color: on ? tone : colorBorder),
), ),
child: text(weekdayInitial(day), 13, child: text(weekdayInitial(day), 13,
@@ -574,7 +576,7 @@ class RoutineBuilderState extends State<RoutineBuilder>
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: kindBackground(_routine.kind), color: kindBackground(_routine.kind),
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(10),
), ),
child: text("${index + 1}", 12, TextType.Bold, color: tone), child: text("${index + 1}", 12, TextType.Bold, color: tone),
), ),

View File

@@ -462,7 +462,7 @@ class RoutineRunnerState extends State<RoutineRunner>
Widget _totalPill() { Widget _totalPill() {
return GlassSurface( return GlassSurface(
onDark: true, onDark: true,
radius: BorderRadius.zero, radius: BorderRadius.circular(14),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 11),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -501,7 +501,7 @@ class RoutineRunnerState extends State<RoutineRunner>
track: colorWhite.withValues(alpha: 0.22), track: colorWhite.withValues(alpha: 0.22),
child: GlassSurface( child: GlassSurface(
onDark: true, onDark: true,
radius: BorderRadius.zero, radius: BorderRadius.circular(14),
child: SizedBox( child: SizedBox(
width: 150, width: 150,
height: 150, height: 150,
@@ -557,7 +557,7 @@ class RoutineRunnerState extends State<RoutineRunner>
), ),
GlassSurface( GlassSurface(
onDark: true, onDark: true,
radius: BorderRadius.zero, radius: BorderRadius.circular(200),
child: SizedBox( child: SizedBox(
width: 120 + 50 * t, width: 120 + 50 * t,
height: 120 + 50 * t, height: 120 + 50 * t,
@@ -593,7 +593,7 @@ class RoutineRunnerState extends State<RoutineRunner>
children: [ children: [
GlassSurface( GlassSurface(
onDark: true, onDark: true,
radius: BorderRadius.zero, radius: BorderRadius.circular(14),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -615,7 +615,7 @@ class RoutineRunnerState extends State<RoutineRunner>
const SizedBox(width: 8), const SizedBox(width: 8),
GlassSurface( GlassSurface(
onDark: true, onDark: true,
radius: BorderRadius.zero, radius: BorderRadius.circular(14),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
child: text("Left app ${_run.backgroundedCount}×", 11, child: text("Left app ${_run.backgroundedCount}×", 11,
TextType.Medium, TextType.Medium,
@@ -732,6 +732,7 @@ class RoutineRunnerState extends State<RoutineRunner>
height: size, height: size,
padding: const EdgeInsets.all(2.5), padding: const EdgeInsets.all(2.5),
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle,
color: isCurrent ? colorNight : colorWhite, color: isCurrent ? colorNight : colorWhite,
boxShadow: const [ boxShadow: const [
BoxShadow( BoxShadow(
@@ -741,7 +742,7 @@ class RoutineRunnerState extends State<RoutineRunner>
), ),
], ],
), ),
child: ClipRect( child: ClipOval(
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
@@ -798,7 +799,7 @@ class RoutineRunnerState extends State<RoutineRunner>
margin: const EdgeInsets.only(right: 4), margin: const EdgeInsets.only(right: 4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: filled || active ? tone : tone.withValues(alpha: 0.15), color: filled || active ? tone : tone.withValues(alpha: 0.15),
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
), ),
); );
}), }),
@@ -898,7 +899,7 @@ class RoutineRunnerState extends State<RoutineRunner>
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorNight, color: colorNight,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
boxShadow: const [ boxShadow: const [
BoxShadow( BoxShadow(
color: Color(0x331A1F1C), color: Color(0x331A1F1C),
@@ -960,8 +961,8 @@ class RoutineRunnerState extends State<RoutineRunner>
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: colorSheetBackground, color: colorSheetBackground,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.zero, topLeft: Radius.circular(16),
topRight: Radius.zero, topRight: Radius.circular(16),
), ),
), ),
child: ListView( child: ListView(
@@ -989,7 +990,7 @@ class RoutineRunnerState extends State<RoutineRunner>
padding: const EdgeInsets.fromLTRB(12, 12, 14, 12), padding: const EdgeInsets.fromLTRB(12, 12, 14, 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: current ? kindBackground(_run.chain.kind) : colorWhite, color: current ? kindBackground(_run.chain.kind) : colorWhite,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(12),
border: Border.all( border: Border.all(
color: current ? tone.withValues(alpha: 0.35) : colorBorder, color: current ? tone.withValues(alpha: 0.35) : colorBorder,
), ),
@@ -1002,6 +1003,7 @@ class RoutineRunnerState extends State<RoutineRunner>
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: step.completed ? tone : colorSecondary, color: step.completed ? tone : colorSecondary,
shape: BoxShape.circle,
), ),
child: step.completed child: step.completed
? Icon(Icons.check_rounded, ? Icon(Icons.check_rounded,
@@ -1075,7 +1077,7 @@ class RoutineRunnerState extends State<RoutineRunner>
children: [ children: [
GlassSurface( GlassSurface(
onDark: true, onDark: true,
radius: BorderRadius.zero, radius: BorderRadius.circular(14),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 12, vertical: 7), horizontal: 12, vertical: 7),
child: text( child: text(
@@ -1099,7 +1101,7 @@ class RoutineRunnerState extends State<RoutineRunner>
track: colorWhite.withValues(alpha: 0.3), track: colorWhite.withValues(alpha: 0.3),
child: GlassSurface( child: GlassSurface(
onDark: true, onDark: true,
radius: BorderRadius.zero, radius: BorderRadius.circular(14),
child: SizedBox( child: SizedBox(
width: 158, width: 158,
height: 158, height: 158,
@@ -1131,7 +1133,7 @@ class RoutineRunnerState extends State<RoutineRunner>
), ),
const SizedBox(height: 22), const SizedBox(height: 22),
GlassSurface( GlassSurface(
radius: BorderRadius.zero, radius: BorderRadius.circular(16),
tint: colorGlassStrong, tint: colorGlassStrong,
padding: const EdgeInsets.fromLTRB(18, 16, 18, 16), padding: const EdgeInsets.fromLTRB(18, 16, 18, 16),
child: Row( child: Row(
@@ -1200,7 +1202,7 @@ class RoutineRunnerState extends State<RoutineRunner>
final int streak = ProgressEngine.streak(withThis, now); final int streak = ProgressEngine.streak(withThis, now);
return GlassSurface( return GlassSurface(
radius: BorderRadius.zero, radius: BorderRadius.circular(16),
tint: colorGlassStrong, tint: colorGlassStrong,
padding: const EdgeInsets.fromLTRB(18, 14, 18, 14), padding: const EdgeInsets.fromLTRB(18, 14, 18, 14),
child: Row( child: Row(

View File

@@ -7,7 +7,11 @@ class Routines extends StatefulWidget {
/// Opens pre-filtered to one kind; null shows the whole library. /// Opens pre-filtered to one kind; null shows the whole library.
final RoutineKind? kind; final RoutineKind? kind;
const Routines({super.key, this.kind}); /// Shown as a tab inside Home: no back arrow, and room at the bottom
/// for the nav.
final bool tab;
const Routines({super.key, this.kind, this.tab = false});
@override @override
State<Routines> createState() => RoutinesState(); State<Routines> createState() => RoutinesState();

View File

@@ -158,7 +158,7 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
backgroundColor: colorAmbientMid, backgroundColor: colorAmbientMid,
body: AmbientBackground( body: AmbientBackground(
child: SingleChildScrollView( child: SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 40), padding: EdgeInsets.only(bottom: widget.tab ? 130 : 40),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -265,7 +265,8 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
padding: const EdgeInsets.fromLTRB(16, 6, 16, 0), padding: const EdgeInsets.fromLTRB(16, 6, 16, 0),
child: Row( child: Row(
children: [ children: [
glassCircle(CupertinoIcons.arrow_left, _onBack), if (!widget.tab)
glassCircle(CupertinoIcons.arrow_left, _onBack),
const Spacer(), const Spacer(),
glassCircle(CupertinoIcons.add, _onBuild), glassCircle(CupertinoIcons.add, _onBuild),
], ],
@@ -330,7 +331,7 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
..sort((a, b) => a.startMinute.compareTo(b.startMinute)); ..sort((a, b) => a.startMinute.compareTo(b.startMinute));
return GlassSurface( return GlassSurface(
radius: BorderRadius.zero, radius: BorderRadius.circular(16),
tint: colorGlassStrong, tint: colorGlassStrong,
padding: const EdgeInsets.fromLTRB(18, 16, 18, 16), padding: const EdgeInsets.fromLTRB(18, 16, 18, 16),
child: Row( child: Row(
@@ -402,6 +403,7 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
height: 78, height: 78,
padding: const EdgeInsets.all(2.5), padding: const EdgeInsets.all(2.5),
decoration: BoxDecoration( decoration: BoxDecoration(
shape: BoxShape.circle,
color: selected ? colorNight : colorWhite, color: selected ? colorNight : colorWhite,
boxShadow: const [ boxShadow: const [
BoxShadow( BoxShadow(
@@ -411,7 +413,7 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
), ),
], ],
), ),
child: ClipRect( child: ClipOval(
child: kind == null child: kind == null
? Container( ? Container(
color: colorWhite, color: colorWhite,
@@ -471,7 +473,7 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
child: Container( child: Container(
width: 244, width: 244,
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(16),
boxShadow: const [ boxShadow: const [
BoxShadow( BoxShadow(
color: Color(0x262A3A30), color: Color(0x262A3A30),
@@ -481,7 +483,7 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
], ],
), ),
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(16),
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
@@ -502,7 +504,7 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
children: [ children: [
GlassSurface( GlassSurface(
onDark: true, onDark: true,
radius: BorderRadius.zero, radius: BorderRadius.circular(14),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5), horizontal: 10, vertical: 5),
child: text( child: text(
@@ -613,7 +615,7 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
width: 90, width: 90,
height: 90, height: 90,
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
child: SceneArt( child: SceneArt(
palette: scenePalette(routine.kind), palette: scenePalette(routine.kind),
seed: sceneSeed(routine.title), seed: sceneSeed(routine.title),
@@ -664,6 +666,7 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: tone, color: tone,
shape: BoxShape.circle,
), ),
child: Icon(Icons.check_rounded, child: Icon(Icons.check_rounded,
size: 22, color: colorWhite), size: 22, color: colorWhite),
@@ -715,8 +718,8 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: colorAmbientMid, color: colorAmbientMid,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.zero, topLeft: Radius.circular(16),
topRight: Radius.zero, topRight: Radius.circular(16),
), ),
), ),
clipBehavior: Clip.antiAlias, clipBehavior: Clip.antiAlias,
@@ -753,7 +756,7 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
height: 4, height: 4,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorWhite.withValues(alpha: 0.7), color: colorWhite.withValues(alpha: 0.7),
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
), ),
), ),
), ),
@@ -770,7 +773,7 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
children: [ children: [
GlassSurface( GlassSurface(
onDark: true, onDark: true,
radius: BorderRadius.zero, radius: BorderRadius.circular(14),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5), horizontal: 10, vertical: 5),
child: text(kindLabel(routine.kind), 10, child: text(kindLabel(routine.kind), 10,
@@ -780,7 +783,7 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
const SizedBox(width: 6), const SizedBox(width: 6),
GlassSurface( GlassSurface(
onDark: true, onDark: true,
radius: BorderRadius.zero, radius: BorderRadius.circular(14),
padding: const EdgeInsets.symmetric( padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 5), horizontal: 10, vertical: 5),
child: text( child: text(
@@ -814,7 +817,7 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
const SizedBox(height: 16), const SizedBox(height: 16),
], ],
GlassSurface( GlassSurface(
radius: BorderRadius.zero, radius: BorderRadius.circular(14),
tint: colorGlassStrong, tint: colorGlassStrong,
padding: const EdgeInsets.fromLTRB(16, 14, 16, 14), padding: const EdgeInsets.fromLTRB(16, 14, 16, 14),
child: Row( child: Row(
@@ -893,7 +896,7 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
padding: const EdgeInsets.fromLTRB(10, 10, 14, 10), padding: const EdgeInsets.fromLTRB(10, 10, 14, 10),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorWhite, color: colorWhite,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
), ),
child: Row( child: Row(
children: [ children: [
@@ -903,6 +906,7 @@ class RoutinesState extends State<Routines> implements ConnectRoutines {
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: tone.withValues(alpha: 0.10), color: tone.withValues(alpha: 0.10),
shape: BoxShape.circle,
), ),
child: text("${index + 1}", 12, TextType.Medium, color: tone), child: text("${index + 1}", 12, TextType.Medium, color: tone),
), ),

View File

@@ -67,8 +67,8 @@ class StepEditorState extends State<StepEditor> {
decoration: const BoxDecoration( decoration: const BoxDecoration(
color: colorSheetBackground, color: colorSheetBackground,
borderRadius: BorderRadius.only( borderRadius: BorderRadius.only(
topLeft: Radius.zero, topLeft: Radius.circular(16),
topRight: Radius.zero, topRight: Radius.circular(16),
), ),
), ),
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -234,7 +234,7 @@ class StepEditorState extends State<StepEditor> {
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration( decoration: BoxDecoration(
color: kindBackground(widget.kind), color: kindBackground(widget.kind),
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(10),
), ),
child: Row( child: Row(
children: [ children: [

View File

@@ -99,8 +99,8 @@ class SettingsState extends State<Settings> implements ConnectSettings {
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorSheetBackground, color: colorSheetBackground,
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
topLeft: Radius.zero, topLeft: Radius.circular(16),
topRight: Radius.zero, topRight: Radius.circular(16),
), ),
), ),
child: SingleChildScrollView( child: SingleChildScrollView(
@@ -118,7 +118,7 @@ class SettingsState extends State<Settings> implements ConnectSettings {
margin: const EdgeInsets.only(bottom: 20), margin: const EdgeInsets.only(bottom: 20),
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorGrey.withValues(alpha: 0.35), color: colorGrey.withValues(alpha: 0.35),
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
), ),
), ),
), ),
@@ -228,7 +228,7 @@ class SettingsState extends State<Settings> implements ConnectSettings {
color: _amnestyRemaining > 0 color: _amnestyRemaining > 0
? colorStandingGoodBg ? colorStandingGoodBg
: colorMuted, : colorMuted,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(12),
), ),
child: Icon( child: Icon(
CupertinoIcons.checkmark_shield_fill, CupertinoIcons.checkmark_shield_fill,

View File

@@ -239,7 +239,7 @@ class SplashState extends State<Splash> implements ConnectSplash {
], ],
), ),
GlassSurface( GlassSurface(
radius: BorderRadius.zero, radius: BorderRadius.circular(14),
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@@ -299,11 +299,11 @@ class SplashState extends State<Splash> implements ConnectSplash {
Widget _mark() { Widget _mark() {
return GlassSurface( return GlassSurface(
radius: BorderRadius.zero, radius: BorderRadius.circular(16),
padding: const EdgeInsets.all(10), padding: const EdgeInsets.all(10),
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(16),
boxShadow: <BoxShadow>[ boxShadow: <BoxShadow>[
BoxShadow( BoxShadow(
color: colorPrimary.withValues(alpha: 0.4), color: colorPrimary.withValues(alpha: 0.4),
@@ -342,7 +342,7 @@ class SplashState extends State<Splash> implements ConnectSplash {
alignment: alignment, alignment: alignment,
child: Container( child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
boxShadow: const [ boxShadow: const [
BoxShadow( BoxShadow(
color: Color(0x142A3A30), color: Color(0x142A3A30),
@@ -352,7 +352,7 @@ class SplashState extends State<Splash> implements ConnectSplash {
], ],
), ),
child: GlassSurface( child: GlassSurface(
radius: BorderRadius.zero, radius: BorderRadius.circular(14),
tint: colorGlassStrong, tint: colorGlassStrong,
padding: const EdgeInsets.fromLTRB(4, 4, 12, 4), padding: const EdgeInsets.fromLTRB(4, 4, 12, 4),
child: Row( child: Row(
@@ -424,7 +424,7 @@ class SplashState extends State<Splash> implements ConnectSplash {
/// The whole idea in three beats, on glass over the valley. /// The whole idea in three beats, on glass over the valley.
Widget _howItWorks() { Widget _howItWorks() {
return GlassSurface( return GlassSurface(
radius: BorderRadius.zero, radius: BorderRadius.circular(16),
tint: colorGlassStrong, tint: colorGlassStrong,
padding: const EdgeInsets.fromLTRB(14, 14, 14, 14), padding: const EdgeInsets.fromLTRB(14, 14, 14, 14),
child: Row( child: Row(
@@ -499,7 +499,7 @@ class SplashState extends State<Splash> implements ConnectSplash {
return Column( return Column(
children: [ children: [
GlassSurface( GlassSurface(
radius: BorderRadius.zero, radius: BorderRadius.circular(14),
padding: const EdgeInsets.all(3), padding: const EdgeInsets.all(3),
child: SizedBox( child: SizedBox(
width: 150, width: 150,
@@ -510,7 +510,7 @@ class SplashState extends State<Splash> implements ConnectSplash {
width: 150, width: 150,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorNight, color: colorNight,
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
), ),
).animate().scaleX( ).animate().scaleX(
begin: 0.04, begin: 0.04,

View File

@@ -71,7 +71,7 @@ class SessionExpiredState extends State<SessionExpired>
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorWhite.withValues(alpha: 0.10), color: colorWhite.withValues(alpha: 0.10),
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
), ),
child: Icon(CupertinoIcons.lock_fill, child: Icon(CupertinoIcons.lock_fill,
size: 24, color: colorWhite), size: 24, color: colorWhite),

View File

@@ -68,7 +68,7 @@ class UpdateMeState extends State<UpdateMe> implements ConnectUpdateMe {
alignment: Alignment.center, alignment: Alignment.center,
decoration: BoxDecoration( decoration: BoxDecoration(
color: colorWhite.withValues(alpha: 0.10), color: colorWhite.withValues(alpha: 0.10),
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
), ),
child: Icon(CupertinoIcons.arrow_up_circle_fill, child: Icon(CupertinoIcons.arrow_up_circle_fill,
size: 24, color: colorWhite), size: 24, color: colorWhite),

View File

@@ -48,12 +48,13 @@ class GroundedApp extends StatelessWidget {
fontSize: 17, fontSize: 17,
), ),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.zero, borderRadius: BorderRadius.circular(14),
), ),
), ),
// Material 3 rounds sheet tops by default; the app is square. // Material 3 rounds sheet tops by default; the app is square.
bottomSheetTheme: const BottomSheetThemeData( bottomSheetTheme: const BottomSheetThemeData(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.zero), shape: RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20))),
), ),
), ),
home: const Splash(), home: const Splash(),

View File

@@ -82,7 +82,7 @@ flutter:
- assets/images/ - assets/images/
- .env - .env
# General Sans (Fontshare, FFL) — see fonts/LICENSE-GeneralSans.txt # Inter (SIL OFL) — see fonts/LICENSE-Inter.txt. Light is Inter Display Regular.
fonts: fonts:
- family: GroundedBold - family: GroundedBold