2025-11-25 16:34:13 +09:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
|
import 'package:playwith_core/playwith_core.dart'; // AvatarWidget 포함됨
|
2025-11-26 18:10:10 +09:00
|
|
|
import 'package:url_launcher/url_launcher.dart'; // [추가] 링크 이동용
|
2025-11-25 16:34:13 +09:00
|
|
|
|
|
|
|
|
class SettingsScreen extends StatefulWidget {
|
|
|
|
|
const SettingsScreen({super.key});
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
State<SettingsScreen> createState() => _SettingsScreenState();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class _SettingsScreenState extends State<SettingsScreen> {
|
|
|
|
|
final _nickController = TextEditingController();
|
|
|
|
|
final _settings = SettingsNotifier();
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void initState() {
|
|
|
|
|
super.initState();
|
|
|
|
|
_nickController.text = _settings.nickname;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@override
|
|
|
|
|
void dispose() {
|
|
|
|
|
_nickController.dispose();
|
|
|
|
|
super.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-26 18:10:10 +09:00
|
|
|
// [추가] 홈페이지 열기 함수
|
|
|
|
|
Future<void> _launchHomepage() async {
|
|
|
|
|
// 이동할 홈페이지 주소를 입력하세요
|
|
|
|
|
final Uri url = Uri.parse('https://lunaticbum.kr"');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
if (!await launchUrl(url, mode: LaunchMode.externalApplication)) {
|
|
|
|
|
throw Exception('Could not launch $url');
|
|
|
|
|
}
|
|
|
|
|
} catch (e) {
|
|
|
|
|
if (mounted) {
|
|
|
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
|
|
|
const SnackBar(content: Text("페이지를 열 수 없습니다.")),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-11-25 16:34:13 +09:00
|
|
|
@override
|
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
|
return Scaffold(
|
|
|
|
|
appBar: AppBar(title: const Text("설정")),
|
|
|
|
|
body: ListenableBuilder(
|
|
|
|
|
listenable: _settings,
|
|
|
|
|
builder: (context, _) {
|
|
|
|
|
return ListView(
|
|
|
|
|
padding: const EdgeInsets.all(16),
|
|
|
|
|
children: [
|
|
|
|
|
// 1. 프로필 설정 섹션
|
|
|
|
|
_buildSectionTitle("프로필 설정"),
|
|
|
|
|
Card(
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.all(16.0),
|
|
|
|
|
child: Column(
|
|
|
|
|
children: [
|
|
|
|
|
GestureDetector(
|
|
|
|
|
onTap: () => _settings.pickProfileImage(),
|
|
|
|
|
child: Stack(
|
|
|
|
|
alignment: Alignment.bottomRight,
|
|
|
|
|
children: [
|
|
|
|
|
AvatarWidget(
|
|
|
|
|
base64Image: _settings.profileImageBase64,
|
|
|
|
|
colorValue: Colors.primaries[_settings.avatarIndex % Colors.primaries.length].value,
|
|
|
|
|
nickname: _nickController.text,
|
|
|
|
|
size: 100,
|
|
|
|
|
),
|
|
|
|
|
Container(
|
|
|
|
|
padding: const EdgeInsets.all(6),
|
|
|
|
|
decoration: const BoxDecoration(color: Colors.blue, shape: BoxShape.circle),
|
|
|
|
|
child: const Icon(Icons.edit, size: 16, color: Colors.white),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
if (_settings.profileImageBase64 != null)
|
|
|
|
|
TextButton(
|
|
|
|
|
onPressed: () => _settings.clearProfileImage(),
|
|
|
|
|
child: const Text("이미지 삭제 (기본값 사용)", style: TextStyle(color: Colors.red)),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
|
|
|
|
|
TextField(
|
|
|
|
|
controller: _nickController,
|
|
|
|
|
decoration: const InputDecoration(
|
|
|
|
|
labelText: "닉네임",
|
|
|
|
|
border: OutlineInputBorder(),
|
|
|
|
|
helperText: "게임에서 사용할 이름을 입력하세요.",
|
|
|
|
|
),
|
|
|
|
|
onChanged: (val) => _settings.setProfile(val, _settings.avatarIndex),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
|
|
|
|
|
const Align(alignment: Alignment.centerLeft, child: Text("기본 배경색")),
|
|
|
|
|
const SizedBox(height: 5),
|
|
|
|
|
SingleChildScrollView(
|
|
|
|
|
scrollDirection: Axis.horizontal,
|
|
|
|
|
child: Row(
|
|
|
|
|
children: List.generate(Colors.primaries.length, (index) {
|
|
|
|
|
final isSelected = _settings.avatarIndex == index;
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
onTap: () => _settings.setProfile(_nickController.text, index),
|
|
|
|
|
child: Container(
|
|
|
|
|
margin: const EdgeInsets.only(right: 8),
|
|
|
|
|
width: 30,
|
|
|
|
|
height: 30,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: Colors.primaries[index],
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
border: isSelected ? Border.all(color: Colors.black, width: 2) : null,
|
|
|
|
|
),
|
|
|
|
|
child: isSelected ? const Icon(Icons.check, size: 16, color: Colors.white) : null,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
|
|
|
|
|
// 2. 디스플레이 설정 섹션
|
|
|
|
|
_buildSectionTitle("화면 설정"),
|
|
|
|
|
Card(
|
|
|
|
|
child: Padding(
|
|
|
|
|
padding: const EdgeInsets.all(16.0),
|
|
|
|
|
child: Column(
|
|
|
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
|
|
|
children: [
|
|
|
|
|
SwitchListTile(
|
|
|
|
|
title: const Text("다크 모드"),
|
|
|
|
|
value: _settings.isDarkMode,
|
|
|
|
|
onChanged: (val) => _settings.toggleDarkMode(val),
|
|
|
|
|
),
|
|
|
|
|
const Divider(),
|
|
|
|
|
|
|
|
|
|
const Text("글자 크기", style: TextStyle(fontWeight: FontWeight.bold)),
|
|
|
|
|
Slider(
|
|
|
|
|
value: _settings.fontScale,
|
|
|
|
|
min: 0.8,
|
|
|
|
|
max: 1.5,
|
|
|
|
|
divisions: 7,
|
|
|
|
|
label: "${(_settings.fontScale * 100).toInt()}%",
|
|
|
|
|
onChanged: (val) => _settings.setFontScale(val),
|
|
|
|
|
),
|
|
|
|
|
Text(
|
|
|
|
|
"이 크기로 보입니다.",
|
|
|
|
|
style: TextStyle(fontSize: 16 * _settings.fontScale),
|
|
|
|
|
),
|
|
|
|
|
const Divider(),
|
|
|
|
|
|
|
|
|
|
const Text("테마 색상", style: TextStyle(fontWeight: FontWeight.bold)),
|
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
|
Wrap(
|
|
|
|
|
spacing: 10,
|
|
|
|
|
runSpacing: 10,
|
|
|
|
|
children: appColors.entries.map((entry) {
|
|
|
|
|
final isSelected = _settings.themeColorName == entry.key;
|
|
|
|
|
return GestureDetector(
|
|
|
|
|
onTap: () => _settings.setThemeColor(entry.key),
|
|
|
|
|
child: Container(
|
|
|
|
|
width: 40, height: 40,
|
|
|
|
|
decoration: BoxDecoration(
|
|
|
|
|
color: entry.value,
|
|
|
|
|
shape: BoxShape.circle,
|
|
|
|
|
border: isSelected ? Border.all(color: Colors.black, width: 3) : null,
|
|
|
|
|
boxShadow: [if(isSelected) const BoxShadow(blurRadius: 5, color: Colors.black26)],
|
|
|
|
|
),
|
|
|
|
|
child: isSelected ? const Icon(Icons.check, color: Colors.white) : null,
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}).toList(),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
),
|
2025-11-26 18:10:10 +09:00
|
|
|
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
|
|
|
|
|
// 3. 개발자 옵션 (디버그 로그)
|
|
|
|
|
_buildSectionTitle("개발자 옵션"),
|
|
|
|
|
Card(
|
|
|
|
|
child: SwitchListTile(
|
|
|
|
|
title: const Text("디버그 로그 표시"),
|
|
|
|
|
subtitle: const Text("로비 화면 하단에 네트워크 로그를 표시합니다."),
|
|
|
|
|
value: _settings.isShowDebugLog,
|
|
|
|
|
onChanged: (val) => _settings.toggleDebugLog(val),
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
|
|
|
|
|
|
// [추가] 4. 정보 섹션 (라이선스)
|
|
|
|
|
_buildSectionTitle("정보"),
|
|
|
|
|
Card(
|
|
|
|
|
child: ListTile(
|
|
|
|
|
leading: const Icon(Icons.description_outlined),
|
|
|
|
|
title: const Text("오픈소스 라이선스"),
|
|
|
|
|
subtitle: const Text("앱에 사용된 라이브러리 정보"),
|
|
|
|
|
trailing: const Icon(Icons.arrow_forward_ios, size: 16, color: Colors.grey),
|
|
|
|
|
onTap: () {
|
|
|
|
|
// 플러터 내장 라이선스 페이지 호출
|
|
|
|
|
showLicensePage(
|
|
|
|
|
context: context,
|
|
|
|
|
applicationName: "PlayWith",
|
|
|
|
|
applicationVersion: "1.0.0",
|
|
|
|
|
// applicationIcon: Image.asset('assets/icon.png', width: 50), // 아이콘이 있다면 주석 해제
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
|
|
|
|
|
const SizedBox(height: 40),
|
|
|
|
|
|
|
|
|
|
// [추가] 하단 카피라이트 & 링크
|
|
|
|
|
GestureDetector(
|
|
|
|
|
onTap: _launchHomepage,
|
|
|
|
|
child: Column(
|
|
|
|
|
children: const [
|
|
|
|
|
Text(
|
|
|
|
|
"© 2025 lunaticbum. All rights reserved.",
|
|
|
|
|
style: TextStyle(color: Colors.grey, fontSize: 12),
|
|
|
|
|
),
|
|
|
|
|
SizedBox(height: 4),
|
|
|
|
|
Text(
|
|
|
|
|
"https://lunaticbum.kr", // 보여줄 텍스트
|
|
|
|
|
style: TextStyle(
|
|
|
|
|
color: Colors.blueAccent,
|
|
|
|
|
fontSize: 12,
|
|
|
|
|
decoration: TextDecoration.underline
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
),
|
|
|
|
|
),
|
|
|
|
|
const SizedBox(height: 30),
|
2025-11-25 16:34:13 +09:00
|
|
|
],
|
|
|
|
|
);
|
|
|
|
|
},
|
|
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Widget _buildSectionTitle(String title) {
|
|
|
|
|
return Padding(
|
|
|
|
|
padding: const EdgeInsets.only(left: 8, bottom: 8),
|
|
|
|
|
child: Text(title, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.grey)),
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
}
|