live_markdown_editor 0.5.4 copy "live_markdown_editor: ^0.5.4" to clipboard
live_markdown_editor: ^0.5.4 copied to clipboard

A source-preserving Flutter Markdown editor with raw, live, and read modes.

example/lib/main.dart

import 'package:flutter/widgets.dart';
import 'package:live_markdown_editor/live_markdown_editor.dart';

import 'cupertino_tab.dart';
import 'demo_palette.dart';
import 'demo_presets.dart';
import 'demo_settings.dart';
import 'demo_widgets.dart';
import 'foundation_tab.dart';
import 'material_tab.dart';

void main() => runApp(const EditorDemoApp());

/// The manuscript casebook: a masthead, a live stats ticker, a catalog rail
/// of curated documents, and the editor hosted in a Foundation (neutral),
/// Material, or Cupertino shell — all editing one shared controller.
final class EditorDemoApp extends StatefulWidget {
  const EditorDemoApp({super.key});

  @override
  State<EditorDemoApp> createState() => _EditorDemoAppState();
}

final class _EditorDemoAppState extends State<EditorDemoApp> {
  late final MarkdownEditorController _controller = MarkdownEditorController(
    text: demoPresets.first.markdown,
  );
  DemoSettings _settings = const DemoSettings();
  var _host = 0;
  var _plate = 0;
  var _mode = MarkdownEditorMode.live;
  var _words = 0;
  var _chars = 0;
  var _lines = 1;
  String _action = '';

  @override
  void initState() {
    super.initState();
    _controller.addListener(_refreshStats);
    _refreshStats();
  }

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

  void _refreshStats() {
    final text = _controller.text;
    final words = text.trim().isEmpty
        ? 0
        : text.trim().split(RegExp(r'\s+')).length;
    final lines = '\n'.allMatches(text).length + 1;
    setState(() {
      _words = words;
      _chars = text.length;
      _lines = lines;
    });
  }

  void _loadPlate(int index) {
    setState(() {
      _plate = index;
      _controller.text = demoPresets[index].markdown;
      _controller.mode = _mode;
      _action = '';
    });
  }

  void _setMode(MarkdownEditorMode mode) {
    setState(() => _mode = mode);
    _controller.mode = mode;
  }

  void _onLink(String url) => setState(() => _action = 'Link · $url');

  @override
  Widget build(BuildContext context) {
    final palette = _settings.dark ? Palette.dark : Palette.light;
    final preset = demoPresets[_plate];
    final footnote = _action.isEmpty
        ? 'Right-click or long-press opens the platform\'s native menu — on web the browser owns it.'
        : '$_action — right-click or long-press opens the native menu.';
    return Directionality(
      textDirection: TextDirection.ltr,
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 260),
        color: palette.paper,
        child: SafeArea(
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.stretch,
            children: [
              _Masthead(
                palette: palette,
                mode: _mode,
                host: _host,
                dark: _settings.dark,
                onMode: _setMode,
                onHost: (h) => setState(() => _host = h),
                onDark: (v) =>
                    setState(() => _settings = _settings.copyWith(dark: v)),
              ),
              if (MediaQuery.sizeOf(context).height >= 640)
                _Ticker(
                  palette: palette,
                  words: _words,
                  chars: _chars,
                  lines: _lines,
                  mode: _mode,
                  host: _host,
                  direction: _settings.direction,
                  menu: 'native · browser on web',
                ),
              Expanded(
                child: LayoutBuilder(
                  builder: (context, constraints) {
                    if (constraints.maxWidth < 760) {
                      return Column(
                        crossAxisAlignment: CrossAxisAlignment.stretch,
                        children: [
                          _RailStrip(
                            palette: palette,
                            selected: _plate,
                            onSelect: _loadPlate,
                          ),
                          Container(height: 1, color: palette.hairline),
                          Expanded(child: _hostPage(palette, preset, footnote)),
                        ],
                      );
                    }
                    return Row(
                      crossAxisAlignment: CrossAxisAlignment.stretch,
                      children: [
                        _Rail(
                          palette: palette,
                          selected: _plate,
                          onSelect: _loadPlate,
                        ),
                        Container(width: 1, color: palette.hairline),
                        Expanded(child: _hostPage(palette, preset, footnote)),
                      ],
                    );
                  },
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  Widget _hostPage(Palette palette, DemoPreset preset, String footnote) {
    void onSettingsChanged(DemoSettings s) => setState(() => _settings = s);
    final shared = (
      controller: _controller,
      settings: _settings,
      palette: palette,
      preset: preset,
      footnote: footnote,
      onSettingsChanged: onSettingsChanged,
      onAction: _onLink,
    );
    return switch (_host) {
      1 => MaterialTab(
        controller: shared.controller,
        settings: shared.settings,
        palette: shared.palette,
        preset: shared.preset,
        footnote: shared.footnote,
        onSettingsChanged: shared.onSettingsChanged,
        onAction: shared.onAction,
      ),
      2 => CupertinoTab(
        controller: shared.controller,
        settings: shared.settings,
        palette: shared.palette,
        preset: shared.preset,
        footnote: shared.footnote,
        onSettingsChanged: shared.onSettingsChanged,
        onAction: shared.onAction,
      ),
      _ => FoundationTab(
        controller: shared.controller,
        settings: shared.settings,
        palette: shared.palette,
        preset: shared.preset,
        footnote: shared.footnote,
        onSettingsChanged: shared.onSettingsChanged,
        onAction: shared.onAction,
      ),
    };
  }
}

/// Masthead: kicker, bilingual display title, and the mode/host/dark controls.
class _Masthead extends StatelessWidget {
  const _Masthead({
    required this.palette,
    required this.mode,
    required this.host,
    required this.dark,
    required this.onMode,
    required this.onHost,
    required this.onDark,
  });

  final Palette palette;
  final MarkdownEditorMode mode;
  final int host;
  final bool dark;
  final ValueChanged<MarkdownEditorMode> onMode;
  final ValueChanged<int> onHost;
  final ValueChanged<bool> onDark;

  @override
  Widget build(BuildContext context) {
    // The masthead compacts on short viewports so chrome never dwarfs the
    // editor: two tiers below 640px/520px of window height.
    final height = MediaQuery.sizeOf(context).height;
    final compact = height < 640;
    final tiny = height < 520;
    final horizontal = tiny
        ? 20.0
        : compact
        ? 24.0
        : 28.0;
    return Container(
      padding: EdgeInsets.fromLTRB(
        horizontal,
        tiny
            ? 10
            : compact
            ? 12
            : 18,
        horizontal,
        tiny
            ? 10
            : compact
            ? 12
            : 16,
      ),
      decoration: BoxDecoration(
        border: Border(bottom: BorderSide(color: palette.hairline)),
      ),
      child: Stagger(
        children: [
          Row(
            children: [
              Text('LIVE MARKDOWN EDITOR', style: smallCaps(palette)),
              const Spacer(),
              Text('v0.5.4', style: smallCaps(palette)),
            ],
          ),
          SizedBox(
            height: tiny
                ? 4
                : compact
                ? 6
                : 10,
          ),
          Text(
            'The Manuscript',
            style: TextStyle(
              color: palette.ink,
              fontFamily: kLatinDisplay,
              fontSize: tiny
                  ? 26
                  : compact
                  ? 32
                  : 42,
              height: 1,
              fontVariations: const [FontVariation('wght', 700)],
            ),
          ),
          SizedBox(height: tiny ? 3 : 6),
          Text('Casebook — three hosts, one source', style: smallCaps(palette)),
          SizedBox(
            height: tiny
                ? 8
                : compact
                ? 10
                : 16,
          ),
          SingleChildScrollView(
            scrollDirection: Axis.horizontal,
            child: Row(
              children: [
                EditorialSegmented<MarkdownEditorMode>(
                  value: mode,
                  palette: palette,
                  onChanged: onMode,
                  options: const [
                    (MarkdownEditorMode.raw, 'RAW'),
                    (MarkdownEditorMode.live, 'LIVE'),
                    (MarkdownEditorMode.read, 'READ'),
                  ],
                ),
                const SizedBox(width: 18),
                EditorialSegmented<int>(
                  value: host,
                  palette: palette,
                  onChanged: onHost,
                  options: const [
                    (0, 'FOUNDATION'),
                    (1, 'MATERIAL'),
                    (2, 'CUPERTINO'),
                  ],
                ),
                const SizedBox(width: 18),
                EditorialSwitch(
                  value: dark,
                  palette: palette,
                  label: 'NIGHT',
                  onChanged: onDark,
                ),
              ],
            ),
          ),
        ],
      ),
    );
  }
}

/// The live ticker: document stats and current state, in tabular small caps.
class _Ticker extends StatelessWidget {
  const _Ticker({
    required this.palette,
    required this.words,
    required this.chars,
    required this.lines,
    required this.mode,
    required this.host,
    required this.direction,
    required this.menu,
  });

  final Palette palette;
  final int words;
  final int chars;
  final int lines;
  final MarkdownEditorMode mode;
  final int host;
  final MarkdownDirectionMode direction;
  final String menu;

  static const _hostNames = ['foundation', 'material', 'cupertino'];

  @override
  Widget build(BuildContext context) {
    Widget cell(String value, String label) => Text.rich(
      TextSpan(
        children: [
          TextSpan(
            text: value,
            style: smallCaps(palette, color: palette.accent),
          ),
          TextSpan(text: ' $label', style: smallCaps(palette)),
        ],
      ),
    );
    return Container(
      padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 8),
      decoration: BoxDecoration(
        border: Border(bottom: BorderSide(color: palette.hairline)),
      ),
      child: Wrap(
        spacing: 18,
        runSpacing: 6,
        crossAxisAlignment: WrapCrossAlignment.center,
        children: [
          cell('$words', 'WORD'),
          cell('$chars', 'CHAR'),
          cell('$lines', 'LINE'),
          cell(mode.name.toUpperCase(), 'MODE'),
          cell(_hostNames[host].toUpperCase(), 'HOST'),
          cell(direction.name.toUpperCase(), 'DIR'),
          cell(menu, 'MENU'),
        ],
      ),
    );
  }
}

/// Narrow-layout rail: the same plates as a horizontal strip.
class _RailStrip extends StatelessWidget {
  const _RailStrip({
    required this.palette,
    required this.selected,
    required this.onSelect,
  });

  final Palette palette;
  final int selected;
  final ValueChanged<int> onSelect;

  @override
  Widget build(BuildContext context) {
    return Container(
      height: 62,
      color: palette.paper,
      padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
      child: SingleChildScrollView(
        scrollDirection: Axis.horizontal,
        child: Row(
          children: [
            Padding(
              padding: const EdgeInsets.symmetric(horizontal: 10),
              child: Text('Casebook', style: smallCaps(palette)),
            ),
            for (var i = 0; i < demoPresets.length; i++)
              _StripTile(
                preset: demoPresets[i],
                palette: palette,
                selected: i == selected,
                onTap: () => onSelect(i),
              ),
          ],
        ),
      ),
    );
  }
}

class _StripTile extends StatelessWidget {
  const _StripTile({
    required this.preset,
    required this.palette,
    required this.selected,
    required this.onTap,
  });

  final DemoPreset preset;
  final Palette palette;
  final bool selected;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: onTap,
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 180),
        curve: Curves.easeOutCubic,
        margin: const EdgeInsets.symmetric(horizontal: 4, vertical: 4),
        padding: const EdgeInsets.symmetric(horizontal: 12),
        decoration: BoxDecoration(
          color: selected ? palette.card : const Color(0x00000000),
          border: Border.all(
            color: selected ? palette.accent : palette.hairline,
          ),
        ),
        child: Row(
          children: [
            Text(
              preset.number.toString().padLeft(2, '0'),
              style: TextStyle(
                color: selected ? palette.accent : palette.muted,
                fontFamily: kLatinDisplay,
                fontSize: 15,
                height: 1,
                fontVariations: const [FontVariation('wght', 700)],
              ),
            ),
            const SizedBox(width: 8),
            Text(
              preset.title,
              style: TextStyle(
                color: selected ? palette.ink : palette.muted,
                fontFamily: kLatinBody,
                fontSize: 13,
                fontWeight: FontWeight.w600,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

/// The catalog rail: the casebook plates, one active at a time.
class _Rail extends StatelessWidget {
  const _Rail({
    required this.palette,
    required this.selected,
    required this.onSelect,
  });

  final Palette palette;
  final int selected;
  final ValueChanged<int> onSelect;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: 236,
      color: palette.paper,
      child: ListView(
        padding: const EdgeInsets.only(bottom: 16),
        children: [
          Padding(
            padding: const EdgeInsets.fromLTRB(24, 18, 24, 10),
            child: Text('Casebook', style: smallCaps(palette)),
          ),
          for (var i = 0; i < demoPresets.length; i++)
            _PlateTile(
              preset: demoPresets[i],
              palette: palette,
              selected: i == selected,
              onTap: () => onSelect(i),
            ),
          Padding(
            padding: const EdgeInsets.fromLTRB(24, 14, 24, 0),
            child: Text(
              'Every plate loads into the same editor',
              style: TextStyle(
                color: palette.muted,
                fontFamily: kLatinBody,
                fontSize: 11.5,
                height: 1.4,
              ),
            ),
          ),
        ],
      ),
    );
  }
}

class _PlateTile extends StatelessWidget {
  const _PlateTile({
    required this.preset,
    required this.palette,
    required this.selected,
    required this.onTap,
  });

  final DemoPreset preset;
  final Palette palette;
  final bool selected;
  final VoidCallback onTap;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      behavior: HitTestBehavior.opaque,
      onTap: onTap,
      child: AnimatedContainer(
        duration: const Duration(milliseconds: 180),
        curve: Curves.easeOutCubic,
        padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
        decoration: BoxDecoration(
          color: selected ? palette.card : const Color(0x00000000),
          border: Border(
            left: BorderSide(
              width: 3,
              color: selected ? palette.accent : palette.paper,
            ),
          ),
        ),
        child: Row(
          children: [
            Text(
              preset.number.toString().padLeft(2, '0'),
              style: TextStyle(
                color: selected ? palette.accent : palette.muted,
                fontFamily: kLatinDisplay,
                fontSize: 18,
                height: 1,
                fontVariations: const [FontVariation('wght', 700)],
              ),
            ),
            const SizedBox(width: 12),
            Expanded(
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text(
                    preset.title,
                    style: TextStyle(
                      color: selected ? palette.ink : palette.muted,
                      fontFamily: kLatinBody,
                      fontSize: 14,
                      height: 1.25,
                      fontWeight: FontWeight.w600,
                    ),
                  ),
                  const SizedBox(height: 2),
                  Text(
                    preset.subtitle,
                    style: TextStyle(
                      color: selected ? palette.accent : palette.muted,
                      fontFamily: kArabicBody,
                      fontSize: 11.5,
                      height: 1.3,
                      fontWeight: FontWeight.w500,
                    ),
                  ),
                ],
              ),
            ),
          ],
        ),
      ),
    );
  }
}
3
likes
0
points
746
downloads

Publisher

unverified uploader

Weekly Downloads

A source-preserving Flutter Markdown editor with raw, live, and read modes.

Homepage
Repository (GitLab)
View/report issues

Topics

#markdown #editor #text-editor #flutter #widget

License

unknown (license)

Dependencies

flutter, native_adaptive_toolbox

More

Packages that depend on live_markdown_editor