overmark 0.1.0 copy "overmark: ^0.1.0" to clipboard
overmark: ^0.1.0 copied to clipboard

Spotlight coach marks for Flutter onboarding tours. Dims the screen, highlights any widget through a cut-out, and explains it in a fully customizable card. Zero dependencies.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:overmark/overmark.dart';
import 'package:shared_preferences/shared_preferences.dart';

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

/// Persists the "already seen" flag across launches.
///
/// Overmark ships without dependencies, so this adapter lives in your app.
class PrefsOvermarkStorage implements OvermarkStorage {
  String _key(String tourId) => 'overmark_$tourId';

  @override
  Future<bool> hasShown(String tourId) async =>
      (await SharedPreferences.getInstance()).getBool(_key(tourId)) ?? false;

  @override
  Future<void> markShown(String tourId) async =>
      (await SharedPreferences.getInstance()).setBool(_key(tourId), true);

  Future<void> reset(String tourId) async =>
      (await SharedPreferences.getInstance()).remove(_key(tourId));
}

class OvermarkExampleApp extends StatefulWidget {
  const OvermarkExampleApp({super.key});

  @override
  State<OvermarkExampleApp> createState() => _OvermarkExampleAppState();
}

class _OvermarkExampleAppState extends State<OvermarkExampleApp> {
  ThemeMode _mode = ThemeMode.light;

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Overmark',
      debugShowCheckedModeBanner: false,
      themeMode: _mode,
      theme: ThemeData(colorSchemeSeed: const Color(0xFF173F34)),
      darkTheme: ThemeData(
        colorSchemeSeed: const Color(0xFF173F34),
        brightness: Brightness.dark,
      ),
      home: HomePage(
        onToggleBrightness: () => setState(() {
          _mode = _mode == ThemeMode.light ? ThemeMode.dark : ThemeMode.light;
        }),
      ),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({required this.onToggleBrightness, super.key});

  final VoidCallback onToggleBrightness;

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  static const String _tourId = 'home_tour_v1';

  final PrefsOvermarkStorage _storage = PrefsOvermarkStorage();
  final GlobalKey _searchKey = GlobalKey();
  final GlobalKey _shelfKey = GlobalKey();
  final GlobalKey _libraryKey = GlobalKey();
  final GlobalKey _profileKey = GlobalKey();

  int _tab = 0;
  String _lastOutcome = '—';

  @override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addPostFrameCallback((_) => _showFirstRunTour());
  }

  List<OvermarkStep> get _steps => <OvermarkStep>[
        OvermarkStep(
          anchorKey: _searchKey,
          title: 'Find your next book',
          message: 'Search by title, author, or topic — or browse the shelves.',
        ),
        OvermarkStep(
          anchorKey: _shelfKey,
          title: 'Tap any cover',
          message:
              'Every book is a short visual summary. Tap one to preview it.',
        ),
        OvermarkStep(
          anchorKey: _libraryKey,
          title: 'Your library',
          message: 'Saved books and reading progress live here.',
          shape: OvermarkShape.circle,
        ),
        OvermarkStep(
          anchorKey: _profileKey,
          title: 'Your progress',
          message: 'Streaks and stats, all in one place.',
          shape: OvermarkShape.circle,
        ),
      ];

  Future<void> _showFirstRunTour() async {
    final OvermarkOutcome outcome = await Overmark.showOnce(
      context,
      tourId: _tourId,
      storage: _storage,
      steps: _steps,
    );
    _record(outcome);
  }

  Future<void> _replayDefault() async {
    _record(await Overmark.show(context, steps: _steps));
  }

  Future<void> _replayCustom() async {
    _record(
      await Overmark.show(
        context,
        labels: const OvermarkLabels(
          next: 'Lanjut',
          skip: 'Lewati',
          done: 'Mengerti',
        ),
        theme: OvermarkTheme(
          scrimColor: const Color(0xFF1B0E2B).withAlpha(220),
          spotlightBorderColor: const Color(0xFFFFC857),
          spotlightBorderWidth: 3,
          spotlightRadius: 28,
          spotlightPadding: const EdgeInsets.all(12),
          cardColor: const Color(0xFF2A1B3D),
          cardRadius: 28,
          titleStyle: const TextStyle(
            fontSize: 20,
            fontWeight: FontWeight.w800,
            color: Color(0xFFFFC857),
          ),
          messageStyle: const TextStyle(
            fontSize: 14,
            height: 1.5,
            color: Color(0xFFE7DFF5),
          ),
          buttonColor: const Color(0xFFFFC857),
          buttonLabelStyle: const TextStyle(
            fontSize: 14,
            fontWeight: FontWeight.w800,
            color: Color(0xFF2A1B3D),
          ),
          indicatorActiveColor: const Color(0xFFFFC857),
          indicatorColor: const Color(0xFF54407A),
        ),
        config: const OvermarkConfig(advanceOnBarrierTap: false),
        steps: _steps,
      ),
    );
  }

  Future<void> _resetFlag() async {
    await _storage.reset(_tourId);
    if (!mounted) return;
    ScaffoldMessenger.of(context).showSnackBar(
      const SnackBar(content: Text('Restart the app to see the tour again.')),
    );
  }

  void _record(OvermarkOutcome outcome) {
    if (!mounted) return;
    setState(() => _lastOutcome = outcome.name);
  }

  @override
  Widget build(BuildContext context) {
    final ColorScheme colors = Theme.of(context).colorScheme;

    return Scaffold(
      appBar: AppBar(
        title: const Text('Overmark'),
        actions: <Widget>[
          IconButton(
            onPressed: widget.onToggleBrightness,
            icon: const Icon(Icons.brightness_6_outlined),
            tooltip: 'Toggle brightness',
          ),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.fromLTRB(20, 8, 20, 32),
        children: <Widget>[
          TextField(
            key: _searchKey,
            decoration: InputDecoration(
              hintText: 'Search books, authors, topics',
              prefixIcon: const Icon(Icons.search),
              filled: true,
              border: OutlineInputBorder(
                borderRadius: BorderRadius.circular(14),
                borderSide: BorderSide.none,
              ),
            ),
          ),
          const SizedBox(height: 28),
          Text('Popular', style: Theme.of(context).textTheme.titleMedium),
          const SizedBox(height: 12),
          SizedBox(
            key: _shelfKey,
            height: 150,
            child: ListView.separated(
              scrollDirection: Axis.horizontal,
              itemCount: 8,
              separatorBuilder: (BuildContext context, int index) =>
                  const SizedBox(width: 12),
              itemBuilder: (BuildContext context, int index) => Container(
                width: 104,
                decoration: BoxDecoration(
                  color: colors.surfaceContainerHighest,
                  borderRadius: BorderRadius.circular(12),
                ),
                alignment: Alignment.center,
                child: Text('#${index + 1}'),
              ),
            ),
          ),
          const SizedBox(height: 32),
          FilledButton(
            onPressed: _replayDefault,
            child: const Text('Replay the tour'),
          ),
          const SizedBox(height: 12),
          OutlinedButton(
            onPressed: _replayCustom,
            child: const Text('Replay with a custom theme'),
          ),
          const SizedBox(height: 12),
          TextButton(
            onPressed: _resetFlag,
            child: const Text('Reset the once-only flag'),
          ),
          const SizedBox(height: 24),
          Center(
            child: Text(
              'Last outcome: $_lastOutcome',
              style: Theme.of(context).textTheme.bodySmall,
            ),
          ),
        ],
      ),
      bottomNavigationBar: NavigationBar(
        selectedIndex: _tab,
        onDestinationSelected: (int index) => setState(() => _tab = index),
        destinations: <Widget>[
          const NavigationDestination(
            icon: Icon(Icons.home_outlined),
            selectedIcon: Icon(Icons.home),
            label: 'Home',
          ),
          NavigationDestination(
            icon: Icon(Icons.bookmark_border, key: _libraryKey),
            selectedIcon: const Icon(Icons.bookmark),
            label: 'Library',
          ),
          NavigationDestination(
            icon: Icon(Icons.person_outline, key: _profileKey),
            selectedIcon: const Icon(Icons.person),
            label: 'Profile',
          ),
        ],
      ),
    );
  }
}
7
likes
0
points
226
downloads

Publisher

verified publisherfirdanumar.com

Weekly Downloads

Spotlight coach marks for Flutter onboarding tours. Dims the screen, highlights any widget through a cut-out, and explains it in a fully customizable card. Zero dependencies.

Repository (GitHub)
View/report issues

Topics

#onboarding #coachmark #tutorial #overlay #spotlight

License

unknown (license)

Dependencies

flutter

More

Packages that depend on overmark