mirror of
https://github.com/hanzo-templates/flutter.git
synced 2026-10-30 20:51:30 +00:00
The catalogue shipped 71 web templates and one EMPTY `mobile` repo, so "build an app" had no starting point on the platform people actually ship to. This is the iOS lane — Expo/React Native and Flutter for cross-platform, SwiftUI for native. The constraint that shaped every file here: an iOS archive needs macOS + Xcode + a signing identity, and our arc fleet has none. hanzoai has 519 registered runners and every one reports `os: linux`; luxfi has 151, same story; zooai has none. The only macOS builds in the org today (hanzoai/world build-desktop.yml) run on GitHub-HOSTED macos-14, which is exactly what the house CI rule says not to do. So `hanzo.yml` declares only what a Linux arc runner can genuinely execute — typecheck + web bundle for Expo, analyze + test + web for Flutter, `swiftc -parse` for SwiftUI — and the macOS archive lives in a separate `workflow_dispatch`-only `ios.yml`. A job pinned to a label no runner advertises would sit queued forever if it fired on push; parking it is honest, a permanently-yellow check is not. The README of every template states the gap and the one command that closes it. Same reasoning on the demos. Expo and Flutter both have a real web target, so `<slug>.hanzo.app` serves the SAME source the simulator runs. SwiftUI has no web target, so those two templates ship with no demo link rather than a screenshot pretending to be one. Upstreams are permissive only and recorded per template: expo/expo templates (MIT), flutter/samples (BSD-3-Clause), XcodeGen (MIT). Second measured gap, found while wiring this up: `hanzo-templates` is not in arcd's `orgs:` list either (hanzoai, luxfi, zooai, parsdao, zenlm), so the Linux jobs would queue with no runner too. Both gaps are one installer re-run, no code change; both are spelled out in every README rather than left to be discovered on the first red build. Third gap, found by probing rather than assuming, and FIXED here: every run in this org died as `startup_failure` with no logs and no annotation. A bare `runs-on: [self-hosted, linux]` probe queued fine while the same job importing hanzoai/ci failed identically at @v1 and at a pinned SHA — so it was never the tag. The org defaulted GITHUB_TOKEN to `read`, and the reusable declares `permissions: packages: write`; a caller cannot exceed the org maximum, so GitHub kills the run before creating a job. hanzo-templates now defaults to `write` like hanzoai/luxfi/zooai, and runs reach `queued` — the honest state, waiting on gap 2.
166 lines
5.2 KiB
Dart
166 lines
5.2 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
import 'data.dart';
|
|
import 'detail.dart';
|
|
import 'theme.dart';
|
|
|
|
void main() => runApp(const HanzoApp());
|
|
|
|
class HanzoApp extends StatelessWidget {
|
|
const HanzoApp({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) => MaterialApp(
|
|
title: 'Hanzo Flutter',
|
|
debugShowCheckedModeBanner: false,
|
|
theme: hanzoTheme(),
|
|
home: const Shell(),
|
|
);
|
|
}
|
|
|
|
/// NavigationBar + IndexedStack: each tab keeps its own scroll position and
|
|
/// state, which is what users expect and what a naive `switch` throws away.
|
|
class Shell extends StatefulWidget {
|
|
const Shell({super.key});
|
|
@override
|
|
State<Shell> createState() => _ShellState();
|
|
}
|
|
|
|
class _ShellState extends State<Shell> {
|
|
int _tab = 0;
|
|
|
|
@override
|
|
Widget build(BuildContext context) => Scaffold(
|
|
body: IndexedStack(
|
|
index: _tab,
|
|
children: const [FeedTab(), SearchTab(), AboutTab()],
|
|
),
|
|
bottomNavigationBar: NavigationBar(
|
|
selectedIndex: _tab,
|
|
onDestinationSelected: (i) => setState(() => _tab = i),
|
|
destinations: const [
|
|
NavigationDestination(
|
|
icon: Icon(Icons.grid_view_outlined), selectedIcon: Icon(Icons.grid_view), label: 'Feed'),
|
|
NavigationDestination(
|
|
icon: Icon(Icons.search_outlined), selectedIcon: Icon(Icons.search), label: 'Search'),
|
|
NavigationDestination(
|
|
icon: Icon(Icons.info_outline), selectedIcon: Icon(Icons.info), label: 'About'),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
class FeedTab extends StatelessWidget {
|
|
const FeedTab({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) => CustomScrollView(
|
|
slivers: [
|
|
const SliverAppBar.large(title: Text('Hanzo Flutter')),
|
|
SliverPadding(
|
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 24),
|
|
sliver: SliverList.separated(
|
|
itemCount: items.length,
|
|
separatorBuilder: (_, _) => const SizedBox(height: 12),
|
|
itemBuilder: (context, i) => ItemCard(item: items[i]),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
class ItemCard extends StatelessWidget {
|
|
const ItemCard({super.key, required this.item});
|
|
final Item item;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final text = Theme.of(context).textTheme;
|
|
return Card(
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(14),
|
|
onTap: () => Navigator.of(context).push(
|
|
MaterialPageRoute(builder: (_) => DetailPage(item: item)),
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(item.tag.toUpperCase(),
|
|
style: text.labelSmall?.copyWith(color: seed, letterSpacing: 1)),
|
|
const SizedBox(height: 6),
|
|
Text(item.title, style: text.titleMedium),
|
|
const SizedBox(height: 4),
|
|
Text(item.blurb, style: text.bodyMedium?.copyWith(color: Colors.white60)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class SearchTab extends StatefulWidget {
|
|
const SearchTab({super.key});
|
|
@override
|
|
State<SearchTab> createState() => _SearchTabState();
|
|
}
|
|
|
|
class _SearchTabState extends State<SearchTab> {
|
|
String _q = '';
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final hits = items
|
|
.where((i) => '${i.title}${i.blurb}${i.tag}'.toLowerCase().contains(_q.toLowerCase()))
|
|
.toList();
|
|
return SafeArea(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: [
|
|
SearchBar(
|
|
hintText: 'Search',
|
|
leading: const Icon(Icons.search),
|
|
onChanged: (v) => setState(() => _q = v),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Expanded(
|
|
child: hits.isEmpty
|
|
? const Center(child: Text('No matches.', style: TextStyle(color: Colors.white54)))
|
|
: ListView.builder(
|
|
itemCount: hits.length,
|
|
itemBuilder: (context, i) => ListTile(
|
|
title: Text(hits[i].title),
|
|
trailing: Text(hits[i].tag, style: const TextStyle(color: Colors.white54)),
|
|
onTap: () => Navigator.of(context)
|
|
.push(MaterialPageRoute(builder: (_) => DetailPage(item: hits[i]))),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class AboutTab extends StatelessWidget {
|
|
const AboutTab({super.key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) => SafeArea(
|
|
child: ListView(
|
|
padding: const EdgeInsets.all(16),
|
|
children: const [
|
|
ListTile(title: Text('Framework'), trailing: Text('Flutter', style: TextStyle(color: Colors.white54))),
|
|
Divider(height: 1),
|
|
ListTile(title: Text('Targets'), trailing: Text('iOS · Android · web', style: TextStyle(color: Colors.white54))),
|
|
Divider(height: 1),
|
|
ListTile(title: Text('Design'), trailing: Text('Material 3', style: TextStyle(color: Colors.white54))),
|
|
],
|
|
),
|
|
);
|
|
}
|