├── 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