お局様実装

This commit is contained in:
joe 2026-03-02 11:06:47 +09:00
parent a569f54b0b
commit 81fe44a4b0
8 changed files with 187 additions and 103 deletions

View file

@ -29,7 +29,7 @@ class MothershipServer {
final handler = const Pipeline()
.addMiddleware(logRequests())
.addMiddleware(_apiKeyMiddleware(config.apiKey))
.addHandler(router);
.addHandler(router.call);
final server = await serve(handler, config.host, config.port);
return server;

View file

@ -180,7 +180,7 @@ class _MessageBubble extends StatelessWidget {
borderRadius: borderRadius,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
color: Colors.black.withValues(alpha: 0.05),
blurRadius: 8,
offset: const Offset(0, 4),
),
@ -196,7 +196,7 @@ class _MessageBubble extends StatelessWidget {
const SizedBox(height: 6),
Text(
timeText,
style: theme.textTheme.labelSmall?.copyWith(color: textColor.withOpacity(0.8), fontSize: 11),
style: theme.textTheme.labelSmall?.copyWith(color: textColor.withValues(alpha: 0.8), fontSize: 11),
),
],
),

View file

@ -219,11 +219,12 @@ class _DashboardScreenState extends State<DashboardScreen> {
)
: SlideToUnlock(
isLocked: !_historyUnlocked,
lockedText: 'スライドでロック解除 (A2)',
unlockedText: 'A2解除済',
onUnlocked: () async {
setState(() => _historyUnlocked = true);
await _repo.setDashboardHistoryUnlocked(true);
},
text: 'スライドでロック解除 (A2)',
),
),
if (_statusEnabled)

View file

@ -219,6 +219,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
drawer: (_useDashboardHome || !_isUnlocked)
? null
: Drawer(
child: SafeArea(
child: ListView(
padding: EdgeInsets.zero,
children: [
@ -229,16 +230,25 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
children: const [
Text("販売アシスト1号", style: TextStyle(color: Colors.white, fontSize: 20)),
SizedBox(height: 8),
Text("メニュー", style: TextStyle(color: Colors.white70)),
Text("クイックメニュー", style: TextStyle(color: Colors.white70)),
],
),
),
_drawerHeading("アクション"),
ListTile(
leading: const Icon(Icons.add_circle_outline, color: Colors.indigo),
title: const Text("新しい伝票を作成"),
subtitle: const Text("ドキュメント種別を選択"),
onTap: () {
Navigator.pop(context);
_showCreateTypeMenu();
},
),
_drawerHeading("マスター"),
ListTile(
leading: const Icon(Icons.receipt_long),
title: const Text("伝票マスター"),
onTap: () {
Navigator.pop(context);
},
onTap: () => Navigator.pop(context),
),
ListTile(
leading: const Icon(Icons.people),
@ -256,6 +266,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
Navigator.push(context, MaterialPageRoute(builder: (_) => const ProductMasterScreen()));
},
),
_drawerHeading("システム"),
ListTile(
leading: const Icon(Icons.settings),
title: const Text("設定"),
@ -264,7 +275,6 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen()));
},
),
const Divider(),
ListTile(
leading: const Icon(Icons.admin_panel_settings),
title: const Text("管理メニュー"),
@ -276,6 +286,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
],
),
),
),
appBar: AppBar(
automaticallyImplyLeading: false,
leading: _useDashboardHome
@ -386,11 +397,12 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
children: [
if (!_useDashboardHome)
Padding(
padding: const EdgeInsets.all(16.0),
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
child: SlideToUnlock(
isLocked: !_isUnlocked,
lockedText: "A2をロック解除",
unlockedText: "解除済",
onUnlocked: _toggleUnlock,
text: "スライドでロック解除",
),
),
Expanded(
@ -436,7 +448,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
onPressed: _isUnlocked
? () => _showCreateTypeMenu()
: _requireUnlock,
label: const Text("規伝票作成"),
label: const Text("しい伝票"),
icon: const Icon(Icons.add),
backgroundColor: Colors.indigo,
foregroundColor: Colors.white,
@ -444,6 +456,13 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
);
}
Widget _drawerHeading(String label) {
return Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Text(label, style: const TextStyle(fontSize: 12, color: Colors.grey, letterSpacing: 0.5)),
);
}
void _showCreateTypeMenu() {
showModalBottomSheet(
context: context,

View file

@ -29,13 +29,14 @@ class MothershipClient {
}
final clientId = await ensureClientId();
final remaining = expiryInfo.remaining?.inSeconds;
final payload = <String, dynamic>{'clientId': clientId};
if (remaining != null) {
payload['remainingLifespanSeconds'] = remaining;
}
await _postJson(
uri: config.heartbeatUri,
apiKey: config.apiKey,
payload: {
'clientId': clientId,
if (remaining != null) 'remainingLifespanSeconds': remaining,
},
payload: payload,
logLabel: 'heartbeat',
);
}

View file

@ -2,14 +2,28 @@ import 'package:flutter/material.dart';
class SlideToUnlock extends StatefulWidget {
final VoidCallback onUnlocked;
final String text;
final String lockedText;
final String unlockedText;
final IconData lockedIcon;
final IconData unlockedIcon;
final bool isLocked;
final double? height;
final double? thumbSize;
final Color? backgroundColor;
final Color? accentColor;
const SlideToUnlock({
super.key,
required this.onUnlocked,
this.text = "スライドして解除",
this.lockedText = "スライドして解除",
this.unlockedText = "UNLOCKED",
this.lockedIcon = Icons.lock,
this.unlockedIcon = Icons.check_circle,
this.isLocked = true,
this.height = 72,
this.thumbSize = 52,
this.backgroundColor,
this.accentColor,
});
@override
@ -18,7 +32,8 @@ class SlideToUnlock extends StatefulWidget {
class _SlideToUnlockState extends State<SlideToUnlock> {
double _position = 0.0;
final double _thumbSize = 56.0;
static const double _trackPadding = 14.0;
bool _showSuccessOverlay = false;
@override
Widget build(BuildContext context) {
@ -27,13 +42,20 @@ class _SlideToUnlockState extends State<SlideToUnlock> {
return LayoutBuilder(
builder: (context, constraints) {
final double maxWidth = constraints.maxWidth;
final double trackWidth = (maxWidth - _thumbSize - 12).clamp(0, maxWidth);
final double thumbSize = widget.thumbSize ?? 52;
final double trackWidth = (maxWidth - thumbSize - (_trackPadding * 2)).clamp(0, maxWidth);
final double progressRatio = trackWidth == 0 ? 0 : (_position / trackWidth).clamp(0, 1);
final double innerWidth = thumbSize + trackWidth;
final double progressWidth = (innerWidth * progressRatio + thumbSize * (1 - progressRatio)).clamp(thumbSize, innerWidth);
final Color background = widget.backgroundColor ?? Colors.blueGrey.shade900;
final Color accentStart = (widget.accentColor ?? Colors.indigo.shade600).withValues(alpha: 0.9);
final Color accentEnd = (widget.accentColor ?? Colors.indigo.shade600);
return Container(
height: 64,
height: widget.height ?? 72,
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.blueGrey.shade900,
color: background,
borderRadius: BorderRadius.circular(32),
boxShadow: [
BoxShadow(color: Colors.black26, blurRadius: 8, offset: const Offset(0, 4)),
@ -41,18 +63,54 @@ class _SlideToUnlockState extends State<SlideToUnlock> {
),
child: Stack(
children: [
//
//
Padding(
padding: const EdgeInsets.symmetric(horizontal: _trackPadding, vertical: 12),
child: Align(
alignment: Alignment.centerLeft,
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
curve: Curves.easeOut,
width: progressWidth,
height: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(28),
gradient: LinearGradient(
colors: [
accentStart,
accentEnd,
],
),
boxShadow: const [
BoxShadow(color: Colors.black26, blurRadius: 6, offset: Offset(0, 2)),
],
),
),
),
),
//
Center(
child: Opacity(
opacity: (1 - (_position / trackWidth)).clamp(0.2, 1.0),
child: Row(
child: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: _showSuccessOverlay
? Row(
mainAxisSize: MainAxisSize.min,
key: const ValueKey('unlocked'),
children: [
Icon(widget.unlockedIcon, color: Colors.white, size: 24),
const SizedBox(width: 6),
Text(widget.unlockedText, style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
],
)
: Row(
key: const ValueKey('locked'),
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.keyboard_double_arrow_right, color: Colors.white54, size: 20),
Icon(widget.lockedIcon, color: Colors.white.withValues(alpha: 0.85), size: 20),
const SizedBox(width: 8),
Text(
widget.text,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 1.2),
widget.lockedText,
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 1.1),
),
],
),
@ -60,8 +118,8 @@ class _SlideToUnlockState extends State<SlideToUnlock> {
),
//
Positioned(
left: _position + 4,
top: 4,
left: _trackPadding + _position,
top: ((widget.height ?? 72) - (widget.thumbSize ?? 52)) / 2,
child: GestureDetector(
onHorizontalDragUpdate: (details) {
setState(() {
@ -72,31 +130,34 @@ class _SlideToUnlockState extends State<SlideToUnlock> {
},
onHorizontalDragEnd: (details) {
if (_position >= trackWidth * 0.65) {
setState(() {
_position = trackWidth;
_showSuccessOverlay = true;
});
widget.onUnlocked();
//
setState(() => _position = 0);
Future.delayed(const Duration(milliseconds: 450), () {
if (!mounted) return;
setState(() {
_position = 0;
_showSuccessOverlay = false;
});
});
} else {
//
setState(() => _position = 0);
}
},
child: Container(
width: _thumbSize,
height: 56,
width: thumbSize,
height: widget.thumbSize ?? 52,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Colors.orangeAccent, Colors.deepOrange],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(28),
boxShadow: [
BoxShadow(color: Colors.black45, blurRadius: 4, offset: const Offset(2, 2)),
color: Colors.white,
borderRadius: BorderRadius.circular((widget.thumbSize ?? 52) / 2),
boxShadow: const [
BoxShadow(color: Colors.black38, blurRadius: 8, offset: Offset(0, 4)),
],
),
child: const Center(
child: Icon(Icons.key, color: Colors.white, size: 24),
),
child: Icon(Icons.arrow_forward_ios, color: accentEnd, size: 20),
),
),
),

View file

@ -313,7 +313,7 @@ packages:
source: hosted
version: "1.0.1"
http:
dependency: transitive
dependency: "direct main"
description:
name: http
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"

View file

@ -9,11 +9,13 @@ import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:h_1/main.dart';
import 'package:h_1/utils/build_expiry_info.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
final expiryInfo = BuildExpiryInfo.fromEnvironment();
await tester.pumpWidget(MyApp(expiryInfo: expiryInfo));
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);