Skip to content

Commit

Permalink
Merge pull request #3 from nozomi-koborinai/feature/fix-ui
Browse files Browse the repository at this point in the history
Feature/fix UI
  • Loading branch information
nozomi-koborinai authored Jan 26, 2025
2 parents d375035 + e35fb37 commit 51fc872
Show file tree
Hide file tree
Showing 6 changed files with 194 additions and 79 deletions.
83 changes: 54 additions & 29 deletions lib/character_input_page.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_gakkai_07/character_result_page.dart';
import 'package:flutter_gakkai_07/data/app_exception.dart';
import 'package:flutter_gakkai_07/ui/failure_snackbar.dart';
import 'package:flutter_gakkai_07/usecase/generate_%20character_usecase.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

Expand All @@ -12,11 +14,21 @@ class CharacterInputPage extends ConsumerStatefulWidget {

class _CharacterInputPageState extends ConsumerState<CharacterInputPage> {
final _formKey = GlobalKey<FormState>();
final _nameController = TextEditingController(text: 'cat');
final _personalityController = TextEditingController(text: '猫の見た目');
final _storyController = TextEditingController(text: '道で拾われた');
double _age = 25;
String _gender = '未選択';
final _nameController = TextEditingController(text: 'フラッター');
final _personalityController = TextEditingController(
text: '''好奇心旺盛で新しい技術を学ぶことが大好き。
困っている開発者を見かけると放っておけない優しい性格''',
);
final _storyController = TextEditingController(
text: '''スマートフォンアプリ開発の世界で生まれ育った若きエンジニア。
クロスプラットフォーム開発の可能性に魅了され、世界中の開発者たちと知識を共有しながら成長を続けている。
休日は技術書を読んだり、コミュニティイベントに参加したりして過ごしている。''',
);
int _age = 23;
String _gender = '女性';

// 年齢選択肢の生成
final List<int> _ageList = List.generate(100, (index) => index + 1);

@override
void dispose() {
Expand Down Expand Up @@ -54,14 +66,19 @@ class _CharacterInputPageState extends ConsumerState<CharacterInputPage> {
},
),
const SizedBox(height: 16),
Text('年齢: ${_age.round()}歳'),
Slider(
DropdownButtonFormField<int>(
value: _age,
min: 1,
max: 100,
divisions: 99,
label: _age.round().toString(),
onChanged: (value) => setState(() => _age = value),
decoration: const InputDecoration(
labelText: '年齢',
border: OutlineInputBorder(),
),
items: _ageList
.map((age) => DropdownMenuItem(
value: age,
child: Text('$age歳'),
))
.toList(),
onChanged: (value) => setState(() => _age = value!),
),
const SizedBox(height: 16),
DropdownButtonFormField<String>(
Expand Down Expand Up @@ -113,24 +130,32 @@ class _CharacterInputPageState extends ConsumerState<CharacterInputPage> {
ElevatedButton(
onPressed: () async {
if (_formKey.currentState!.validate()) {
final generatedCharacter =
await ref.read(generateImageUsecaseProvider).invoke(
characterName: _nameController.text,
age: _age.round(),
gender: _gender,
personality: _personalityController.text,
story: _storyController.text,
);
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CharacterResultPage(
description: generatedCharacter.description,
image: generatedCharacter.image,
final scaffoldMessenger = ScaffoldMessenger.of(context);
final navigator = Navigator.of(context);
try {
final generatedCharacter =
await ref.read(generateImageUsecaseProvider).invoke(
characterName: _nameController.text,
age: _age,
gender: _gender,
personality: _personalityController.text,
story: _storyController.text,
);
if (!mounted) return;
navigator.push(
MaterialPageRoute(
builder: (context) => CharacterResultPage(
description: generatedCharacter.description,
image: generatedCharacter.image,
),
),
),
);
);
} on AppException catch (e) {
FailureSnackBar.show(
scaffoldMessenger,
message: e.toString(),
);
}
}
},
child: const Text('キャラクターを生成'),
Expand Down
115 changes: 68 additions & 47 deletions lib/character_result_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,62 +12,83 @@ class CharacterResultPage extends StatelessWidget {

@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final textTheme = Theme.of(context).textTheme;

return Scaffold(
appBar: AppBar(
title: const Text('生成されたキャラクター'),
backgroundColor: Colors.white,
elevation: 0,
title: Text(
'生成されたキャラクター',
style: textTheme.titleLarge?.copyWith(
color: colorScheme.primary,
fontWeight: FontWeight.bold,
),
),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Container(
height: 300,
width: 300,
decoration: BoxDecoration(
border: Border.all(color: Colors.grey),
borderRadius: BorderRadius.circular(8),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(8),
child: Image.memory(
Uri.parse(image).data!.contentAsBytes(),
fit: BoxFit.cover,
),
body: Column(
children: [
Container(
padding: const EdgeInsets.all(24.0),
child: Container(
height: 300,
width: 300,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
spreadRadius: 0,
blurRadius: 10,
offset: const Offset(0, 4),
),
),
],
),
const SizedBox(height: 24),
const Text(
'AI生成された説明:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: Image.memory(
Uri.parse(image).data!.contentAsBytes(),
fit: BoxFit.cover,
),
),
Text(description),
const SizedBox(height: 24),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton.icon(
icon: const Icon(Icons.share),
label: const Text('SNSで共有'),
onPressed: () {
// TODO: SNS共有機能を実装
},
),
),
Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'生成された説明',
style: textTheme.titleMedium?.copyWith(
color: colorScheme.primary,
fontWeight: FontWeight.bold,
),
ElevatedButton.icon(
icon: const Icon(Icons.cloud_upload),
label: const Text('ストレージに保存'),
onPressed: () {
// TODO: ストレージアップロード機能を実装
},
),
const SizedBox(height: 16),
Card(
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(
color: colorScheme.outline.withOpacity(0.1),
),
),
],
),
],
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
description,
style: textTheme.bodyLarge?.copyWith(
height: 1.6,
),
),
),
),
],
),
),
),
],
),
);
}
Expand Down
9 changes: 9 additions & 0 deletions lib/data/app_exception.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
class AppException implements Exception {
final String message;
const AppException([this.message = 'エラーが発生しました']);

@override
String toString() {
return message;
}
}
10 changes: 7 additions & 3 deletions lib/data/genkit_client.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:dio/dio.dart';
import 'package:flutter_gakkai_07/data/app_exception.dart';
import 'package:flutter_gakkai_07/data/genkit_response.dart';

class GenkitClient {
Expand Down Expand Up @@ -38,12 +39,15 @@ class GenkitClient {
);

if (response.statusCode != 200) {
throw Exception('Failed to generate image: ${response.statusCode}');
throw AppException(errorMessage);
}

return GenkitResponse.fromJson(response.data);
} on DioException catch (e) {
throw Exception('Failed to generate image: ${e.message}');
} catch (e) {
throw AppException(errorMessage);
}
}

final errorMessage = '''Imagen3 による画像生成に失敗しました。
再度、キャラクター生成をお試しください。''';
}
36 changes: 36 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,42 @@ class MyApp extends ConsumerWidget {

return MaterialApp(
title: 'Character Generator',
theme: ThemeData(
useMaterial3: true,
colorScheme: ColorScheme.fromSeed(
seedColor: Colors.indigo,
brightness: Brightness.light,
surface: Colors.white,
surfaceContainerHighest: Colors.grey[50],
),
cardTheme: CardTheme(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
color: Colors.white,
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: Colors.grey[50],
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.indigo[300]!),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16,
vertical: 16,
),
),
),
builder: (context, child) {
return Stack(
children: [
Expand Down
20 changes: 20 additions & 0 deletions lib/ui/failure_snackbar.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import 'package:flutter/material.dart';

class FailureSnackBar extends SnackBar {
FailureSnackBar._({required String message})
: super(
content: Text(message),
backgroundColor: Colors.redAccent,
behavior: SnackBarBehavior.floating,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8.0),
),
);

static void show(
ScaffoldMessengerState scaffoldMessenger, {
required String message,
}) {
scaffoldMessenger.showSnackBar(FailureSnackBar._(message: message));
}
}

0 comments on commit 51fc872

Please sign in to comment.