33일차 과제 - rlatkddbs99/Flutter GitHub Wiki

오늘 삽질

  1. url복사 할 때 앞에 공백을 같이 복사 해버려서 오류 발생, 오류내용이 안떴는데 껐다가 키니까 다시 오류 내용 나옴;;

  2. customWidget좀 씁시다.. 스타일 만들 때 쌩 노가다;;

  3. 회원가입 왜 안되는지 모르겠음.. null-safety도 주고 했는데.. 실패.. 뭘까요??..

image

Android Emulator - flutter_emulator_5554 2023-03-11 00-13-40

main.dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:secret/controller/auth_controller.dart';
import 'package:secret/controller/secret_controller.dart';
import 'package:secret/util/pages.dart';

import 'controller/login_controller.dart';
import 'controller/signup_controller.dart';
import 'controller/upload_controller.dart';

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return GetMaterialApp(
      getPages: AppPages.pages, //페이지 관리 위해 모아놓은 리스트 호출
      title: "SecretCat",
      initialBinding: BindingsBuilder(() {
        //시스템이 시작될 때 마다 자동으로 메모리에 올리기
        Get.put(AuthController());
        Get.lazyPut(() => LoginController()); //login컨트롤러 대기상태로 올림
        Get.put(SecretController(), permanent: true);
        // Get.lazyPut(() => SecretController());
        Get.lazyPut(() => SignupController());
        Get.put(UploadController(),
            permanent: true); //uploading 한번 하고 나가면 컨트롤러 메모리에서 지워짐
        //계속 남아있게 하려고 이렇게 설정, 어떤 상황이어도 컨트롤러 종료 안해
      }),
      initialRoute: 'login', //첫화면 로그인 페이지로
    );
  }
}

controller/auth_controller.dart

import 'package:get/get.dart';
import 'package:secret/model/profile.dart';
import 'package:secret/util/api_routes.dart';
import 'package:secret/view/page/main_page.dart';
import 'package:dio/dio.dart';

class AuthController extends GetxController {
  //profile을 담을거임, 변하고 null일 수도 Rxn
  final Rxn<Profile> _profile = Rxn<Profile>(); //null값으로 일단 하나 만들기
  Dio dio = Dio();
  Profile? get profile =>
      _profile.value; //profile private이니까 get으로 꺼낼 수 있도록 새로 하나 더 만든다 생각해
  //메소드
  login(String id, String pw) async {
    //호출 시 아이디와 pw 가지고 호출 해줘야돼, url routes에 정의 해놨음, post방식으로 하기로 했음
    var res =
        await dio.post(ApiRoutes.login, data: {'identity': id, 'password': pw});
    if (res.statusCode == 200) {
      //핵심 데이터 뽑기, 가공
      var data = Map<String, dynamic>.from(
          res.data['record']); //record에 우리가 원하는 값이 있음, 정보 형태 Map으로 바꾸기
      _profile(Profile.fromMap(data)); //_profile에 현재 정보 등록
      print(res.data);
    }
  }

  signup(String email, String pw, String pw2, String username) async {
    //정보보내기, url요청
    var res = await dio.post(ApiRoutes.secrets, data: {
      //보내는 데이터 정보
      'email': email,
      'password': pw,
      'passwordConfirm': pw2,
      'username': username,
    });
    //데이터 받으면 profile등록
    _profile(Profile.fromMap(res.data));
  }

  logout() {
    _profile.value = null; //로그아웃 누르면 값을 null로 바꿈, 로그인페이지로 이동
  }

  _handleOnProfileChanged(value) {
    if (value != null) {
      //로그인이 된거니까 main으로 이동
      Get.toNamed(MainPage.route);
      return; //없으면 메인 갔다가 밑에 있는 로그인 화면으로 이동
    }
    //로그인화면으로 이동
    Get.toNamed('/login');
    return;
  }

  //프로필이 바뀔 때 마다 새로운 페이지로 이동
  @override
  void onInit() {
    // TODO: implement onInit
    super.onInit();
    //위에 함수 호출 해서 값 가지고 감
    ever(_profile, _handleOnProfileChanged);
  }
}

controller/login_controller.dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:secret/controller/auth_controller.dart';

class LoginController extends GetxController {
  var idController = TextEditingController();
  var pwController = TextEditingController();

  login() {
    //authcontroller에 정의되어 있는 로그인함수 호출, 아이디,pw넘기기로 했으니까
    //컨트롤러에 .text로 같이 넘겨줘
    Get.find<AuthController>().login(idController.text, pwController.text);
  }
}

controller/secret_controller.dart

import 'package:dio/dio.dart';
import 'package:get/get.dart';
import 'package:secret/util/api_routes.dart';

import '../model/secret.dart';

class SecretController extends GetxController {
  //여러개 비밀을 가지고 있으니 List로
  final RxList<Secret> _secrets = <Secret>[].obs;
  Dio dio = Dio();

  List<Secret> get secrets => _secrets; //private 보낼 수 있도록
  //내용 가져오는 함수
  fetchSecrets() async {
    //dio통해 데이터 가져오고 secret에 가져오기
    var res = await dio.get(ApiRoutes.secrets);
    print(res);
    //items에 있는 내용 Map형태로 바꾸기
    var items = List<Map<String, dynamic>>.from(res.data['items']);
    _secrets(items.map((e) => Secret.fromMap(e)).toList()); //한개한개 다 매칭 해서 넣기
    //_secrets에 정보 넣어서 보내기
  }

  @override
  void onInit() {
    // TODO: implement onInit
    super.onInit();
    fetchSecrets(); //컨트롤러 생성되면 바로 실행
  }
}

controller/signup_controller.dart

import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:secret/controller/auth_controller.dart';
import 'package:secret/model/profile.dart';

class SignupController extends GetxController {
  var emailController = TextEditingController(); //이메일
  var usernameController = TextEditingController(); //이름
  var pwController = TextEditingController(); //비밀번호
  var pw2Controller = TextEditingController(); //비밀번호 확인

  signup() async {
    //함수 정의
    //Auth에 있는 정보가져와
    Get.find<AuthController>().signup(emailController.text, pwController.text,
        pw2Controller.text, usernameController.text); //정보 보내기
  }
}

controller/upload_controller.dart

import 'package:dio/dio.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:secret/util/api_routes.dart';

class UploadController extends GetxController {
  var inputController = TextEditingController(); //비밀 작성하는 컨트롤러
  Dio dio = Dio();
  upload() async {
    if (inputController.text.isEmpty) return; //비어있으면 아무것도 안되게
    var res = await dio.post(ApiRoutes.upload, data: {
      'secret': inputController.text //data같이 보내기 컨트롤러에 있는 내가 작성한 비밀
    });
    inputController.text = ''; //비밀올리면 작성했던 내용 지움
    print(res); //내가 작성한 비밀내용 보임
  }
}

model/profile.dart

import 'dart:convert';

// ignore_for_file: public_member_api_docs, sort_constructors_first
class Profile {
  String? email;
  String username;
  String name;
  Profile({
    required this.email,
    required this.username,
    required this.name,
  });

  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'email': email,
      'username': username,
      'name': name,
    };
  }

  factory Profile.fromMap(Map<String, dynamic> map) {
    return Profile(
      email: map['email'],
      username: map['username'] as String,
      name: map['name'] as String,
    );
  }

  String toJson() => json.encode(toMap());

  factory Profile.fromJson(String source) =>
      Profile.fromMap(json.decode(source) as Map<String, dynamic>);
}

model/secret.dart

import 'dart:convert';

// ignore_for_file: public_member_api_docs, sort_constructors_first
class Secret {
  String secret; //비밀 내용
  String author; //비밀 작성자
  Secret({
    required this.secret,
    required this.author,
  });

  Map<String, dynamic> toMap() {
    return <String, dynamic>{
      'secret': secret,
      'author': author,
    };
  }

  factory Secret.fromMap(Map<String, dynamic> map) {
    return Secret(
      secret: map['secret'] as String,
      author: map['author'] as String,
    );
  }

  String toJson() => json.encode(toMap());

  factory Secret.fromJson(String source) =>
      Secret.fromMap(json.decode(source) as Map<String, dynamic>);
}

util/api_routes.dart

class ApiRoutes {
  static String login =
      "http://52.79.115.43:8090/api/collections/users/auth-with-password";
  static String signup =
      "http://52.79.115.43:8090/api/collections/users/records";
  static String secrets =
      "http://52.79.115.43:8090/api/collections/secrets/records?sort=-created";
  static String users =
      "http://52.79.115.43:8090/api/collections/users/records?sort=-created";
  static String upload =
      "http://52.79.115.43:8090/api/collections/secrets/records";
}

util/pages.dart

import 'package:get/get.dart';
import 'package:secret/view/page/login_page.dart';
import 'package:secret/view/page/main_page.dart';
import 'package:secret/view/page/secret_page.dart';
import 'package:secret/view/page/setting_page.dart';
import 'package:secret/view/page/signup_page.dart';
import 'package:secret/view/page/upload_page.dart';

class AppPages {
  static final pages = [
    //라우팅 연동위해서
    GetPage(name: MainPage.route, page: () => const MainPage()),
    GetPage(name: '/login', page: () => const LoginPage()),
    GetPage(name: SecretPage.route, page: () => const SecretPage()),
    GetPage(name: SettinPage.route, page: () => const SettinPage()),
    GetPage(name: SignupPage.route, page: () => const SignupPage()),
    GetPage(name: UploadPage.route, page: () => const UploadPage()),
  ];
}

view/page/login_page.dart

import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/container.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:get/get.dart';
import 'package:secret/controller/login_controller.dart';
import 'package:secret/view/page/signup_page.dart';

class LoginPage extends GetView<LoginController> {
  const LoginPage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Color.fromARGB(255, 17, 100, 93),
      body: Center(
        child: Padding(
          padding: const EdgeInsets.all(16),
          child: SingleChildScrollView(
            child: Column(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Text(
                  "비밀듣는 고양이",
                  style: TextStyle(
                      color: Colors.white,
                      fontSize: 15,
                      fontWeight: FontWeight.bold),
                ),
                SizedBox(
                  height: 10,
                ),
                CircleAvatar(
                  radius: 56,
                  backgroundColor: Colors.white38,
                  backgroundImage: AssetImage('assets/images/cat.jpg'),
                ),
                SizedBox(
                  height: 20,
                ),
                //id, 패스워드 입력하는 Textfield만들기
                TextField(
                  decoration: InputDecoration(
                    label: Text('ID'),
                    hintText: "ID를 입력하세요",
                    labelStyle: TextStyle(color: Colors.redAccent),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(10.0)),
                      borderSide: BorderSide(width: 1, color: Colors.redAccent),
                    ),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(10.0)),
                      borderSide: BorderSide(width: 1, color: Colors.redAccent),
                    ),
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(10.0)),
                    ),
                  ),
                  controller: controller
                      .idController, //Getview에서 온 LoginController안에 아이디컨트롤러가져오기
                ),
                SizedBox(
                  height: 15,
                ),
                TextField(
                  decoration: InputDecoration(
                    label: Text('Password'),
                    hintText: "비밀번호를 입력하세요",
                    labelStyle: TextStyle(color: Colors.redAccent),
                    focusedBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(10.0)),
                      borderSide: BorderSide(width: 1, color: Colors.redAccent),
                    ),
                    enabledBorder: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(10.0)),
                      borderSide: BorderSide(width: 1, color: Colors.redAccent),
                    ),
                    border: OutlineInputBorder(
                      borderRadius: BorderRadius.all(Radius.circular(10.0)),
                    ),
                  ),
                  controller: controller.pwController,
                ),
                ElevatedButton(
                    onPressed: controller.login, //로그인컨트롤러의 로그인 함수 실행 ()안써야돼
                    child: const Text("Login")),
                TextButton(
                    onPressed: () => Get.toNamed(SignupPage.route),
                    child: Text("회원가입하기"))
              ],
            ),
          ),
        ),
      ),
    );
  }
}

view/page/main_page.dart

import 'package:flutter/src/widgets/container.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:secret/view/page/secret_page.dart';
import 'package:secret/view/page/setting_page.dart';
import 'package:secret/view/page/upload_page.dart';

class MainPage extends StatelessWidget {
  const MainPage({super.key});
  static const route = '/main';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Color.fromARGB(255, 17, 100, 93),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Text(
            "비밀듣는 고양이",
            style: TextStyle(
                color: Colors.white, fontSize: 15, fontWeight: FontWeight.bold),
          ),
          SizedBox(
            height: 20,
          ),
          CircleAvatar(
            radius: 56,
            backgroundColor: Colors.white38,
            backgroundImage: AssetImage('assets/images/cat.jpg'),
          ),
          SizedBox(
            height: 15,
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: ListTile(
              tileColor: Colors.orangeAccent,
              title: const Text(
                "비밀보기",
                style: TextStyle(
                    color: Colors.white,
                    fontSize: 15,
                    fontWeight: FontWeight.bold),
              ),
              trailing: CircleAvatar(
                radius: 15,
                backgroundColor: Colors.white38,
                backgroundImage: AssetImage('assets/images/cat.jpg'),
              ),
              onTap: () => Get.toNamed(SecretPage.route), //페이지이동
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: ListTile(
              tileColor: Colors.orangeAccent,
              trailing: CircleAvatar(
                radius: 15,
                backgroundColor: Colors.white38,
                backgroundImage: AssetImage('assets/images/cat.jpg'),
              ),
              title: const Text(
                "비밀올리기",
                style: TextStyle(
                    color: Colors.white,
                    fontSize: 15,
                    fontWeight: FontWeight.bold),
              ),
              onTap: () => Get.toNamed(UploadPage.route),
            ),
          ),
          Padding(
            padding: const EdgeInsets.all(8.0),
            child: ListTile(
              tileColor: Colors.orangeAccent,
              trailing: CircleAvatar(
                radius: 15,
                backgroundColor: Colors.white38,
                backgroundImage: AssetImage('assets/images/cat.jpg'),
              ),
              title: const Text(
                "앱설정",
                style: TextStyle(
                    color: Colors.white,
                    fontSize: 15,
                    fontWeight: FontWeight.bold),
              ),
              onTap: () => Get.toNamed(SettinPage.route),
            ),
          )
        ],
      ),
    );
  }
}

view/page/secret_page.dart

import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/container.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:get/get.dart';
import 'package:secret/controller/secret_controller.dart';

var backgroundImg =
    "https://images.unsplash.com/photo-1574144611937-0df059b5ef3e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxzZWFyY2h8MjR8fGNhdHxlbnwwfHwwfHw%3D&auto=format&fit=crop&w=500&q=60";

class SecretPage extends GetView<SecretController> {
  const SecretPage({super.key});
  static const route = '/secret';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      extendBodyBehindAppBar: true, //body가 앱바 영역까지 차지
      appBar: AppBar(
        centerTitle: false,
        title: Text("뒤로가기"),
        backgroundColor: Colors.transparent,
        elevation: 0,
      ),
      body: Container(
        alignment: Alignment.center,
        decoration: BoxDecoration(
            image: DecorationImage(
                image: NetworkImage(backgroundImg),
                fit: BoxFit.cover,
                colorFilter:
                    ColorFilter.mode(Colors.black54, BlendMode.darken))),
        child: Obx(() => PageView.builder(
              itemCount: controller.secrets.length, //get을 통해 가져온 secret 갯수만큼
              itemBuilder: (context, index) => Center(
                  child: Text(
                controller.secrets[index].secret,
                style:
                    TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
              )),
            )),
      ),
    );
  }
}

view/page/setting_page.dart

import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/container.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:get/get.dart';
import 'package:secret/controller/auth_controller.dart';

//원하는 함수 변수들 다 Auth에 해놨어 그거 가져와
class SettinPage extends GetView<AuthController> {
  const SettinPage({super.key});
  static const route = '/setting';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Color.fromARGB(255, 17, 100, 93),
      body: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          const Text(
            "내 정보!",
            style: TextStyle(
                color: Colors.white, fontSize: 15, fontWeight: FontWeight.bold),
          ),
          SizedBox(
            height: 20,
          ),
          CircleAvatar(
            radius: 56,
            backgroundColor: Colors.white38,
            backgroundImage: AssetImage('assets/images/cat.jpg'),
          ),
          SizedBox(
            height: 15,
          ),
          ListTile(
            title: Text(controller.profile!
                .username), //?로 get을 설정해 놨음. 이미 로그인 한 상태이기 때문에 무조건 데이터 있어 !
            subtitle: Text(controller.profile!.name),
          ),
          ElevatedButton(
              onPressed: controller.logout, child: const Text("로그아웃")),
          //Auth에 있는 로그아웃 함수 실행, 이미 정의해놔서 ()쓰지마
        ],
      ),
    );
  }
}

view/page/signup_page.dart


import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/container.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:get/get.dart';

import '../../controller/signup_controller.dart';

class SignupPage extends GetView<SignupController> {
  const SignupPage({super.key});
  static const route = '/signup';
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Color.fromARGB(255, 17, 100, 93),
      body: SingleChildScrollView(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            const Text(
              "비밀듣는 고양이",
              style: TextStyle(
                  color: Colors.white,
                  fontSize: 15,
                  fontWeight: FontWeight.bold),
            ),
            SizedBox(
              height: 20,
            ),
            CircleAvatar(
              radius: 56,
              backgroundColor: Colors.white38,
              backgroundImage: AssetImage('assets/images/cat.jpg'),
            ),
            SizedBox(
              height: 15,
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: TextField(
                decoration: InputDecoration(
                  label: Text('Email'),
                  hintText: "Email을 입력하세요",
                  labelStyle: TextStyle(color: Colors.redAccent),
                  focusedBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.all(Radius.circular(10.0)),
                    borderSide: BorderSide(width: 1, color: Colors.redAccent),
                  ),
                  enabledBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.all(Radius.circular(10.0)),
                    borderSide: BorderSide(width: 1, color: Colors.redAccent),
                  ),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.all(Radius.circular(10.0)),
                  ),
                ),
                controller: controller.emailController, //컨트롤러 연결
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: TextField(
                decoration: InputDecoration(
                  label: Text('Username'),
                  hintText: "이름을 입력하세요",
                  labelStyle: TextStyle(color: Colors.redAccent),
                  focusedBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.all(Radius.circular(10.0)),
                    borderSide: BorderSide(width: 1, color: Colors.redAccent),
                  ),
                  enabledBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.all(Radius.circular(10.0)),
                    borderSide: BorderSide(width: 1, color: Colors.redAccent),
                  ),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.all(Radius.circular(10.0)),
                  ),
                ),
                controller: controller.usernameController, //컨트롤러 연결
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: TextField(
                decoration: InputDecoration(
                  label: Text('Password'),
                  hintText: "비밀번호를 입력하세요",
                  labelStyle: TextStyle(color: Colors.redAccent),
                  focusedBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.all(Radius.circular(10.0)),
                    borderSide: BorderSide(width: 1, color: Colors.redAccent),
                  ),
                  enabledBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.all(Radius.circular(10.0)),
                    borderSide: BorderSide(width: 1, color: Colors.redAccent),
                  ),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.all(Radius.circular(10.0)),
                  ),
                ),
                controller: controller.pwController,
              ),
            ),
            Padding(
              padding: const EdgeInsets.all(8.0),
              child: TextField(
                decoration: InputDecoration(
                  label: Text('PwConfirm'),
                  hintText: "비밀번호를 다시 입력하세요",
                  labelStyle: TextStyle(color: Colors.redAccent),
                  focusedBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.all(Radius.circular(10.0)),
                    borderSide: BorderSide(width: 1, color: Colors.redAccent),
                  ),
                  enabledBorder: OutlineInputBorder(
                    borderRadius: BorderRadius.all(Radius.circular(10.0)),
                    borderSide: BorderSide(width: 1, color: Colors.redAccent),
                  ),
                  border: OutlineInputBorder(
                    borderRadius: BorderRadius.all(Radius.circular(10.0)),
                  ),
                ),
                controller: controller.pw2Controller,
              ),
            ),
            ElevatedButton(onPressed: controller.signup, child: Text("회원가입"))
          ],
        ),
      ),
    );
  }
}

view/page/upload_page.dart

import 'package:flutter/material.dart';
import 'package:flutter/src/widgets/container.dart';
import 'package:flutter/src/widgets/framework.dart';
import 'package:get/get.dart';
import 'package:secret/controller/upload_controller.dart';

class UploadPage extends GetView<UploadController> {
  const UploadPage({super.key});
  static const route = '/upload';

  @override
  Widget build(BuildContext context) {
    return Scaffold(
        backgroundColor: Color.fromARGB(255, 17, 100, 93),
        body: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            Text(
              "UploadPage",
              style: TextStyle(
                  color: Colors.white,
                  fontSize: 15,
                  fontWeight: FontWeight.bold),
            ),
            SizedBox(
              height: 15,
            ),
            CircleAvatar(
              radius: 56,
              backgroundColor: Colors.white38,
              backgroundImage: AssetImage('assets/images/cat.jpg'),
            ),
            SizedBox(
              height: 15,
            ),
            TextField(
              controller: controller.inputController,
              maxLines: 10,
              minLines: 8,
              decoration: InputDecoration(
                  filled: true,
                  fillColor: Colors.white24,
                  hintText: "비밀을 입력하세요!!!!"),
            ),
            ElevatedButton(onPressed: controller.upload, child: Text("Upload"))
          ],
        ));
  }
}
⚠️ **GitHub.com Fallback** ⚠️