window_title 1.0.0 copy "window_title: ^1.0.0" to clipboard
window_title: ^1.0.0 copied to clipboard

Set the window, browser tab and app switcher title at runtime, with a WindowTitle widget that follows your navigation.

example/lib/main.dart

import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:window_title/window_title.dart';

void main() {
  // The app-level title. It is the whole title when nothing else contributes
  // one, and the last segment when something does.
  WindowTitle.base = 'Demo';
  runApp(const DemoApp());
}

// ────────────────────────────────────────────────────────────────── routing ──

final GoRouter _router = GoRouter(
  routes: <RouteBase>[
    ShellRoute(
      builder: (_, _, Widget child) => DemoScaffold(child: child),
      routes: <RouteBase>[
        GoRoute(
          path: '/',
          // A route's builder is the natural home for a screen title: the
          // widget is mounted for exactly as long as the route is.
          builder: (_, _) =>
              const WindowTitle(title: 'Home', child: HomeScreen()),
        ),
        GoRoute(
          path: '/items',
          builder: (_, _) =>
              const WindowTitle(title: 'Items', child: ItemsScreen()),
          routes: <RouteBase>[
            GoRoute(
              path: ':id',
              builder: (_, GoRouterState state) => WindowTitle(
                // Starts generic, and is refined from deeper in the tree once
                // the item has loaded.
                title: 'Item',
                child: ItemScreen(id: state.pathParameters['id']!),
              ),
            ),
          ],
        ),
        GoRoute(
          path: '/settings',
          builder: (_, _) =>
              const WindowTitle(title: 'Settings', child: SettingsScreen()),
        ),
      ],
    ),
  ],
);

class DemoApp extends StatelessWidget {
  const DemoApp({super.key});

  @override
  Widget build(BuildContext context) => MaterialApp.router(
    // MaterialApp.router leaves `title` null, which is what you want: the
    // package owns the title from here on.
    routerConfig: _router,
    theme: ThemeData(colorSchemeSeed: Colors.indigo),
    darkTheme: ThemeData(
      colorSchemeSeed: Colors.indigo,
      brightness: Brightness.dark,
    ),
  );
}

// ─────────────────────────────────────────────────────────────────── chrome ──

class DemoScaffold extends StatefulWidget {
  const DemoScaffold({super.key, required this.child});

  final Widget child;

  @override
  State<DemoScaffold> createState() => _DemoScaffoldState();
}

class _DemoScaffoldState extends State<DemoScaffold> {
  final WindowTitleTicker _ticker = WindowTitleTicker();

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

  @override
  Widget build(BuildContext context) {
    final String location = GoRouterState.of(context).uri.path;
    return Scaffold(
      appBar: AppBar(
        // Mirrors the real window title, so the demo is legible on platforms
        // that do not show one.
        title: ValueListenableBuilder<String>(
          valueListenable: WindowTitle.current,
          builder: (_, String title, _) => Text(
            title.isEmpty ? '(no title yet)' : title,
            style: const TextStyle(
              fontFeatures: <FontFeature>[FontFeature.tabularFigures()],
            ),
          ),
        ),
        actions: <Widget>[
          TextButton.icon(
            onPressed: () => setState(
              () => _ticker.isRunning ? _ticker.stop() : _ticker.start(),
            ),
            icon: Icon(_ticker.isRunning ? Icons.stop : Icons.play_arrow),
            label: Text(_ticker.isRunning ? 'Stop ticker' : 'Start ticker'),
          ),
        ],
      ),
      body: widget.child,
      bottomNavigationBar: NavigationBar(
        selectedIndex: switch (location) {
          final String p when p.startsWith('/items') => 1,
          '/settings' => 2,
          _ => 0,
        },
        onDestinationSelected: (int i) =>
            context.go(<String>['/', '/items', '/settings'][i]),
        destinations: const <Widget>[
          NavigationDestination(icon: Icon(Icons.home), label: 'Home'),
          NavigationDestination(icon: Icon(Icons.list), label: 'Items'),
          NavigationDestination(icon: Icon(Icons.settings), label: 'Settings'),
        ],
      ),
    );
  }
}

// ────────────────────────────────────────────────────────────────── screens ──

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) => const Padding(
    padding: EdgeInsets.all(24),
    child: Text(
      'The title in the bar above mirrors the real window or tab title.\n\n'
      '· Move between tabs to see the title follow the route.\n'
      '· Open an item to see a title refined after an async load.\n'
      '· Open the dialog to see a title stack on top of a screen.\n'
      '· Start the ticker to see a decoration survive navigation.\n'
      '· Use Settings to change how segments are composed.',
    ),
  );
}

class ItemsScreen extends StatelessWidget {
  const ItemsScreen({super.key});

  @override
  Widget build(BuildContext context) => ListView(
    children: <Widget>[
      for (final String id in <String>['1', '2', '3'])
        ListTile(
          title: Text('Item $id'),
          onTap: () => context.go('/items/$id'),
        ),
      const Divider(),
      ListTile(
        leading: const Icon(Icons.open_in_new),
        title: const Text('Open a dialog'),
        onTap: () => showDialog<void>(
          context: context,
          builder: (_) => const WindowTitle(
            title: 'Dialog',
            child: AlertDialog(
              content: Text(
                'A dialog is its own route, so its title wins while it is open '
                'and is dropped when it closes.',
              ),
            ),
          ),
        ),
      ),
    ],
  );
}

class ItemScreen extends StatefulWidget {
  const ItemScreen({super.key, required this.id});

  final String id;

  @override
  State<ItemScreen> createState() => _ItemScreenState();
}

class _ItemScreenState extends State<ItemScreen> {
  String? _name;

  @override
  void initState() {
    super.initState();
    // Stands in for loading the real name from somewhere.
    Future<void>.delayed(const Duration(seconds: 1), () {
      if (mounted) {
        setState(() => _name = 'Item ${widget.id}');
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    final String? name = _name;
    return Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: <Widget>[
          // An empty title contributes nothing, so the route's generic 'Item'
          // keeps showing while this loads. Once the name arrives it takes
          // over, and `standalone` drops the generic one.
          WindowTitle(title: name ?? '', standalone: name != null),
          Text(
            name ?? 'Loading…',
            style: Theme.of(context).textTheme.headlineSmall,
          ),
        ],
      ),
    );
  }
}

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

  @override
  State<SettingsScreen> createState() => _SettingsScreenState();
}

class _SettingsScreenState extends State<SettingsScreen> {
  @override
  Widget build(BuildContext context) => ListView(
    children: <Widget>[
      ListTile(
        title: const Text('Separator'),
        subtitle: Text('"${WindowTitle.separator}"'),
        trailing: SegmentedButton<String>(
          segments: const <ButtonSegment<String>>[
            ButtonSegment<String>(value: ' · ', label: Text('·')),
            ButtonSegment<String>(value: ' — ', label: Text('—')),
            ButtonSegment<String>(value: ' | ', label: Text('|')),
          ],
          selected: <String>{WindowTitle.separator},
          onSelectionChanged: (Set<String> value) =>
              setState(() => WindowTitle.separator = value.first),
        ),
      ),
      SwitchListTile(
        title: const Text('Limit to 2 segments'),
        value: WindowTitle.maxSegments == 2,
        onChanged: (bool value) =>
            setState(() => WindowTitle.maxSegments = value ? 2 : null),
      ),
      SwitchListTile(
        title: const Text('Add a prefix'),
        subtitle: const Text('"(3) "'),
        value: WindowTitle.prefix.isNotEmpty,
        onChanged: (bool value) =>
            setState(() => WindowTitle.prefix = value ? '(3) ' : ''),
      ),
      ListTile(
        title: const Text('Override the whole title for 3 seconds'),
        onTap: () {
          WindowTitle.overrideTitle = 'Overridden';
          Future<void>.delayed(
            const Duration(seconds: 3),
            () => WindowTitle.overrideTitle = null,
          );
        },
      ),
    ],
  );
}
0
likes
155
points
1.1k
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Set the window, browser tab and app switcher title at runtime, with a WindowTitle widget that follows your navigation.

Repository (GitHub)
View/report issues

Topics

#window #title #navigation #desktop #web

License

MIT (license)

Dependencies

flutter, flutter_web_plugins, web

More

Packages that depend on window_title

Packages that implement window_title