お局様実装
This commit is contained in:
parent
a569f54b0b
commit
81fe44a4b0
8 changed files with 187 additions and 103 deletions
|
|
@ -29,7 +29,7 @@ class MothershipServer {
|
||||||
final handler = const Pipeline()
|
final handler = const Pipeline()
|
||||||
.addMiddleware(logRequests())
|
.addMiddleware(logRequests())
|
||||||
.addMiddleware(_apiKeyMiddleware(config.apiKey))
|
.addMiddleware(_apiKeyMiddleware(config.apiKey))
|
||||||
.addHandler(router);
|
.addHandler(router.call);
|
||||||
|
|
||||||
final server = await serve(handler, config.host, config.port);
|
final server = await serve(handler, config.host, config.port);
|
||||||
return server;
|
return server;
|
||||||
|
|
|
||||||
|
|
@ -180,7 +180,7 @@ class _MessageBubble extends StatelessWidget {
|
||||||
borderRadius: borderRadius,
|
borderRadius: borderRadius,
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
color: Colors.black.withOpacity(0.05),
|
color: Colors.black.withValues(alpha: 0.05),
|
||||||
blurRadius: 8,
|
blurRadius: 8,
|
||||||
offset: const Offset(0, 4),
|
offset: const Offset(0, 4),
|
||||||
),
|
),
|
||||||
|
|
@ -196,7 +196,7 @@ class _MessageBubble extends StatelessWidget {
|
||||||
const SizedBox(height: 6),
|
const SizedBox(height: 6),
|
||||||
Text(
|
Text(
|
||||||
timeText,
|
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),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -219,11 +219,12 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||||
)
|
)
|
||||||
: SlideToUnlock(
|
: SlideToUnlock(
|
||||||
isLocked: !_historyUnlocked,
|
isLocked: !_historyUnlocked,
|
||||||
|
lockedText: 'スライドでロック解除 (A2)',
|
||||||
|
unlockedText: 'A2解除済',
|
||||||
onUnlocked: () async {
|
onUnlocked: () async {
|
||||||
setState(() => _historyUnlocked = true);
|
setState(() => _historyUnlocked = true);
|
||||||
await _repo.setDashboardHistoryUnlocked(true);
|
await _repo.setDashboardHistoryUnlocked(true);
|
||||||
},
|
},
|
||||||
text: 'スライドでロック解除 (A2)',
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (_statusEnabled)
|
if (_statusEnabled)
|
||||||
|
|
|
||||||
|
|
@ -219,6 +219,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
drawer: (_useDashboardHome || !_isUnlocked)
|
drawer: (_useDashboardHome || !_isUnlocked)
|
||||||
? null
|
? null
|
||||||
: Drawer(
|
: Drawer(
|
||||||
|
child: SafeArea(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
padding: EdgeInsets.zero,
|
padding: EdgeInsets.zero,
|
||||||
children: [
|
children: [
|
||||||
|
|
@ -229,16 +230,25 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
children: const [
|
children: const [
|
||||||
Text("販売アシスト1号", style: TextStyle(color: Colors.white, fontSize: 20)),
|
Text("販売アシスト1号", style: TextStyle(color: Colors.white, fontSize: 20)),
|
||||||
SizedBox(height: 8),
|
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(
|
ListTile(
|
||||||
leading: const Icon(Icons.receipt_long),
|
leading: const Icon(Icons.receipt_long),
|
||||||
title: const Text("伝票マスター"),
|
title: const Text("伝票マスター"),
|
||||||
onTap: () {
|
onTap: () => Navigator.pop(context),
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.people),
|
leading: const Icon(Icons.people),
|
||||||
|
|
@ -256,6 +266,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const ProductMasterScreen()));
|
Navigator.push(context, MaterialPageRoute(builder: (_) => const ProductMasterScreen()));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
_drawerHeading("システム"),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.settings),
|
leading: const Icon(Icons.settings),
|
||||||
title: const Text("設定"),
|
title: const Text("設定"),
|
||||||
|
|
@ -264,7 +275,6 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen()));
|
Navigator.push(context, MaterialPageRoute(builder: (_) => const SettingsScreen()));
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
const Divider(),
|
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.admin_panel_settings),
|
leading: const Icon(Icons.admin_panel_settings),
|
||||||
title: const Text("管理メニュー"),
|
title: const Text("管理メニュー"),
|
||||||
|
|
@ -276,6 +286,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
automaticallyImplyLeading: false,
|
automaticallyImplyLeading: false,
|
||||||
leading: _useDashboardHome
|
leading: _useDashboardHome
|
||||||
|
|
@ -386,11 +397,12 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
children: [
|
children: [
|
||||||
if (!_useDashboardHome)
|
if (!_useDashboardHome)
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.all(16.0),
|
padding: const EdgeInsets.fromLTRB(16, 16, 16, 8),
|
||||||
child: SlideToUnlock(
|
child: SlideToUnlock(
|
||||||
isLocked: !_isUnlocked,
|
isLocked: !_isUnlocked,
|
||||||
|
lockedText: "A2をロック解除",
|
||||||
|
unlockedText: "解除済",
|
||||||
onUnlocked: _toggleUnlock,
|
onUnlocked: _toggleUnlock,
|
||||||
text: "スライドでロック解除",
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
|
|
@ -436,7 +448,7 @@ class _InvoiceHistoryScreenState extends State<InvoiceHistoryScreen> {
|
||||||
onPressed: _isUnlocked
|
onPressed: _isUnlocked
|
||||||
? () => _showCreateTypeMenu()
|
? () => _showCreateTypeMenu()
|
||||||
: _requireUnlock,
|
: _requireUnlock,
|
||||||
label: const Text("新規伝票作成"),
|
label: const Text("新しい伝票"),
|
||||||
icon: const Icon(Icons.add),
|
icon: const Icon(Icons.add),
|
||||||
backgroundColor: Colors.indigo,
|
backgroundColor: Colors.indigo,
|
||||||
foregroundColor: Colors.white,
|
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() {
|
void _showCreateTypeMenu() {
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
|
|
|
||||||
|
|
@ -29,13 +29,14 @@ class MothershipClient {
|
||||||
}
|
}
|
||||||
final clientId = await ensureClientId();
|
final clientId = await ensureClientId();
|
||||||
final remaining = expiryInfo.remaining?.inSeconds;
|
final remaining = expiryInfo.remaining?.inSeconds;
|
||||||
|
final payload = <String, dynamic>{'clientId': clientId};
|
||||||
|
if (remaining != null) {
|
||||||
|
payload['remainingLifespanSeconds'] = remaining;
|
||||||
|
}
|
||||||
await _postJson(
|
await _postJson(
|
||||||
uri: config.heartbeatUri,
|
uri: config.heartbeatUri,
|
||||||
apiKey: config.apiKey,
|
apiKey: config.apiKey,
|
||||||
payload: {
|
payload: payload,
|
||||||
'clientId': clientId,
|
|
||||||
if (remaining != null) 'remainingLifespanSeconds': remaining,
|
|
||||||
},
|
|
||||||
logLabel: 'heartbeat',
|
logLabel: 'heartbeat',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,28 @@ import 'package:flutter/material.dart';
|
||||||
|
|
||||||
class SlideToUnlock extends StatefulWidget {
|
class SlideToUnlock extends StatefulWidget {
|
||||||
final VoidCallback onUnlocked;
|
final VoidCallback onUnlocked;
|
||||||
final String text;
|
final String lockedText;
|
||||||
|
final String unlockedText;
|
||||||
|
final IconData lockedIcon;
|
||||||
|
final IconData unlockedIcon;
|
||||||
final bool isLocked;
|
final bool isLocked;
|
||||||
|
final double? height;
|
||||||
|
final double? thumbSize;
|
||||||
|
final Color? backgroundColor;
|
||||||
|
final Color? accentColor;
|
||||||
|
|
||||||
const SlideToUnlock({
|
const SlideToUnlock({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onUnlocked,
|
required this.onUnlocked,
|
||||||
this.text = "スライドして解除",
|
this.lockedText = "スライドして解除",
|
||||||
|
this.unlockedText = "UNLOCKED",
|
||||||
|
this.lockedIcon = Icons.lock,
|
||||||
|
this.unlockedIcon = Icons.check_circle,
|
||||||
this.isLocked = true,
|
this.isLocked = true,
|
||||||
|
this.height = 72,
|
||||||
|
this.thumbSize = 52,
|
||||||
|
this.backgroundColor,
|
||||||
|
this.accentColor,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|
@ -18,7 +32,8 @@ class SlideToUnlock extends StatefulWidget {
|
||||||
|
|
||||||
class _SlideToUnlockState extends State<SlideToUnlock> {
|
class _SlideToUnlockState extends State<SlideToUnlock> {
|
||||||
double _position = 0.0;
|
double _position = 0.0;
|
||||||
final double _thumbSize = 56.0;
|
static const double _trackPadding = 14.0;
|
||||||
|
bool _showSuccessOverlay = false;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
|
@ -27,13 +42,20 @@ class _SlideToUnlockState extends State<SlideToUnlock> {
|
||||||
return LayoutBuilder(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
final double maxWidth = constraints.maxWidth;
|
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(
|
return Container(
|
||||||
height: 64,
|
height: widget.height ?? 72,
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.blueGrey.shade900,
|
color: background,
|
||||||
borderRadius: BorderRadius.circular(32),
|
borderRadius: BorderRadius.circular(32),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(color: Colors.black26, blurRadius: 8, offset: const Offset(0, 4)),
|
BoxShadow(color: Colors.black26, blurRadius: 8, offset: const Offset(0, 4)),
|
||||||
|
|
@ -41,18 +63,54 @@ class _SlideToUnlockState extends State<SlideToUnlock> {
|
||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
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(
|
Center(
|
||||||
child: Opacity(
|
child: AnimatedSwitcher(
|
||||||
opacity: (1 - (_position / trackWidth)).clamp(0.2, 1.0),
|
duration: const Duration(milliseconds: 200),
|
||||||
child: Row(
|
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,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
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),
|
const SizedBox(width: 8),
|
||||||
Text(
|
Text(
|
||||||
widget.text,
|
widget.lockedText,
|
||||||
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 1.2),
|
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold, letterSpacing: 1.1),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
@ -60,8 +118,8 @@ class _SlideToUnlockState extends State<SlideToUnlock> {
|
||||||
),
|
),
|
||||||
// スライドつまみ
|
// スライドつまみ
|
||||||
Positioned(
|
Positioned(
|
||||||
left: _position + 4,
|
left: _trackPadding + _position,
|
||||||
top: 4,
|
top: ((widget.height ?? 72) - (widget.thumbSize ?? 52)) / 2,
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
onHorizontalDragUpdate: (details) {
|
onHorizontalDragUpdate: (details) {
|
||||||
setState(() {
|
setState(() {
|
||||||
|
|
@ -72,31 +130,34 @@ class _SlideToUnlockState extends State<SlideToUnlock> {
|
||||||
},
|
},
|
||||||
onHorizontalDragEnd: (details) {
|
onHorizontalDragEnd: (details) {
|
||||||
if (_position >= trackWidth * 0.65) {
|
if (_position >= trackWidth * 0.65) {
|
||||||
|
setState(() {
|
||||||
|
_position = trackWidth;
|
||||||
|
_showSuccessOverlay = true;
|
||||||
|
});
|
||||||
widget.onUnlocked();
|
widget.onUnlocked();
|
||||||
// 成功時はアニメーションで戻すのではなく、状態が変わるのでリセット
|
Future.delayed(const Duration(milliseconds: 450), () {
|
||||||
setState(() => _position = 0);
|
if (!mounted) return;
|
||||||
|
setState(() {
|
||||||
|
_position = 0;
|
||||||
|
_showSuccessOverlay = false;
|
||||||
|
});
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// 失敗時はバネのように戻る(簡易)
|
// 失敗時はバネのように戻る(簡易)
|
||||||
setState(() => _position = 0);
|
setState(() => _position = 0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
width: _thumbSize,
|
width: thumbSize,
|
||||||
height: 56,
|
height: widget.thumbSize ?? 52,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: const LinearGradient(
|
color: Colors.white,
|
||||||
colors: [Colors.orangeAccent, Colors.deepOrange],
|
borderRadius: BorderRadius.circular((widget.thumbSize ?? 52) / 2),
|
||||||
begin: Alignment.topLeft,
|
boxShadow: const [
|
||||||
end: Alignment.bottomRight,
|
BoxShadow(color: Colors.black38, blurRadius: 8, offset: Offset(0, 4)),
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(28),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(color: Colors.black45, blurRadius: 4, offset: const Offset(2, 2)),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: Icon(Icons.arrow_forward_ios, color: accentEnd, size: 20),
|
||||||
child: Icon(Icons.key, color: Colors.white, size: 24),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -313,7 +313,7 @@ packages:
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.1"
|
version: "1.0.1"
|
||||||
http:
|
http:
|
||||||
dependency: transitive
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: http
|
name: http
|
||||||
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
sha256: "87721a4a50b19c7f1d49001e51409bddc46303966ce89a65af4f4e6004896412"
|
||||||
|
|
|
||||||
|
|
@ -9,11 +9,13 @@ import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
|
|
||||||
import 'package:h_1/main.dart';
|
import 'package:h_1/main.dart';
|
||||||
|
import 'package:h_1/utils/build_expiry_info.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
|
||||||
// Build our app and trigger a frame.
|
// 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.
|
// Verify that our counter starts at 0.
|
||||||
expect(find.text('0'), findsOneWidget);
|
expect(find.text('0'), findsOneWidget);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue