build method

  1. @override
Widget build(
  1. BuildContext context
)
override

///////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////

Implementation

// BUILD
////////////////////////////////////////////////////////////////////////

@override
Widget build(BuildContext context) {
  // Build the Timer Widget.

  ////////////////////////////////////
  // APP BUTTONS
  ////////////////////////////////////

  // We begin with building the six main app buttons that are displayed on the
  // home screen. Each button has a label, tootltip, and a callback for when
  // the button is pressed.

  final startButton = AppButton(
    title: 'Start',
    tooltip: '''

Tap here to begin a session of silence for ${(_duration / 60).round()}
minutes, beginning and ending with three chimes. The blue progress
circle indicates an active session.

'''
        .trim(),
    onPressed: () {
      logMessage('Start Session');
      if (mounted) {
        _reset();
        dingDong(_player);
        _controller.restart(duration: _duration);
        _stopSleep();
        setState(() {
          _sessionType = 'bell';
          _startTime = DateTime.now();
        });
      }
    },
    fontWeight: FontWeight.bold,
    backgroundColor: Colors.lightGreenAccent.shade100,
  );

  final pauseResumeButton = AppButton(
    title: _isPaused ? 'Resume' : 'Pause',
    tooltip: _isPaused
        ? '''

Tap here to Resume the timer and the audio from where they were paused.

'''
            .trim()
        : '''

Tap here to Pause the timer and the audio. They can be resumed with a press
of the Resume button.

'''
            .trim(),
    onPressed: () {
      setState(() {
        if (_isPaused) {
          // Resume the timer and audio.
          _controller.resume();
          _player.resume();
          _stopSleep();
          _isPaused = false;
        } else {
          // Pause the timer and audio.
          _controller.pause();
          _player.pause();
          _allowSleep();
          _isPaused = true;
        }
      });
    },
    backgroundColor: Colors.grey[200] ?? Colors.grey,
  );

  final introButton = AppButton(
    title: 'Intro',
    tooltip: '''

Tap here to play a short introduction for a session.  After the introduction a
${(_duration / 60).round()} minute session of silence will begin and end with
three dings. The blue progress circle indicates an active session.

'''
        .trim(),
    onPressed: _intro,
    fontWeight: FontWeight.bold,
    backgroundColor: Colors.blue.shade100,
  );

  final guidedButton = AppButton(
    title: 'Guided',
    tooltip: '''

Tap here to play a ${10 + (_duration / 60).round()} minute guided session.
The session begins with instructions for meditation from John Main.
Introductory music is followed by three chimes and a ${(_duration / 60).round()}
minute silent session which is then finished with another three chimes. The
blue progress circle indicates an active session.  The
audio may take a little time to download for the Web version.

'''
        .trim(),
    onPressed: _guided,
    fontWeight: FontWeight.bold,
    backgroundColor: Colors.purple.shade100,
  );

  ////////////////////////////////////
  // DURATION CHOICE
  ////////////////////////////////////

  final Widget durationChoice = Wrap(
    spacing: 8.0, // Gap between adjacent chips.
    runSpacing: 4.0, // Gap between lines.
    children: [5, 10, 15, 20, 25, 30].map((number) {
      final isSelected = _duration == number * 60;
      return ChoiceChip(
        label: Text(
          number.toString(),
          style: TextStyle(
            color: isSelected
                ? Theme.of(context).colorScheme.primary
                : Theme.of(context).colorScheme.onSurface,
            fontWeight: isSelected ? FontWeight.w700 : FontWeight.w500,
          ),
        ),
        selected: isSelected,
        backgroundColor: Theme.of(context).colorScheme.surface,
        selectedColor:
            Theme.of(context).colorScheme.primary.withValues(alpha: 0.15),
        side: BorderSide(
          color: isSelected
              ? Theme.of(context).colorScheme.primary.withValues(alpha: 0.4)
              : Colors.grey.withValues(alpha: 0.3),
        ),
        showCheckmark: false, // This will hide the tick mark.
        onSelected: (selected) {
          if (selected) {
            setState(() {
              _duration = number * 60;
              debugPrint('CHOOSE: duration $_duration');
              _controller.restart(duration: _duration);
              _controller.pause();
              _player.stop();
              _allowSleep();
            });
            _saveSettings();
          }
        },
      );
    }).toList(),
  );

  ////////////////////////////////////
  // RETURN
  ////////////////////////////////////

  final timerDisplay = AppCircularCountDownTimer(
    duration: _duration,
    controller: _controller,
    onComplete: _complete,
  );

  final buttonsMatrix = Container(
    padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 24),
    decoration: BoxDecoration(
      // 20260220 gjw Choose a darker background for the active button
      // area. This darker background, same as title bar and timer central,
      // more clearly distinguishes the buttons matrix from the app
      // background.

      color: background,
      // color: Colors.white.withValues(alpha: 0.5),
      borderRadius: BorderRadius.circular(32),
      border: Border.all(
        color: Colors.white.withValues(alpha: 0.5),
      ),
    ),
    child: Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            introButton,
            const SizedBox(width: widthSpacer),
            startButton,
          ],
        ),
        const SizedBox(height: heightSpacer),
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            guidedButton,
            const SizedBox(width: widthSpacer),
            pauseResumeButton,
          ],
        ),
        const SizedBox(height: 32),
        Text(
          'Select duration (minutes)',
          style: TextStyle(
            fontSize: 18.0,
            fontWeight: FontWeight.w600,
            color:
                Theme.of(context).colorScheme.primary.withValues(alpha: 0.7),
          ),
        ),
        const SizedBox(height: 12),
        durationChoice,
        const SizedBox(height: 32),
        Padding(
          padding: const EdgeInsets.symmetric(horizontal: 16.0),
          child: Column(
            children: [
              TextField(
                controller: _titleController,
                decoration: InputDecoration(
                  labelText: 'Title',
                  hintText: 'Enter session title (optional)',
                  prefixIcon: const Icon(Icons.label_outline),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(16),
                    borderSide: BorderSide.none,
                  ),
                  filled: true,
                  fillColor: Colors.white.withValues(alpha: 0.8),
                ),
              ),
              const SizedBox(height: 12),
              TextField(
                controller: _descriptionController,
                decoration: InputDecoration(
                  labelText: 'Description',
                  hintText: 'Enter session description (optional)',
                  prefixIcon: const Icon(Icons.notes),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.circular(16),
                    borderSide: BorderSide.none,
                  ),
                  filled: true,
                  fillColor: Colors.white.withValues(alpha: 0.8),
                ),
                maxLines: 2,
              ),
            ],
          ),
        ),
      ],
    ),
  );

  return OrientationBuilder(
    builder: (context, orientation) {
      if (orientation == Orientation.portrait) {
        return SingleChildScrollView(
          child: Padding(
            // Add some top and bottom padding so the timer is not clipped at the
            // top nor the chips at the bottom.
            padding: const EdgeInsets.only(top: 10, bottom: 5),
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                const SizedBox(height: 2 * heightSpacer),
                timerDisplay,
                const SizedBox(height: 2 * heightSpacer),
                buttonsMatrix,
              ],
            ),
          ),
        );
      } else {
        return SingleChildScrollView(
          child: Padding(
            padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 10),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.center,
              crossAxisAlignment: CrossAxisAlignment.center,
              children: [
                Expanded(child: Center(child: timerDisplay)),
                const SizedBox(width: 2 * widthSpacer),
                Expanded(child: Center(child: buttonsMatrix)),
              ],
            ),
          ),
        );
      }
    },
  );
}