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

PlatformWindows

Flutter access to desktop OS surfaces: window frame, system tray, login item, deep links, notifications and handoff to the default app, on Windows, macOS and Linux.

dovetail_platform_channel #

O canal entre o Flutter e o sistema operacional no desktop. Janela, bandeja, item de login, deep link, notificação e entrega ao aplicativo padrão — em Windows, macOS e Linux.

Quando o Tauri sai, o webview vai com ele, e com o webview vão coisas que ninguém tinha listado: a janela sem borda, o ícone de bandeja, abrir um link no navegador do sistema. Este package é o lugar dessas capacidades.

A fronteira #

app Flutter desktop            <- a regra de negócio, e só aqui
  ├── desktop_core_bridge      <- canal para o núcleo Rust
  ├── dovetail_platform_channel <- este package: canal para o SO
  └── dovetail_rust_core                <- mecânica Dart<->Rust

Este package não decide nada. Ele não esconde a janela em vez de fechar, não liga o autostart na primeira execução, não escolhe o texto do tooltip. Ele expõe a capacidade e devolve o evento; quem decide é o app.

O exemplo mais claro: WindowSurface.closeRequests() é um Stream. Fechar-para-bandeja é política, e política é do app.

Instalação #

dependencies:
  dovetail_platform_channel:
    path: ../dovetail_platform_channel

Uso #

import 'package:dovetail_platform_channel/dovetail_platform_channel.dart';

Future<void> main() async {
  final SingleInstanceVerdict verdict =
      await DesktopPlatformChannel.claimSingleInstance('com.example.app');
  if (!verdict.mayRun) {
    return;
  }

  final DesktopPlatform platform = await DesktopPlatformChannel.ensureInitialized(
    const DesktopAppSpec(
      window: WindowSpec(
        size: Size(1200, 720),
        minimumSize: Size(1024, 640),
      ),
      applicationId: 'com.example.app',
      displayName: 'Example',
      notificationGuid: '00000000-0000-0000-0000-000000000000',
    ),
  );

  platform.window.closeRequests().listen((_) => platform.window.hide());

  await platform.tray.attach(
    icon: const TrayIconAsset('assets/tray.png'),
    menu: const <TrayEntry>[
      TrayCommand(id: 'open', label: 'Open'),
      TraySeparator(),
      TrayCommand(id: 'quit', label: 'Quit'),
    ],
  );
  platform.tray.commands().listen(handleTrayCommand);

  runApp(const App());
}

Nenhum número aqui é padrão do package: 1200x720 e o nome vêm do app, porque tamanho de janela e nome são decisão de produto — e no white-label, decisão de revenda.

A sonda de capacidade #

Em vez de Platform.isLinux espalhado pelo app:

if (platform.supports(PlatformCapability.trayTooltip)) {
  await platform.tray.setTooltip(message);
}

A tabela hoje, medida:

Capacidade Windows macOS Linux
janela sem borda, prevent close, skip taskbar
ícone e menu de bandeja
tooltip de bandeja
painel ancorado na bandeja
item de login
deep link
notificação
abrir no aplicativo padrão
instância única

Um deep link que chega enquanto o app já roda vem por um segundo processo, e não pelo app_links. O deepLinks que o canal entrega junta as duas fontes: quem escuta links() vê tanto a URL do sistema quanto a que o guarda de instância única encaminhou. Sem essa junção o ForwardedLaunch chegava com a URL em arguments e nunca alcançava o inbox.

No Windows claimSingleInstance responde unavailable, e não primary: o guarda nomeado existe em windows/single_instance_guard.cpp e ainda não está ligado ao Dart, então dois cliques abrem duas janelas e cada deep link abre uma terceira. mayRun é true nos dois estados que podem seguir — use-o para decidir se roda, e isGuarded para saber se a garantia de fato existe nesta plataforma.

O tooltip não existe no Linux porque o tray_manager não o implementa lá. Como o tooltip é hoje o único canal de uma mensagem que importa, setTooltip cai para uma entrada desabilitada no topo do menu em vez de sumir em silêncio — e essa entrada nunca aparece em commands().

O painel ancorado não existe no Linux por dois motivos somados: o StatusNotifierItem publica menu e nada mais, e o Wayland não deixa um cliente posicionar o próprio toplevel. Lá MenuOnlyPanelSurface reporta que não há painel em vez de mostrar uma janela vazia.

Instância única é ✓ no macOS e no Linux, onde um socket de domínio Unix resolve — o dart:io os suporta nessas duas. No Windows não: lá precisa de mutex nomeado mais janela oculta com WM_COPYDATA, e isso é C++ que nunca foi compilado, porque nenhuma máquina aqui é Windows. Está declarado como falso de propósito, com teste, para ninguém confundir "escrito" com "funciona".

Superfícies #

Superfície Contrato
WindowSurface show/hide/focus/minimize/restore/maximize, arraste da barra própria, prevent close, skip taskbar, always-on-top, setBounds/bounds, frameChanges(), closeRequests()
TraySurface ícone, menu como dado selado, tooltip com queda no Linux, commands(), gestures()
PanelSurface abre a janela principal ancorada na bandeja, dismissals(); no Linux reporta ausência
DisplayProbe ponto do cursor, área útil do monitor sob um ponto, todas as áreas úteis
WindowPlacement · WindowStateStore · WindowStateKeeper lembra tamanho, posição e maximizado entre sessões, quando DesktopAppSpec.stateDirectory diz onde
SingleInstanceVerdict · ForwardedLaunch primary/secondary, e os argumentos que a segunda instância encaminhou
LaunchAtLogin isEnabled · enable · disable
DeepLinkInbox initialLink() · links()
SystemNotifier show · cancel · cancelAll
ExternalOpener openUrl · canOpenUrl
BundleInfo nome, versão, build, identificador

Isso é opt-in por um motivo: o pacote não escolhe onde escrever no disco do usuário. Passe stateDirectory no DesktopAppSpec — o diretório de suporte do app, tipicamente — e o canal carrega a posição salva, prende-a às telas que existem agora e passa a salvar de volta. Sem ele, nada é lido nem escrito.

Uma posição salva num monitor que foi desconectado entre sessões não é aplicada: a janela abriria onde o usuário não a encontra. E o salvamento espera a janela assentar, porque arrastar emite um evento por pixel e gravar em cada um transforma um movimento de janela em centenas de escritas em disco.

O menu é dado, não callback: TrayCommand, TraySeparator e TraySubmenu numa hierarquia selada. O id volta por commands() e o app decide o que fazer — igual ao que o Tauri fazia emitindo evento em vez de chamar função.

O que não entra aqui #

  • Clipboard. O Flutter já faz, com Clipboard.setData. Package para isso seria peso morto.
  • Registrar o esquema de deep link. No Windows é escrita em HKLM que só o instalador elevado faz; no Linux é um .desktop instalado pelo pacote. É do instalador, não do app em execução.
  • Instalar o serviço privilegiado e o updater. São do instalador e da esteira.
  • Qualquer política. Ver acima.

Desenvolvimento #

flutter test
flutter analyze

As decisões e o que ficou de fora estão em ARCHITECTURE.md.

0
likes
130
points
138
downloads

Documentation

API reference

Publisher

unverified uploader

Weekly Downloads

Flutter access to desktop OS surfaces: window frame, system tray, login item, deep links, notifications and handoff to the default app, on Windows, macOS and Linux.

Repository (GitHub)
View/report issues
Contributing

License

MIT (license)

Dependencies

app_links, async, ffi, flutter, flutter_local_notifications, launch_at_startup, package_info_plus, path, screen_retriever, tray_manager, url_launcher, window_manager

More

Packages that depend on dovetail_platform_channel

Packages that implement dovetail_platform_channel