├── backend ├── .env.example ├── src │ ├── core │ │ ├── constants │ │ │ └── constants.ts │ │ └── utils │ │ │ ├── types │ │ │ └── hono.ts │ │ │ ├── genai │ │ │ ├── vertexAi.ts │ │ │ ├── flowExtensions.ts │ │ │ └── prompts.ts │ │ │ ├── firebase │ │ │ ├── fetchFileAsDataUri.ts │ │ │ └── firebaseStorage.ts │ │ │ └── schema │ │ │ └── schema.ts │ ├── middleware │ │ └── firebaseBearerAuth.ts │ ├── index.ts │ └── routes │ │ └── genai.ts ├── compose.yaml ├── .dockerignore ├── Dockerfile ├── README.md ├── .gitignore ├── biome.json ├── tsconfig.json └── package.json ├── .gitignore ├── frontend ├── .fvmrc ├── ios │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner.xcodeproj │ │ └── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ ├── RunnerTests │ │ └── RunnerTests.swift │ ├── .gitignore │ ├── scripts │ │ └── extract_dart_defines.sh │ ├── firebase │ │ ├── prod │ │ │ └── GoogleService-Info.plist │ │ └── dev │ │ │ └── GoogleService-Info.plist │ └── Podfile ├── .vscode │ ├── settings.json │ └── launch.json ├── lib │ ├── config │ │ ├── router │ │ │ ├── router.dart │ │ │ ├── branches │ │ │ │ └── shell_route.dart │ │ │ └── codecs │ │ │ │ └── extra_codec.dart │ │ ├── environment │ │ │ ├── firebase_initializer.dart │ │ │ ├── firebase_options_prod.dart │ │ │ └── firebase_options_dev.dart │ │ └── providers │ │ │ ├── firebase_providers.dart │ │ │ └── firebase_providers.g.dart │ ├── core │ │ ├── presentation │ │ │ ├── keyboard │ │ │ │ ├── keyboard.dart │ │ │ │ ├── keyboard_visibility.g.dart │ │ │ │ └── keyboard_visibility.dart │ │ │ ├── widgets │ │ │ │ ├── offsets │ │ │ │ │ ├── offsets.dart │ │ │ │ │ ├── custom_offset_theme.dart │ │ │ │ │ ├── offset_text.dart │ │ │ │ │ ├── offset_icon.dart │ │ │ │ │ ├── offset_theme.dart │ │ │ │ │ ├── offset_container.dart │ │ │ │ │ └── halftone_background.dart │ │ │ │ ├── bars │ │ │ │ │ ├── custom_divider.dart │ │ │ │ │ ├── sticky_bar_delegate.dart │ │ │ │ │ ├── sliver_tab_bar.dart │ │ │ │ │ └── fixed_bottom_bar.dart │ │ │ │ ├── layouts │ │ │ │ │ ├── wrap_scaffold.dart │ │ │ │ │ ├── unfocus_layout.dart │ │ │ │ │ ├── shimmer_widget.dart │ │ │ │ │ └── async_error_widget.dart │ │ │ │ ├── typographies │ │ │ │ │ └── label.dart │ │ │ │ ├── clippers │ │ │ │ │ └── rounded_hexagon_clipper.dart │ │ │ │ ├── cards │ │ │ │ │ ├── custom_switch_list_tile.dart │ │ │ │ │ ├── custom_list_tile.dart │ │ │ │ │ └── mnemonic_list_card.dart │ │ │ │ ├── listeners │ │ │ │ │ └── level_up_listener.dart │ │ │ │ ├── builder │ │ │ │ │ └── async_value_builder.dart │ │ │ │ └── modals │ │ │ │ │ └── level_up_modal.dart │ │ │ └── dialogs │ │ │ │ └── auto_dismiss_dialog.dart │ │ ├── extensions │ │ │ ├── string_extensions.dart │ │ │ ├── int_extensions.dart │ │ │ ├── color_extensions.dart │ │ │ ├── go_router_extensions.dart │ │ │ ├── ref_extentions.dart │ │ │ └── firestore_extensions.dart │ │ ├── constants │ │ │ ├── app_theme.dart │ │ │ ├── constants.dart │ │ │ └── app_colors.dart │ │ └── data │ │ │ ├── logger │ │ │ └── logger.dart │ │ │ └── converters │ │ │ └── timestamp_converter.dart │ ├── features │ │ ├── quiz │ │ │ ├── application │ │ │ │ ├── quiz_state_provider.dart │ │ │ │ ├── quiz_play_state.g.dart │ │ │ │ ├── quiz_state_provider.g.dart │ │ │ │ └── quiz_play_state.dart │ │ │ ├── domain │ │ │ │ └── entities │ │ │ │ │ ├── quiz_log.dart │ │ │ │ │ └── quiz_log.g.dart │ │ │ ├── data │ │ │ │ └── repositories │ │ │ │ │ ├── quiz_repository_provider.g.dart │ │ │ │ │ └── quiz_repository_provider.dart │ │ │ └── presentation │ │ │ │ └── widgets │ │ │ │ └── status_content.dart │ │ ├── profile │ │ │ ├── application │ │ │ │ ├── edit_interests_state.dart │ │ │ │ ├── app_user_form_state.dart │ │ │ │ ├── edit_cat_level_state.dart │ │ │ │ ├── app_user_form_provider.g.dart │ │ │ │ ├── edit_cat_level_provider.g.dart │ │ │ │ ├── edit_interests_provider.g.dart │ │ │ │ ├── edit_interests_provider.dart │ │ │ │ ├── edit_cat_level_provider.dart │ │ │ │ └── app_user_form_provider.dart │ │ │ └── presentation │ │ │ │ └── widgets │ │ │ │ └── edit_text_form.dart │ │ ├── generate │ │ │ ├── application │ │ │ │ ├── voice_input_state.dart │ │ │ │ ├── re_generate_state.dart │ │ │ │ ├── voice_input_providers.g.dart │ │ │ │ └── re_generate_provider.dart │ │ │ ├── presentation │ │ │ │ └── widgets │ │ │ │ │ ├── show_voice_input_bottom_sheet.dart │ │ │ │ │ ├── generate_loading.dart │ │ │ │ │ └── recording_effect.dart │ │ │ ├── domain │ │ │ │ └── entities │ │ │ │ │ ├── mnemonic_response.dart │ │ │ │ │ └── mnemonic_response.g.dart │ │ │ └── data │ │ │ │ └── repositories │ │ │ │ └── generate_repository_provider.g.dart │ │ ├── mnemonics │ │ │ ├── application │ │ │ │ ├── mnemonics_details_provider.dart │ │ │ │ ├── mnemonics_state.dart │ │ │ │ └── mnemonics_provider.dart │ │ │ ├── domain │ │ │ │ └── entities │ │ │ │ │ ├── mnemonic_detail_page_extra.dart │ │ │ │ │ ├── mnemonic_detail_page_extra.g.dart │ │ │ │ │ ├── mnemonic.dart │ │ │ │ │ └── mnemonic.g.dart │ │ │ ├── data │ │ │ │ └── repositories │ │ │ │ │ ├── mnemonics_repository_provider.g.dart │ │ │ │ │ └── mnemonics_repository_provider.dart │ │ │ └── presentation │ │ │ │ ├── widgets │ │ │ │ └── mnemonics_tab_view.dart │ │ │ │ └── pages │ │ │ │ └── mnemonics_page.dart │ │ ├── auth │ │ │ ├── application │ │ │ │ ├── combined_auth_state.dart │ │ │ │ ├── auth_state_changes_provider.dart │ │ │ │ ├── app_user_provider.dart │ │ │ │ ├── app_user_provider.g.dart │ │ │ │ ├── combined_auth_provider.g.dart │ │ │ │ └── auth_state_changes_provider.g.dart │ │ │ ├── data │ │ │ │ └── repositories │ │ │ │ │ ├── auth_repository_provider.dart │ │ │ │ │ ├── auth_repository_provider.g.dart │ │ │ │ │ └── app_user_repository_provider.g.dart │ │ │ ├── presentation │ │ │ │ └── builders │ │ │ │ │ └── auth_builder.dart │ │ │ └── domain │ │ │ │ └── entities │ │ │ │ ├── app_user.dart │ │ │ │ └── app_user.g.dart │ │ ├── home │ │ │ ├── application │ │ │ │ ├── home_state.dart │ │ │ │ ├── home_provider.g.dart │ │ │ │ └── home_provider.dart │ │ │ └── presentation │ │ │ │ └── widgets │ │ │ │ └── status_content.dart │ │ └── shell │ │ │ └── presentation │ │ │ └── pages │ │ │ └── shell_page.dart │ ├── app.dart │ └── main.dart ├── assets │ └── images │ │ ├── quiz_cat.png │ │ ├── welcome.png │ │ ├── example_cat_1.png │ │ ├── example_cat_2.png │ │ ├── is_completed.png │ │ ├── mic_fill.svg │ │ ├── chevrons-up-down.svg │ │ ├── annoyed.svg │ │ ├── home_fill.svg │ │ ├── paw_print_fill.svg │ │ ├── sticker_fill.svg │ │ ├── mic.svg │ │ ├── meh.svg │ │ ├── smile.svg │ │ ├── laugh.svg │ │ ├── home.svg │ │ ├── paw_print.svg │ │ ├── sticker.svg │ │ ├── cat_fill.svg │ │ ├── cat.svg │ │ ├── settings_fill.svg │ │ └── settings.svg ├── android │ ├── gradle.properties │ ├── app │ │ └── src │ │ │ ├── main │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ └── ic_launcher.png │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── goronyan │ │ │ │ │ └── goronyan │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── profile │ │ │ └── AndroidManifest.xml │ │ │ └── firebase │ │ │ ├── prod │ │ │ └── google-services.json │ │ │ └── dev │ │ │ └── google-services.json │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── build.gradle │ └── settings.gradle ├── dart_defines │ ├── prod.env │ └── dev.env ├── analysis_options.yaml ├── firebase.json ├── README.md ├── pubspec.yaml ├── .metadata └── .gitignore ├── firestore.indexes.json ├── .firebaserc ├── functions ├── src │ ├── core │ │ ├── constants │ │ │ └── constants.ts │ │ └── utils │ │ │ ├── calculations │ │ │ └── levelCalculator.ts │ │ │ └── schema │ │ │ └── schema.ts │ ├── index.ts │ └── triggers │ │ └── users.ts ├── .gitignore ├── tsconfig.json ├── biome.json └── package.json ├── .vscode ├── settings.json └── launch.json ├── README.md ├── package.json ├── firebase.json ├── storage.rules └── firestore.rules /backend/.env.example: -------------------------------------------------------------------------------- 1 | STORAGE_BUCKET= 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | /node_modules 3 | -------------------------------------------------------------------------------- /frontend/.fvmrc: -------------------------------------------------------------------------------- 1 | { 2 | "flutter": "3.27.1" 3 | } -------------------------------------------------------------------------------- /backend/src/core/constants/constants.ts: -------------------------------------------------------------------------------- 1 | export const aiRegion = "us-central1"; 2 | -------------------------------------------------------------------------------- /firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [], 3 | "fieldOverrides": [] 4 | } 5 | -------------------------------------------------------------------------------- /frontend/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "hackathon2024-4cb8e" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /frontend/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "dart.flutterSdkPath": ".fvm/versions/3.27.1" 3 | } -------------------------------------------------------------------------------- /backend/src/core/utils/types/hono.ts: -------------------------------------------------------------------------------- 1 | export type HonoVariables = { 2 | userId: string; 3 | }; 4 | -------------------------------------------------------------------------------- /backend/compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: . 4 | ports: 5 | - "8080:8080" 6 | -------------------------------------------------------------------------------- /frontend/lib/config/router/router.dart: -------------------------------------------------------------------------------- 1 | export 'app_routes.dart'; 2 | export 'branches/shell_route.dart'; 3 | -------------------------------------------------------------------------------- /backend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .git/ 3 | Dockerfile 4 | compose.yaml 5 | .dockerignore 6 | README.md 7 | -------------------------------------------------------------------------------- /frontend/assets/images/quiz_cat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/assets/images/quiz_cat.png -------------------------------------------------------------------------------- /frontend/assets/images/welcome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/assets/images/welcome.png -------------------------------------------------------------------------------- /functions/src/core/constants/constants.ts: -------------------------------------------------------------------------------- 1 | export const defaultRegion = "asia-northeast1"; 2 | export const aiRegion = "us-central1"; 3 | -------------------------------------------------------------------------------- /frontend/assets/images/example_cat_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/assets/images/example_cat_1.png -------------------------------------------------------------------------------- /frontend/assets/images/example_cat_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/assets/images/example_cat_2.png -------------------------------------------------------------------------------- /frontend/assets/images/is_completed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/assets/images/is_completed.png -------------------------------------------------------------------------------- /frontend/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /frontend/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | #include "Dart-Defines.xcconfig" 4 | -------------------------------------------------------------------------------- /frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /frontend/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | #include "Dart-Defines.xcconfig" 4 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/keyboard/keyboard.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | void dismissKeyboard() { 4 | FocusManager.instance.primaryFocus?.unfocus(); 5 | } 6 | -------------------------------------------------------------------------------- /frontend/lib/core/extensions/string_extensions.dart: -------------------------------------------------------------------------------- 1 | extension NullableStringExtensions on String? { 2 | bool get isNotNullOrEmpty { 3 | return this != null && this!.isNotEmpty; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /frontend/android/app/src/main/kotlin/com/goronyan/goronyan/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.goronyan.goronyan 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /frontend/dart_defines/prod.env: -------------------------------------------------------------------------------- 1 | flavor="prod" 2 | appName="Goroにゃーん" 3 | appCFBundleName="goronyan" 4 | appId="com.goronyan.goronyan" 5 | apiBaseUrl="https://ai-agent-hackathon-1005658646293.asia-northeast1.run.app" 6 | -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/ai-agent-hackathon/main/frontend/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /frontend/dart_defines/dev.env: -------------------------------------------------------------------------------- 1 | flavor="dev" 2 | appName="Goroにゃーん dev" 3 | appCFBundleName="goronyandev" 4 | appId="com.goronyan.goronyan.dev" 5 | apiBaseUrl="https://ai-agent-hackathon-1005658646293.asia-northeast1.run.app" 6 | -------------------------------------------------------------------------------- /frontend/lib/core/extensions/int_extensions.dart: -------------------------------------------------------------------------------- 1 | extension IntNullableExtension on int? { 2 | String toStringOrDefault([int defaultValue = 0]) { 3 | return this?.toString() ?? defaultValue.toString(); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:22 2 | 3 | WORKDIR /usr/src/app 4 | 5 | COPY package*.json ./ 6 | RUN npm install 7 | 8 | COPY . . 9 | 10 | RUN npm run build 11 | 12 | EXPOSE 8080 13 | 14 | CMD ["npm", "run", "start"] 15 | -------------------------------------------------------------------------------- /frontend/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "[typescript]": { 4 | "editor.codeActionsOnSave": { 5 | "source.organizeImports": "explicit", 6 | "source.sortImports": "explicit" 7 | } 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /frontend/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip 6 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/offsets/offsets.dart: -------------------------------------------------------------------------------- 1 | export 'custom_offset_theme.dart'; 2 | export 'offset_button.dart'; 3 | export 'offset_container.dart'; 4 | export 'offset_icon.dart'; 5 | export 'offset_icon_svg.dart'; 6 | export 'offset_text.dart'; 7 | export 'offset_theme.dart'; 8 | -------------------------------------------------------------------------------- /frontend/assets/images/mic_fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/assets/images/chevrons-up-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/lib/core/extensions/color_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | extension ColorX on Color { 4 | Color withDoubleOpacity(double opacity) { 5 | return withValues( 6 | alpha: opacity, 7 | red: r, 8 | green: g, 9 | blue: b, 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /functions/.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled JavaScript files 2 | lib/ 3 | 4 | # TypeScript v1 declaration files 5 | typings/ 6 | 7 | # Node.js dependency directory 8 | node_modules/ 9 | *.local 10 | 11 | # env files 12 | .env 13 | 14 | # Firebase 15 | GOOGLE_APPLICATION_CREDENTIALS.json 16 | 17 | # genkit 18 | .genkit 19 | -------------------------------------------------------------------------------- /frontend/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # include: package:flutter_lints/flutter.yaml 2 | # https://pub.dev/packages/pedantic_mono 3 | include: package:pedantic_mono/analysis_options.yaml 4 | 5 | analyzer: 6 | exclude: 7 | - '**/*.g.dart' 8 | 9 | linter: 10 | rules: 11 | avoid_redundant_argument_values: false 12 | -------------------------------------------------------------------------------- /frontend/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /frontend/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /frontend/lib/core/constants/app_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'app_colors.dart'; 3 | 4 | class AppTheme { 5 | static const boxShadows = [ 6 | BoxShadow( 7 | color: AppColors.lightGray, 8 | blurRadius: 24, 9 | offset: Offset(0, 4), 10 | ), 11 | ]; 12 | } 13 | -------------------------------------------------------------------------------- /frontend/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/to/reference-keystore 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /frontend/assets/images/annoyed.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /frontend/assets/images/home_fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /frontend/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Goroにゃーん 2 | 3 | Backend起動 4 | ```shell 5 | cd backend 6 | docker compose up --build 7 | ``` 8 | 9 | dev Flutterアプリ起動 10 | ```shell 11 | cd app 12 | flutter run --dart-define-from-file=dart_defines/dev.env 13 | ``` 14 | 15 | テスト用 Android APKファイル ダウンロードリンク 16 | https://drive.google.com/file/d/1WmMQ2sE4Dlb6BFXfnsKSTHMq3CG_oIwE/view?usp=sharing 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat_app", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1" 7 | }, 8 | "keywords": [], 9 | "author": "", 10 | "license": "ISC", 11 | "description": "", 12 | "devDependencies": { 13 | "firebase-tools": "^13.29.1" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /frontend/assets/images/paw_print_fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/assets/images/sticker_fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/README.md: -------------------------------------------------------------------------------- 1 | # Goroにゃーん Backend 2 | 3 | コンテナ起動 4 | ```shell 5 | docker compose up --build 6 | ``` 7 | 8 | コンテナ終了 9 | ```shell 10 | docker compose down 11 | ``` 12 | 13 | デプロイ 14 | ```shell 15 | docker build -t goronyan . 16 | 17 | gcloud run deploy ai-agent-hackathon \ 18 | --source . \ 19 | --region asia-northeast1 \ 20 | --allow-unauthenticated 21 | ``` 22 | -------------------------------------------------------------------------------- /frontend/assets/images/mic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /frontend/assets/images/meh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/assets/images/smile.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as admin from "firebase-admin"; 2 | import { setGlobalOptions } from "firebase-functions/v2"; 3 | import { defaultRegion } from "./core/constants/constants"; 4 | import { onUserXpChange } from "./triggers/users"; 5 | 6 | admin.initializeApp(); 7 | 8 | setGlobalOptions({ 9 | region: defaultRegion, 10 | }); 11 | 12 | export { onUserXpChange }; 13 | -------------------------------------------------------------------------------- /frontend/assets/images/laugh.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /frontend/assets/images/home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/lib/features/quiz/application/quiz_state_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 2 | 3 | part 'quiz_state_provider.g.dart'; 4 | 5 | @riverpod 6 | class QuizState extends _$QuizState { 7 | @override 8 | bool build() { 9 | return true; 10 | } 11 | 12 | Future updateState({required bool value}) async { 13 | state = value; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /backend/src/core/utils/genai/vertexAi.ts: -------------------------------------------------------------------------------- 1 | import { 2 | gemini15Flash, 3 | gemini20FlashExp, 4 | imagen3, 5 | vertexAI, 6 | } from "@genkit-ai/vertexai"; 7 | import { genkit } from "genkit"; 8 | import { aiRegion } from "../../constants/constants.js"; 9 | 10 | export const ai = genkit({ 11 | plugins: [vertexAI({ location: aiRegion })], 12 | }); 13 | 14 | export { gemini15Flash, gemini20FlashExp, imagen3 }; 15 | -------------------------------------------------------------------------------- /frontend/android/build.gradle: -------------------------------------------------------------------------------- 1 | allprojects { 2 | repositories { 3 | google() 4 | mavenCentral() 5 | } 6 | } 7 | 8 | rootProject.buildDir = "../build" 9 | subprojects { 10 | project.buildDir = "${rootProject.buildDir}/${project.name}" 11 | } 12 | subprojects { 13 | project.evaluationDependsOn(":app") 14 | } 15 | 16 | tasks.register("clean", Delete) { 17 | delete rootProject.buildDir 18 | } 19 | -------------------------------------------------------------------------------- /frontend/lib/core/data/logger/logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:logger/logger.dart'; 3 | 4 | final logger = Logger( 5 | level: kReleaseMode ? Level.error : Level.debug, 6 | printer: PrettyPrinter( 7 | methodCount: kReleaseMode ? 0 : 2, 8 | dateTimeFormat: kReleaseMode 9 | ? DateTimeFormat.none 10 | : DateTimeFormat.onlyTimeAndSinceStart, 11 | ), 12 | ); 13 | -------------------------------------------------------------------------------- /frontend/assets/images/paw_print.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/assets/images/sticker.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /frontend/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "include": [ 4 | "src" 5 | ], 6 | "compilerOptions": { 7 | "module": "NodeNext", 8 | "noImplicitReturns": true, 9 | "outDir": "lib", 10 | "sourceMap": true, 11 | "strict": true, 12 | "target": "es2017", 13 | "skipLibCheck": true, 14 | "esModuleInterop": true, 15 | "moduleResolution": "nodenext", 16 | "noUnusedLocals": true 17 | } 18 | } -------------------------------------------------------------------------------- /frontend/lib/features/profile/application/edit_interests_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'edit_interests_state.freezed.dart'; 4 | 5 | @freezed 6 | class EditInterestsState with _$EditInterestsState { 7 | factory EditInterestsState({ 8 | @Default('') String interests, 9 | @Default(false) bool isUpdating, 10 | }) = _EditInterestsState; 11 | 12 | EditInterestsState._(); 13 | } 14 | -------------------------------------------------------------------------------- /frontend/lib/features/generate/application/voice_input_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'voice_input_state.freezed.dart'; 4 | 5 | @freezed 6 | class VoiceInputState with _$VoiceInputState { 7 | factory VoiceInputState({ 8 | @Default(false) bool isInitialized, 9 | @Default(false) bool isRecording, 10 | String? filePath, 11 | }) = _VoiceInputState; 12 | 13 | VoiceInputState._(); 14 | } 15 | -------------------------------------------------------------------------------- /backend/src/core/utils/firebase/fetchFileAsDataUri.ts: -------------------------------------------------------------------------------- 1 | import { getStorage } from "firebase-admin/storage"; 2 | 3 | export const fetchFileAsDataUri = async ( 4 | filePath: string, 5 | prefix = "data:audio/mp4;base64", 6 | ) => { 7 | const storage = getStorage(); 8 | const [buffer] = await storage.bucket().file(filePath).download(); 9 | 10 | const base64 = buffer.toString("base64"); 11 | const dataUri = `${prefix},${base64}`; 12 | return dataUri; 13 | }; 14 | -------------------------------------------------------------------------------- /frontend/assets/images/cat_fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/lib/features/generate/presentation/widgets/show_voice_input_bottom_sheet.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/features/generate/presentation/widgets/voice_input_bottom_sheet.dart'; 3 | 4 | Future showVoiceInputBottomSheet(BuildContext context) { 5 | return showModalBottomSheet( 6 | context: context, 7 | showDragHandle: true, 8 | builder: (context) => const VoiceInputBottomSheet(), 9 | ); 10 | } 11 | -------------------------------------------------------------------------------- /frontend/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /frontend/lib/core/extensions/go_router_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:go_router/go_router.dart'; 2 | 3 | extension GoRouterLocation on GoRouter { 4 | /// Get the current location of the router. 5 | String get location { 6 | final lastMatch = routerDelegate.currentConfiguration.last; 7 | final matchList = lastMatch is ImperativeRouteMatch 8 | ? lastMatch.matches 9 | : routerDelegate.currentConfiguration; 10 | return matchList.uri.toString(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /frontend/lib/features/mnemonics/application/mnemonics_details_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 2 | 3 | part 'mnemonics_details_provider.g.dart'; 4 | 5 | @riverpod 6 | class MnemonicsDetails extends _$MnemonicsDetails { 7 | @override 8 | int build(int initialIndex) { 9 | return initialIndex; 10 | } 11 | 12 | // ignore: use_setters_to_change_properties 13 | void onChanged(int newIndex) { 14 | state = newIndex; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/lib/features/profile/application/app_user_form_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:goronyan/core/constants/constants.dart'; 3 | 4 | part 'app_user_form_state.freezed.dart'; 5 | 6 | @freezed 7 | class AppUserFormState with _$AppUserFormState { 8 | factory AppUserFormState({ 9 | @Default('') String interests, 10 | @Default(defaultCatLevel) double catLevel, 11 | }) = _AppUserFormState; 12 | 13 | AppUserFormState._(); 14 | } 15 | -------------------------------------------------------------------------------- /frontend/lib/features/generate/application/re_generate_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:goronyan/features/mnemonics/domain/entities/mnemonic.dart'; 3 | 4 | part 're_generate_state.freezed.dart'; 5 | 6 | @freezed 7 | class ReGenerateState with _$ReGenerateState { 8 | factory ReGenerateState({ 9 | @Default(true) bool isReGenerateEnabled, 10 | required Mnemonic mnemonic, 11 | }) = _ReGenerateState; 12 | 13 | ReGenerateState._(); 14 | } 15 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/bars/custom_divider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/core/constants/app_colors.dart'; 3 | 4 | class CustomDivider extends StatelessWidget { 5 | const CustomDivider({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return const Divider( 10 | thickness: 0.5, 11 | color: AppColors.subColor, 12 | height: 0, 13 | indent: 16, 14 | endIndent: 16, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /frontend/lib/features/profile/application/edit_cat_level_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:goronyan/core/constants/constants.dart'; 3 | 4 | part 'edit_cat_level_state.freezed.dart'; 5 | 6 | @freezed 7 | class EditCatLevelState with _$EditCatLevelState { 8 | factory EditCatLevelState({ 9 | @Default(defaultCatLevel) double catLevel, 10 | @Default(false) bool isUpdating, 11 | }) = _EditCatLevelState; 12 | 13 | EditCatLevelState._(); 14 | } 15 | -------------------------------------------------------------------------------- /backend/.gitignore: -------------------------------------------------------------------------------- 1 | # dev 2 | .yarn/ 3 | !.yarn/releases 4 | .vscode/* 5 | !.vscode/launch.json 6 | !.vscode/*.code-snippets 7 | .idea/workspace.xml 8 | .idea/usage.statistics.xml 9 | .idea/shelf 10 | 11 | # deps 12 | node_modules/ 13 | 14 | # env 15 | .env 16 | .env.production 17 | 18 | # logs 19 | logs/ 20 | *.log 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | pnpm-debug.log* 25 | lerna-debug.log* 26 | 27 | # misc 28 | .DS_Store 29 | 30 | # Firebase 31 | GOOGLE_APPLICATION_CREDENTIALS.json 32 | -------------------------------------------------------------------------------- /backend/src/middleware/firebaseBearerAuth.ts: -------------------------------------------------------------------------------- 1 | import { getAuth } from "firebase-admin/auth"; 2 | import { bearerAuth } from "hono/bearer-auth"; 3 | 4 | export const firebaseBearerAuth = bearerAuth({ 5 | verifyToken: async (token, c) => { 6 | try { 7 | const auth = getAuth(); 8 | const decoded = await auth.verifyIdToken(token); 9 | c.set("userId", decoded.uid); 10 | return true; 11 | } catch (err) { 12 | console.error("Invalid Firebase token:", err); 13 | return false; 14 | } 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /frontend/lib/core/data/converters/timestamp_converter.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | class TimestampConverter implements JsonConverter { 5 | const TimestampConverter(); 6 | 7 | @override 8 | DateTime fromJson(Timestamp timestamp) { 9 | return timestamp.toDate(); 10 | } 11 | 12 | @override 13 | Timestamp toJson(DateTime date) { 14 | return Timestamp.fromDate(date); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/layouts/wrap_scaffold.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class WrapScaffold extends StatelessWidget { 4 | const WrapScaffold({ 5 | super.key, 6 | required this.isWrap, 7 | required this.child, 8 | }); 9 | 10 | final bool isWrap; 11 | final Widget child; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return isWrap 16 | ? Scaffold( 17 | body: child, 18 | ) 19 | : child; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/lib/features/auth/application/combined_auth_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | 4 | part 'combined_auth_state.freezed.dart'; 5 | 6 | @freezed 7 | class CombinedAuthState with _$CombinedAuthState { 8 | factory CombinedAuthState({ 9 | User? user, 10 | @Default(false) bool isProfileComplete, 11 | }) = _CombinedAuthState; 12 | 13 | CombinedAuthState._(); 14 | 15 | bool get isLoggedIn => user != null; 16 | } 17 | -------------------------------------------------------------------------------- /frontend/lib/features/home/application/home_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:goronyan/features/auth/domain/entities/app_user.dart'; 3 | import 'package:goronyan/features/mnemonics/domain/entities/mnemonic.dart'; 4 | 5 | part 'home_state.freezed.dart'; 6 | 7 | @freezed 8 | class HomeState with _$HomeState { 9 | factory HomeState({ 10 | AppUser? appUser, 11 | @Default([]) List mnemonics, 12 | }) = _HomeState; 13 | 14 | HomeState._(); 15 | } 16 | -------------------------------------------------------------------------------- /frontend/assets/images/cat.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/lib/features/auth/application/auth_state_changes_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:firebase_auth/firebase_auth.dart'; 4 | import 'package:goronyan/config/providers/firebase_providers.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 7 | 8 | part 'auth_state_changes_provider.g.dart'; 9 | 10 | @riverpod 11 | Stream authStateChanges(Ref ref) { 12 | return ref.watch(firebaseAuthProvider).authStateChanges(); 13 | } 14 | -------------------------------------------------------------------------------- /frontend/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/offsets/custom_offset_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/core/constants/app_colors.dart'; 3 | import 'package:goronyan/core/presentation/widgets/offsets/offset_theme.dart'; 4 | 5 | const grayOffsetTheme = OffsetTheme( 6 | offsetColor: AppColors.backgroundGray, 7 | offset: Offset(5, 5), 8 | blurRadius: 0, 9 | ); 10 | 11 | const micOffsetTheme = OffsetTheme( 12 | offsetColor: AppColors.accent1, 13 | offset: Offset(4, 4), 14 | blurRadius: 0, 15 | ); 16 | -------------------------------------------------------------------------------- /functions/src/core/utils/calculations/levelCalculator.ts: -------------------------------------------------------------------------------- 1 | const requiredXp = (level: number) => { 2 | return (10 * (level - 1) * level) / 2; 3 | }; 4 | 5 | export const calculateLevel = (xp: number) => { 6 | const n = (1 + Math.sqrt(1 + (4 * xp) / 5)) / 2; 7 | let tentativeLevel = Math.floor(n); 8 | 9 | while (requiredXp(tentativeLevel) > xp && tentativeLevel > 1) { 10 | tentativeLevel--; 11 | } 12 | while (requiredXp(tentativeLevel + 1) <= xp) { 13 | tentativeLevel++; 14 | } 15 | 16 | return Math.max(tentativeLevel, 1); 17 | }; 18 | -------------------------------------------------------------------------------- /frontend/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /frontend/lib/features/mnemonics/application/mnemonics_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:goronyan/features/mnemonics/domain/entities/mnemonic.dart'; 3 | 4 | part 'mnemonics_state.freezed.dart'; 5 | 6 | enum MenemonicsStateType { 7 | all, 8 | unmemorized, 9 | } 10 | 11 | @freezed 12 | class MnemonicsState with _$MnemonicsState { 13 | factory MnemonicsState({ 14 | @Default([]) List mnemonics, 15 | }) = _MnemonicsState; 16 | 17 | MnemonicsState._(); 18 | } 19 | -------------------------------------------------------------------------------- /frontend/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /backend/src/core/utils/firebase/firebaseStorage.ts: -------------------------------------------------------------------------------- 1 | import { getStorage } from "firebase-admin/storage"; 2 | 3 | export const uploadImageAndGetPath = async ( 4 | userId: string, 5 | buffer: Buffer, 6 | contentType = "image/png", 7 | ): Promise => { 8 | const storage = getStorage(); 9 | const filePath = `images/${userId}/${Date.now()}.png`; 10 | const bucket = storage.bucket(); 11 | const file = bucket.file(filePath); 12 | 13 | await file.save(buffer, { 14 | metadata: { 15 | contentType, 16 | }, 17 | }); 18 | 19 | return filePath; 20 | }; 21 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/layouts/unfocus_layout.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/core/presentation/keyboard/keyboard.dart'; 3 | 4 | class UnfocusLayout extends StatelessWidget { 5 | const UnfocusLayout({ 6 | super.key, 7 | required this.child, 8 | }); 9 | 10 | final Widget child; 11 | 12 | @override 13 | Widget build(BuildContext context) { 14 | return GestureDetector( 15 | onTap: dismissKeyboard, 16 | behavior: HitTestBehavior.opaque, 17 | child: child, 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /frontend/firebase.json: -------------------------------------------------------------------------------- 1 | {"flutter":{"platforms":{"android":{"default":{"projectId":"hackathon2024-4cb8e","appId":"1:1005658646293:android:99fbdd9127e3bb9a312b1c","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"hackathon2024-4cb8e","appId":"1:1005658646293:ios:946756d7702b4ed8312b1c","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"hackathon2024-4cb8e","configurations":{"android":"1:1005658646293:android:99fbdd9127e3bb9a312b1c","ios":"1:1005658646293:ios:946756d7702b4ed8312b1c"}}}}}} -------------------------------------------------------------------------------- /backend/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "tab" 15 | }, 16 | "organizeImports": { 17 | "enabled": true 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true 23 | } 24 | }, 25 | "javascript": { 26 | "formatter": { 27 | "quoteStyle": "double" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /functions/biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/1.9.4/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "ignore": [] 11 | }, 12 | "formatter": { 13 | "enabled": true, 14 | "indentStyle": "tab" 15 | }, 16 | "organizeImports": { 17 | "enabled": true 18 | }, 19 | "linter": { 20 | "enabled": true, 21 | "rules": { 22 | "recommended": true 23 | } 24 | }, 25 | "javascript": { 26 | "formatter": { 27 | "quoteStyle": "double" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /backend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | "src" 4 | ], 5 | "compilerOptions": { 6 | "target": "ESNext", 7 | "module": "NodeNext", 8 | "strict": true, 9 | "verbatimModuleSyntax": true, 10 | "skipLibCheck": true, 11 | "esModuleInterop": true, 12 | "types": [ 13 | "node" 14 | ], 15 | "jsx": "react-jsx", 16 | "jsxImportSource": "hono/jsx", 17 | "rootDir": "src", 18 | "outDir": "dist", 19 | "noUnusedLocals": true, 20 | "noImplicitReturns": true, 21 | "paths": { 22 | "@/*": [ 23 | "./src/*" 24 | ] 25 | }, 26 | } 27 | } -------------------------------------------------------------------------------- /frontend/lib/core/extensions/ref_extentions.dart: -------------------------------------------------------------------------------- 1 | import 'package:goronyan/features/home/application/home_provider.dart'; 2 | import 'package:goronyan/features/mnemonics/application/mnemonics_provider.dart'; 3 | import 'package:goronyan/features/mnemonics/application/mnemonics_state.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | 6 | extension RefExtensions on Ref { 7 | void invalidateProviders() { 8 | final _ = refresh(homeProvider); 9 | invalidate(mnemonicsProvider(MenemonicsStateType.all)); 10 | invalidate(mnemonicsProvider(MenemonicsStateType.unmemorized)); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "functions": [ 7 | { 8 | "source": "functions", 9 | "codebase": "default", 10 | "predeploy": [ 11 | "npm --prefix \"$RESOURCE_DIR\" run lint", 12 | "npm --prefix \"$RESOURCE_DIR\" run build" 13 | ], 14 | "ignore": [ 15 | "node_modules", 16 | ".git", 17 | "firebase-debug.log", 18 | "firebase-debug.*.log", 19 | "*.local" 20 | ] 21 | } 22 | ], 23 | "storage": { 24 | "rules": "storage.rules" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/typographies/label.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/core/constants/app_colors.dart'; 3 | 4 | class Label extends StatelessWidget { 5 | const Label({ 6 | super.key, 7 | required this.text, 8 | this.style, 9 | }); 10 | 11 | final String text; 12 | final TextStyle? style; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | return Text( 17 | text, 18 | style: style ?? 19 | Theme.of(context).textTheme.labelSmall?.copyWith( 20 | color: AppColors.textLightDark, 21 | ), 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /frontend/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug dev", 6 | "request": "launch", 7 | "type": "dart", 8 | "flutterMode": "debug", 9 | "args": [ 10 | "--dart-define-from-file=dart_defines/dev.env" 11 | ] 12 | }, 13 | { 14 | "name": "Debug prod", 15 | "request": "launch", 16 | "type": "dart", 17 | "flutterMode": "debug", 18 | "args": [ 19 | "--dart-define-from-file=dart_defines/prod.env" 20 | ] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /functions/src/core/utils/schema/schema.ts: -------------------------------------------------------------------------------- 1 | import { Timestamp } from "firebase-admin/firestore"; 2 | import { z } from "zod"; 3 | 4 | export const UserSchema = z.object({ 5 | avatarURL: z.string(), 6 | displayName: z.string(), 7 | interests: z.string(), 8 | xp: z.number(), 9 | level: z.number(), 10 | catLevel: z.number(), 11 | generatedCount: z.number(), 12 | isProfileCompleted: z.boolean(), 13 | createdAt: z.instanceof(Timestamp), 14 | updatedAt: z.instanceof(Timestamp), 15 | }); 16 | 17 | export const AvatarSchema = z.object({ 18 | avatarURL: z.string(), 19 | level: z.number(), 20 | createdAt: z.instanceof(Timestamp), 21 | updatedAt: z.instanceof(Timestamp), 22 | }); 23 | -------------------------------------------------------------------------------- /frontend/lib/features/generate/domain/entities/mnemonic_response.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | 3 | part 'mnemonic_response.freezed.dart'; 4 | part 'mnemonic_response.g.dart'; 5 | 6 | @freezed 7 | class MnemonicResponse with _$MnemonicResponse { 8 | const factory MnemonicResponse({ 9 | required String question, 10 | required String answer, 11 | required String meaning, 12 | required String episode, 13 | required List goroTexts, 14 | required String imagePath, 15 | }) = _MnemonicResponse; 16 | 17 | factory MnemonicResponse.fromJson(Map json) => 18 | _$MnemonicResponseFromJson(json); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/lib/features/quiz/domain/entities/quiz_log.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:goronyan/core/data/converters/timestamp_converter.dart'; 4 | 5 | part 'quiz_log.freezed.dart'; 6 | part 'quiz_log.g.dart'; 7 | 8 | @freezed 9 | class QuizLog with _$QuizLog { 10 | const factory QuizLog({ 11 | required String id, 12 | required String mnemonicId, 13 | required bool isMemorized, 14 | @TimestampConverter() required DateTime createdAt, 15 | }) = _QuizLog; 16 | 17 | factory QuizLog.fromJson(Map json) => 18 | _$QuizLogFromJson(json); 19 | } 20 | -------------------------------------------------------------------------------- /frontend/lib/config/environment/firebase_initializer.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_core/firebase_core.dart'; 2 | import 'package:goronyan/config/environment/firebase_options_dev.dart' as dev; 3 | import 'package:goronyan/config/environment/firebase_options_prod.dart' as prod; 4 | 5 | Future initializeFirebaseApp() async { 6 | const flavor = String.fromEnvironment('flavor'); 7 | final firebaseOptions = switch (flavor) { 8 | 'prod' => prod.DefaultFirebaseOptions.currentPlatform, 9 | 'dev' => dev.DefaultFirebaseOptions.currentPlatform, 10 | _ => throw UnsupportedError('Invalid flavor: $flavor'), 11 | }; 12 | await Firebase.initializeApp(options: firebaseOptions); 13 | } 14 | -------------------------------------------------------------------------------- /frontend/lib/features/mnemonics/domain/entities/mnemonic_detail_page_extra.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:goronyan/features/mnemonics/domain/entities/mnemonic.dart'; 3 | 4 | part 'mnemonic_detail_page_extra.freezed.dart'; 5 | part 'mnemonic_detail_page_extra.g.dart'; 6 | 7 | @freezed 8 | class MnemonicDetailPageExtra with _$MnemonicDetailPageExtra { 9 | const factory MnemonicDetailPageExtra({ 10 | required List mnemonics, 11 | required int initialIndex, 12 | }) = _MnemonicDetailPageExtra; 13 | 14 | factory MnemonicDetailPageExtra.fromJson(Map json) => 15 | _$MnemonicDetailPageExtraFromJson(json); 16 | } 17 | -------------------------------------------------------------------------------- /storage.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | 3 | service firebase.storage { 4 | match /b/{bucket}/o { 5 | function isSignedIn() { 6 | return request.auth != null; 7 | } 8 | 9 | function isOwner(userId) { 10 | return isSignedIn() && request.auth.uid == userId; 11 | } 12 | 13 | match /voices/{userId}/{allPaths=**} { 14 | allow read, write: if isOwner(userId); 15 | } 16 | 17 | match /images/{userId}/{allPaths=**} { 18 | allow read, write: if isOwner(userId); 19 | } 20 | 21 | match /avatars/{allPaths=**} { 22 | allow read: if isSignedIn(); 23 | } 24 | 25 | match /{allPaths=**} { 26 | allow read, write: if false; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "backend", 3 | "type": "module", 4 | "scripts": { 5 | "build": "tsc", 6 | "start": "node dist/index.js", 7 | "dev": "tsx watch src/index.ts", 8 | "lint": "biome lint ./src/", 9 | "format": "biome format --write ./src/", 10 | "check": "biome check --write ./src/" 11 | }, 12 | "dependencies": { 13 | "@genkit-ai/firebase": "^0.9.12", 14 | "@genkit-ai/vertexai": "^0.9.12", 15 | "@hono/node-server": "^1.13.8", 16 | "firebase-admin": "^13.0.2", 17 | "hono": "^4.6.20" 18 | }, 19 | "devDependencies": { 20 | "@biomejs/biome": "1.9.4", 21 | "@types/node": "^20.11.17", 22 | "tsx": "^4.7.1", 23 | "typescript": "^5.7.3" 24 | } 25 | } -------------------------------------------------------------------------------- /frontend/lib/config/providers/firebase_providers.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:firebase_storage/firebase_storage.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 6 | 7 | part 'firebase_providers.g.dart'; 8 | 9 | @riverpod 10 | FirebaseFirestore firebaseFirestore(Ref ref) { 11 | return FirebaseFirestore.instance; 12 | } 13 | 14 | @riverpod 15 | FirebaseAuth firebaseAuth(Ref ref) { 16 | return FirebaseAuth.instance; 17 | } 18 | 19 | @riverpod 20 | FirebaseStorage firebaseStorage(Ref ref) { 21 | return FirebaseStorage.instance; 22 | } 23 | -------------------------------------------------------------------------------- /backend/src/index.ts: -------------------------------------------------------------------------------- 1 | import { serve } from "@hono/node-server"; 2 | import { initializeApp } from "firebase-admin/app"; 3 | import { Hono } from "hono"; 4 | import { logger } from "hono/logger"; 5 | import { firebaseBearerAuth } from "./middleware/firebaseBearerAuth.js"; 6 | import { genaiRoute } from "./routes/genai.js"; 7 | 8 | initializeApp({ 9 | storageBucket: process.env.STORAGE_BUCKET, 10 | }); 11 | 12 | const app = new Hono(); 13 | 14 | app.use(logger()); 15 | 16 | app.use("/api/*", firebaseBearerAuth); 17 | 18 | app.route("/api/", genaiRoute); 19 | 20 | app.get("/", (c) => { 21 | return c.text("Hello Hono!"); 22 | }); 23 | 24 | const port = 8080; 25 | 26 | serve({ 27 | fetch: app.fetch, 28 | port, 29 | }); 30 | -------------------------------------------------------------------------------- /frontend/lib/features/auth/data/repositories/auth_repository_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:goronyan/config/providers/firebase_providers.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 4 | 5 | part 'auth_repository_provider.g.dart'; 6 | 7 | @riverpod 8 | AuthRepository authRepository(Ref ref) => AuthRepository(ref); 9 | 10 | class AuthRepository { 11 | AuthRepository(this._ref); 12 | final Ref _ref; 13 | 14 | Future signInAnonymously() async { 15 | await _ref.read(firebaseAuthProvider).signInAnonymously(); 16 | } 17 | 18 | Future signOut() async { 19 | await _ref.read(firebaseAuthProvider).signOut(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /frontend/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/clippers/rounded_hexagon_clipper.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HexagonClipper extends CustomClipper { 4 | @override 5 | Path getClip(Size size) { 6 | final path = Path() 7 | ..moveTo(size.width / 2, 0) // moving to topCenter 1st, then draw the path 8 | ..lineTo(size.width, size.height * .25) 9 | ..lineTo(size.width, size.height * .75) 10 | ..lineTo(size.width * .5, size.height) 11 | ..lineTo(0, size.height * .75) 12 | ..lineTo(0, size.height * .25) 13 | ..close(); 14 | 15 | return path; 16 | } 17 | 18 | @override 19 | bool shouldReclip(covariant CustomClipper oldClipper) { 20 | return false; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /frontend/assets/images/settings_fill.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /backend/src/core/utils/schema/schema.ts: -------------------------------------------------------------------------------- 1 | import { z } from "genkit"; 2 | 3 | export const AnswerDivisionSchema = z.object({ 4 | question: z.string(), 5 | answer: z.string(), 6 | }); 7 | 8 | export const InterestsSchema = AnswerDivisionSchema.extend({ 9 | interests: z.string(), 10 | }); 11 | 12 | export const GoroSchema = AnswerDivisionSchema.extend({ 13 | catLevel: z.number(), 14 | }); 15 | 16 | export const GoroOutputSchema = z.object({ 17 | goroTexts: z.array(z.string()), 18 | }); 19 | 20 | export const MeaningSchema = z.object({ 21 | japanese: z.string(), 22 | english: z.string(), 23 | }); 24 | 25 | export type AnswerDivision = z.infer; 26 | export type Meaning = z.infer; 27 | export type Goro = z.infer; 28 | -------------------------------------------------------------------------------- /frontend/lib/core/constants/constants.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | const flavor = String.fromEnvironment('flavor'); 4 | 5 | const apiBaseUrl = String.fromEnvironment('apiBaseUrl'); 6 | 7 | final rootNavigatorKey = GlobalKey(); 8 | 9 | const catLevelTexts = [ 10 | '真面目', 11 | '普通', 12 | 'Max', 13 | ]; 14 | 15 | const defaultCatLevel = 2.0; 16 | 17 | const defaultAvatarURL = 18 | 'https://firebasestorage.googleapis.com/v0/b/hackathon2024-4cb8e.firebasestorage.app/o/avatars%2F1.png?alt=media&token=f06bd8f1-5f2d-4599-b354-a52bd302e444'; 19 | 20 | const tabBarHeight = 40.0; 21 | 22 | const catLevelDescription = 23 | '上にするほど、よりふざけた語呂を作れるにゃ。\nMaxにすると、もはや語呂ではなくなるにゃ🐈🐈🐈'; 24 | 25 | const interestsText = '興味に関連するものを生成できるようになるにゃ🐈'; 26 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/offsets/offset_text.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'offset_theme.dart'; 3 | 4 | class OffsetText extends StatelessWidget { 5 | const OffsetText({ 6 | super.key, 7 | required this.text, 8 | this.style, 9 | this.offsetTheme = OffsetTheme.defaultOffsetTheme, 10 | }); 11 | 12 | final String text; 13 | final TextStyle? style; 14 | final OffsetTheme offsetTheme; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final baseStyle = style ?? Theme.of(context).textTheme.bodyMedium; 19 | final mergedStyle = baseStyle?.copyWith( 20 | shadows: offsetTheme.toTextShadows(), 21 | ); 22 | 23 | return Text( 24 | text, 25 | style: mergedStyle, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/lib/features/auth/application/app_user_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:goronyan/features/auth/application/auth_state_changes_provider.dart'; 4 | import 'package:goronyan/features/auth/data/repositories/app_user_repository_provider.dart'; 5 | import 'package:goronyan/features/auth/domain/entities/app_user.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 8 | 9 | part 'app_user_provider.g.dart'; 10 | 11 | @Riverpod(keepAlive: true) 12 | Stream appUser(Ref ref) { 13 | final user = ref.watch(authStateChangesProvider).value; 14 | if (user == null) { 15 | return const Stream.empty(); 16 | } 17 | return ref.read(appUserRepositoryProvider).watchAppUser(user.uid); 18 | } 19 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/layouts/shimmer_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:shimmer/shimmer.dart'; 3 | 4 | class ShimmerWidget extends StatelessWidget { 5 | const ShimmerWidget({ 6 | super.key, 7 | required this.shimmerChild, 8 | required this.child, 9 | required this.isLoading, 10 | }); 11 | 12 | final Widget shimmerChild; 13 | final Widget child; 14 | final bool isLoading; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | if (isLoading) { 19 | return Shimmer.fromColors( 20 | baseColor: Colors.grey.shade300, 21 | highlightColor: Colors.grey.shade100, 22 | enabled: true, 23 | child: shimmerChild, 24 | ); 25 | } else { 26 | return child; 27 | } 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | 3 | service cloud.firestore { 4 | match /databases/{database}/documents { 5 | function isSignedIn() { 6 | return request.auth != null; 7 | } 8 | 9 | function isOwner(userId) { 10 | return isSignedIn() && request.auth.uid == userId; 11 | } 12 | 13 | match /users/{userId} { 14 | allow read, write: if isOwner(userId); 15 | 16 | match /mnemonics/{mnemonicId} { 17 | allow read, write: if isOwner(userId); 18 | } 19 | 20 | match /quizLogs/{quizLogId} { 21 | allow read, write: if isOwner(userId); 22 | } 23 | } 24 | 25 | match /avatars/{avatarId} { 26 | allow read: if isSignedIn(); 27 | } 28 | 29 | match /{document=**} { 30 | allow read, write: if false; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/lib/features/generate/presentation/widgets/generate_loading.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class GenerateLoading extends StatelessWidget { 4 | const GenerateLoading({super.key}); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | final theme = Theme.of(context); 9 | 10 | return Center( 11 | child: Column( 12 | mainAxisSize: MainAxisSize.min, 13 | children: [ 14 | const CircularProgressIndicator(), 15 | const SizedBox(height: 24), 16 | Text( 17 | '記憶カードを生成中だにゃ…', 18 | style: theme.textTheme.bodyMedium?.copyWith( 19 | color: Colors.white, 20 | fontWeight: FontWeight.bold, 21 | ), 22 | ), 23 | ], 24 | ), 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /frontend/assets/images/settings.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/ios/scripts/extract_dart_defines.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Dart defineを書き出すファイルパスを指定します。 4 | # ここでは `Dart-Defines.xcconfig` というファイル名で作成することにします。 5 | OUTPUT_FILE="${SRCROOT}/Flutter/Dart-Defines.xcconfig" 6 | # Dart defineの中身を変更した時に古いプロパティが残らないように、初めにファイルを空にしています。 7 | : > $OUTPUT_FILE 8 | 9 | # この関数でDart defineをデコードします。 10 | function decode_url() { echo "${*}" | base64 --decode; } 11 | 12 | IFS=',' read -r -a define_items <<< "$DART_DEFINES" 13 | 14 | for index in "${!define_items[@]}" 15 | do 16 | item=$(decode_url "${define_items[$index]}") 17 | # Dartの定義にはFlutter側で自動定義された項目も含まれます。 18 | # しかし、それらの定義を書き出してしまうとエラーによりビルドができなくなるので、 19 | # flutterやFLUTTERで始まる項目は出力しないようにしています。 20 | lowercase_item=$(echo "$item" | tr '[:upper:]' '[:lower:]') 21 | if [[ $lowercase_item != flutter* ]]; then 22 | echo "$item" >> "$OUTPUT_FILE" 23 | fi 24 | done 25 | -------------------------------------------------------------------------------- /frontend/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /frontend/lib/features/home/presentation/widgets/status_content.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/core/constants/app_colors.dart'; 3 | 4 | class StatusContent extends StatelessWidget { 5 | const StatusContent({ 6 | super.key, 7 | required this.label, 8 | required this.value, 9 | }); 10 | 11 | final String label; 12 | final String value; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | final theme = Theme.of(context); 17 | 18 | return Column( 19 | mainAxisSize: MainAxisSize.min, 20 | spacing: 4, 21 | children: [ 22 | Text( 23 | value, 24 | style: theme.textTheme.headlineMedium, 25 | ), 26 | Text( 27 | label, 28 | style: theme.textTheme.bodyMedium?.copyWith( 29 | color: AppColors.textLightDark, 30 | ), 31 | ), 32 | ], 33 | ); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/bars/sticky_bar_delegate.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class StickyBarDelegate extends SliverPersistentHeaderDelegate { 4 | const StickyBarDelegate({ 5 | required this.child, 6 | this.color, 7 | }); 8 | 9 | final Color? color; 10 | final PreferredSizeWidget child; 11 | 12 | @override 13 | double get minExtent => child.preferredSize.height; 14 | 15 | @override 16 | double get maxExtent => child.preferredSize.height; 17 | 18 | @override 19 | Widget build( 20 | BuildContext context, 21 | double shrinkOffset, 22 | bool overlapsContent, 23 | ) { 24 | return Container( 25 | height: child.preferredSize.height, 26 | color: color ?? Theme.of(context).scaffoldBackgroundColor, 27 | child: child, 28 | ); 29 | } 30 | 31 | @override 32 | bool shouldRebuild(StickyBarDelegate oldDelegate) { 33 | return child != oldDelegate.child; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/offsets/offset_icon.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'offset_theme.dart'; 3 | 4 | class OffsetIcon extends StatelessWidget { 5 | const OffsetIcon({ 6 | super.key, 7 | required this.icon, 8 | this.size = 24.0, 9 | this.color = Colors.black, 10 | this.offsetTheme = OffsetTheme.defaultOffsetTheme, 11 | }); 12 | 13 | final IconData icon; 14 | final double size; 15 | final Color color; 16 | final OffsetTheme offsetTheme; 17 | 18 | @override 19 | Widget build(BuildContext context) { 20 | return Stack( 21 | children: [ 22 | Transform.translate( 23 | offset: offsetTheme.offset, 24 | child: Icon( 25 | icon, 26 | size: size, 27 | color: offsetTheme.offsetColor, 28 | ), 29 | ), 30 | Icon( 31 | icon, 32 | size: size, 33 | color: color, 34 | ), 35 | ], 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/lib/features/quiz/domain/entities/quiz_log.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'quiz_log.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$QuizLogImpl _$$QuizLogImplFromJson(Map json) => 10 | _$QuizLogImpl( 11 | id: json['id'] as String, 12 | mnemonicId: json['mnemonicId'] as String, 13 | isMemorized: json['isMemorized'] as bool, 14 | createdAt: 15 | const TimestampConverter().fromJson(json['createdAt'] as Timestamp), 16 | ); 17 | 18 | Map _$$QuizLogImplToJson(_$QuizLogImpl instance) => 19 | { 20 | 'id': instance.id, 21 | 'mnemonicId': instance.mnemonicId, 22 | 'isMemorized': instance.isMemorized, 23 | 'createdAt': const TimestampConverter().toJson(instance.createdAt), 24 | }; 25 | -------------------------------------------------------------------------------- /frontend/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | }() 9 | 10 | includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") 11 | 12 | repositories { 13 | google() 14 | mavenCentral() 15 | gradlePluginPortal() 16 | } 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 21 | id "com.android.application" version "8.2.1" apply false 22 | // START: FlutterFire Configuration 23 | id "com.google.gms.google-services" version "4.3.15" apply false 24 | // END: FlutterFire Configuration 25 | id "org.jetbrains.kotlin.android" version "1.8.22" apply false 26 | } 27 | 28 | include ":app" 29 | -------------------------------------------------------------------------------- /frontend/lib/features/mnemonics/domain/entities/mnemonic_detail_page_extra.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'mnemonic_detail_page_extra.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$MnemonicDetailPageExtraImpl _$$MnemonicDetailPageExtraImplFromJson( 10 | Map json) => 11 | _$MnemonicDetailPageExtraImpl( 12 | mnemonics: (json['mnemonics'] as List) 13 | .map((e) => Mnemonic.fromJson(e as Map)) 14 | .toList(), 15 | initialIndex: (json['initialIndex'] as num).toInt(), 16 | ); 17 | 18 | Map _$$MnemonicDetailPageExtraImplToJson( 19 | _$MnemonicDetailPageExtraImpl instance) => 20 | { 21 | 'mnemonics': instance.mnemonics, 22 | 'initialIndex': instance.initialIndex, 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/bars/sliver_tab_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class SliverTabBar extends StatelessWidget implements PreferredSizeWidget { 4 | const SliverTabBar({ 5 | super.key, 6 | required this.tabController, 7 | required this.tabs, 8 | }); 9 | 10 | final TabController tabController; 11 | final List tabs; 12 | 13 | @override 14 | Size get preferredSize => const Size.fromHeight(40); 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return TabBar( 19 | controller: tabController, 20 | padding: const EdgeInsets.only(bottom: 8), 21 | tabs: tabs 22 | .map( 23 | (String value) => Tab( 24 | child: Container( 25 | alignment: Alignment.center, 26 | padding: const EdgeInsets.only(top: 2), 27 | child: Text(value), 28 | ), 29 | ), 30 | ) 31 | .toList(), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Goroにゃーん Frontend 2 | 3 | コード生成 4 | ```shell 5 | dart run build_runner build --delete-conflicting-outputs 6 | ``` 7 | 8 | Firebase設定ファイル作成 9 | ```shell 10 | cd app 11 | dart run flutterfire_cli:flutterfire configure 12 | ``` 13 | 14 | 上記で生成されたファイルを下記の場所に保存 15 | ```shell 16 | lib/config/environment/firebase_options_dev.dart 17 | lib/config/environment/firebase_options_prod.dart 18 | ``` 19 | 20 | ```shell 21 | ios/firebase/dev/ 22 | ios/firebase/prod/に 23 | firebaseからダウンロードしたGoogleService-Info.plistを保存。 24 | ``` 25 | 26 | ```shell 27 | android/src/firebase/dev/ 28 | android/src/firebase/prod/に 29 | firebaseからダウンロードしたgoogle-services.jsonを保存。 30 | ``` 31 | 32 | 下記のファイルのapiBaseUrlをCloud Functions for FirebaseのURLに変更する。 33 | ```shell 34 | dart_defines/dev.env 35 | dart_defines/prod.env 36 | ``` 37 | 38 | 開発版アプリ起動 39 | ```shell 40 | flutter run --dart-define-from-file=dart_defines/dev.env 41 | ``` 42 | 43 | APK ビルド 44 | ```shell 45 | flutter build apk --dart-define-from-file=dart_defines/prod.env 46 | ``` 47 | -------------------------------------------------------------------------------- /frontend/lib/features/home/application/home_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'home_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$homeHash() => r'77dbd1dbbc13b3c5bf0b4f412cd589fa41674b30'; 10 | 11 | /// See also [Home]. 12 | @ProviderFor(Home) 13 | final homeProvider = AutoDisposeAsyncNotifierProvider.internal( 14 | Home.new, 15 | name: r'homeProvider', 16 | debugGetCreateSourceHash: 17 | const bool.fromEnvironment('dart.vm.product') ? null : _$homeHash, 18 | dependencies: null, 19 | allTransitiveDependencies: null, 20 | ); 21 | 22 | typedef _$Home = AutoDisposeAsyncNotifier; 23 | // ignore_for_file: type=lint 24 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 25 | -------------------------------------------------------------------------------- /frontend/lib/features/quiz/application/quiz_play_state.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'quiz_play_state.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$MemorizedPlayImpl _$$MemorizedPlayImplFromJson(Map json) => 10 | _$MemorizedPlayImpl( 11 | isFront: json['isFront'] as bool? ?? true, 12 | isFilteredAnswer: json['isFilteredAnswer'] as bool? ?? false, 13 | isMemorized: json['isMemorized'] as bool? ?? false, 14 | isCompleted: json['isCompleted'] as bool? ?? false, 15 | ); 16 | 17 | Map _$$MemorizedPlayImplToJson(_$MemorizedPlayImpl instance) => 18 | { 19 | 'isFront': instance.isFront, 20 | 'isFilteredAnswer': instance.isFilteredAnswer, 21 | 'isMemorized': instance.isMemorized, 22 | 'isCompleted': instance.isCompleted, 23 | }; 24 | -------------------------------------------------------------------------------- /frontend/lib/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/config/router/router.dart'; 3 | import 'package:goronyan/config/theme/theme.dart'; 4 | import 'package:goronyan/core/presentation/widgets/listeners/level_up_listener.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | import 'features/auth/presentation/builders/auth_builder.dart'; 7 | 8 | class App extends ConsumerWidget { 9 | const App({super.key}); 10 | 11 | @override 12 | Widget build(BuildContext context, WidgetRef ref) { 13 | final router = ref.watch(routerProvider); 14 | 15 | return LevelUpListener( 16 | child: MaterialApp.router( 17 | debugShowCheckedModeBanner: false, 18 | routeInformationParser: router.routeInformationParser, 19 | routerDelegate: router.routerDelegate, 20 | routeInformationProvider: router.routeInformationProvider, 21 | theme: theme, 22 | builder: (context, child) { 23 | return AuthBuilder(child: child); 24 | }, 25 | ), 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /frontend/lib/features/auth/presentation/builders/auth_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/core/presentation/widgets/builder/async_value_builder.dart'; 3 | import 'package:goronyan/features/auth/application/combined_auth_provider.dart'; 4 | import 'package:goronyan/features/auth/application/combined_auth_state.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | 7 | class AuthBuilder extends ConsumerWidget { 8 | const AuthBuilder({ 9 | super.key, 10 | required this.child, 11 | }); 12 | 13 | final Widget? child; 14 | 15 | @override 16 | Widget build(BuildContext context, WidgetRef ref) { 17 | final asyncValue = ref.watch(combinedAuthProvider); 18 | 19 | return AsyncValueBuilder( 20 | asyncValue: asyncValue, 21 | isWrapScaffoldError: true, 22 | isWrapScaffoldLoading: true, 23 | onReload: () { 24 | ref.invalidate(combinedAuthProvider); 25 | }, 26 | child: (_) => child ?? const SizedBox.shrink(), 27 | ); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /frontend/lib/features/quiz/application/quiz_state_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'quiz_state_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$quizStateHash() => r'e13f4612e709bd85cfa420d75a8ac9a82910e7f8'; 10 | 11 | /// See also [QuizState]. 12 | @ProviderFor(QuizState) 13 | final quizStateProvider = AutoDisposeNotifierProvider.internal( 14 | QuizState.new, 15 | name: r'quizStateProvider', 16 | debugGetCreateSourceHash: 17 | const bool.fromEnvironment('dart.vm.product') ? null : _$quizStateHash, 18 | dependencies: null, 19 | allTransitiveDependencies: null, 20 | ); 21 | 22 | typedef _$QuizState = AutoDisposeNotifier; 23 | // ignore_for_file: type=lint 24 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 25 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/offsets/offset_theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/core/constants/app_colors.dart'; 3 | 4 | class OffsetTheme { 5 | const OffsetTheme({ 6 | this.offsetColor = AppColors.accent1, 7 | this.offset = const Offset(3, 3), 8 | this.blurRadius = 0.0, 9 | }); 10 | 11 | factory OffsetTheme.fromContext( 12 | BuildContext context, { 13 | Offset offset = const Offset(3, 3), 14 | double blurRadius = 0.0, 15 | }) { 16 | final color = Theme.of(context).colorScheme.primary; 17 | return OffsetTheme( 18 | offsetColor: color, 19 | offset: offset, 20 | blurRadius: blurRadius, 21 | ); 22 | } 23 | 24 | final Color offsetColor; 25 | final Offset offset; 26 | final double blurRadius; 27 | static const OffsetTheme defaultOffsetTheme = OffsetTheme(); 28 | 29 | List toTextShadows() => [ 30 | Shadow( 31 | color: offsetColor, 32 | offset: offset, 33 | blurRadius: blurRadius, 34 | ), 35 | ]; 36 | } 37 | -------------------------------------------------------------------------------- /frontend/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /frontend/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /frontend/lib/features/auth/application/app_user_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'app_user_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$appUserHash() => r'31e605b90cb098d8542f8da93b86bba4af315806'; 10 | 11 | /// See also [appUser]. 12 | @ProviderFor(appUser) 13 | final appUserProvider = StreamProvider.internal( 14 | appUser, 15 | name: r'appUserProvider', 16 | debugGetCreateSourceHash: 17 | const bool.fromEnvironment('dart.vm.product') ? null : _$appUserHash, 18 | dependencies: null, 19 | allTransitiveDependencies: null, 20 | ); 21 | 22 | @Deprecated('Will be removed in 3.0. Use Ref instead') 23 | // ignore: unused_element 24 | typedef AppUserRef = StreamProviderRef; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 27 | -------------------------------------------------------------------------------- /frontend/lib/features/generate/application/voice_input_providers.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'voice_input_providers.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$voiceInputHash() => r'383b54661d7653bf20072e26d54862eed03264e2'; 10 | 11 | /// See also [VoiceInput]. 12 | @ProviderFor(VoiceInput) 13 | final voiceInputProvider = 14 | AutoDisposeAsyncNotifierProvider.internal( 15 | VoiceInput.new, 16 | name: r'voiceInputProvider', 17 | debugGetCreateSourceHash: 18 | const bool.fromEnvironment('dart.vm.product') ? null : _$voiceInputHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | typedef _$VoiceInput = AutoDisposeAsyncNotifier; 24 | // ignore_for_file: type=lint 25 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 26 | -------------------------------------------------------------------------------- /frontend/lib/features/auth/domain/entities/app_user.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:goronyan/core/constants/constants.dart'; 4 | import 'package:goronyan/core/data/converters/timestamp_converter.dart'; 5 | 6 | part 'app_user.freezed.dart'; 7 | part 'app_user.g.dart'; 8 | 9 | @freezed 10 | class AppUser with _$AppUser { 11 | const factory AppUser({ 12 | required String id, 13 | @Default('') String displayName, 14 | @Default('') String interests, 15 | @Default(0) int xp, 16 | @Default(1) int level, 17 | @Default(2) int catLevel, 18 | @Default(defaultAvatarURL) String avatarURL, 19 | @Default(0) int generatedCount, 20 | @Default(false) bool isProfileCompleted, 21 | @TimestampConverter() required DateTime createdAt, 22 | @TimestampConverter() required DateTime updatedAt, 23 | }) = _AppUser; 24 | 25 | factory AppUser.fromJson(Map json) => 26 | _$AppUserFromJson(json); 27 | 28 | static const List timestampKeys = ['createdAt', 'updatedAt']; 29 | } 30 | -------------------------------------------------------------------------------- /frontend/lib/features/profile/application/app_user_form_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'app_user_form_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$appUserFormHash() => r'7331a16d5b0b8009909be59d075b2f2e8426a488'; 10 | 11 | /// See also [AppUserForm]. 12 | @ProviderFor(AppUserForm) 13 | final appUserFormProvider = 14 | AutoDisposeAsyncNotifierProvider.internal( 15 | AppUserForm.new, 16 | name: r'appUserFormProvider', 17 | debugGetCreateSourceHash: 18 | const bool.fromEnvironment('dart.vm.product') ? null : _$appUserFormHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | typedef _$AppUserForm = AutoDisposeAsyncNotifier; 24 | // ignore_for_file: type=lint 25 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 26 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/dialogs/auto_dismiss_dialog.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/core/extensions/string_extensions.dart'; 3 | 4 | Future showAutoDismissDialog( 5 | BuildContext context, { 6 | required String title, 7 | String? message, 8 | Duration duration = const Duration(seconds: 2), 9 | }) async { 10 | await showDialog( 11 | context: context, 12 | builder: (context) { 13 | Future.delayed(duration, () { 14 | if (!context.mounted) { 15 | return; 16 | } 17 | if (Navigator.canPop(context)) { 18 | Navigator.pop(context); 19 | } 20 | }); 21 | return AlertDialog( 22 | title: Text(title), 23 | content: message.isNotNullOrEmpty ? Text(message!) : null, 24 | actions: [ 25 | TextButton( 26 | onPressed: () { 27 | if (Navigator.canPop(context)) { 28 | Navigator.pop(context); 29 | } 30 | }, 31 | child: const Text('とじる'), 32 | ), 33 | ], 34 | ); 35 | }, 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /frontend/lib/features/auth/application/combined_auth_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'combined_auth_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$combinedAuthHash() => r'e3e5c984ed46fa42bf2cf5dc85b7a0fe75d3cfcd'; 10 | 11 | /// See also [CombinedAuth]. 12 | @ProviderFor(CombinedAuth) 13 | final combinedAuthProvider = 14 | AutoDisposeAsyncNotifierProvider.internal( 15 | CombinedAuth.new, 16 | name: r'combinedAuthProvider', 17 | debugGetCreateSourceHash: 18 | const bool.fromEnvironment('dart.vm.product') ? null : _$combinedAuthHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | typedef _$CombinedAuth = AutoDisposeAsyncNotifier; 24 | // ignore_for_file: type=lint 25 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 26 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/bars/fixed_bottom_bar.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class FixedBottomBar extends StatelessWidget { 4 | const FixedBottomBar({ 5 | super.key, 6 | required this.children, 7 | this.isBorder = true, 8 | }); 9 | 10 | final List children; 11 | final bool isBorder; 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | final theme = Theme.of(context); 16 | 17 | return DecoratedBox( 18 | decoration: BoxDecoration( 19 | color: theme.colorScheme.onSecondary, 20 | border: isBorder 21 | ? Border( 22 | top: BorderSide( 23 | color: theme.colorScheme.outlineVariant, 24 | ), 25 | ) 26 | : null, 27 | ), 28 | child: SafeArea( 29 | child: Padding( 30 | padding: const EdgeInsets.fromLTRB(24, 12, 24, 16), 31 | child: Column( 32 | mainAxisSize: MainAxisSize.min, 33 | spacing: 4, 34 | children: children, 35 | ), 36 | ), 37 | ), 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/offsets/offset_container.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'offset_theme.dart'; 3 | 4 | class OffsetContainer extends StatelessWidget { 5 | const OffsetContainer({ 6 | super.key, 7 | required this.child, 8 | this.offsetTheme = OffsetTheme.defaultOffsetTheme, 9 | this.isOffset = true, 10 | }); 11 | 12 | final Widget child; 13 | final OffsetTheme offsetTheme; 14 | final bool isOffset; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | if (!isOffset) { 19 | return child; 20 | } 21 | 22 | return DecoratedBox( 23 | decoration: BoxDecoration( 24 | borderRadius: BorderRadius.circular(16), 25 | border: Border.all( 26 | color: Theme.of(context).colorScheme.onPrimary, 27 | ), 28 | boxShadow: [ 29 | BoxShadow( 30 | color: offsetTheme.offsetColor, 31 | offset: offsetTheme.offset, 32 | blurRadius: offsetTheme.blurRadius, 33 | ), 34 | ], 35 | ), 36 | child: child, 37 | ); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /frontend/lib/features/profile/application/edit_cat_level_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'edit_cat_level_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$editCatLevelHash() => r'554f8820f4ea24c2836712bcc4c7ee2810d0a814'; 10 | 11 | /// See also [EditCatLevel]. 12 | @ProviderFor(EditCatLevel) 13 | final editCatLevelProvider = 14 | AutoDisposeAsyncNotifierProvider.internal( 15 | EditCatLevel.new, 16 | name: r'editCatLevelProvider', 17 | debugGetCreateSourceHash: 18 | const bool.fromEnvironment('dart.vm.product') ? null : _$editCatLevelHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | typedef _$EditCatLevel = AutoDisposeAsyncNotifier; 24 | // ignore_for_file: type=lint 25 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 26 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/keyboard/keyboard_visibility.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'keyboard_visibility.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$keyboardVisibilityHash() => 10 | r'9462aea8b2269b18de86a369bab47b174779b47a'; 11 | 12 | /// See also [KeyboardVisibility]. 13 | @ProviderFor(KeyboardVisibility) 14 | final keyboardVisibilityProvider = 15 | NotifierProvider.internal( 16 | KeyboardVisibility.new, 17 | name: r'keyboardVisibilityProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$keyboardVisibilityHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef _$KeyboardVisibility = Notifier; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 28 | -------------------------------------------------------------------------------- /frontend/lib/features/profile/application/edit_interests_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'edit_interests_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$editInterestsHash() => r'b837f2051e97c71a09fdec2004b4603f45c335eb'; 10 | 11 | /// See also [EditInterests]. 12 | @ProviderFor(EditInterests) 13 | final editInterestsProvider = AutoDisposeAsyncNotifierProvider.internal( 15 | EditInterests.new, 16 | name: r'editInterestsProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$editInterestsHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef _$EditInterests = AutoDisposeAsyncNotifier; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 27 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Debug dev", 6 | "cwd": "frontend", 7 | "request": "launch", 8 | "type": "dart", 9 | "flutterMode": "debug", 10 | "args": [ 11 | "--dart-define-from-file=dart_defines/dev.env" 12 | ] 13 | }, 14 | { 15 | "name": "Debug prod", 16 | "cwd": "frontend", 17 | "request": "launch", 18 | "type": "dart", 19 | "flutterMode": "debug", 20 | "args": [ 21 | "--dart-define-from-file=dart_defines/prod.env" 22 | ] 23 | }, 24 | { 25 | "name": "frontend (profile mode)", 26 | "cwd": "frontend", 27 | "request": "launch", 28 | "type": "dart", 29 | "flutterMode": "profile" 30 | }, 31 | { 32 | "name": "frontend (release mode)", 33 | "cwd": "frontend", 34 | "request": "launch", 35 | "type": "dart", 36 | "flutterMode": "release" 37 | } 38 | ] 39 | } -------------------------------------------------------------------------------- /frontend/lib/features/generate/presentation/widgets/recording_effect.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/core/extensions/color_extensions.dart'; 3 | 4 | class RecordingEffect extends StatelessWidget { 5 | const RecordingEffect({ 6 | super.key, 7 | required this.rotation, 8 | required this.scale, 9 | required this.color, 10 | }); 11 | 12 | final double rotation; 13 | final double scale; 14 | final Color color; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | return Transform.scale( 19 | scale: scale, 20 | child: Transform.rotate( 21 | angle: rotation, 22 | child: Container( 23 | width: 240, 24 | height: 240, 25 | decoration: BoxDecoration( 26 | color: color.withDoubleOpacity(0.65), 27 | borderRadius: const BorderRadius.only( 28 | topLeft: Radius.circular(150), 29 | topRight: Radius.circular(240), 30 | bottomLeft: Radius.circular(220), 31 | bottomRight: Radius.circular(180), 32 | ), 33 | ), 34 | ), 35 | ), 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/lib/features/quiz/application/quiz_play_state.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:goronyan/features/mnemonics/domain/entities/mnemonic.dart'; 3 | 4 | part 'quiz_play_state.freezed.dart'; 5 | part 'quiz_play_state.g.dart'; 6 | 7 | @freezed 8 | class MemorizedPlay with _$MemorizedPlay { 9 | const factory MemorizedPlay({ 10 | @Default(true) bool isFront, 11 | @Default(false) bool isFilteredAnswer, 12 | @Default(false) bool isMemorized, 13 | @Default(false) bool isCompleted, 14 | }) = _MemorizedPlay; 15 | 16 | factory MemorizedPlay.fromJson(Map json) => 17 | _$MemorizedPlayFromJson(json); 18 | } 19 | 20 | @freezed 21 | class QuizPlayState with _$QuizPlayState { 22 | factory QuizPlayState({ 23 | @Default([]) List mnemonics, 24 | @Default([]) List memorizeds, 25 | @Default(0) int currentPage, 26 | @Default(false) bool isCompleted, 27 | }) = _QuizPlayState; 28 | 29 | QuizPlayState._(); 30 | 31 | int get totalMemorizedCount => memorizeds.where((e) => e.isMemorized).length; 32 | int get rewardPoint => totalMemorizedCount * 5 + 5; 33 | } 34 | -------------------------------------------------------------------------------- /frontend/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: goronyan 2 | description: "A new Flutter project." 3 | publish_to: 'none' 4 | version: 0.1.0 5 | 6 | environment: 7 | sdk: ^3.5.4 8 | 9 | dependencies: 10 | cloud_firestore: ^5.6.0 11 | dio: ^5.7.0 12 | firebase_auth: ^5.3.4 13 | firebase_core: ^3.9.0 14 | firebase_storage: ^12.4.0 15 | flutter: 16 | sdk: flutter 17 | flutter_hooks: ^0.20.5 18 | flutter_svg: ^2.0.16 19 | freezed_annotation: ^2.4.4 20 | go_router: ^14.6.2 21 | google_fonts: ^6.2.1 22 | hooks_riverpod: ^2.6.1 23 | json_annotation: ^4.9.0 24 | logger: ^2.5.0 25 | package_info_plus: ^8.1.2 26 | path_provider: ^2.1.5 27 | record: ^5.2.0 28 | riverpod_annotation: ^2.6.1 29 | shimmer: ^3.0.0 30 | 31 | dev_dependencies: 32 | build_runner: ^2.4.13 33 | change_app_package_name: ^1.4.0 34 | flutter_launcher_icons: ^0.14.2 35 | flutter_lints: ^4.0.0 36 | flutter_test: 37 | sdk: flutter 38 | flutterfire_cli: ^1.0.0 39 | freezed: ^2.5.7 40 | go_router_builder: ^2.7.1 41 | json_serializable: ^6.9.0 42 | pedantic_mono: ^1.28.0 43 | riverpod_generator: ^2.6.3 44 | 45 | flutter: 46 | uses-material-design: true 47 | 48 | assets: 49 | - assets/images/ 50 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/keyboard/keyboard_visibility.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/widgets.dart'; 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | 4 | part 'keyboard_visibility.g.dart'; 5 | 6 | @Riverpod(keepAlive: true) 7 | class KeyboardVisibility extends _$KeyboardVisibility { 8 | @override 9 | double build() { 10 | final observer = _KeyboardVisibilityObserver((value) => state = value); 11 | 12 | final binding = WidgetsBinding.instance..addObserver(observer); 13 | ref.onDispose(() => binding.removeObserver(observer)); 14 | 15 | return 0; 16 | } 17 | } 18 | 19 | class _KeyboardVisibilityObserver extends WidgetsBindingObserver { 20 | _KeyboardVisibilityObserver(this._didChangeMetrics); 21 | 22 | final ValueChanged _didChangeMetrics; 23 | 24 | @override 25 | void didChangeMetrics() { 26 | final viewInsets = EdgeInsets.fromViewPadding( 27 | WidgetsBinding.instance.platformDispatcher.views.first.viewInsets, 28 | WidgetsBinding.instance.platformDispatcher.views.first.devicePixelRatio, 29 | ); 30 | final bottomInsets = viewInsets.bottom; 31 | _didChangeMetrics(bottomInsets); 32 | super.didChangeMetrics(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /frontend/lib/features/auth/data/repositories/auth_repository_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'auth_repository_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$authRepositoryHash() => r'93c29bd9890eade73d00dce359a9a491b93ead4e'; 10 | 11 | /// See also [authRepository]. 12 | @ProviderFor(authRepository) 13 | final authRepositoryProvider = AutoDisposeProvider.internal( 14 | authRepository, 15 | name: r'authRepositoryProvider', 16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 17 | ? null 18 | : _$authRepositoryHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | @Deprecated('Will be removed in 3.0. Use Ref instead') 24 | // ignore: unused_element 25 | typedef AuthRepositoryRef = AutoDisposeProviderRef; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 28 | -------------------------------------------------------------------------------- /frontend/lib/features/quiz/data/repositories/quiz_repository_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'quiz_repository_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$quizRepositoryHash() => r'64e0e94820a4b42a563d942e8bee31a2b645b4ab'; 10 | 11 | /// See also [quizRepository]. 12 | @ProviderFor(quizRepository) 13 | final quizRepositoryProvider = AutoDisposeProvider.internal( 14 | quizRepository, 15 | name: r'quizRepositoryProvider', 16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 17 | ? null 18 | : _$quizRepositoryHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | @Deprecated('Will be removed in 3.0. Use Ref instead') 24 | // ignore: unused_element 25 | typedef QuizRepositoryRef = AutoDisposeProviderRef; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 28 | -------------------------------------------------------------------------------- /frontend/lib/features/auth/application/auth_state_changes_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'auth_state_changes_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$authStateChangesHash() => r'7e2230d665098f97101510d80be5c9dd82d44903'; 10 | 11 | /// See also [authStateChanges]. 12 | @ProviderFor(authStateChanges) 13 | final authStateChangesProvider = AutoDisposeStreamProvider.internal( 14 | authStateChanges, 15 | name: r'authStateChangesProvider', 16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 17 | ? null 18 | : _$authStateChangesHash, 19 | dependencies: null, 20 | allTransitiveDependencies: null, 21 | ); 22 | 23 | @Deprecated('Will be removed in 3.0. Use Ref instead') 24 | // ignore: unused_element 25 | typedef AuthStateChangesRef = AutoDisposeStreamProviderRef; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 28 | -------------------------------------------------------------------------------- /frontend/lib/features/generate/domain/entities/mnemonic_response.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'mnemonic_response.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$MnemonicResponseImpl _$$MnemonicResponseImplFromJson( 10 | Map json) => 11 | _$MnemonicResponseImpl( 12 | question: json['question'] as String, 13 | answer: json['answer'] as String, 14 | meaning: json['meaning'] as String, 15 | episode: json['episode'] as String, 16 | goroTexts: 17 | (json['goroTexts'] as List).map((e) => e as String).toList(), 18 | imagePath: json['imagePath'] as String, 19 | ); 20 | 21 | Map _$$MnemonicResponseImplToJson( 22 | _$MnemonicResponseImpl instance) => 23 | { 24 | 'question': instance.question, 25 | 'answer': instance.answer, 26 | 'meaning': instance.meaning, 27 | 'episode': instance.episode, 28 | 'goroTexts': instance.goroTexts, 29 | 'imagePath': instance.imagePath, 30 | }; 31 | -------------------------------------------------------------------------------- /frontend/lib/core/constants/app_colors.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class AppColors { 4 | // やや黄みがかったホワイト。全体を明るく見せ、コンテンツやイラスト、アウトラインが映える。 5 | static const Color background = Color(0xFFFDFCF9); 6 | // 茶トラ感のあるオレンジ。主要アクションや重要なUI要素で使用。 7 | static const Color main = Color(0xFFF3B64D); 8 | // コーラルピンク。通知バッジ、ハイライト、イラストのズレ色などで使うとポップで映える。 9 | static const Color accent1 = Color(0xFFFF6F61); 10 | // ブルーグレー。アウトラインの色替えにも使いやすく、全体を引き締める役割。 11 | static const Color accent2 = Color(0xFF5E6A71); 12 | // 濃いめグレーで可読性を担保。アウトラインとしてもベースに使いやすい。 13 | static const Color textDark = Color(0xFF333333); 14 | // textDarkよりも少し軽めの文字色。サブ的なテキストや補足説明などに。 15 | static const Color textLightDark = Color(0xFF6D6D6D); 16 | // 淡いグリーンがかったグレー。ボックス背景・セクション分けなどに。多用しすぎない程度に取り入れる。 17 | static const Color subColor = Color(0xFF97C1A9); 18 | // 「ボトムモーダルのハンドル」「区切り線」「控えめな背景」などで使いやすい 19 | static const Color lightGray = Color(0xFFCDCDCD); 20 | // lightGrayよりも淡いグレー。背景など少しだけ控えめにグレーを使いたい場合に。 21 | static const Color backgroundGray = Color(0xFFE2E2E2); 22 | // エラーメッセージや警告などに使用。注意を喚起したいUI要素へ。 23 | static const Color error = Color(0xFFD32F2F); 24 | // 成功や完了を示す要素に使用。落ち着きや安心感を与えるグリーン。 25 | static const Color success = Color(0xFF388E3C); 26 | } 27 | -------------------------------------------------------------------------------- /functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "lib/index.js", 3 | "scripts": { 4 | "genkit:start": "genkit start -- tsx --watch src/genkit-sample.ts", 5 | "build": "tsc", 6 | "build:watch": "tsc --watch", 7 | "emulator": "firebase emulators:start --only functions", 8 | "dev": "concurrently \"npm run build:watch\" \"npm run emulator\"", 9 | "serve": "npm run build && firebase emulators:start --only functions", 10 | "shell": "npm run build && firebase functions:shell", 11 | "start": "npm run shell", 12 | "lint": "biome lint ./src/", 13 | "format": "biome format --write ./src/", 14 | "check": "biome check --write ./src/", 15 | "deploy": "firebase deploy --only functions", 16 | "logs": "firebase functions:log" 17 | }, 18 | "name": "functions", 19 | "engines": { 20 | "node": "22" 21 | }, 22 | "dependencies": { 23 | "express": "^4.21.2", 24 | "firebase-admin": "^12.6.0", 25 | "firebase-functions": "^6.0.1", 26 | "zod": "^3.24.1" 27 | }, 28 | "devDependencies": { 29 | "@biomejs/biome": "1.9.4", 30 | "concurrently": "^9.1.0", 31 | "firebase-functions-test": "^3.1.0", 32 | "tsx": "^4.19.2", 33 | "typescript": "^4.9.5" 34 | }, 35 | "private": true 36 | } 37 | -------------------------------------------------------------------------------- /frontend/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:goronyan/config/environment/firebase_initializer.dart'; 4 | import 'package:goronyan/core/constants/constants.dart'; 5 | import 'package:goronyan/core/data/logger/logger.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | import 'package:package_info_plus/package_info_plus.dart'; 8 | 9 | import 'app.dart'; 10 | 11 | Future main() async { 12 | WidgetsFlutterBinding.ensureInitialized(); 13 | 14 | logger.d('flavor: $flavor'); 15 | 16 | final packageInfo = await PackageInfo.fromPlatform(); 17 | final appName = packageInfo.appName; 18 | final packageName = packageInfo.packageName; 19 | final version = packageInfo.version; 20 | final buildNumber = packageInfo.buildNumber; 21 | 22 | logger 23 | ..d('appName: $appName') 24 | ..d('packageName: $packageName') 25 | ..d('version: $version') 26 | ..d('buildNumber: $buildNumber'); 27 | 28 | await initializeFirebaseApp(); 29 | 30 | await SystemChrome.setPreferredOrientations([ 31 | DeviceOrientation.portraitUp, 32 | ]); 33 | 34 | runApp( 35 | const ProviderScope( 36 | child: App(), 37 | ), 38 | ); 39 | } 40 | -------------------------------------------------------------------------------- /frontend/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 17 | base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 18 | - platform: android 19 | create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 20 | base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 21 | - platform: ios 22 | create_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 23 | base_revision: dec2ee5c1f98f8e84a7d5380c05eb8a3d0a81668 24 | 25 | # User provided section 26 | 27 | # List of Local paths (relative to this file) that should be 28 | # ignored by the migrate tool. 29 | # 30 | # Files that are not part of the templates will be ignored by default. 31 | unmanaged_files: 32 | - 'lib/main.dart' 33 | - 'ios/Runner.xcodeproj/project.pbxproj' 34 | -------------------------------------------------------------------------------- /frontend/lib/features/auth/data/repositories/app_user_repository_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'app_user_repository_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$appUserRepositoryHash() => r'2e87015c2ae1f58cc1965a6aa664235bde0b3b43'; 10 | 11 | /// See also [appUserRepository]. 12 | @ProviderFor(appUserRepository) 13 | final appUserRepositoryProvider = 14 | AutoDisposeProvider.internal( 15 | appUserRepository, 16 | name: r'appUserRepositoryProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$appUserRepositoryHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | @Deprecated('Will be removed in 3.0. Use Ref instead') 25 | // ignore: unused_element 26 | typedef AppUserRepositoryRef = AutoDisposeProviderRef; 27 | // ignore_for_file: type=lint 28 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 29 | -------------------------------------------------------------------------------- /frontend/lib/features/generate/data/repositories/generate_repository_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'generate_repository_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$generateRepositoryHash() => 10 | r'153282d9e3c01acf9f747633b033a5aaad1d9ff2'; 11 | 12 | /// See also [generateRepository]. 13 | @ProviderFor(generateRepository) 14 | final generateRepositoryProvider = 15 | AutoDisposeProvider.internal( 16 | generateRepository, 17 | name: r'generateRepositoryProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$generateRepositoryHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | @Deprecated('Will be removed in 3.0. Use Ref instead') 26 | // ignore: unused_element 27 | typedef GenerateRepositoryRef = AutoDisposeProviderRef; 28 | // ignore_for_file: type=lint 29 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 30 | -------------------------------------------------------------------------------- /frontend/ios/firebase/prod/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 1005658646293-0fpp68ugq06jtcffuuduuvusaks6td19.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.1005658646293-0fpp68ugq06jtcffuuduuvusaks6td19 9 | API_KEY 10 | AIzaSyD9CoPAWUje1WeSOz6UYDi88WxvI7oTXNw 11 | GCM_SENDER_ID 12 | 1005658646293 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | com.goronyan.goronyan 17 | PROJECT_ID 18 | hackathon2024-4cb8e 19 | STORAGE_BUCKET 20 | hackathon2024-4cb8e.firebasestorage.app 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:1005658646293:ios:e61edacc45c2cd71312b1c 33 | 34 | -------------------------------------------------------------------------------- /frontend/lib/features/mnemonics/data/repositories/mnemonics_repository_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'mnemonics_repository_provider.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$mnemonicsRepositoryHash() => 10 | r'846b46bb885053a17b8403c4ab9b30d589d62e8c'; 11 | 12 | /// See also [mnemonicsRepository]. 13 | @ProviderFor(mnemonicsRepository) 14 | final mnemonicsRepositoryProvider = 15 | AutoDisposeProvider.internal( 16 | mnemonicsRepository, 17 | name: r'mnemonicsRepositoryProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$mnemonicsRepositoryHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | @Deprecated('Will be removed in 3.0. Use Ref instead') 26 | // ignore: unused_element 27 | typedef MnemonicsRepositoryRef = AutoDisposeProviderRef; 28 | // ignore_for_file: type=lint 29 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 30 | -------------------------------------------------------------------------------- /frontend/ios/firebase/dev/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 1005658646293-71oi1mr5mlsdm0bd8re8b6nqbl15ov6d.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.1005658646293-71oi1mr5mlsdm0bd8re8b6nqbl15ov6d 9 | API_KEY 10 | AIzaSyD9CoPAWUje1WeSOz6UYDi88WxvI7oTXNw 11 | GCM_SENDER_ID 12 | 1005658646293 13 | PLIST_VERSION 14 | 1 15 | BUNDLE_ID 16 | com.goronyan.goronyan.dev 17 | PROJECT_ID 18 | hackathon2024-4cb8e 19 | STORAGE_BUCKET 20 | hackathon2024-4cb8e.firebasestorage.app 21 | IS_ADS_ENABLED 22 | 23 | IS_ANALYTICS_ENABLED 24 | 25 | IS_APPINVITE_ENABLED 26 | 27 | IS_GCM_ENABLED 28 | 29 | IS_SIGNIN_ENABLED 30 | 31 | GOOGLE_APP_ID 32 | 1:1005658646293:ios:946756d7702b4ed8312b1c 33 | 34 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | 47 | # FVM Version Cache 48 | .fvm/ 49 | 50 | # Flavor 51 | # Ignore these to use per-flavor files. 52 | **/ios/Runner/GoogleService-Info.plist 53 | **/android/app/google-services.json 54 | **/ios/Flutter/Dart-Defines.xcconfig 55 | 56 | # Not used but generated by flutterfire configure command. 57 | # ios/firebase_app_id_file.json 58 | 59 | # Firebase 60 | # ios/firebase/ 61 | # android/app/src/firebase/ 62 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/cards/custom_switch_list_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | 4 | class CustomSwitchListTile extends ConsumerWidget { 5 | const CustomSwitchListTile({ 6 | super.key, 7 | required this.title, 8 | required this.value, 9 | this.isTop = false, 10 | this.isBottom = false, 11 | required this.onChanged, 12 | }); 13 | 14 | final String title; 15 | final bool value; 16 | final bool isTop; 17 | final bool isBottom; 18 | final void Function({required bool value}) onChanged; 19 | 20 | @override 21 | Widget build(BuildContext context, WidgetRef ref) { 22 | final theme = Theme.of(context); 23 | 24 | return SwitchListTile( 25 | value: value, 26 | onChanged: (value) { 27 | onChanged(value: value); 28 | }, 29 | shape: RoundedRectangleBorder( 30 | borderRadius: BorderRadius.vertical( 31 | top: isTop ? const Radius.circular(16) : Radius.zero, 32 | bottom: isBottom ? const Radius.circular(16) : Radius.zero, 33 | ), 34 | ), 35 | title: Text( 36 | title, 37 | style: theme.textTheme.bodyMedium?.copyWith( 38 | fontWeight: FontWeight.bold, 39 | ), 40 | ), 41 | ); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /backend/src/core/utils/genai/flowExtensions.ts: -------------------------------------------------------------------------------- 1 | import type { Genkit, z } from "genkit"; 2 | 3 | export const withRetry = async ( 4 | fn: () => Promise, 5 | maxAttempts = 2, 6 | delayMs = 100, 7 | ): Promise => { 8 | let attempt = 0; 9 | while (attempt < maxAttempts) { 10 | try { 11 | return await fn(); 12 | } catch (error) { 13 | attempt++; 14 | console.warn(`Attempt ${attempt} of ${maxAttempts} failed:`, error); 15 | if (attempt < maxAttempts) { 16 | await new Promise((resolve) => setTimeout(resolve, delayMs)); 17 | } 18 | } 19 | } 20 | console.error(`All ${maxAttempts} attempts have failed.`); 21 | throw new Error(`All ${maxAttempts} attempts have failed.`); 22 | }; 23 | 24 | export const defineRetryFlow = ( 25 | ai: Genkit, 26 | config: { 27 | name: string; 28 | inputSchema: z.Schema; 29 | outputSchema: z.Schema; 30 | maxAttempts?: number; 31 | delayMs?: number; 32 | }, 33 | flowFn: (input: I) => Promise, 34 | ) => { 35 | const { 36 | name, 37 | inputSchema, 38 | outputSchema, 39 | maxAttempts = 2, 40 | delayMs = 100, 41 | } = config; 42 | 43 | return ai.defineFlow( 44 | { 45 | name, 46 | inputSchema, 47 | outputSchema, 48 | }, 49 | async (input) => { 50 | return withRetry(() => flowFn(input), maxAttempts, delayMs); 51 | }, 52 | ); 53 | }; 54 | -------------------------------------------------------------------------------- /frontend/lib/features/quiz/presentation/widgets/status_content.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/core/constants/app_colors.dart'; 3 | 4 | class StatusContent extends StatelessWidget { 5 | const StatusContent({ 6 | super.key, 7 | required this.label, 8 | required this.value, 9 | this.subText, 10 | }); 11 | 12 | final String label; 13 | final String value; 14 | final String? subText; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final theme = Theme.of(context); 19 | 20 | return Column( 21 | mainAxisSize: MainAxisSize.min, 22 | spacing: 8, 23 | children: [ 24 | Text( 25 | label, 26 | style: theme.textTheme.titleSmall?.copyWith( 27 | color: AppColors.textLightDark, 28 | ), 29 | ), 30 | RichText( 31 | text: TextSpan( 32 | style: theme.textTheme.displayLarge, 33 | children: [ 34 | TextSpan(text: value), 35 | if (subText != null) 36 | const WidgetSpan( 37 | child: SizedBox(width: 4), 38 | ), 39 | if (subText != null) 40 | TextSpan( 41 | text: subText, 42 | style: theme.textTheme.bodySmall, 43 | ), 44 | ], 45 | ), 46 | ), 47 | ], 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/lib/features/home/application/home_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:goronyan/core/data/logger/logger.dart'; 4 | import 'package:goronyan/features/auth/application/app_user_provider.dart'; 5 | import 'package:goronyan/features/home/application/home_state.dart'; 6 | import 'package:goronyan/features/mnemonics/data/repositories/mnemonics_repository_provider.dart'; 7 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 8 | 9 | part 'home_provider.g.dart'; 10 | 11 | @riverpod 12 | class Home extends _$Home { 13 | @override 14 | FutureOr build() async { 15 | try { 16 | final appUser = await ref.watch(appUserProvider.future); 17 | if (appUser == null) { 18 | throw Exception('User is not signed in'); 19 | } 20 | 21 | final mnemonics = 22 | await ref.read(mnemonicsRepositoryProvider).fetchMnemonics( 23 | uid: appUser.id, 24 | createAtdescending: true, 25 | limit: 10, 26 | ); 27 | 28 | return HomeState(mnemonics: mnemonics ?? [], appUser: appUser); 29 | } on FirebaseException catch (e, st) { 30 | logger.e('Failed to fetch mnemonics: $e, $st'); 31 | rethrow; 32 | } on Exception catch (e, st) { 33 | logger.e('Failed to fetch mnemonics: $e, $st'); 34 | rethrow; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /frontend/android/app/src/firebase/prod/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "1005658646293", 4 | "project_id": "hackathon2024-4cb8e", 5 | "storage_bucket": "hackathon2024-4cb8e.firebasestorage.app" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:1005658646293:android:39031e56f12b3ebb312b1c", 11 | "android_client_info": { 12 | "package_name": "com.goronyan.goronyan" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "1005658646293-3ampka7v3mt5ee0pg7e7k281cnprnoip.apps.googleusercontent.com", 18 | "client_type": 3 19 | } 20 | ], 21 | "api_key": [ 22 | { 23 | "current_key": "AIzaSyBi38T55t7rWs9tmKEcUaBAlQNX29EiJdw" 24 | } 25 | ], 26 | "services": { 27 | "appinvite_service": { 28 | "other_platform_oauth_client": [ 29 | { 30 | "client_id": "1005658646293-3ampka7v3mt5ee0pg7e7k281cnprnoip.apps.googleusercontent.com", 31 | "client_type": 3 32 | }, 33 | { 34 | "client_id": "1005658646293-0fpp68ugq06jtcffuuduuvusaks6td19.apps.googleusercontent.com", 35 | "client_type": 2, 36 | "ios_info": { 37 | "bundle_id": "com.goronyan.goronyan" 38 | } 39 | } 40 | ] 41 | } 42 | } 43 | } 44 | ], 45 | "configuration_version": "1" 46 | } -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/listeners/level_up_listener.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/core/constants/constants.dart'; 3 | import 'package:goronyan/core/presentation/widgets/modals/level_up_modal.dart'; 4 | import 'package:goronyan/features/auth/application/app_user_provider.dart'; 5 | import 'package:goronyan/features/auth/domain/entities/app_user.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | 8 | class LevelUpListener extends ConsumerWidget { 9 | const LevelUpListener({ 10 | super.key, 11 | required this.child, 12 | }); 13 | 14 | final Widget child; 15 | 16 | @override 17 | Widget build(BuildContext context, WidgetRef ref) { 18 | ref.listen>(appUserProvider, (previous, next) { 19 | final prevData = previous?.value; 20 | final nextData = next.value; 21 | 22 | if (prevData != null && 23 | nextData != null && 24 | prevData.level < nextData.level) { 25 | WidgetsBinding.instance.addPostFrameCallback((_) { 26 | final ctx = rootNavigatorKey.currentContext; 27 | if (ctx != null) { 28 | showDialog( 29 | context: ctx, 30 | builder: (_) => LevelUpModal( 31 | prevLevel: prevData.level, 32 | nextLevel: nextData.level, 33 | avatarURL: nextData.avatarURL, 34 | ), 35 | ); 36 | } 37 | }); 38 | } 39 | }); 40 | 41 | return child; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /frontend/lib/features/mnemonics/application/mnemonics_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:goronyan/config/providers/firebase_providers.dart'; 4 | import 'package:goronyan/core/data/logger/logger.dart'; 5 | import 'package:goronyan/features/mnemonics/application/mnemonics_state.dart'; 6 | import 'package:goronyan/features/mnemonics/data/repositories/mnemonics_repository_provider.dart'; 7 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 8 | 9 | part 'mnemonics_provider.g.dart'; 10 | 11 | @riverpod 12 | class Mnemonics extends _$Mnemonics { 13 | @override 14 | FutureOr build(MenemonicsStateType type) async { 15 | try { 16 | final user = ref.read(firebaseAuthProvider).currentUser; 17 | if (user == null) { 18 | throw Exception('User is not signed in'); 19 | } 20 | 21 | final mnemonics = 22 | await ref.read(mnemonicsRepositoryProvider).fetchMnemonics( 23 | uid: user.uid, 24 | lastMemorized: 25 | type == MenemonicsStateType.unmemorized ? false : null, 26 | createAtdescending: true, 27 | ); 28 | 29 | return MnemonicsState(mnemonics: mnemonics ?? []); 30 | } on FirebaseException catch (e, st) { 31 | logger.e('Failed to fetch mnemonics: $e, $st'); 32 | rethrow; 33 | } on Exception catch (e, st) { 34 | logger.e('Failed to fetch mnemonics: $e, $st'); 35 | rethrow; 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/layouts/async_error_widget.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/config/router/router.dart'; 3 | import 'package:goronyan/core/presentation/widgets/offsets/offset_button.dart'; 4 | 5 | class AsyncErrorWidget extends StatelessWidget { 6 | const AsyncErrorWidget({ 7 | super.key, 8 | required this.e, 9 | required this.st, 10 | this.isHomeButton = true, 11 | this.onReload, 12 | }); 13 | 14 | final Object e; 15 | final StackTrace? st; 16 | final bool isHomeButton; 17 | final void Function()? onReload; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | final theme = Theme.of(context); 22 | 23 | return Center( 24 | child: Column( 25 | mainAxisAlignment: MainAxisAlignment.center, 26 | children: [ 27 | Icon( 28 | Icons.error, 29 | color: theme.colorScheme.error, 30 | size: 64, 31 | ), 32 | Text( 33 | 'エラーが発生しました\n$e', 34 | textAlign: TextAlign.center, 35 | style: theme.textTheme.bodyMedium, 36 | ), 37 | if (isHomeButton) ...[ 38 | const SizedBox(height: 32), 39 | OffsetButton( 40 | label: 'ホームに戻る', 41 | onPressed: () { 42 | if (onReload != null) { 43 | onReload!(); 44 | } else { 45 | const HomeRouteData().go(context); 46 | } 47 | }, 48 | ), 49 | ], 50 | ], 51 | ), 52 | ); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /backend/src/core/utils/genai/prompts.ts: -------------------------------------------------------------------------------- 1 | export const answerDivisionPrompt = `# 以下の指示に従って、入力された音声文字起こしを行い、そこからクイズ形式の「問題(Question)」と「答え(Answer)」を生成してください。 2 | 3 | ## 注意事項 4 | - 問題に答えの全文を含めないようにしてください。 5 | `; 6 | 7 | export const meaningPrompt = `問題は「{QUESTION}」、答えは「{ANSWER}」です。 8 | 答えをより詳しく、わかりやすい意味で100文字以内で日本語、英語で解説してください。`; 9 | 10 | export const interestsPrompt = `# あなたは暗記アプリのアシスタントです。ユーザーが覚えたい【問題】と【答え】を、ユーザーの【興味】と関連づけて理解しやすい短いうんちくやエピソードを作成してください。 11 | 12 | 【問題】: 「{QUESTION}」 13 | 【答え】: 「{ANSWER}」 14 | 【興味】: 「{INTERESTS}」 15 | 16 | ## 注意事項 17 | 1. 出力は「短文」の1つのエピソードとする。 18 | 3. 読んでいて楽しく、かつ覚えやすい内容にする。 19 | 4. 解説や前置きは不要で、直接エピソードを提示する。`; 20 | 21 | export const catLevelPromptTexts = [ 22 | "シンプルで真面目な語呂合わせやフレーズを作成してください。", 23 | "ユーモアを含んだ語呂合わせやフレーズを作成してください", 24 | "猫語を使ったハイテンションなフレーズを作成してください。語呂合わせでなくても構いません。とにかくテンション高く、自由にふざけて表現してください(「にゃんにゃん」などの猫要素を盛り込むこと)", 25 | ]; 26 | 27 | export const goroPrompt = `# 指定された内容に関して記憶しやすい短い語呂合わせ、面白いフレーズ、言葉遊びを1から3つ作成してください。 28 | 29 | ## 出力形式 30 | 31 | - 各語呂合わせや言葉遊びを含む1から3つの箇条書きを提示してください。 32 | - 各語呂合わせや言葉遊びは指定された内容に直接関連し、簡潔かつ記憶に残りやすいものにしてください。 33 | 34 | ## 注意事項 35 | 36 | - 語呂合わせや言葉遊びは、音や意味の関連性を意識して作成してください。 37 | - 説明やコメントは避け、フレーズ自体に集中してください。 38 | - {CAT_LEVEL_TEXT} 39 | 40 | ## 語呂合わせを作成する内容: 41 | - 問題: {QUESTION} 42 | - 答え: {ANSWER}`; 43 | 44 | export const generateImagePrompt = `An anthropomorphic cat avatar (or multiple anthropomorphic cats if appropriate) in a slightly loose and deformed anime style, 45 | with thick line art, minimal use of color (mostly monotone), and designed to be memorable or attention-catching. 46 | They should be integrated into a scene that visually represents the theme or context described by the following text: 47 | 48 | "{MEANING}"`; 49 | -------------------------------------------------------------------------------- /frontend/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /frontend/lib/features/profile/presentation/widgets/edit_text_form.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | 5 | class EditTextForm extends HookConsumerWidget { 6 | const EditTextForm({ 7 | super.key, 8 | required this.originalText, 9 | required this.formKey, 10 | required this.onChanged, 11 | this.validator, 12 | required this.label, 13 | required this.hint, 14 | this.helper, 15 | }); 16 | 17 | final String originalText; 18 | final GlobalKey formKey; 19 | final void Function(String) onChanged; 20 | final String? Function(String?)? validator; 21 | final String label; 22 | final String hint; 23 | final String? helper; 24 | 25 | @override 26 | Widget build(BuildContext context, WidgetRef ref) { 27 | final textController = useTextEditingController(text: originalText); 28 | 29 | return Form( 30 | key: formKey, 31 | child: Padding( 32 | padding: const EdgeInsets.symmetric(horizontal: 16), 33 | child: Column( 34 | crossAxisAlignment: CrossAxisAlignment.start, 35 | children: [ 36 | const SizedBox(height: 8), 37 | TextFormField( 38 | controller: textController, 39 | keyboardType: TextInputType.text, 40 | decoration: InputDecoration( 41 | labelText: label, 42 | hintText: hint, 43 | helperText: helper, 44 | ), 45 | validator: validator, 46 | onChanged: onChanged, 47 | ), 48 | ], 49 | ), 50 | ), 51 | ); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/offsets/halftone_background.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class HalftoneBackground extends StatelessWidget { 4 | const HalftoneBackground({ 5 | super.key, 6 | required this.child, 7 | this.color, 8 | this.spacing = 6, 9 | this.radius = 2.0, 10 | this.offset, 11 | }); 12 | 13 | final Widget child; 14 | final Color? color; 15 | final double spacing; 16 | final double radius; 17 | final Offset? offset; 18 | 19 | @override 20 | Widget build(BuildContext context) { 21 | return CustomPaint( 22 | painter: RetroHalftonePainter( 23 | color: color ?? Theme.of(context).colorScheme.secondary, 24 | spacing: spacing, 25 | radius: radius, 26 | offset: offset, 27 | ), 28 | child: child, 29 | ); 30 | } 31 | } 32 | 33 | class RetroHalftonePainter extends CustomPainter { 34 | RetroHalftonePainter({ 35 | required this.color, 36 | required this.spacing, 37 | required this.radius, 38 | this.offset, 39 | }); 40 | 41 | final Color color; 42 | final double spacing; 43 | final double radius; 44 | final Offset? offset; 45 | 46 | @override 47 | void paint(Canvas canvas, Size size) { 48 | final paint = Paint()..color = color; 49 | final effectiveOffset = 50 | offset ?? Offset.zero; 51 | 52 | for (var y = -spacing + effectiveOffset.dy; 53 | y < size.height; 54 | y += spacing) { 55 | for (var x = -spacing + effectiveOffset.dx; 56 | x < size.width; 57 | x += spacing) { 58 | canvas.drawCircle(Offset(x, y), radius, paint); 59 | } 60 | } 61 | } 62 | 63 | @override 64 | bool shouldRepaint(covariant CustomPainter oldDelegate) { 65 | return false; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/cards/custom_list_tile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/core/constants/app_colors.dart'; 3 | import 'package:goronyan/core/extensions/string_extensions.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | 6 | class CustomListTile extends ConsumerWidget { 7 | const CustomListTile({ 8 | super.key, 9 | required this.title, 10 | this.label, 11 | this.isTop = false, 12 | this.isBottom = false, 13 | this.onTap, 14 | }); 15 | 16 | final String title; 17 | final String? label; 18 | final bool isTop; 19 | final bool isBottom; 20 | final void Function()? onTap; 21 | 22 | @override 23 | Widget build(BuildContext context, WidgetRef ref) { 24 | final theme = Theme.of(context); 25 | 26 | return ListTile( 27 | onTap: onTap, 28 | shape: RoundedRectangleBorder( 29 | borderRadius: BorderRadius.vertical( 30 | top: isTop ? const Radius.circular(16) : Radius.zero, 31 | bottom: isBottom ? const Radius.circular(16) : Radius.zero, 32 | ), 33 | ), 34 | leading: label.isNotNullOrEmpty 35 | ? SizedBox( 36 | width: 72, 37 | child: Text( 38 | label!, 39 | style: theme.textTheme.bodySmall?.copyWith( 40 | color: AppColors.textLightDark, 41 | ), 42 | ), 43 | ) 44 | : null, 45 | title: Text( 46 | title, 47 | style: label.isNotNullOrEmpty 48 | ? null 49 | : theme.textTheme.bodyMedium?.copyWith( 50 | fontWeight: FontWeight.bold, 51 | ), 52 | ), 53 | trailing: const Icon(Icons.chevron_right_rounded), 54 | ); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /frontend/lib/features/auth/domain/entities/app_user.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'app_user.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$AppUserImpl _$$AppUserImplFromJson(Map json) => 10 | _$AppUserImpl( 11 | id: json['id'] as String, 12 | displayName: json['displayName'] as String? ?? '', 13 | interests: json['interests'] as String? ?? '', 14 | xp: (json['xp'] as num?)?.toInt() ?? 0, 15 | level: (json['level'] as num?)?.toInt() ?? 1, 16 | catLevel: (json['catLevel'] as num?)?.toInt() ?? 2, 17 | avatarURL: json['avatarURL'] as String? ?? defaultAvatarURL, 18 | generatedCount: (json['generatedCount'] as num?)?.toInt() ?? 0, 19 | isProfileCompleted: json['isProfileCompleted'] as bool? ?? false, 20 | createdAt: 21 | const TimestampConverter().fromJson(json['createdAt'] as Timestamp), 22 | updatedAt: 23 | const TimestampConverter().fromJson(json['updatedAt'] as Timestamp), 24 | ); 25 | 26 | Map _$$AppUserImplToJson(_$AppUserImpl instance) => 27 | { 28 | 'id': instance.id, 29 | 'displayName': instance.displayName, 30 | 'interests': instance.interests, 31 | 'xp': instance.xp, 32 | 'level': instance.level, 33 | 'catLevel': instance.catLevel, 34 | 'avatarURL': instance.avatarURL, 35 | 'generatedCount': instance.generatedCount, 36 | 'isProfileCompleted': instance.isProfileCompleted, 37 | 'createdAt': const TimestampConverter().toJson(instance.createdAt), 38 | 'updatedAt': const TimestampConverter().toJson(instance.updatedAt), 39 | }; 40 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/builder/async_value_builder.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/core/presentation/widgets/layouts/async_error_widget.dart'; 3 | import 'package:goronyan/core/presentation/widgets/layouts/wrap_scaffold.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | 6 | class AsyncValueBuilder extends StatelessWidget { 7 | const AsyncValueBuilder({ 8 | super.key, 9 | required this.asyncValue, 10 | required this.child, 11 | this.errorBuilder, 12 | this.loadingWidget, 13 | this.isWrapScaffoldLoading = false, 14 | this.isWrapScaffoldError = false, 15 | this.isErrorHomeButton = true, 16 | this.onReload, 17 | }); 18 | 19 | final AsyncValue asyncValue; 20 | final Widget Function(T data) child; 21 | final Widget Function(Object e, StackTrace? st)? errorBuilder; 22 | final Widget? loadingWidget; 23 | final bool isWrapScaffoldLoading; 24 | final bool isWrapScaffoldError; 25 | final bool isErrorHomeButton; 26 | final void Function()? onReload; 27 | 28 | @override 29 | Widget build(BuildContext context) { 30 | return switch (asyncValue) { 31 | AsyncData(value: final data) => child(data), 32 | AsyncError(error: final e, stackTrace: final st) => errorBuilder != null 33 | ? errorBuilder!(e, st) 34 | : WrapScaffold( 35 | isWrap: isWrapScaffoldError, 36 | child: AsyncErrorWidget( 37 | e: e, 38 | st: st, 39 | isHomeButton: isErrorHomeButton, 40 | onReload: onReload, 41 | ), 42 | ), 43 | _ => loadingWidget ?? 44 | WrapScaffold( 45 | isWrap: isWrapScaffoldLoading, 46 | child: const Center(child: CircularProgressIndicator()), 47 | ), 48 | }; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/lib/features/profile/application/edit_interests_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:goronyan/config/providers/firebase_providers.dart'; 2 | import 'package:goronyan/core/data/logger/logger.dart'; 3 | import 'package:goronyan/features/auth/application/app_user_provider.dart'; 4 | import 'package:goronyan/features/auth/data/repositories/app_user_repository_provider.dart'; 5 | import 'package:goronyan/features/profile/application/edit_interests_state.dart'; 6 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 7 | 8 | part 'edit_interests_provider.g.dart'; 9 | 10 | @riverpod 11 | class EditInterests extends _$EditInterests { 12 | @override 13 | FutureOr build() async { 14 | final appUser = await ref.watch(appUserProvider.future); 15 | return EditInterestsState(interests: appUser?.interests ?? ''); 16 | } 17 | 18 | Future onChangedInterests(String value) async { 19 | await update((data) { 20 | return data.copyWith(interests: value); 21 | }); 22 | } 23 | 24 | Future onChangedIsUpdeting({required bool value}) async { 25 | await update((data) { 26 | return data.copyWith(isUpdating: value); 27 | }); 28 | } 29 | 30 | Future updateInterests() async { 31 | final user = ref.read(firebaseAuthProvider).currentUser; 32 | final data = state.asData?.value; 33 | if (user == null || data == null) { 34 | return null; 35 | } 36 | 37 | await onChangedIsUpdeting(value: true); 38 | 39 | try { 40 | final result = await ref 41 | .read(appUserRepositoryProvider) 42 | .updateInterests(user.uid, data.interests); 43 | await onChangedIsUpdeting(value: false); 44 | return result; 45 | } on Exception catch (e, st) { 46 | logger.e('Failed to update, $e, $st'); 47 | state = AsyncValue.error(e, st); 48 | return null; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /frontend/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | $(appName) 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(appCFBundleName) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | NSMicrophoneUsageDescription 49 | 音声入力機能を使用するためにマイクへのアクセスが必要です。 50 | 51 | 52 | -------------------------------------------------------------------------------- /frontend/lib/config/router/branches/shell_route.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:goronyan/features/home/presentation/pages/home_page.dart'; 4 | import 'package:goronyan/features/mnemonics/presentation/pages/mnemonics_page.dart'; 5 | import 'package:goronyan/features/quiz/presentation/pages/quiz_page.dart'; 6 | import 'package:goronyan/features/settings/presentation/pages/settings_page.dart'; 7 | 8 | class HomeBranchData extends StatefulShellBranchData { 9 | const HomeBranchData(); 10 | } 11 | 12 | class HomeRouteData extends GoRouteData { 13 | const HomeRouteData(); 14 | 15 | @override 16 | Widget build(BuildContext context, GoRouterState state) => const HomePage(); 17 | } 18 | 19 | class MnemonicsBranchData extends StatefulShellBranchData { 20 | const MnemonicsBranchData(); 21 | } 22 | 23 | class MnemonicsRouteData extends GoRouteData { 24 | const MnemonicsRouteData(); 25 | 26 | @override 27 | Widget build(BuildContext context, GoRouterState state) => 28 | const MnemonicsPage(); 29 | } 30 | 31 | class CenterBranchData extends StatefulShellBranchData { 32 | const CenterBranchData(); 33 | } 34 | 35 | class CenterRouteData extends GoRouteData { 36 | const CenterRouteData(); 37 | 38 | @override 39 | Widget build(BuildContext context, GoRouterState state) => Container(); 40 | } 41 | 42 | class QuizBranchData extends StatefulShellBranchData { 43 | const QuizBranchData(); 44 | } 45 | 46 | class QuizRouteData extends GoRouteData { 47 | const QuizRouteData(); 48 | 49 | @override 50 | Widget build(BuildContext context, GoRouterState state) => const QuizPage(); 51 | } 52 | 53 | class SettingsBranchData extends StatefulShellBranchData { 54 | const SettingsBranchData(); 55 | } 56 | 57 | class SettingsRouteData extends GoRouteData { 58 | const SettingsRouteData(); 59 | 60 | @override 61 | Widget build(BuildContext context, GoRouterState state) => 62 | const SettingsPage(); 63 | } 64 | -------------------------------------------------------------------------------- /frontend/lib/features/mnemonics/domain/entities/mnemonic.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:goronyan/core/data/converters/timestamp_converter.dart'; 4 | 5 | part 'mnemonic.freezed.dart'; 6 | part 'mnemonic.g.dart'; 7 | 8 | @freezed 9 | class Mnemonic with _$Mnemonic { 10 | const factory Mnemonic({ 11 | required String id, 12 | String? question, 13 | @Default('') String answer, 14 | @Default('') String meaning, 15 | @Default('') String episode, 16 | @Default([]) List goroTexts, 17 | String? voicePath, 18 | String? questionImagePath, 19 | String? outputImagePath, 20 | @Default(0) int memorizedCount, 21 | @Default(0) int unmemorizedCount, 22 | @Default(false) bool lastMemorized, 23 | @TimestampConverter() required DateTime lastQuizAt, 24 | @TimestampConverter() required DateTime createdAt, 25 | @TimestampConverter() required DateTime updatedAt, 26 | }) = _Mnemonic; 27 | 28 | factory Mnemonic.fromJson(Map json) => 29 | _$MnemonicFromJson(json); 30 | 31 | const Mnemonic._(); 32 | 33 | Map toJsonAsStringDates() { 34 | return { 35 | 'id': id, 36 | 'question': question, 37 | 'answer': answer, 38 | 'meaning': meaning, 39 | 'episode': episode, 40 | 'goroTexts': goroTexts, 41 | 'voicePath': voicePath, 42 | 'questionImagePath': questionImagePath, 43 | 'outputImagePath': outputImagePath, 44 | 'memorizedCount': memorizedCount, 45 | 'unmemorizedCount': unmemorizedCount, 46 | 'lastMemorized': lastMemorized, 47 | 'lastQuizAt': lastQuizAt.toIso8601String(), 48 | 'createdAt': createdAt.toIso8601String(), 49 | 'updatedAt': updatedAt.toIso8601String(), 50 | }; 51 | } 52 | 53 | static const List timestampKeys = [ 54 | 'lastQuizAt', 55 | 'createdAt', 56 | 'updatedAt', 57 | ]; 58 | } 59 | -------------------------------------------------------------------------------- /frontend/lib/features/generate/application/re_generate_provider.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:goronyan/config/providers/firebase_providers.dart'; 4 | import 'package:goronyan/core/data/logger/logger.dart'; 5 | import 'package:goronyan/features/auth/application/app_user_provider.dart'; 6 | import 'package:goronyan/features/generate/application/re_generate_state.dart'; 7 | import 'package:goronyan/features/generate/data/repositories/generate_repository_provider.dart'; 8 | import 'package:goronyan/features/mnemonics/domain/entities/mnemonic.dart'; 9 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 10 | 11 | part 're_generate_provider.g.dart'; 12 | 13 | @riverpod 14 | class ReGenerate extends _$ReGenerate { 15 | @override 16 | FutureOr build(Mnemonic originalMnemonic) async { 17 | return ReGenerateState(mnemonic: originalMnemonic); 18 | } 19 | 20 | Future onReGenerate() async { 21 | final mnemonic = state.value?.mnemonic; 22 | if (mnemonic == null) { 23 | return; 24 | } 25 | state = const AsyncValue.loading(); 26 | state = await AsyncValue.guard(() async { 27 | final user = ref.read(firebaseAuthProvider).currentUser; 28 | final appUser = await ref.read(appUserProvider.future); 29 | if (user == null || appUser == null) { 30 | throw Exception('User is not signed in'); 31 | } 32 | 33 | final newMemonic = 34 | await ref.read(generateRepositoryProvider).reGenerateMnemonic( 35 | prevMnemonic: mnemonic, 36 | user: user, 37 | catLevel: appUser.catLevel, 38 | interests: appUser.interests, 39 | ); 40 | if (newMemonic == null) { 41 | logger.e('Failed to re generate mnemonic'); 42 | throw Exception('Failed to re generate mnemonic'); 43 | } 44 | return ReGenerateState( 45 | mnemonic: newMemonic, 46 | isReGenerateEnabled: false, 47 | ); 48 | }); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/lib/features/profile/application/edit_cat_level_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:goronyan/config/providers/firebase_providers.dart'; 2 | import 'package:goronyan/core/constants/constants.dart'; 3 | import 'package:goronyan/core/data/logger/logger.dart'; 4 | import 'package:goronyan/features/auth/application/app_user_provider.dart'; 5 | import 'package:goronyan/features/auth/data/repositories/app_user_repository_provider.dart'; 6 | import 'package:goronyan/features/profile/application/edit_cat_level_state.dart'; 7 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 8 | 9 | part 'edit_cat_level_provider.g.dart'; 10 | 11 | @riverpod 12 | class EditCatLevel extends _$EditCatLevel { 13 | @override 14 | FutureOr build() async { 15 | final appUser = await ref.watch(appUserProvider.future); 16 | return EditCatLevelState( 17 | catLevel: (appUser?.catLevel ?? defaultCatLevel).toDouble(), 18 | ); 19 | } 20 | 21 | Future onChangedCatLevel(double value) async { 22 | await update((data) async { 23 | return data.copyWith(catLevel: value); 24 | }); 25 | } 26 | 27 | Future onChangedIsUpdeting({required bool value}) async { 28 | await update((data) async { 29 | return data.copyWith(isUpdating: value); 30 | }); 31 | } 32 | 33 | Future updateCatLevel() async { 34 | final user = ref.read(firebaseAuthProvider).currentUser; 35 | final data = state.asData?.value; 36 | if (user == null || data == null) { 37 | return null; 38 | } 39 | 40 | await onChangedIsUpdeting(value: true); 41 | 42 | try { 43 | final result = await ref 44 | .read(appUserRepositoryProvider) 45 | .updateCatLevel(user.uid, data.catLevel.toInt()); 46 | await onChangedIsUpdeting(value: false); 47 | return result; 48 | } on Exception catch (e, st) { 49 | logger.e('Failed to update, $e, $st'); 50 | state = AsyncValue.error(e, st); 51 | return null; 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /frontend/lib/core/extensions/firestore_extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:firebase_storage/firebase_storage.dart'; 3 | 4 | extension DocumentSnapshotX on DocumentSnapshot { 5 | Map _extractBaseData() { 6 | final map = data() as Map?; 7 | if (map == null) { 8 | throw Exception( 9 | 'Document does not exist or data is null. (doc.id = $id)', 10 | ); 11 | } 12 | map['id'] = id; 13 | return map; 14 | } 15 | 16 | Map _fillMissingTimestamps({ 17 | required Map data, 18 | required List timestampKeys, 19 | }) { 20 | for (final key in timestampKeys) { 21 | if (data[key] == null) { 22 | data[key] = Timestamp.fromDate(DateTime.now()); 23 | } 24 | } 25 | return data; 26 | } 27 | 28 | T toModellFixingTimestamps({ 29 | required T Function(Map) fromJson, 30 | List timestampKeys = const [], 31 | }) { 32 | final addIdData = _extractBaseData(); 33 | final fixingData = _fillMissingTimestamps( 34 | data: addIdData, 35 | timestampKeys: timestampKeys, 36 | ); 37 | return fromJson(fixingData); 38 | } 39 | 40 | Future toModelFixingTimestampsAndDownloadUrl({ 41 | required T Function(Map) fromJson, 42 | List timestampKeys = const [], 43 | required String storagePathKey, 44 | String? downloadUrlKey, 45 | }) async { 46 | final addIdData = _extractBaseData(); 47 | final fixingData = _fillMissingTimestamps( 48 | data: addIdData, 49 | timestampKeys: timestampKeys, 50 | ); 51 | 52 | final pathValue = fixingData[storagePathKey]; 53 | if (pathValue is String) { 54 | final ref = FirebaseStorage.instance.ref(pathValue); 55 | final url = await ref.getDownloadURL(); 56 | fixingData[downloadUrlKey ?? storagePathKey] = url; 57 | } 58 | 59 | return fromJson(fixingData); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /functions/src/triggers/users.ts: -------------------------------------------------------------------------------- 1 | import * as admin from "firebase-admin"; 2 | import * as logger from "firebase-functions/logger"; 3 | import { onDocumentUpdated } from "firebase-functions/v2/firestore"; 4 | import { defaultRegion } from "../core/constants/constants"; 5 | import { calculateLevel } from "../core/utils/calculations/levelCalculator"; 6 | import { AvatarSchema, UserSchema } from "../core/utils/schema/schema"; 7 | 8 | export const onUserXpChange = onDocumentUpdated( 9 | { 10 | document: "users/{userId}", 11 | region: defaultRegion, 12 | }, 13 | async (event) => { 14 | const beforeRawData = event.data?.before.data(); 15 | const afterRawData = event.data?.after.data(); 16 | 17 | if (!beforeRawData || !afterRawData) { 18 | return; 19 | } 20 | 21 | try { 22 | const beforeUser = UserSchema.parse(beforeRawData); 23 | const afterUser = UserSchema.parse(afterRawData); 24 | 25 | if (beforeUser.xp === afterUser.xp) { 26 | return; 27 | } 28 | 29 | const newLevel = calculateLevel(afterUser.xp); 30 | 31 | if (newLevel === afterUser.level) { 32 | return; 33 | } 34 | 35 | const snapshot = await admin 36 | .firestore() 37 | .collection("avatars") 38 | .where("level", "==", Math.min(newLevel, 8)) 39 | .limit(1) 40 | .get(); 41 | 42 | if (snapshot.empty) { 43 | logger.error(`No avatar found for level = ${newLevel}`); 44 | await event.data?.after.ref.update({ 45 | level: newLevel, 46 | updatedAt: admin.firestore.FieldValue.serverTimestamp(), 47 | }); 48 | return; 49 | } 50 | 51 | const doc = snapshot.docs[0]; 52 | const avatarData = AvatarSchema.parse(doc.data()); 53 | 54 | await event.data?.after.ref.update({ 55 | level: newLevel, 56 | avatarURL: avatarData.avatarURL, 57 | updatedAt: admin.firestore.FieldValue.serverTimestamp(), 58 | }); 59 | 60 | await event.data?.after.ref.update({ 61 | level: newLevel, 62 | updatedAt: admin.firestore.FieldValue.serverTimestamp(), 63 | }); 64 | } catch (err) { 65 | logger.error("Zod validation error:", err); 66 | return; 67 | } 68 | }, 69 | ); 70 | -------------------------------------------------------------------------------- /frontend/lib/features/mnemonics/presentation/widgets/mnemonics_tab_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | import 'package:goronyan/config/router/app_routes.dart'; 4 | import 'package:goronyan/core/presentation/widgets/builder/async_value_builder.dart'; 5 | import 'package:goronyan/core/presentation/widgets/cards/mnemonic_list_card.dart'; 6 | import 'package:goronyan/features/mnemonics/application/mnemonics_provider.dart'; 7 | import 'package:goronyan/features/mnemonics/application/mnemonics_state.dart'; 8 | import 'package:goronyan/features/mnemonics/domain/entities/mnemonic_detail_page_extra.dart'; 9 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 10 | 11 | class MnemonicsTabView extends HookConsumerWidget { 12 | const MnemonicsTabView({ 13 | super.key, 14 | required this.type, 15 | }); 16 | 17 | final MenemonicsStateType type; 18 | 19 | @override 20 | Widget build(BuildContext context, WidgetRef ref) { 21 | useAutomaticKeepAlive(); 22 | final asyncValue = ref.watch(mnemonicsProvider(type)); 23 | 24 | return AsyncValueBuilder( 25 | asyncValue: asyncValue, 26 | child: (data) { 27 | return data.mnemonics.isEmpty 28 | ? const Center( 29 | child: Text('記憶カードがありません'), 30 | ) 31 | : ListView.builder( 32 | itemCount: data.mnemonics.length, 33 | padding: const EdgeInsets.symmetric(horizontal: 8), 34 | itemBuilder: (context, index) { 35 | final mnemonic = data.mnemonics[index]; 36 | return MnemonicListCard( 37 | mnemonic: mnemonic, 38 | isDivider: index != data.mnemonics.length - 1, 39 | onTap: () { 40 | final extra = MnemonicDetailPageExtra( 41 | mnemonics: data.mnemonics, 42 | initialIndex: index, 43 | ); 44 | MnemonicsDetailsRouteData(extra).push(context); 45 | }, 46 | ); 47 | }, 48 | ); 49 | }, 50 | ); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /frontend/lib/features/mnemonics/data/repositories/mnemonics_repository_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:goronyan/config/providers/firebase_providers.dart'; 3 | import 'package:goronyan/core/data/logger/logger.dart'; 4 | import 'package:goronyan/core/extensions//firestore_extensions.dart'; 5 | import 'package:goronyan/features/mnemonics/domain/entities/mnemonic.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 8 | 9 | part 'mnemonics_repository_provider.g.dart'; 10 | 11 | @riverpod 12 | MnemonicsRepository mnemonicsRepository(Ref ref) => MnemonicsRepository(ref); 13 | 14 | class MnemonicsRepository { 15 | MnemonicsRepository(this._ref); 16 | final Ref _ref; 17 | 18 | Future?> fetchMnemonics({ 19 | required String uid, 20 | int? limit, 21 | bool? lastMemorized, 22 | bool? createAtdescending, 23 | bool? memorizedCountdescending, 24 | }) async { 25 | try { 26 | Query query = _ref 27 | .read(firebaseFirestoreProvider) 28 | .collection('users') 29 | .doc(uid) 30 | .collection('mnemonics'); 31 | 32 | if (lastMemorized != null) { 33 | query = query.where('lastMemorized', isEqualTo: lastMemorized); 34 | } 35 | 36 | if (createAtdescending != null) { 37 | query = query.orderBy('createdAt', descending: createAtdescending); 38 | } 39 | 40 | if (memorizedCountdescending != null) { 41 | query = query.orderBy( 42 | 'memorizedCount', 43 | descending: memorizedCountdescending, 44 | ); 45 | } 46 | 47 | if (limit != null) { 48 | query = query.limit(limit); 49 | } 50 | 51 | final querySnapshot = await query.get(); 52 | final futures = querySnapshot.docs.map((docSnap) { 53 | return docSnap.toModelFixingTimestampsAndDownloadUrl( 54 | fromJson: Mnemonic.fromJson, 55 | timestampKeys: Mnemonic.timestampKeys, 56 | storagePathKey: 'outputImagePath', 57 | ); 58 | }); 59 | 60 | final mnemonics = await Future.wait(futures); 61 | 62 | return mnemonics; 63 | } catch (e, st) { 64 | logger.e('Failed to fetch mnemonics, $e, $st'); 65 | rethrow; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /frontend/lib/features/profile/application/app_user_form_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:firebase_auth/firebase_auth.dart'; 3 | import 'package:goronyan/config/providers/firebase_providers.dart'; 4 | import 'package:goronyan/core/constants/constants.dart'; 5 | import 'package:goronyan/core/data/logger/logger.dart'; 6 | import 'package:goronyan/features/auth/data/repositories/app_user_repository_provider.dart'; 7 | import 'package:goronyan/features/auth/domain/entities/app_user.dart'; 8 | import 'package:goronyan/features/profile/application/app_user_form_state.dart'; 9 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 10 | 11 | part 'app_user_form_provider.g.dart'; 12 | 13 | @riverpod 14 | class AppUserForm extends _$AppUserForm { 15 | @override 16 | FutureOr build() async { 17 | return AppUserFormState(); 18 | } 19 | 20 | Future onChangedInterests(String value) async { 21 | await update((data) async { 22 | return data.copyWith(interests: value); 23 | }); 24 | } 25 | 26 | Future onChangedCatLevel(double value) async { 27 | await update((data) async { 28 | return data.copyWith(catLevel: value); 29 | }); 30 | } 31 | 32 | Future onCreateAppUser() async { 33 | final user = ref.read(firebaseAuthProvider).currentUser; 34 | final data = state.asData?.value; 35 | if (user == null || data == null) { 36 | return null; 37 | } 38 | 39 | final newAppUser = AppUser( 40 | id: user.uid, 41 | displayName: '', 42 | interests: data.interests, 43 | catLevel: data.catLevel.toInt(), 44 | avatarURL: defaultAvatarURL, 45 | xp: 0, 46 | level: 1, 47 | generatedCount: 0, 48 | isProfileCompleted: true, 49 | createdAt: DateTime.now(), 50 | updatedAt: DateTime.now(), 51 | ); 52 | 53 | state = const AsyncValue.loading(); 54 | try { 55 | final appUser = 56 | await ref.read(appUserRepositoryProvider).createAppUser(newAppUser); 57 | return appUser; 58 | } on FirebaseException catch (e, st) { 59 | logger.e('Failed to create user, $e, $st'); 60 | state = AsyncValue.error(e, st); 61 | return null; 62 | } on Exception catch (e, st) { 63 | logger.e('Failed to create user, $e, $st'); 64 | state = AsyncValue.error(e, st); 65 | return null; 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /frontend/lib/features/shell/presentation/pages/shell_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:goronyan/core/presentation/widgets/offsets/offset_icon_svg.dart'; 4 | import 'package:goronyan/features/generate/presentation/widgets/show_voice_input_bottom_sheet.dart'; 5 | 6 | class AssetWithLabel { 7 | const AssetWithLabel({ 8 | required this.path, 9 | required this.label, 10 | }); 11 | 12 | final String path; 13 | final String label; 14 | } 15 | 16 | const List paths = [ 17 | AssetWithLabel(path: 'ホーム', label: 'cat'), 18 | AssetWithLabel(path: 'リスト', label: 'paw_print'), 19 | AssetWithLabel(path: '作成', label: 'mic'), 20 | AssetWithLabel(path: 'クイズ', label: 'sticker'), 21 | AssetWithLabel(path: '設定', label: 'settings'), 22 | ]; 23 | 24 | class ShellPage extends StatelessWidget { 25 | const ShellPage({ 26 | super.key, 27 | required this.navigationShell, 28 | }); 29 | 30 | final StatefulNavigationShell navigationShell; 31 | 32 | @override 33 | Widget build(BuildContext context) { 34 | void goBranch(int index) { 35 | if (index == 2) { 36 | showVoiceInputBottomSheet(context); 37 | } else { 38 | navigationShell.goBranch( 39 | index, 40 | initialLocation: index == navigationShell.currentIndex, 41 | ); 42 | } 43 | } 44 | 45 | return Scaffold( 46 | body: navigationShell, 47 | bottomNavigationBar: NavigationBar( 48 | selectedIndex: navigationShell.currentIndex, 49 | indicatorColor: Colors.transparent, 50 | overlayColor: WidgetStateProperty.all(Colors.transparent), 51 | destinations: paths 52 | .asMap() 53 | .entries 54 | .map( 55 | (e) => NavigationDestination( 56 | icon: OffsetIconSvg( 57 | outlineAssetPath: 'assets/images/${e.value.label}.svg', 58 | fillAssetPath: 'assets/images/${e.value.label}_fill.svg', 59 | size: 24, 60 | isActive: navigationShell.currentIndex == e.key, 61 | ), 62 | label: e.value.path, 63 | ), 64 | ) 65 | .toList(), 66 | onDestinationSelected: goBranch, 67 | ), 68 | floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked, 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /frontend/lib/features/mnemonics/domain/entities/mnemonic.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'mnemonic.dart'; 4 | 5 | // ************************************************************************** 6 | // JsonSerializableGenerator 7 | // ************************************************************************** 8 | 9 | _$MnemonicImpl _$$MnemonicImplFromJson(Map json) => 10 | _$MnemonicImpl( 11 | id: json['id'] as String, 12 | question: json['question'] as String?, 13 | answer: json['answer'] as String? ?? '', 14 | meaning: json['meaning'] as String? ?? '', 15 | episode: json['episode'] as String? ?? '', 16 | goroTexts: (json['goroTexts'] as List?) 17 | ?.map((e) => e as String) 18 | .toList() ?? 19 | const [], 20 | voicePath: json['voicePath'] as String?, 21 | questionImagePath: json['questionImagePath'] as String?, 22 | outputImagePath: json['outputImagePath'] as String?, 23 | memorizedCount: (json['memorizedCount'] as num?)?.toInt() ?? 0, 24 | unmemorizedCount: (json['unmemorizedCount'] as num?)?.toInt() ?? 0, 25 | lastMemorized: json['lastMemorized'] as bool? ?? false, 26 | lastQuizAt: 27 | const TimestampConverter().fromJson(json['lastQuizAt'] as Timestamp), 28 | createdAt: 29 | const TimestampConverter().fromJson(json['createdAt'] as Timestamp), 30 | updatedAt: 31 | const TimestampConverter().fromJson(json['updatedAt'] as Timestamp), 32 | ); 33 | 34 | Map _$$MnemonicImplToJson(_$MnemonicImpl instance) => 35 | { 36 | 'id': instance.id, 37 | 'question': instance.question, 38 | 'answer': instance.answer, 39 | 'meaning': instance.meaning, 40 | 'episode': instance.episode, 41 | 'goroTexts': instance.goroTexts, 42 | 'voicePath': instance.voicePath, 43 | 'questionImagePath': instance.questionImagePath, 44 | 'outputImagePath': instance.outputImagePath, 45 | 'memorizedCount': instance.memorizedCount, 46 | 'unmemorizedCount': instance.unmemorizedCount, 47 | 'lastMemorized': instance.lastMemorized, 48 | 'lastQuizAt': const TimestampConverter().toJson(instance.lastQuizAt), 49 | 'createdAt': const TimestampConverter().toJson(instance.createdAt), 50 | 'updatedAt': const TimestampConverter().toJson(instance.updatedAt), 51 | }; 52 | -------------------------------------------------------------------------------- /frontend/lib/features/mnemonics/presentation/pages/mnemonics_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | import 'package:goronyan/core/constants/constants.dart'; 4 | import 'package:goronyan/core/presentation/widgets/bars/sliver_tab_bar.dart'; 5 | import 'package:goronyan/core/presentation/widgets/bars/sticky_bar_delegate.dart'; 6 | import 'package:goronyan/features/mnemonics/application/mnemonics_state.dart'; 7 | import 'package:goronyan/features/mnemonics/presentation/widgets/mnemonics_tab_view.dart'; 8 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 9 | 10 | const tabs = [ 11 | '覚えていない', 12 | 'すべて', 13 | ]; 14 | 15 | class MnemonicsPage extends HookConsumerWidget { 16 | const MnemonicsPage({super.key}); 17 | 18 | static const String pagePath = '/mnemonics'; 19 | 20 | @override 21 | Widget build(BuildContext context, WidgetRef ref) { 22 | final scrollController = useScrollController(); 23 | final tabController = useTabController( 24 | initialIndex: 0, 25 | initialLength: tabs.length, 26 | ); 27 | 28 | return SafeArea( 29 | child: Scaffold( 30 | body: NestedScrollView( 31 | floatHeaderSlivers: true, 32 | controller: scrollController, 33 | headerSliverBuilder: (context, innerBoxIsScrolled) { 34 | return [ 35 | const SliverAppBar(title: Text('記憶カード')), 36 | SliverOverlapAbsorber( 37 | handle: 38 | NestedScrollView.sliverOverlapAbsorberHandleFor(context), 39 | sliver: SliverPersistentHeader( 40 | pinned: true, 41 | delegate: StickyBarDelegate( 42 | child: SliverTabBar( 43 | tabController: tabController, 44 | tabs: tabs, 45 | ), 46 | ), 47 | ), 48 | ), 49 | ]; 50 | }, 51 | body: Padding( 52 | padding: const EdgeInsets.only(top: tabBarHeight), 53 | child: TabBarView( 54 | controller: tabController, 55 | children: const [ 56 | MnemonicsTabView(type: MenemonicsStateType.unmemorized), 57 | MnemonicsTabView(type: MenemonicsStateType.all), 58 | ], 59 | ), 60 | ), 61 | ), 62 | ), 63 | ); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /frontend/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/cards/mnemonic_list_card.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/core/constants/app_colors.dart'; 3 | import 'package:goronyan/core/extensions/color_extensions.dart'; 4 | import 'package:goronyan/core/presentation/widgets/bars/custom_divider.dart'; 5 | import 'package:goronyan/features/mnemonics/domain/entities/mnemonic.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | 8 | class MnemonicListCard extends ConsumerWidget { 9 | const MnemonicListCard({ 10 | super.key, 11 | required this.mnemonic, 12 | this.isDivider = false, 13 | required this.onTap, 14 | }); 15 | 16 | final Mnemonic mnemonic; 17 | final bool isDivider; 18 | final void Function() onTap; 19 | 20 | @override 21 | Widget build(BuildContext context, WidgetRef ref) { 22 | final theme = Theme.of(context); 23 | final totalMemorizedCount = 24 | mnemonic.memorizedCount + mnemonic.unmemorizedCount; 25 | 26 | return Column( 27 | children: [ 28 | ListTile( 29 | onTap: onTap, 30 | leading: Visibility( 31 | visible: totalMemorizedCount > 0, 32 | maintainSize: true, 33 | maintainAnimation: true, 34 | maintainState: true, 35 | child: Text( 36 | '${mnemonic.memorizedCount}/$totalMemorizedCount', 37 | overflow: TextOverflow.ellipsis, 38 | style: theme.textTheme.titleSmall?.copyWith( 39 | color: AppColors.textLightDark, 40 | ), 41 | ), 42 | ), 43 | title: Text( 44 | mnemonic.question ?? '', 45 | maxLines: 2, 46 | overflow: TextOverflow.ellipsis, 47 | style: theme.textTheme.bodySmall?.copyWith( 48 | color: AppColors.textLightDark, 49 | ), 50 | ), 51 | subtitle: Padding( 52 | padding: const EdgeInsets.only(top: 4), 53 | child: Text( 54 | mnemonic.answer, 55 | maxLines: 2, 56 | overflow: TextOverflow.ellipsis, 57 | style: theme.textTheme.titleSmall, 58 | ), 59 | ), 60 | trailing: Icon( 61 | Icons.keyboard_arrow_up, 62 | size: 24, 63 | color: AppColors.textLightDark.withDoubleOpacity(0.5), 64 | ), 65 | ), 66 | if (isDivider) const CustomDivider(), 67 | ], 68 | ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /frontend/lib/features/quiz/data/repositories/quiz_repository_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:goronyan/config/providers/firebase_providers.dart'; 3 | import 'package:goronyan/features/quiz/application/quiz_play_state.dart'; 4 | import 'package:goronyan/features/quiz/domain/entities/quiz_log.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 7 | 8 | part 'quiz_repository_provider.g.dart'; 9 | 10 | @riverpod 11 | QuizRepository quizRepository(Ref ref) => QuizRepository(ref); 12 | 13 | class QuizRepository { 14 | QuizRepository(this._ref); 15 | final Ref _ref; 16 | 17 | Future updateMemorizedCounts( 18 | String uid, 19 | QuizPlayState quizPlayState, 20 | ) async { 21 | final firestore = _ref.read(firebaseFirestoreProvider); 22 | 23 | final batch = firestore.batch(); 24 | 25 | final userDocRef = firestore.collection('users').doc(uid); 26 | batch.update(userDocRef, { 27 | 'xp': FieldValue.increment(quizPlayState.rewardPoint), 28 | 'updatedAt': FieldValue.serverTimestamp(), 29 | }); 30 | 31 | for (var i = 0; i < quizPlayState.mnemonics.length; i++) { 32 | final mnemonic = quizPlayState.mnemonics[i]; 33 | final memorizedPlay = quizPlayState.memorizeds[i]; 34 | 35 | final mnemonicsDocRef = firestore 36 | .collection('users') 37 | .doc(uid) 38 | .collection('mnemonics') 39 | .doc(mnemonic.id); 40 | 41 | final fieldName = 42 | memorizedPlay.isMemorized ? 'memorizedCount' : 'unmemorizedCount'; 43 | 44 | batch.update(mnemonicsDocRef, { 45 | fieldName: FieldValue.increment(1), 46 | 'lastMemorized': memorizedPlay.isMemorized, 47 | 'lastQuizAt': FieldValue.serverTimestamp(), 48 | 'updatedAt': FieldValue.serverTimestamp(), 49 | }); 50 | 51 | final quizLog = QuizLog( 52 | id: '', 53 | mnemonicId: mnemonic.id, 54 | isMemorized: memorizedPlay.isMemorized, 55 | createdAt: DateTime.now(), 56 | ); 57 | 58 | final quizLogDocData = quizLog.toJson(); 59 | quizLogDocData['createdAt'] = FieldValue.serverTimestamp(); 60 | quizLogDocData.remove('id'); 61 | 62 | final quizLogsDocRef = 63 | firestore.collection('users').doc(uid).collection('quizLogs').doc(); 64 | 65 | batch.set(quizLogsDocRef, quizLogDocData); 66 | } 67 | 68 | await batch.commit(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /frontend/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /frontend/lib/config/providers/firebase_providers.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | part of 'firebase_providers.dart'; 4 | 5 | // ************************************************************************** 6 | // RiverpodGenerator 7 | // ************************************************************************** 8 | 9 | String _$firebaseFirestoreHash() => r'963402713bf9b7cc1fb259d619d9b0184d4dcec1'; 10 | 11 | /// See also [firebaseFirestore]. 12 | @ProviderFor(firebaseFirestore) 13 | final firebaseFirestoreProvider = 14 | AutoDisposeProvider.internal( 15 | firebaseFirestore, 16 | name: r'firebaseFirestoreProvider', 17 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 18 | ? null 19 | : _$firebaseFirestoreHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | @Deprecated('Will be removed in 3.0. Use Ref instead') 25 | // ignore: unused_element 26 | typedef FirebaseFirestoreRef = AutoDisposeProviderRef; 27 | String _$firebaseAuthHash() => r'912368c3df3f72e4295bf7a8cda93b9c5749d923'; 28 | 29 | /// See also [firebaseAuth]. 30 | @ProviderFor(firebaseAuth) 31 | final firebaseAuthProvider = AutoDisposeProvider.internal( 32 | firebaseAuth, 33 | name: r'firebaseAuthProvider', 34 | debugGetCreateSourceHash: 35 | const bool.fromEnvironment('dart.vm.product') ? null : _$firebaseAuthHash, 36 | dependencies: null, 37 | allTransitiveDependencies: null, 38 | ); 39 | 40 | @Deprecated('Will be removed in 3.0. Use Ref instead') 41 | // ignore: unused_element 42 | typedef FirebaseAuthRef = AutoDisposeProviderRef; 43 | String _$firebaseStorageHash() => r'aa6946fd2a3470c4f3e2e72956076591cc63b435'; 44 | 45 | /// See also [firebaseStorage]. 46 | @ProviderFor(firebaseStorage) 47 | final firebaseStorageProvider = AutoDisposeProvider.internal( 48 | firebaseStorage, 49 | name: r'firebaseStorageProvider', 50 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 51 | ? null 52 | : _$firebaseStorageHash, 53 | dependencies: null, 54 | allTransitiveDependencies: null, 55 | ); 56 | 57 | @Deprecated('Will be removed in 3.0. Use Ref instead') 58 | // ignore: unused_element 59 | typedef FirebaseStorageRef = AutoDisposeProviderRef; 60 | // ignore_for_file: type=lint 61 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package 62 | -------------------------------------------------------------------------------- /backend/src/routes/genai.ts: -------------------------------------------------------------------------------- 1 | import type { HonoVariables } from "@/core/utils/types/hono.js"; 2 | import { Hono } from "hono"; 3 | import { fetchFileAsDataUri } from "../core/utils/firebase/fetchFileAsDataUri.js"; 4 | import { uploadImageAndGetPath } from "../core/utils/firebase/firebaseStorage.js"; 5 | import { 6 | answerDivisionFlowWithSchema, 7 | generateImageFlow, 8 | goroFlow, 9 | interestsFlow, 10 | meaningFlow, 11 | } from "../core/utils/genai/flow.js"; 12 | 13 | export const genaiRoute = new Hono<{ Variables: HonoVariables }>(); 14 | 15 | genaiRoute.post("/generate-mnemonic", async (c) => { 16 | try { 17 | const { audioPath, catLevel, interests } = await c.req.json<{ 18 | audioPath?: string; 19 | catLevel?: number; 20 | interests?: string; 21 | }>(); 22 | if (!audioPath) { 23 | return c.json({ error: "No audioPath provided" }, 400); 24 | } 25 | if (!catLevel) { 26 | return c.json({ error: "No catLevel provided" }, 400); 27 | } 28 | 29 | const userId = c.get("userId") as string | undefined; 30 | 31 | if (!userId) { 32 | return c.json({ error: "userId is required." }, 400); 33 | } 34 | console.info("Received userId in Hono:", userId); 35 | 36 | const audioBase64 = await fetchFileAsDataUri(audioPath); 37 | 38 | const { question, answer } = 39 | await answerDivisionFlowWithSchema(audioBase64); 40 | console.info("STEP1 (音声テキスト化) response:", { question, answer }); 41 | 42 | const meaning = await meaningFlow({ question, answer }); 43 | console.info("STEP2 (意味生成) response:", meaning); 44 | 45 | const episode = interests 46 | ? await interestsFlow({ question, answer, interests }) 47 | : null; 48 | console.info("STEP3 (意味生成) response:", episode); 49 | 50 | const goro = await goroFlow({ question, answer, catLevel }); 51 | console.info("STEP4 (ゴロ生成) response:", goro); 52 | 53 | const { url, contentType } = await generateImageFlow(meaning.english); 54 | console.info("STEP5 (画像生成) response:", url, contentType); 55 | 56 | const base64Data = url.replace(/^data:image\/\w+;base64,/, ""); 57 | const buffer = Buffer.from(base64Data, "base64"); 58 | const imagePath = await uploadImageAndGetPath(userId, buffer, contentType); 59 | console.info("STEP6 (画像保存) response:", imagePath); 60 | 61 | return c.json( 62 | { 63 | question, 64 | answer, 65 | meaning: meaning.japanese, 66 | episode, 67 | goroTexts: goro.goroTexts, 68 | imagePath, 69 | }, 70 | 200, 71 | ); 72 | } catch (error) { 73 | console.error("Error in /generate-mnemonic:", error); 74 | return c.json({ error: String(error) }, 500); 75 | } 76 | }); 77 | -------------------------------------------------------------------------------- /frontend/lib/core/presentation/widgets/modals/level_up_modal.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:goronyan/core/presentation/widgets/clippers/rounded_hexagon_clipper.dart'; 3 | 4 | class LevelUpModal extends StatelessWidget { 5 | const LevelUpModal({ 6 | super.key, 7 | required this.prevLevel, 8 | required this.nextLevel, 9 | required this.avatarURL, 10 | }); 11 | 12 | final int prevLevel; 13 | final int nextLevel; 14 | final String avatarURL; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final theme = Theme.of(context); 19 | 20 | return AlertDialog( 21 | title: Center( 22 | child: Text( 23 | 'Level Up!', 24 | style: theme.textTheme.headlineLarge, 25 | ), 26 | ), 27 | content: Column( 28 | mainAxisSize: MainAxisSize.min, 29 | children: [ 30 | Stack( 31 | alignment: Alignment.center, 32 | children: [ 33 | ClipPath( 34 | clipper: HexagonClipper(), 35 | child: Container( 36 | width: 100, 37 | height: 110, 38 | color: theme.colorScheme.primary, 39 | ), 40 | ), 41 | ClipPath( 42 | clipper: HexagonClipper(), 43 | child: Container( 44 | width: 80, 45 | height: 85, 46 | color: theme.colorScheme.secondary, 47 | child: Center( 48 | child: Text( 49 | '$nextLevel', 50 | style: theme.textTheme.displayLarge?.copyWith( 51 | color: Colors.white, 52 | ), 53 | ), 54 | ), 55 | ), 56 | ), 57 | ], 58 | ), 59 | const SizedBox(height: 16), 60 | SizedBox( 61 | width: 144, 62 | height: 144, 63 | child: Image.network( 64 | avatarURL, 65 | fit: BoxFit.contain, 66 | ), 67 | ), 68 | const SizedBox(height: 16), 69 | Text( 70 | 'レベルが $prevLevel から $nextLevel に上がったにゃ!', 71 | style: theme.textTheme.bodyMedium?.copyWith( 72 | fontWeight: FontWeight.bold, 73 | ), 74 | ), 75 | ], 76 | ), 77 | actions: [ 78 | TextButton( 79 | onPressed: () => Navigator.pop(context), 80 | child: const Text('とじる'), 81 | ), 82 | ], 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /frontend/android/app/src/firebase/dev/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "1005658646293", 4 | "project_id": "hackathon2024-4cb8e", 5 | "storage_bucket": "hackathon2024-4cb8e.firebasestorage.app" 6 | }, 7 | "client": [ 8 | { 9 | "client_info": { 10 | "mobilesdk_app_id": "1:1005658646293:android:39031e56f12b3ebb312b1c", 11 | "android_client_info": { 12 | "package_name": "com.goronyan.goronyan" 13 | } 14 | }, 15 | "oauth_client": [ 16 | { 17 | "client_id": "1005658646293-3ampka7v3mt5ee0pg7e7k281cnprnoip.apps.googleusercontent.com", 18 | "client_type": 3 19 | } 20 | ], 21 | "api_key": [ 22 | { 23 | "current_key": "AIzaSyBi38T55t7rWs9tmKEcUaBAlQNX29EiJdw" 24 | } 25 | ], 26 | "services": { 27 | "appinvite_service": { 28 | "other_platform_oauth_client": [ 29 | { 30 | "client_id": "1005658646293-3ampka7v3mt5ee0pg7e7k281cnprnoip.apps.googleusercontent.com", 31 | "client_type": 3 32 | }, 33 | { 34 | "client_id": "1005658646293-0fpp68ugq06jtcffuuduuvusaks6td19.apps.googleusercontent.com", 35 | "client_type": 2, 36 | "ios_info": { 37 | "bundle_id": "com.goronyan.goronyan" 38 | } 39 | } 40 | ] 41 | } 42 | } 43 | }, 44 | { 45 | "client_info": { 46 | "mobilesdk_app_id": "1:1005658646293:android:99fbdd9127e3bb9a312b1c", 47 | "android_client_info": { 48 | "package_name": "com.goronyan.goronyan.dev" 49 | } 50 | }, 51 | "oauth_client": [ 52 | { 53 | "client_id": "1005658646293-3ampka7v3mt5ee0pg7e7k281cnprnoip.apps.googleusercontent.com", 54 | "client_type": 3 55 | } 56 | ], 57 | "api_key": [ 58 | { 59 | "current_key": "AIzaSyBi38T55t7rWs9tmKEcUaBAlQNX29EiJdw" 60 | } 61 | ], 62 | "services": { 63 | "appinvite_service": { 64 | "other_platform_oauth_client": [ 65 | { 66 | "client_id": "1005658646293-3ampka7v3mt5ee0pg7e7k281cnprnoip.apps.googleusercontent.com", 67 | "client_type": 3 68 | }, 69 | { 70 | "client_id": "1005658646293-0fpp68ugq06jtcffuuduuvusaks6td19.apps.googleusercontent.com", 71 | "client_type": 2, 72 | "ios_info": { 73 | "bundle_id": "com.goronyan.goronyan" 74 | } 75 | } 76 | ] 77 | } 78 | } 79 | } 80 | ], 81 | "configuration_version": "1" 82 | } -------------------------------------------------------------------------------- /frontend/lib/config/router/codecs/extra_codec.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | 3 | import 'package:goronyan/features/mnemonics/domain/entities/mnemonic.dart'; 4 | import 'package:goronyan/features/mnemonics/domain/entities/mnemonic_detail_page_extra.dart'; 5 | 6 | class ExtraCodec extends Codec { 7 | const ExtraCodec(); 8 | 9 | @override 10 | Converter get decoder => const _ExtraDecoder(); 11 | 12 | @override 13 | Converter get encoder => const _ExtraEncoder(); 14 | } 15 | 16 | class _ExtraDecoder extends Converter { 17 | const _ExtraDecoder(); 18 | 19 | @override 20 | Object? convert(Object? input) { 21 | if (input == null) { 22 | return null; 23 | } 24 | 25 | final list = input as List; 26 | 27 | final typeName = list[0] as String?; 28 | if (typeName == null) { 29 | throw const FormatException('The first element (typeName) is null.'); 30 | } 31 | switch (typeName) { 32 | case 'MnemonicDetailPageExtra': 33 | final rawMnemonics = list[1] as List?; 34 | if (rawMnemonics == null) { 35 | throw const FormatException('rawMnemonics is null.'); 36 | } 37 | 38 | final mnemonics = rawMnemonics 39 | .map((item) => Mnemonic.fromJson(item as Map)) 40 | .toList(); 41 | 42 | final initialIndex = list[2] as int?; 43 | if (initialIndex == null) { 44 | throw const FormatException('initialIndex is null.'); 45 | } 46 | 47 | return MnemonicDetailPageExtra( 48 | mnemonics: mnemonics, 49 | initialIndex: initialIndex, 50 | ); 51 | case 'Mnemonic': 52 | final rawMnemonic = list[1] as Map?; 53 | if (rawMnemonic == null) { 54 | throw const FormatException('rawMnemonic is null.'); 55 | } 56 | 57 | return Mnemonic.fromJson(rawMnemonic); 58 | 59 | default: 60 | throw FormatException('Unable to parse input: $input'); 61 | } 62 | } 63 | } 64 | 65 | class _ExtraEncoder extends Converter { 66 | const _ExtraEncoder(); 67 | 68 | @override 69 | Object? convert(Object? input) { 70 | if (input == null) { 71 | return null; 72 | } 73 | if (input is MnemonicDetailPageExtra) { 74 | return [ 75 | 'MnemonicDetailPageExtra', 76 | input.mnemonics.map((m) => m.toJsonAsStringDates()).toList(), 77 | input.initialIndex, 78 | ]; 79 | } else if (input is Mnemonic) { 80 | return [ 81 | 'Mnemonic', 82 | input.toJsonAsStringDates(), 83 | ]; 84 | } 85 | 86 | throw FormatException('Cannot encode type ${input.runtimeType}'); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /frontend/lib/config/environment/firebase_options_prod.dart: -------------------------------------------------------------------------------- 1 | // File generated by FlutterFire CLI. 2 | // ignore_for_file: type=lint 3 | import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; 4 | import 'package:flutter/foundation.dart' 5 | show defaultTargetPlatform, kIsWeb, TargetPlatform; 6 | 7 | /// Default [FirebaseOptions] for use with your Firebase apps. 8 | /// 9 | /// Example: 10 | /// ```dart 11 | /// import 'firebase_options.dart'; 12 | /// // ... 13 | /// await Firebase.initializeApp( 14 | /// options: DefaultFirebaseOptions.currentPlatform, 15 | /// ); 16 | /// ``` 17 | class DefaultFirebaseOptions { 18 | static FirebaseOptions get currentPlatform { 19 | if (kIsWeb) { 20 | throw UnsupportedError( 21 | 'DefaultFirebaseOptions have not been configured for web - ' 22 | 'you can reconfigure this by running the FlutterFire CLI again.', 23 | ); 24 | } 25 | switch (defaultTargetPlatform) { 26 | case TargetPlatform.android: 27 | return android; 28 | case TargetPlatform.iOS: 29 | return ios; 30 | case TargetPlatform.macOS: 31 | throw UnsupportedError( 32 | 'DefaultFirebaseOptions have not been configured for macos - ' 33 | 'you can reconfigure this by running the FlutterFire CLI again.', 34 | ); 35 | case TargetPlatform.windows: 36 | throw UnsupportedError( 37 | 'DefaultFirebaseOptions have not been configured for windows - ' 38 | 'you can reconfigure this by running the FlutterFire CLI again.', 39 | ); 40 | case TargetPlatform.linux: 41 | throw UnsupportedError( 42 | 'DefaultFirebaseOptions have not been configured for linux - ' 43 | 'you can reconfigure this by running the FlutterFire CLI again.', 44 | ); 45 | default: 46 | throw UnsupportedError( 47 | 'DefaultFirebaseOptions are not supported for this platform.', 48 | ); 49 | } 50 | } 51 | 52 | static const FirebaseOptions android = FirebaseOptions( 53 | apiKey: 'AIzaSyBi38T55t7rWs9tmKEcUaBAlQNX29EiJdw', 54 | appId: '1:1005658646293:android:39031e56f12b3ebb312b1c', 55 | messagingSenderId: '1005658646293', 56 | projectId: 'hackathon2024-4cb8e', 57 | storageBucket: 'hackathon2024-4cb8e.firebasestorage.app', 58 | ); 59 | 60 | static const FirebaseOptions ios = FirebaseOptions( 61 | apiKey: 'AIzaSyD9CoPAWUje1WeSOz6UYDi88WxvI7oTXNw', 62 | appId: '1:1005658646293:ios:e61edacc45c2cd71312b1c', 63 | messagingSenderId: '1005658646293', 64 | projectId: 'hackathon2024-4cb8e', 65 | storageBucket: 'hackathon2024-4cb8e.firebasestorage.app', 66 | iosClientId: '1005658646293-0fpp68ugq06jtcffuuduuvusaks6td19.apps.googleusercontent.com', 67 | iosBundleId: 'com.goronyan.goronyan', 68 | ); 69 | 70 | } -------------------------------------------------------------------------------- /frontend/lib/config/environment/firebase_options_dev.dart: -------------------------------------------------------------------------------- 1 | // File generated by FlutterFire CLI. 2 | // ignore_for_file: type=lint 3 | import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; 4 | import 'package:flutter/foundation.dart' 5 | show defaultTargetPlatform, kIsWeb, TargetPlatform; 6 | 7 | /// Default [FirebaseOptions] for use with your Firebase apps. 8 | /// 9 | /// Example: 10 | /// ```dart 11 | /// import 'firebase_options.dart'; 12 | /// // ... 13 | /// await Firebase.initializeApp( 14 | /// options: DefaultFirebaseOptions.currentPlatform, 15 | /// ); 16 | /// ``` 17 | class DefaultFirebaseOptions { 18 | static FirebaseOptions get currentPlatform { 19 | if (kIsWeb) { 20 | throw UnsupportedError( 21 | 'DefaultFirebaseOptions have not been configured for web - ' 22 | 'you can reconfigure this by running the FlutterFire CLI again.', 23 | ); 24 | } 25 | switch (defaultTargetPlatform) { 26 | case TargetPlatform.android: 27 | return android; 28 | case TargetPlatform.iOS: 29 | return ios; 30 | case TargetPlatform.macOS: 31 | throw UnsupportedError( 32 | 'DefaultFirebaseOptions have not been configured for macos - ' 33 | 'you can reconfigure this by running the FlutterFire CLI again.', 34 | ); 35 | case TargetPlatform.windows: 36 | throw UnsupportedError( 37 | 'DefaultFirebaseOptions have not been configured for windows - ' 38 | 'you can reconfigure this by running the FlutterFire CLI again.', 39 | ); 40 | case TargetPlatform.linux: 41 | throw UnsupportedError( 42 | 'DefaultFirebaseOptions have not been configured for linux - ' 43 | 'you can reconfigure this by running the FlutterFire CLI again.', 44 | ); 45 | default: 46 | throw UnsupportedError( 47 | 'DefaultFirebaseOptions are not supported for this platform.', 48 | ); 49 | } 50 | } 51 | 52 | static const FirebaseOptions android = FirebaseOptions( 53 | apiKey: 'AIzaSyBi38T55t7rWs9tmKEcUaBAlQNX29EiJdw', 54 | appId: '1:1005658646293:android:99fbdd9127e3bb9a312b1c', 55 | messagingSenderId: '1005658646293', 56 | projectId: 'hackathon2024-4cb8e', 57 | storageBucket: 'hackathon2024-4cb8e.firebasestorage.app', 58 | ); 59 | 60 | static const FirebaseOptions ios = FirebaseOptions( 61 | apiKey: 'AIzaSyD9CoPAWUje1WeSOz6UYDi88WxvI7oTXNw', 62 | appId: '1:1005658646293:ios:946756d7702b4ed8312b1c', 63 | messagingSenderId: '1005658646293', 64 | projectId: 'hackathon2024-4cb8e', 65 | storageBucket: 'hackathon2024-4cb8e.firebasestorage.app', 66 | iosClientId: '1005658646293-71oi1mr5mlsdm0bd8re8b6nqbl15ov6d.apps.googleusercontent.com', 67 | iosBundleId: 'com.goronyan.goronyan.dev', 68 | ); 69 | } 70 | -------------------------------------------------------------------------------- /frontend/ios/Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '13.0' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_ios_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 34 | 35 | pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => '11.6.0' 36 | 37 | target 'RunnerTests' do 38 | inherit! :search_paths 39 | end 40 | end 41 | 42 | post_install do |installer| 43 | installer.pods_project.targets.each do |target| 44 | flutter_additional_ios_build_settings(target) 45 | 46 | target.build_configurations.each do |config| 47 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [ 48 | '$(inherited)', 49 | ## dart: PermissionGroup.calendar 50 | # 'PERMISSION_EVENTS=1', 51 | 52 | ## dart: PermissionGroup.reminders 53 | # 'PERMISSION_REMINDERS=1', 54 | 55 | ## dart: PermissionGroup.contacts 56 | # 'PERMISSION_CONTACTS=1', 57 | 58 | ## dart: PermissionGroup.camera 59 | # 'PERMISSION_CAMERA=1', 60 | 61 | ## dart: PermissionGroup.microphone 62 | 'PERMISSION_MICROPHONE=1', 63 | 64 | ## dart: PermissionGroup.speech 65 | # 'PERMISSION_SPEECH_RECOGNIZER=1', 66 | 67 | ## dart: PermissionGroup.photos 68 | # 'PERMISSION_PHOTOS=1', 69 | 70 | ## dart: [PermissionGroup.location, PermissionGroup.locationAlways, PermissionGroup.locationWhenInUse] 71 | # 'PERMISSION_LOCATION=1', 72 | 73 | ## dart: PermissionGroup.notification 74 | # 'PERMISSION_NOTIFICATIONS=1', 75 | 76 | ## dart: PermissionGroup.mediaLibrary 77 | # 'PERMISSION_MEDIA_LIBRARY=1', 78 | 79 | ## dart: PermissionGroup.sensors 80 | # 'PERMISSION_SENSORS=1' 81 | ] 82 | end 83 | end 84 | end 85 | --------------------------------------------------------------------------------