├── .gitignore
├── .metadata
├── Makefile
├── README.md
├── analysis_options.yaml
├── android
├── .gitignore
├── app
│ ├── build.gradle
│ └── src
│ │ ├── debug
│ │ └── AndroidManifest.xml
│ │ ├── main
│ │ ├── AndroidManifest.xml
│ │ ├── kotlin
│ │ │ └── com
│ │ │ │ └── example
│ │ │ │ └── zenn_ai_hackathon
│ │ │ │ └── MainActivity.kt
│ │ └── res
│ │ │ ├── drawable-v21
│ │ │ └── launch_background.xml
│ │ │ ├── drawable
│ │ │ └── launch_background.xml
│ │ │ ├── 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
│ │ │ ├── values-night
│ │ │ └── styles.xml
│ │ │ └── values
│ │ │ └── styles.xml
│ │ └── profile
│ │ └── AndroidManifest.xml
├── build.gradle
├── gradle.properties
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
└── settings.gradle
├── assets
├── .gitkeep
└── bannzai.programmer.png
├── firebase.json
├── functions
├── .env.sample
├── .eslintrc.js
├── .gitignore
├── package-lock.json
├── package.json
├── src
│ ├── entity
│ │ ├── grounding.ts
│ │ ├── location.ts
│ │ ├── response.ts
│ │ ├── task.ts
│ │ ├── todo.ts
│ │ ├── user.ts
│ │ ├── userRequest.ts
│ │ └── util
│ │ │ └── timestamp.ts
│ ├── functions
│ │ ├── fillLocation
│ │ │ ├── enqueue_task.ts
│ │ │ ├── execute_task.ts
│ │ │ ├── flow.ts
│ │ │ └── input.ts
│ │ ├── fillTODOLocation
│ │ │ ├── enqueue_task.ts
│ │ │ ├── execute_task.ts
│ │ │ ├── flow.ts
│ │ │ └── input.ts
│ │ ├── playground
│ │ │ └── flow.ts
│ │ ├── taskCreate
│ │ │ ├── enqueue_task.ts
│ │ │ ├── execute_task.ts
│ │ │ ├── flow.ts
│ │ │ └── input.ts
│ │ └── todoPrepare
│ │ │ ├── enqueue_task.ts
│ │ │ ├── execute_task.ts
│ │ │ ├── flow.ts
│ │ │ └── input.ts
│ ├── index.ts
│ └── utils
│ │ ├── ai
│ │ ├── ai.ts
│ │ └── authPolicy.ts
│ │ ├── error
│ │ ├── message.ts
│ │ └── taskRetry.ts
│ │ ├── firebase
│ │ ├── converter.ts
│ │ ├── firebase.ts
│ │ └── gcp.ts
│ │ ├── queryLocation.ts
│ │ └── stdlib
│ │ ├── nullable.ts
│ │ └── type_guard.ts
├── tsconfig.dev.json
└── tsconfig.json
├── ios
├── .gitignore
├── Firebase
│ └── .gitignore
├── Flutter
│ ├── AppFrameworkInfo.plist
│ ├── Debug.xcconfig
│ └── Release.xcconfig
├── Podfile
├── Podfile.lock
├── Runner.xcodeproj
│ ├── project.pbxproj
│ ├── project.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── IDEWorkspaceChecks.plist
│ │ │ └── WorkspaceSettings.xcsettings
│ └── xcshareddata
│ │ └── xcschemes
│ │ └── Runner.xcscheme
├── Runner.xcworkspace
│ ├── contents.xcworkspacedata
│ └── xcshareddata
│ │ ├── IDEWorkspaceChecks.plist
│ │ └── WorkspaceSettings.xcsettings
├── Runner
│ ├── AppDelegate.swift
│ ├── Assets.xcassets
│ │ ├── AppIcon.appiconset
│ │ │ ├── Contents.json
│ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ ├── 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-83.5x83.5@2x.png
│ │ └── LaunchImage.imageset
│ │ │ ├── Contents.json
│ │ │ ├── LaunchImage.png
│ │ │ ├── LaunchImage@2x.png
│ │ │ ├── LaunchImage@3x.png
│ │ │ └── README.md
│ ├── Base.lproj
│ │ ├── LaunchScreen.storyboard
│ │ └── Main.storyboard
│ ├── Info.plist
│ └── Runner-Bridging-Header.h
└── RunnerTests
│ └── RunnerTests.swift
├── lib
├── app.dart
├── components
│ ├── alert
│ │ ├── discard.dart
│ │ ├── help.dart
│ │ ├── image_picker.dart
│ │ └── ok.dart
│ ├── calendar
│ │ ├── components
│ │ │ └── submit_button.dart
│ │ └── form.dart
│ ├── error
│ │ └── error_alert.dart
│ ├── form
│ │ └── question_form.dart
│ ├── grounding_data
│ │ └── list.dart
│ ├── loading
│ │ ├── bot.dart
│ │ ├── indicator.dart
│ │ └── loading.dart
│ ├── location
│ │ └── form.dart
│ ├── retry
│ │ ├── button.dart
│ │ └── page.dart
│ ├── todo
│ │ ├── help.dart
│ │ └── list.dart
│ ├── todo_locations
│ │ ├── ask.dart
│ │ ├── list.dart
│ │ └── row.dart
│ └── todo_time_required
│ │ ├── functions.dart
│ │ └── row.dart
├── entity
│ ├── app_user.dart
│ ├── app_user.freezed.dart
│ ├── app_user.g.dart
│ ├── grounding_data.dart
│ ├── grounding_data.freezed.dart
│ ├── grounding_data.g.dart
│ ├── location.dart
│ ├── location.freezed.dart
│ ├── location.g.dart
│ ├── location_form.dart
│ ├── location_form.freezed.dart
│ ├── location_form.g.dart
│ ├── remote_config_parameter.dart
│ ├── remote_config_parameter.freezed.dart
│ ├── remote_config_parameter.g.dart
│ ├── task.dart
│ ├── task.freezed.dart
│ ├── task.g.dart
│ ├── timestamp.dart
│ ├── todo.dart
│ ├── todo.freezed.dart
│ └── todo.g.dart
├── features
│ ├── home
│ │ └── page.dart
│ ├── onboarding
│ │ ├── components
│ │ │ ├── body_1.dart
│ │ │ ├── body_2.dart
│ │ │ └── event.dart
│ │ ├── const.dart
│ │ ├── page.dart
│ │ └── resolver.dart
│ ├── root
│ │ ├── page.dart
│ │ └── resolver
│ │ │ ├── app_user_create.dart
│ │ │ ├── app_user_create.g.dart
│ │ │ ├── auth.dart
│ │ │ ├── auth.g.dart
│ │ │ ├── database.dart
│ │ │ ├── database.g.dart
│ │ │ └── force_update.dart
│ ├── task
│ │ ├── components
│ │ │ ├── location
│ │ │ │ ├── ask.dart
│ │ │ │ └── location.dart
│ │ │ └── time_required
│ │ │ │ └── todos.dart
│ │ └── page.dart
│ ├── tasks
│ │ ├── components
│ │ │ └── section.dart
│ │ └── page.dart
│ ├── todo
│ │ ├── components
│ │ │ └── memo.dart
│ │ └── page.dart
│ └── todo_locations
│ │ └── page.dart
├── main.dart
├── provider
│ ├── app_user.dart
│ ├── app_user.g.dart
│ ├── force_update.dart
│ ├── remote_config_parameter.dart
│ ├── remote_config_parameter.g.dart
│ ├── shared_preferences.dart
│ ├── shared_preferences.g.dart
│ ├── task.dart
│ ├── task.g.dart
│ ├── todo.dart
│ └── todo.g.dart
├── style
│ ├── button.dart
│ └── color.dart
└── utils
│ ├── analytics
│ ├── analytics.dart
│ └── error.dart
│ ├── config
│ ├── platform.dart
│ ├── remote_config.dart
│ └── version.dart
│ ├── date_time
│ ├── date_time_ext.dart
│ ├── date_time_range_ext.dart
│ └── weekday.dart
│ ├── format
│ └── time_of_day.dart
│ ├── functions
│ └── firebase_functions.dart
│ ├── image
│ └── image.dart
│ ├── log
│ └── debug_print.dart
│ ├── native
│ ├── method_channel.dart
│ └── text_field_context_menu.dart
│ ├── picker
│ ├── time.dart
│ └── toolbar.dart
│ ├── platform
│ └── environment.dart
│ ├── shared_preferences
│ └── keys.dart
│ └── storage
│ └── firebase_cloud_storage.dart
├── pubspec.lock
├── pubspec.yaml
└── test
└── widget_test.dart
/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 | migrate_working_dir/
12 |
13 | # IntelliJ related
14 | *.iml
15 | *.ipr
16 | *.iws
17 | .idea/
18 |
19 | # The .vscode folder contains launch configuration and tasks you configure in
20 | # VS Code which you may wish to be included in version control, so this line
21 | # is commented out by default.
22 | #.vscode/
23 |
24 | # Flutter/Dart/Pub related
25 | **/doc/api/
26 | **/ios/Flutter/.last_build_id
27 | .dart_tool/
28 | .flutter-plugins
29 | .flutter-plugins-dependencies
30 | .pub-cache/
31 | .pub/
32 | /build/
33 |
34 | # Symbolication related
35 | app.*.symbols
36 |
37 | # Obfuscation related
38 | app.*.map.json
39 |
40 | # Android Studio will place build artifacts here
41 | /android/app/debug
42 | /android/app/profile
43 | /android/app/release
44 |
45 | secret.dart
46 |
47 |
48 | #### Backend
49 |
50 | # Logs
51 | logs
52 | *.log
53 | npm-debug.log*
54 | yarn-debug.log*
55 | yarn-error.log*
56 | firebase-debug.log*
57 | firebase-debug.*.log*
58 |
59 | # Firebase cache
60 | .firebase/
61 |
62 | # Firebase config
63 |
64 | # Uncomment this if you'd like others to create their own Firebase project.
65 | # For a team working on the same Firebase project(s), it is recommended to leave
66 | # it commented so all members can deploy to the same project(s) in .firebaserc.
67 | # .firebaserc
68 |
69 | # Runtime data
70 | pids
71 | *.pid
72 | *.seed
73 | *.pid.lock
74 |
75 | # Directory for instrumented libs generated by jscoverage/JSCover
76 | lib-cov
77 |
78 | # Coverage directory used by tools like istanbul
79 | coverage
80 |
81 | # nyc test coverage
82 | .nyc_output
83 |
84 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
85 | .grunt
86 |
87 | # Bower dependency directory (https://bower.io/)
88 | bower_components
89 |
90 | # node-waf configuration
91 | .lock-wscript
92 |
93 | # Compiled binary addons (http://nodejs.org/api/addons.html)
94 | build/Release
95 |
96 | # Dependency directories
97 | node_modules/
98 |
99 | # Optional npm cache directory
100 | .npm
101 |
102 | # Optional eslint cache
103 | .eslintcache
104 |
105 | # Optional REPL history
106 | .node_repl_history
107 |
108 | # Output of 'npm pack'
109 | *.tgz
110 |
111 | # Yarn Integrity file
112 | .yarn-integrity
113 |
114 | # dotenv environment variables file
115 | .env
116 |
117 | # dataconnect generated files
118 | .dataconnect
119 |
120 | .firebaserc
121 |
122 | dist
123 | .genkit
124 |
125 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: secret
2 | secret:
3 | echo $(FILE_FIREBASE_IOS) | base64 -D > ios/Firebase/GoogleService-Info.plist
4 | echo $(FILE_FIREBASE_ANDROID) | base64 -D > android/app/google-services.json
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 注意点
2 | - 直前にモノレポにしたので、困ったらこっちのリポジトリでバックエンドを立ち上げてください。一応一回はdeployして動作確認してます
3 | * https://github.com/bannzai/zennAIHackathonBackend/
4 | - `TODOMaker Flutter App` と `TODOMaker Backend` とセットアップ方法が分かれています。すべてこのREADMEに記載されています
5 |
6 | ### 動作
7 | ref: https://zenn.dev/bannzai/articles/12d0443426876e#%E5%8B%95%E4%BD%9C%E3%81%95%E3%81%9B%E3%82%8B%E4%B8%8A%E3%81%A7%E3%81%AE%E6%B3%A8%E6%84%8F%E7%82%B9
8 | - Geminiのエラーで429 Too Many Requestが時たま発生します。Cloud Tasksを利用して並行にGeminiを使用しているので発生しやすくなります。リトライもある程度設定してますが、クライアント側の表示とCloud Tasksを利用しての非同期処理の進捗状況やエラー表示について同期を取る仕組みは用意していません。雑に「時間がかかったら停止してやり直してね」という機能は用意されているので5分とかかかってたらやり直してみてください。一番良いのはGeminiの制限を緩めるようにGoogleにお願いすることです
9 | - GeminiのJSON Modeで string | null の型を指定した場合に 「"null"」って文字が返ってくることがしばしばあります。AIの精度が上がれば解決するのでそのまま表示される場所もあったりします
10 | - 実機では全部動きます。Simulator,Emulatorだと動かなかったり、動作が微妙機能があります。Geo Coding, LocationやCalendarへの追加機能がありそこら辺です
11 |
12 |
13 | # TODOMaker Flutter App
14 | ## Setup
15 | 1. Firebaseプロジェクトを用意してください。バックエンドと一緒なプロジェクトにする必要があります。
16 | 2. iOS・Androidのプロジェクトを作成します。その際にiOS→GoogleService-Info.plist,Android→google-services.json を落としてください
17 | 3. 次の環境変数を用意してください。Makefileを参考に
18 | - FILE_FIREBASE_IOS: GoogleService-Info.plist を base64したもの
19 | - FILE_FIREBASE_ANDROID: google-services.json を base64したもの
20 | 4. `$ make secret` を実行します
21 | 5. 好きな方法でFlutterアプリを起動してください。著者はいつもVSCodeのFlutter extensionで起動しています。CLIだと `$ flutter run`だと思います。参考までに実際使っているVSCodeのlaunch.jsonを貼っておきます
22 |
23 | ```
24 | {
25 | // Use IntelliSense to learn about possible attributes.
26 | // Hover to view descriptions of existing attributes.
27 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
28 | "version": "0.2.0",
29 | "configurations": [
30 | {
31 | "name": "zenn_ai_hackathon",
32 | "request": "launch",
33 | "type": "dart"
34 | },
35 | {
36 | "name": "zenn_ai_hackathon (profile mode)",
37 | "request": "launch",
38 | "type": "dart",
39 | "flutterMode": "profile"
40 | },
41 | {
42 | "name": "zenn_ai_hackathon (release mode)",
43 | "request": "launch",
44 | "type": "dart",
45 | "flutterMode": "release"
46 | }
47 | ]
48 | }
49 |
50 | ```
51 |
52 | ## 実行時の注意点
53 | iOSの実機実行はApple Developerのチームに属する必要があったかもしれません(違うかもしれない)。実機実行する際は何かしらエラーが出た場合はその点を疑ってください。Androidに関しては全く覚えてないですが、何もしなくても`$ flutter run` や`$ flutter build appbundle --release` で作成したapkさえインストールできれば起動できる気がします
54 |
55 |
56 | # TODOMaker Backend
57 | ## Setup
58 | 1. node のバージョンはv22系です
59 | ```bash
60 | $ node --version
61 | v22.12.0
62 | ```
63 | 2. Firebaseプロジェクトを一つ作ってください
64 | 3. 次に `.firebaserc` を作成します。このリポジトリでは .gitignoreしています
65 |
66 | ```
67 | {
68 | "projects": {
69 | "default": "PROJECT_NAME" ← ここにプロジェクトネーム入れる
70 | }
71 | }
72 | ```
73 |
74 | ## Env
75 | See [.env.sample](./functions/.env.sample)
76 |
77 | ```
78 | # localhostで起動する場合は local を設定。あとはdev,prodどちらでも良い
79 | APP_ENV=dev
80 | # 多分ここから取得。https://aistudio.google.com/app/apikey?hl=ja
81 | GOOGLE_GENAI_API_KEY=
82 | # この命名のサービスアカウントがいるのでそれを使います。何必要かは忘れました(Cloud TasksのURLを取得だったかな)
83 | GOOGLE_APPLICATION_CREDENTIALS_SERVICE_ACCOUNT_ID=PROJECT_ID@appspot.gserviceaccount.com
84 | ```
85 |
86 | ## Development
87 | `Env` を用意します。ただ、localhostで動作確認する場合は APP_ENV=local に設定してください。authが無効になるのでdeploy時は気をつけてください
88 | `functions` ディレクトリに移動して `npm run genkit:start` をします
89 |
90 | `
91 | $ cd functions
92 | $ npm run genkit:start
93 | `
94 |
95 | http://localhost:4000/ からgenkitのWeb UIから動作確認できます
96 |
97 | ## Deploy
98 |
99 | functionsディレクトリに移動します。firebase deployをすればdeployされますが、./lib とかも消したい場合は以下のようにしています
100 |
101 | ```
102 | $ cd functions
103 | $ rm -rf ./lib && npm run build && firebase deploy --only functions
104 | ```
105 |
106 | また、IAMの権限の問題で何かが必要なことがあったかもしれません。なんの権限か忘れましたが、エラーメッセージに表示されると思います。必要があれば連絡ください
107 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | # This file configures the analyzer, which statically analyzes Dart code to
2 | # check for errors, warnings, and lints.
3 | #
4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled
5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
6 | # invoked from the command line by running `flutter analyze`.
7 |
8 | # The following line activates a set of recommended lints for Flutter apps,
9 | # packages, and plugins designed to encourage good coding practices.
10 | include: package:flutter_lints/flutter.yaml
11 |
12 | linter:
13 | # The lint rules applied to this project can be customized in the
14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml`
15 | # included above or to enable additional rules. A list of all available lints
16 | # and their documentation is published at
17 | # https://dart-lang.github.io/linter/lints/index.html.
18 | #
19 | # Instead of disabling a lint rule for the entire project in the
20 | # section below, it can also be suppressed for a single line of code
21 | # or a specific dart file by using the `// ignore: name_of_lint` and
22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file
23 | # producing the lint.
24 | rules:
25 | always_declare_return_types: true
26 | prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
27 | # avoid_print: false # Uncomment to disable the `avoid_print` rule
28 |
29 | # Additional information about this file can be found at
30 | # https://dart.dev/guides/language/analysis-options
31 | analyzer:
32 | plugins:
33 | - custom_lint
34 | errors:
35 | missing_required_param: error
36 | missing_return: error
37 | invalid_annotation_target: ignore
38 | exclude:
39 | - "lib/**/*.g.dart"
40 | - "lib/**/*.freezed.dart"
--------------------------------------------------------------------------------
/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 |
15 | google-services.json
16 |
--------------------------------------------------------------------------------
/android/app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id "com.android.application"
3 | id "kotlin-android"
4 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
5 | id "dev.flutter.flutter-gradle-plugin"
6 | id 'com.google.gms.google-services'
7 | }
8 |
9 | dependencies {
10 | // Import the Firebase BoM
11 | implementation platform('com.google.firebase:firebase-bom:33.9.0')
12 |
13 | // TODO: Add the dependencies for Firebase products you want to use
14 | // When using the BoM, don't specify versions in Firebase dependencies
15 | implementation 'com.google.firebase:firebase-analytics'
16 |
17 |
18 | // Add the dependencies for any other desired Firebase products
19 | // https://firebase.google.com/docs/android/setup#available-libraries
20 | }
21 |
22 | android {
23 | namespace = "com.bannzai.todomaker"
24 | compileSdk = 35
25 | ndkVersion = flutter.ndkVersion
26 |
27 | compileOptions {
28 | sourceCompatibility = JavaVersion.VERSION_1_8
29 | targetCompatibility = JavaVersion.VERSION_1_8
30 | }
31 |
32 | kotlinOptions {
33 | jvmTarget = JavaVersion.VERSION_1_8
34 | }
35 |
36 | defaultConfig {
37 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
38 | applicationId = "com.bannzai.todomaker"
39 | // You can update the following values to match your application needs.
40 | // For more information, see: https://flutter.dev/to/review-gradle-config.
41 | minSdk = 23
42 | targetSdk = 35
43 | versionCode = flutter.versionCode
44 | versionName = flutter.versionName
45 | }
46 |
47 | buildTypes {
48 | release {
49 | // TODO: Add your own signing config for the release build.
50 | // Signing with the debug keys for now, so `flutter run --release` works.
51 | signingConfig = signingConfigs.debug
52 | }
53 | }
54 | }
55 |
56 | flutter {
57 | source = "../.."
58 | }
59 |
--------------------------------------------------------------------------------
/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
14 |
23 |
27 |
31 |
32 |
33 |
34 |
35 |
36 |
38 |
41 |
42 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
--------------------------------------------------------------------------------
/android/app/src/main/kotlin/com/example/zenn_ai_hackathon/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.bannzai.todomaker
2 |
3 | import io.flutter.embedding.android.FlutterActivity
4 |
5 | class MainActivity: FlutterActivity()
6 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable-v21/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/android/app/src/main/res/values-night/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
2 | android.useAndroidX=true
3 | android.enableJetifier=true
4 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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.1.0" apply false
22 | id "org.jetbrains.kotlin.android" version "2.1.10" apply false
23 | id 'com.google.gms.google-services' version '4.4.2' apply false
24 | }
25 |
26 | include ":app"
27 |
--------------------------------------------------------------------------------
/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/assets/.gitkeep
--------------------------------------------------------------------------------
/assets/bannzai.programmer.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/assets/bannzai.programmer.png
--------------------------------------------------------------------------------
/firebase.json:
--------------------------------------------------------------------------------
1 | {
2 | "functions": [
3 | {
4 | "source": "functions",
5 | "codebase": "default",
6 | "predeploy": [
7 | "npm --prefix \"$RESOURCE_DIR\" run lint",
8 | "npm --prefix \"$RESOURCE_DIR\" run build"
9 | ]
10 | }
11 | ]
12 | }
13 |
--------------------------------------------------------------------------------
/functions/.env.sample:
--------------------------------------------------------------------------------
1 |
2 | # localhostで起動する場合は local を設定。あとはdev,prodどちらでも良い
3 | APP_ENV=dev
4 | # 多分ここから取得。https://aistudio.google.com/app/apikey?hl=ja
5 | GOOGLE_GENAI_API_KEY=
6 | # この命名のサービスアカウントがいるのでそれを使います。何必要かは忘れました(Cloud TasksのURLを取得だったかな)
7 | GOOGLE_APPLICATION_CREDENTIALS_SERVICE_ACCOUNT_ID=PROJECT_ID@appspot.gserviceaccount.com
8 |
--------------------------------------------------------------------------------
/functions/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | node: true,
5 | es2021: true,
6 | },
7 | extends: [
8 | "eslint:recommended",
9 | "plugin:@typescript-eslint/recommended",
10 | "prettier",
11 | ],
12 | parser: "@typescript-eslint/parser",
13 | parserOptions: {
14 | ecmaVersion: "latest",
15 | },
16 | plugins: ["@typescript-eslint"],
17 | ignorePatterns: ["*.test.ts", "*.test.js", "lib"],
18 | rules: {
19 | "@typescript-eslint/no-explicit-any": "off",
20 | },
21 | };
22 |
--------------------------------------------------------------------------------
/functions/.gitignore:
--------------------------------------------------------------------------------
1 | # Compiled JavaScript files
2 | lib/**/*.js
3 | lib/**/*.js.map
4 |
5 | # TypeScript v1 declaration files
6 | typings/
7 |
8 | # Node.js dependency directory
9 | node_modules/
10 | *.local
11 |
12 | service-account.json
13 |
--------------------------------------------------------------------------------
/functions/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "main": "lib/index.js",
3 | "scripts": {
4 | "genkit:start": "genkit start -- tsx --watch src/index.ts",
5 | "lint": "eslint --ext .js,.ts .",
6 | "lint:fix": "eslint --ext .js,.ts . --fix",
7 | "format": "prettier --write .",
8 | "build": "tsc",
9 | "build:watch": "tsc --watch",
10 | "serve": "npm run build && firebase emulators:start --only functions",
11 | "shell": "npm run build && firebase functions:shell",
12 | "start": "npm run shell",
13 | "deploy": "firebase deploy --only functions",
14 | "logs": "firebase functions:log"
15 | },
16 | "name": "functions",
17 | "engines": {
18 | "node": "22"
19 | },
20 | "dependencies": {
21 | "@genkit-ai/ai": "^0.9.12",
22 | "@genkit-ai/firebase": "^0.9.12",
23 | "@genkit-ai/flow": "^0.5.17",
24 | "@genkit-ai/vertexai": "^0.9.12",
25 | "@google/generative-ai": "^0.21.0",
26 | "@types/express": "^5.0.0",
27 | "express": "^4.21.2",
28 | "firebase-admin": "^13.0.2",
29 | "firebase-functions": "^6.0.1",
30 | "genkit": "^0.9.12",
31 | "google-auth-library": "^9.15.1",
32 | "uuid": "^11.0.5",
33 | "zod": "^3.24.1",
34 | "zod-to-json-schema": "^3.24.1"
35 | },
36 | "devDependencies": {
37 | "@typescript-eslint/eslint-plugin": "^5.12.0",
38 | "@typescript-eslint/parser": "^5.12.0",
39 | "eslint": "^8.9.0",
40 | "eslint-config-google": "^0.14.0",
41 | "eslint-config-prettier": "^10.0.1",
42 | "eslint-plugin-import": "^2.25.4",
43 | "eslint-plugin-prettier": "^5.2.3",
44 | "firebase-functions-test": "^3.1.0",
45 | "genkit-cli": "^0.9.12",
46 | "prettier": "^3.4.2",
47 | "tsx": "^4.19.2",
48 | "typescript": "^5.7.3"
49 | },
50 | "private": true
51 | }
52 |
--------------------------------------------------------------------------------
/functions/src/entity/grounding.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const GroundingDataSchema = z.object({
4 | url: z.string().optional(),
5 | title: z.string().optional(),
6 | index: z.number().optional(),
7 | });
8 |
9 | export type GroundingData = z.infer;
10 |
--------------------------------------------------------------------------------
/functions/src/entity/location.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const LocationSchema = z.object({
4 | name: z.string().describe("場所の名称"),
5 | postalCode: z.string().nullable().describe("郵便番号"),
6 | address: z.string().nullable().describe("住所。郵便番号を除く"),
7 | tel: z.string().nullable().describe("電話番号"),
8 | email: z.string().nullable().describe("メールアドレス"),
9 | });
10 |
--------------------------------------------------------------------------------
/functions/src/entity/response.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 | import { errorMessage } from "../utils/error/message";
3 |
4 | export const DataResponseSchema = z.object({
5 | result: z.literal("OK"),
6 | statusCode: z.number().optional(),
7 | data: z.unknown(),
8 | });
9 |
10 | export const ErrorResponseSchema = z.object({
11 | result: z.literal("NG"),
12 | statusCode: z.number().optional(),
13 | error: z.object({
14 | message: z.string(),
15 | }),
16 | });
17 |
18 | export const AppResponseSchema = z.union([
19 | DataResponseSchema,
20 | ErrorResponseSchema,
21 | ]);
22 |
23 | export type DataResponse = z.infer;
24 | export type ErrorResponse = z.infer;
25 | export type AppResponse = z.infer;
26 |
27 | export function errorResponse(error: unknown): ErrorResponse {
28 | return {
29 | result: "NG",
30 | statusCode: 500,
31 | error: { message: errorMessage(error) },
32 | };
33 | }
34 |
--------------------------------------------------------------------------------
/functions/src/entity/task.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { GroundingDataSchema } from "./grounding";
3 | import {
4 | FirestoreTimestampSchema,
5 | ServerTimestampSchema,
6 | } from "./util/timestamp";
7 | import { nullable } from "../utils/stdlib/nullable";
8 | import { LocationSchema } from "./location";
9 |
10 | export const TaskPreparedSchema = z
11 | .object({
12 | status: z.literal("prepared"),
13 | id: z.string(),
14 | userID: z.string(),
15 | // 質問の内容
16 | question: z.string(),
17 | // TODOの質問の内容の回答をAIに渡して、AIが回答した内容
18 | todosAITextResponseMarkdown: z.string(),
19 | // TODOのAIの回答のソースとなったもの
20 | todosGroundings: z.array(GroundingDataSchema),
21 | // 質問の内容を短く回答したもの
22 | shortAnswer: z.string(),
23 | // 質問の内容の対象となるトピック。例) question: 「確定申告の方法」だと「確定申告」
24 | topic: z.string(),
25 | // 質問の内容の対象となるトピックについての解説
26 | definitionAITextResponse: z.string(),
27 | // TODOのAIの回答のソースとなったもの
28 | definitionGroundings: z.array(GroundingDataSchema),
29 | completed: z.boolean(),
30 | // タスクの代表的な場所
31 | // 未処理の場合はnull。処理完了の場合は空配列の可能性がある
32 | locations: z.array(LocationSchema).nullish(),
33 | locationsAITextResponse: z.string().nullish(),
34 | locationsGroundings: z.array(GroundingDataSchema).nullish(),
35 |
36 | preparedDateTime: FirestoreTimestampSchema,
37 | })
38 | .merge(ServerTimestampSchema);
39 |
40 | export const TaskPreparingSchema = nullable(TaskPreparedSchema)
41 | .partial()
42 | .required({
43 | id: true,
44 | userID: true,
45 | question: true,
46 | })
47 | .omit({
48 | preparedDateTime: true,
49 | completed: true,
50 | })
51 | .merge(
52 | z.object({
53 | status: z.literal("preparing"),
54 | })
55 | );
56 | export const TaskSchema = z.union([TaskPreparedSchema, TaskPreparingSchema]);
57 |
58 | export type Task = z.infer;
59 | export type TaskPrepared = z.infer;
60 | export type TaskPreparing = z.infer;
61 |
--------------------------------------------------------------------------------
/functions/src/entity/todo.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { GroundingDataSchema } from "./grounding";
3 | import { ServerTimestampSchema } from "./util/timestamp";
4 | import { LocationSchema } from "./location";
5 |
6 | export const TODOSchema = z
7 | .object({
8 | id: z.string(),
9 | userID: z.string(),
10 | taskID: z.string(),
11 | content: z.string().describe("TODOの内容"),
12 | supplement: z.string().describe("補足情報"),
13 | aiTextResponseMarkdown: z.string().nullable(),
14 | groundings: z.array(GroundingDataSchema).nullable(),
15 | // TODOの代表的な場所
16 | // 未処理の場合はnull。処理完了の場合は空配列の可能性がある
17 | locations: z.array(LocationSchema).nullable(),
18 | locationsAITextResponse: z.string().nullable(),
19 | locationsGroundings: z.array(GroundingDataSchema).nullable(),
20 |
21 | // 所要時間(秒)
22 | timeRequired: z.number().describe("所要時間(秒)").nullish(),
23 | timeRequiredAITextResponse: z.string().nullish(),
24 | timeRequiredGroundings: z.array(GroundingDataSchema).nullish(),
25 | })
26 | .merge(ServerTimestampSchema);
27 |
28 | export type TODO = z.infer;
29 |
--------------------------------------------------------------------------------
/functions/src/entity/user.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | // Firestore schema definitions using zod
4 | export const UserSchema = z.object({
5 | uid: z.string(),
6 | createdAt: z.string().datetime(),
7 | lastLogin: z.string().datetime(),
8 | });
9 |
10 | export type User = z.infer;
--------------------------------------------------------------------------------
/functions/src/entity/userRequest.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 |
3 | export const UserRequestSchema = z.object({
4 | userID: z.string(),
5 | });
6 |
7 | export type UserRequest = z.infer;
8 |
--------------------------------------------------------------------------------
/functions/src/entity/util/timestamp.ts:
--------------------------------------------------------------------------------
1 | import { Timestamp } from "firebase-admin/firestore";
2 | import { z } from "zod";
3 |
4 | export const FirestoreTimestampSchema = z.object({
5 | seconds: z.number(),
6 | nanoseconds: z.number(),
7 | });
8 |
9 | export type FirestoreTimestamp = z.infer;
10 |
11 | // 動作未確認
12 | export function rawFirsetoreTimestamp(
13 | timestamp: FirestoreTimestamp
14 | ): Timestamp {
15 | return new Timestamp(timestamp.seconds, timestamp.nanoseconds);
16 | }
17 |
18 | // NOTE: Firestore の Timestamp を JSON に変換する。z.instanceOf(Timestamp)をそのままスキーマとして使い、JSONで返却すると { _seconds, _nanoseconds } という形式になるため、この関数を使って変換する
19 | export function firestoreTimestampJSON(
20 | timestamp: Timestamp
21 | ): FirestoreTimestamp {
22 | return {
23 | seconds: timestamp.seconds,
24 | nanoseconds: timestamp.nanoseconds,
25 | };
26 | }
27 |
28 | export const ServerTimestampSchema = z.object({
29 | serverCreatedDateTime: FirestoreTimestampSchema,
30 | serverUpdatedDateTime: FirestoreTimestampSchema,
31 | });
32 |
33 | export type ServerTimestamp = z.infer;
34 |
--------------------------------------------------------------------------------
/functions/src/functions/fillLocation/enqueue_task.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { FillLocationSchema } from "./input";
3 | import { DataResponseSchema, ErrorResponseSchema } from "../../entity/response";
4 | import { getFunctions } from "firebase-admin/functions";
5 | import { getFunctionURL } from "../../utils/firebase/gcp";
6 | import { onFlow } from "@genkit-ai/firebase/functions";
7 | import { genkitAI } from "../../utils/ai/ai";
8 | import { appAuthPolicy } from "../../utils/ai/authPolicy";
9 | import { enqueueFillTODOLocations } from "../fillTODOLocation/enqueue_task";
10 |
11 | const ResponseSchema = z.union([
12 | DataResponseSchema.extend({
13 | data: z.object({
14 | taskID: z.string(),
15 | }),
16 | }),
17 | ErrorResponseSchema,
18 | ]);
19 |
20 | export const enqueueFillLocation = onFlow(
21 | genkitAI,
22 | {
23 | name: "enqueueFillLocation",
24 | inputSchema: FillLocationSchema,
25 | outputSchema: ResponseSchema,
26 | authPolicy: appAuthPolicy("enqueueFillLocation"),
27 | },
28 | async (input) => {
29 | const queue = getFunctions().taskQueue("executeFillLocation");
30 | const executeFillLocationURL = await getFunctionURL("executeFillLocation");
31 | await queue.enqueue(input, {
32 | uri: executeFillLocationURL,
33 | headers: {
34 | "Content-Type": "application/json",
35 | },
36 | });
37 |
38 | await enqueueFillTODOLocations(input);
39 |
40 | const response: z.infer = {
41 | result: "OK",
42 | statusCode: 200,
43 | data: {
44 | taskID: input.taskID,
45 | },
46 | };
47 |
48 | return response;
49 | }
50 | );
51 |
--------------------------------------------------------------------------------
/functions/src/functions/fillLocation/execute_task.ts:
--------------------------------------------------------------------------------
1 | import * as functions from "firebase-functions";
2 | import { z } from "zod";
3 | import { onTaskDispatched } from "firebase-functions/tasks";
4 | import { fillTaskLocation } from "./flow";
5 | import { FillLocationSchema } from "./input";
6 | import { errorMessage } from "../../utils/error/message";
7 | import { TaskRetryError } from "../../utils/error/taskRetry";
8 |
9 | export const executeFillLocation = onTaskDispatched(
10 | {
11 | retryConfig: {
12 | maxAttempts: 10,
13 | minBackoffSeconds: 60,
14 | },
15 | rateLimits: {
16 | maxConcurrentDispatches: 3,
17 | },
18 | timeoutSeconds: 10 * 60,
19 | memory: "1GiB",
20 | },
21 | async (req) => {
22 | console.log("#executeFillTaskLocation");
23 | try {
24 | const input = req.data as z.infer;
25 | const fillTaskLocationResponse = await fillTaskLocation(input);
26 | console.log({ fillTaskLocationResponse });
27 | } catch (err) {
28 | functions.logger.error(errorMessage(err));
29 | if (err instanceof TaskRetryError) {
30 | throw err;
31 | }
32 | }
33 | }
34 | );
35 |
--------------------------------------------------------------------------------
/functions/src/functions/fillLocation/flow.ts:
--------------------------------------------------------------------------------
1 | import { z } from "genkit";
2 | import { genkitAI } from "../../utils/ai/ai";
3 | import { database } from "../../utils/firebase/firebase";
4 | import {
5 | DataResponseSchema,
6 | errorResponse,
7 | ErrorResponseSchema,
8 | } from "../../entity/response";
9 | import { GroundingDataSchema } from "../../entity/grounding";
10 | import { zodTypeGuard } from "../../utils/stdlib/type_guard";
11 | import { FillLocationSchema } from "./input";
12 | import { TaskPreparedSchema } from "../../entity/task";
13 | import { LocationSchema } from "../../entity/location";
14 | import { queryLocation } from "../../utils/queryLocation";
15 | import { TaskRetryError } from "../../utils/error/taskRetry";
16 | import { errorMessage } from "../../utils/error/message";
17 |
18 | const TaskResponseSchema = TaskPreparedSchema.extend({
19 | locations: z.array(LocationSchema),
20 | locationsAITextResponse: z.string(),
21 | locationsGroundings: z.array(GroundingDataSchema),
22 | });
23 | const ResponseSchema = z.union([
24 | DataResponseSchema.extend({
25 | data: z.object({
26 | task: TaskResponseSchema,
27 | }),
28 | }),
29 | ErrorResponseSchema,
30 | ]);
31 |
32 | // このflowはCloud Taskから使用されるのでエラーハンドリングは慎重に
33 | export const fillTaskLocation = genkitAI.defineFlow(
34 | {
35 | name: "fillTaskLocation",
36 | inputSchema: FillLocationSchema,
37 | outputSchema: ResponseSchema,
38 | },
39 | async (input) => {
40 | console.log(`#fillTaskLocation: ${JSON.stringify({ input }, null, 2)}`);
41 |
42 | try {
43 | const {
44 | taskID,
45 | userLocation,
46 | userRequest: { userID },
47 | } = input;
48 |
49 | const taskDocRef = database
50 | .collection(`/users/${userID}/tasks`)
51 | .doc(taskID);
52 | const taskSnapshot = await taskDocRef.get();
53 | const task = taskSnapshot.data();
54 | if (!zodTypeGuard(TaskPreparedSchema, task)) {
55 | return errorResponse(
56 | new Error(`task prepared parse error. taskPrepared: ${task}`)
57 | );
58 | }
59 |
60 | // e.g) 杉並区成田東4丁目周辺 確定申告に関しての問い合わせ先を教えてください「申告方法の選択: e-Taxを利用するか、郵送または税務署への持参するか決定します
61 | const { aiTextResponse, groundings, locations } = await queryLocation({
62 | query: `${userLocation.name}周辺 ${task.question}に関しての問い合わせ先を教えてください`,
63 | }).catch((err) => {
64 | throw new TaskRetryError(`queryLocation failed. ${errorMessage(err)}`);
65 | });
66 |
67 | const taskUpdate: z.infer = {
68 | ...task,
69 | locations: locations,
70 | locationsAITextResponse: aiTextResponse,
71 | locationsGroundings: groundings,
72 | };
73 |
74 | await taskDocRef.set(taskUpdate, { merge: true });
75 |
76 | const response: z.infer = {
77 | result: "OK",
78 | statusCode: 200,
79 | data: {
80 | task: taskUpdate,
81 | },
82 | };
83 |
84 | return response;
85 | } catch (error) {
86 | if (error instanceof TaskRetryError) {
87 | throw error;
88 | }
89 | return errorResponse(error);
90 | }
91 | }
92 | );
93 |
--------------------------------------------------------------------------------
/functions/src/functions/fillLocation/input.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { UserRequestSchema } from "../../entity/userRequest";
3 |
4 | export const FillLocationSchema = z.object({
5 | taskID: z.string(),
6 | userLocation: z.object({
7 | name: z.string(),
8 | }),
9 | userRequest: UserRequestSchema,
10 | });
11 |
--------------------------------------------------------------------------------
/functions/src/functions/fillTODOLocation/enqueue_task.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { FillTODOLocationSchema } from "./input";
3 | import { DataResponseSchema, ErrorResponseSchema } from "../../entity/response";
4 | import { getFunctions } from "firebase-admin/functions";
5 | import { getFunctionURL } from "../../utils/firebase/gcp";
6 | import { onFlow } from "@genkit-ai/firebase/functions";
7 | import { genkitAI } from "../../utils/ai/ai";
8 | import { appAuthPolicy } from "../../utils/ai/authPolicy";
9 | import { database } from "../../utils/firebase/firebase";
10 | import { TODOSchema } from "../../entity/todo";
11 | import { zodTypeGuard } from "../../utils/stdlib/type_guard";
12 | import { FillLocationSchema } from "../fillLocation/input";
13 |
14 | const ResponseSchema = z.union([
15 | DataResponseSchema.extend({
16 | data: z.object({
17 | taskID: z.string(),
18 | }),
19 | }),
20 | ErrorResponseSchema,
21 | ]);
22 |
23 | export const enqueueFillTODOLocation = onFlow(
24 | genkitAI,
25 | {
26 | name: "enqueueFillTODOLocation",
27 | inputSchema: FillTODOLocationSchema,
28 | outputSchema: ResponseSchema,
29 | authPolicy: appAuthPolicy("enqueueFillTODOLocation"),
30 | },
31 | async (input) => {
32 | const queue = getFunctions().taskQueue("executeFillTODOLocation");
33 | const executeFillTODOLocationURL = await getFunctionURL(
34 | "executeFillTODOLocation"
35 | );
36 | await queue.enqueue(input, {
37 | uri: executeFillTODOLocationURL,
38 | headers: {
39 | "Content-Type": "application/json",
40 | },
41 | });
42 |
43 | const response: z.infer = {
44 | result: "OK",
45 | statusCode: 200,
46 | data: {
47 | taskID: input.taskID,
48 | },
49 | };
50 |
51 | return response;
52 | }
53 | );
54 |
55 | export const enqueueFillTODOLocations = genkitAI.defineTool(
56 | {
57 | name: "enqueueFillTODOLocations",
58 | description: "enqueueFillTODOLocations",
59 | inputSchema: FillLocationSchema,
60 | outputSchema: ResponseSchema,
61 | },
62 | async (input) => {
63 | const queue = getFunctions().taskQueue("executeFillTODOLocation");
64 | const executeFillTODOLocationURL = await getFunctionURL(
65 | "executeFillTODOLocation"
66 | );
67 |
68 | const todosCollectionRef = database.collection(
69 | `/users/${input.userRequest.userID}/tasks/${input.taskID}/todos`
70 | );
71 | const todoDocRefs = await todosCollectionRef.listDocuments();
72 | for (const todoDocRef of todoDocRefs) {
73 | const todoDoc = await todoDocRef.get();
74 | const todo = todoDoc.data();
75 | if (!zodTypeGuard(TODOSchema, todo)) {
76 | continue;
77 | }
78 |
79 | const taskInput: z.infer = {
80 | ...input,
81 | todoID: todo.id,
82 | };
83 | await queue.enqueue(taskInput, {
84 | uri: executeFillTODOLocationURL,
85 | headers: {
86 | "Content-Type": "application/json",
87 | },
88 | });
89 | }
90 |
91 | const response: z.infer = {
92 | result: "OK",
93 | statusCode: 200,
94 | data: {
95 | taskID: input.taskID,
96 | },
97 | };
98 |
99 | return response;
100 | }
101 | );
102 |
--------------------------------------------------------------------------------
/functions/src/functions/fillTODOLocation/execute_task.ts:
--------------------------------------------------------------------------------
1 | import * as functions from "firebase-functions";
2 | import { z } from "zod";
3 | import { onTaskDispatched } from "firebase-functions/tasks";
4 | import { errorMessage } from "../../utils/error/message";
5 | import { FillTODOLocationSchema } from "../fillTODOLocation/input";
6 | import { fillTODOLocation } from "../fillTODOLocation/flow";
7 | import { TaskRetryError } from "../../utils/error/taskRetry";
8 |
9 | export const executeFillTODOLocation = onTaskDispatched(
10 | {
11 | retryConfig: {
12 | maxAttempts: 10,
13 | minBackoffSeconds: 60,
14 | },
15 | rateLimits: {
16 | maxConcurrentDispatches: 3,
17 | },
18 | timeoutSeconds: 10 * 60,
19 | memory: "1GiB",
20 | },
21 | async (req) => {
22 | console.log("#executeFillTODOLocation");
23 | try {
24 | const input = req.data as z.infer;
25 | const fillTODOLocationResponse = await fillTODOLocation(input);
26 | console.log({ fillTODOLocationResponse });
27 | } catch (err) {
28 | functions.logger.error(errorMessage(err));
29 | if (err instanceof TaskRetryError) {
30 | throw err;
31 | }
32 | }
33 | }
34 | );
35 |
--------------------------------------------------------------------------------
/functions/src/functions/fillTODOLocation/flow.ts:
--------------------------------------------------------------------------------
1 | import { z } from "genkit";
2 | import { genkitAI } from "../../utils/ai/ai";
3 | import { TODOSchema } from "../../entity/todo";
4 | import { database } from "../../utils/firebase/firebase";
5 | import {
6 | DataResponseSchema,
7 | errorResponse,
8 | ErrorResponseSchema,
9 | } from "../../entity/response";
10 | import { GroundingDataSchema } from "../../entity/grounding";
11 | import { zodTypeGuard } from "../../utils/stdlib/type_guard";
12 | import { TaskPreparedSchema } from "../../entity/task";
13 | import { LocationSchema } from "../../entity/location";
14 | import { queryLocation } from "../../utils/queryLocation";
15 | import { FillTODOLocationSchema } from "./input";
16 | import { TaskRetryError } from "../../utils/error/taskRetry";
17 | import { errorMessage } from "../../utils/error/message";
18 |
19 | const TODOResponseSchema = TODOSchema.extend({
20 | locations: z.array(LocationSchema),
21 | locationsAITextResponse: z.string(),
22 | locationsGroundings: z.array(GroundingDataSchema),
23 | });
24 | const ResponseSchema = z.union([
25 | DataResponseSchema.extend({
26 | data: z.object({
27 | todo: TODOResponseSchema,
28 | }),
29 | }),
30 | ErrorResponseSchema,
31 | ]);
32 |
33 | // このflowはCloud Taskから使用されるのでエラーハンドリングは慎重に
34 | export const fillTODOLocation = genkitAI.defineFlow(
35 | {
36 | name: "fillTODOLocation",
37 | inputSchema: FillTODOLocationSchema,
38 | outputSchema: ResponseSchema,
39 | },
40 | async (input) => {
41 | console.log(`#fillTODOLocation: ${JSON.stringify({ input }, null, 2)}`);
42 | try {
43 | const {
44 | taskID,
45 | todoID,
46 | userLocation,
47 | userRequest: { userID },
48 | } = input;
49 |
50 | const taskDocRef = database
51 | .collection(`/users/${userID}/tasks`)
52 | .doc(taskID);
53 | const taskSnapshot = await taskDocRef.get();
54 | const task = taskSnapshot.data();
55 | if (!zodTypeGuard(TaskPreparedSchema, task)) {
56 | return errorResponse(
57 | new Error(`task prepared parse error. taskPrepared: ${task}`)
58 | );
59 | }
60 |
61 | const todoDocRef = await database.doc(
62 | `/users/${userID}/tasks/${taskID}/todos/${todoID}`
63 | );
64 | const todoDocSnapshot = await todoDocRef.get();
65 | const todo = todoDocSnapshot.data();
66 | if (!zodTypeGuard(TODOSchema, todo)) {
67 | return errorResponse(
68 | new Error(`todo loading parse error. todoLoading: ${todo}`)
69 | );
70 | }
71 | // e.g) 杉並区成田東4丁目周辺 確定申告に関して、次の文章に対する問い合わせ先を教えてください「申告方法の選択: e-Taxを利用するか、郵送または税務署への持参するか決定します」
72 | const { aiTextResponse, groundings, locations } = await queryLocation({
73 | query: `${userLocation.name}周辺 ${task.question}に関して、次の文章に対する問い合わせ先を教えてください「${todo.content}: ${todo.supplement ?? ""}」`,
74 | }).catch((err) => {
75 | throw new TaskRetryError(`queryLocation failed. ${errorMessage(err)}`);
76 | });
77 |
78 | const todoUpdate: z.infer = {
79 | ...todo,
80 | locations: locations,
81 | locationsAITextResponse: aiTextResponse,
82 | locationsGroundings: groundings,
83 | };
84 | await todoDocRef.set(todoUpdate, { merge: true });
85 |
86 | const response: z.infer = {
87 | result: "OK",
88 | statusCode: 200,
89 | data: {
90 | todo: todoUpdate,
91 | },
92 | };
93 |
94 | return response;
95 | } catch (error) {
96 | if (error instanceof TaskRetryError) {
97 | throw error;
98 | }
99 | return errorResponse(error);
100 | }
101 | }
102 | );
103 |
--------------------------------------------------------------------------------
/functions/src/functions/fillTODOLocation/input.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { UserRequestSchema } from "../../entity/userRequest";
3 |
4 | export const FillTODOLocationSchema = z.object({
5 | taskID: z.string(),
6 | todoID: z.string(),
7 | userLocation: z.object({
8 | name: z.string(),
9 | }),
10 | userRequest: UserRequestSchema,
11 | });
12 |
--------------------------------------------------------------------------------
/functions/src/functions/taskCreate/enqueue_task.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { TaskCreateSchema } from "./input";
3 | import { DataResponseSchema, ErrorResponseSchema } from "../../entity/response";
4 | import { getFunctions } from "firebase-admin/functions";
5 | import { getFunctionURL } from "../../utils/firebase/gcp";
6 | import { onFlow } from "@genkit-ai/firebase/functions";
7 | import { genkitAI } from "../../utils/ai/ai";
8 | import { appAuthPolicy } from "../../utils/ai/authPolicy";
9 |
10 | const ResponseSchema = z.union([
11 | DataResponseSchema.extend({
12 | data: z.object({
13 | taskID: z.string(),
14 | }),
15 | }),
16 | ErrorResponseSchema,
17 | ]);
18 |
19 | export const enqueueTaskCreate = onFlow(
20 | genkitAI,
21 | {
22 | name: "enqueueTaskCreate",
23 | inputSchema: TaskCreateSchema.pick({
24 | taskID: true,
25 | question: true,
26 | userRequest: true,
27 | }),
28 | outputSchema: ResponseSchema,
29 | authPolicy: appAuthPolicy("enqueueTaskCreate"),
30 | },
31 | async (input) => {
32 | const queue = getFunctions().taskQueue("executeTaskCreate");
33 | const executeTaskCreateURL = await getFunctionURL("executeTaskCreate");
34 | await queue.enqueue(input, {
35 | uri: executeTaskCreateURL,
36 | headers: {
37 | "Content-Type": "application/json",
38 | },
39 | });
40 |
41 | const response: z.infer = {
42 | result: "OK",
43 | statusCode: 200,
44 | data: {
45 | taskID: input.taskID,
46 | },
47 | };
48 |
49 | return response;
50 | }
51 | );
52 |
--------------------------------------------------------------------------------
/functions/src/functions/taskCreate/execute_task.ts:
--------------------------------------------------------------------------------
1 | import * as functions from "firebase-functions";
2 | import { z } from "zod";
3 | import { onTaskDispatched } from "firebase-functions/tasks";
4 | import { taskCreate } from "./flow";
5 | import { TaskCreateSchema } from "./input";
6 | import { errorMessage } from "../../utils/error/message";
7 | import { TaskRetryError } from "../../utils/error/taskRetry";
8 |
9 | export const executeTaskCreate = onTaskDispatched(
10 | {
11 | retryConfig: {
12 | maxAttempts: 10,
13 | minBackoffSeconds: 60,
14 | },
15 | rateLimits: {
16 | maxConcurrentDispatches: 3,
17 | },
18 | timeoutSeconds: 10 * 60,
19 | memory: "1GiB",
20 | },
21 | async (req) => {
22 | console.log("#executeTaskCreate");
23 | try {
24 | const input = req.data as z.infer;
25 | const response = await taskCreate(input);
26 | console.log(response);
27 | } catch (err) {
28 | functions.logger.error(errorMessage(err));
29 | if (err instanceof TaskRetryError) {
30 | throw err;
31 | }
32 | }
33 | }
34 | );
35 |
--------------------------------------------------------------------------------
/functions/src/functions/taskCreate/input.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { UserRequestSchema } from "../../entity/userRequest";
3 |
4 | export const TaskCreateSchema = z.object({
5 | taskID: z.string(),
6 | question: z.string(),
7 | userRequest: UserRequestSchema,
8 | });
9 |
--------------------------------------------------------------------------------
/functions/src/functions/todoPrepare/enqueue_task.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { DataResponseSchema, ErrorResponseSchema } from "../../entity/response";
3 | import { getFunctions } from "firebase-admin/functions";
4 | import { getFunctionURL } from "../../utils/firebase/gcp";
5 | import { genkitAI } from "../../utils/ai/ai";
6 | import { TODOPrepareSchema } from "./input";
7 |
8 | const ResponseSchema = z.union([
9 | DataResponseSchema.extend({
10 | data: z.object({
11 | taskID: z.string(),
12 | }),
13 | }),
14 | ErrorResponseSchema,
15 | ]);
16 |
17 | export const enqueueTODOPrepare = genkitAI.defineTool(
18 | {
19 | name: "enqueueTODOPrepare",
20 | description: "enqueueTODOPrepare",
21 | inputSchema: TODOPrepareSchema,
22 | outputSchema: ResponseSchema,
23 | },
24 | async (input) => {
25 | const queue = getFunctions().taskQueue("executeTODOPrepare");
26 | const executeTODOPrepareURL = await getFunctionURL("executeTODOPrepare");
27 | await queue.enqueue(input, {
28 | uri: executeTODOPrepareURL,
29 | headers: {
30 | "Content-Type": "application/json",
31 | },
32 | });
33 |
34 | const response: z.infer = {
35 | result: "OK",
36 | statusCode: 200,
37 | data: {
38 | taskID: input.taskID,
39 | },
40 | };
41 |
42 | return response;
43 | }
44 | );
45 |
--------------------------------------------------------------------------------
/functions/src/functions/todoPrepare/execute_task.ts:
--------------------------------------------------------------------------------
1 | import * as functions from "firebase-functions";
2 | import { z } from "zod";
3 | import { onTaskDispatched } from "firebase-functions/tasks";
4 | import { TODOPrepareSchema } from "./input";
5 | import { errorMessage } from "../../utils/error/message";
6 | import { todoPrepare } from "./flow";
7 | import { TaskRetryError } from "../../utils/error/taskRetry";
8 |
9 | export const executeTODOPrepare = onTaskDispatched(
10 | {
11 | retryConfig: {
12 | // TODO: 提出する時に50とかにしよう
13 | maxAttempts: 10,
14 | minBackoffSeconds: 60,
15 | },
16 | rateLimits: {
17 | maxConcurrentDispatches: 3,
18 | },
19 | timeoutSeconds: 10 * 60,
20 | memory: "1GiB",
21 | },
22 | async (req) => {
23 | console.log("#executeTODOPrepare");
24 | try {
25 | const todoInput = req.data as z.infer;
26 | const response = await todoPrepare(todoInput);
27 | console.log(response);
28 | } catch (err) {
29 | functions.logger.error(errorMessage(err));
30 | if (err instanceof TaskRetryError) {
31 | throw err;
32 | }
33 | }
34 | }
35 | );
36 |
--------------------------------------------------------------------------------
/functions/src/functions/todoPrepare/input.ts:
--------------------------------------------------------------------------------
1 | import { z } from "zod";
2 | import { UserRequestSchema } from "../../entity/userRequest";
3 |
4 | export const TODOPrepareSchema = z.object({
5 | taskID: z.string(),
6 | todoID: z.string(),
7 | question: z.string(),
8 | taskTopic: z.string(),
9 | content: z.string(),
10 | supplement: z.string(),
11 | userRequest: UserRequestSchema,
12 | });
13 |
--------------------------------------------------------------------------------
/functions/src/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-var-requires */
2 |
3 | import { initializeApp } from "firebase-admin/app";
4 |
5 | initializeApp({
6 | serviceAccountId:
7 | process.env.GOOGLE_APPLICATION_CREDENTIALS_SERVICE_ACCOUNT_ID,
8 | });
9 |
10 | export const enqueueTaskCreate =
11 | require("./functions/taskCreate/enqueue_task").enqueueTaskCreate;
12 | export const executeTaskCreate =
13 | require("./functions/taskCreate/execute_task").executeTaskCreate;
14 |
15 | export const enqueueTODOPrepare =
16 | require("./functions/todoPrepare/enqueue_task").enqueueTODOPrepare;
17 | export const executeTODOPrepare =
18 | require("./functions/todoPrepare/execute_task").executeTODOPrepare;
19 |
20 | export const enqueueFillLocation =
21 | require("./functions/fillLocation/enqueue_task").enqueueFillLocation;
22 | export const executeFillLocation =
23 | require("./functions/fillLocation/execute_task").executeFillLocation;
24 |
25 | export const enqueueFillTODOLocation =
26 | require("./functions/fillTODOLocation/enqueue_task").enqueueFillTODOLocation;
27 | export const executeFillTODOLocation =
28 | require("./functions/fillTODOLocation/execute_task").executeFillTODOLocation;
29 |
--------------------------------------------------------------------------------
/functions/src/utils/ai/authPolicy.ts:
--------------------------------------------------------------------------------
1 | import { firebaseAuth } from "@genkit-ai/firebase/auth";
2 | import { FunctionFlowAuth, noAuth } from "@genkit-ai/firebase/functions";
3 | import * as z from "zod";
4 |
5 | export function appAuthPolicy(
6 | fn: string
7 | ): FunctionFlowAuth {
8 | if (process.env.ENV === "local") {
9 | return noAuth();
10 | }
11 | // 他のonFlowからrunFlow経由で呼び出す場合は、FUNCTION_NAMEが一致しないため、noAuthを返す
12 | if (process.env.FUNCTION_NAME !== fn) {
13 | return noAuth();
14 | }
15 | return firebaseAuth((user, input) => {
16 | if (user.exp * 1000 < Date.now()) {
17 | throw new Error("User token is expired");
18 | }
19 |
20 | if (process.env.APP_ENV === "dev") {
21 | console.log(
22 | "[DEBUG] User ID: ",
23 | user.uid,
24 | "Input: ",
25 | JSON.stringify(input)
26 | );
27 | }
28 | if (
29 | input.userRequest?.userID != null &&
30 | input.userRequest.userID !== user.uid
31 | ) {
32 | throw new Error("User ID is not matched");
33 | }
34 | });
35 | }
36 |
--------------------------------------------------------------------------------
/functions/src/utils/error/message.ts:
--------------------------------------------------------------------------------
1 | export function errorMessage(error: unknown): string {
2 | if (error instanceof Error) {
3 | return error.message;
4 | } else if (typeof error === "string") {
5 | return error;
6 | } else {
7 | return "An unknown error occurred";
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/functions/src/utils/error/taskRetry.ts:
--------------------------------------------------------------------------------
1 | export class TaskRetryError extends Error {
2 | constructor(message: string) {
3 | super(message);
4 | this.name = "TaskRetryError";
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/functions/src/utils/firebase/converter.ts:
--------------------------------------------------------------------------------
1 | import * as admin from "firebase-admin";
2 | import { z } from "zod";
3 |
4 | // Usage:
5 | /*
6 | const relatedArticlesCollection = await firestore()
7 | .collectionGroup("articles")
8 | .where(
9 | "symbolicKeywords",
10 | "array-contains-any",
11 | article.symbolicKeywords
12 | )
13 | .withConverter(firestoreConverter(ArticleSchema))
14 | .get();
15 | */
16 | export const firestoreConverter = (
17 | schema: T
18 | ): admin.firestore.FirestoreDataConverter> => ({
19 | toFirestore: (data: z.infer): admin.firestore.DocumentData => {
20 | return schema.parse(data);
21 | },
22 | fromFirestore: (
23 | snapshot: admin.firestore.QueryDocumentSnapshot>
24 | ): z.infer => {
25 | return schema.parse(snapshot.data());
26 | },
27 | });
28 |
--------------------------------------------------------------------------------
/functions/src/utils/firebase/firebase.ts:
--------------------------------------------------------------------------------
1 | import admin = require("firebase-admin");
2 |
3 | export const database = admin.firestore();
4 | database.settings({ ignoreUndefinedProperties: true });
5 |
6 | export const auth = admin.auth();
7 |
--------------------------------------------------------------------------------
/functions/src/utils/firebase/gcp.ts:
--------------------------------------------------------------------------------
1 | import * as functions from "firebase-functions";
2 | import { GoogleAuth } from "google-auth-library";
3 |
4 | // ref: https://firebase.google.com/docs/functions/task-functions?gen=2nd
5 | export async function getFunctionURL(name: string): Promise {
6 | const auth = new GoogleAuth({
7 | scopes: "https://www.googleapis.com/auth/cloud-platform",
8 | });
9 | const projectId = await auth.getProjectId();
10 |
11 | // NOTE: [CloudTask:Region] "asia-northeast1" を使いたいが、なぜか us-central1 じゃないと動かないため明示的に指定する
12 | const location = "us-central1";
13 | const url =
14 | "https://cloudfunctions.googleapis.com/v2/" +
15 | `projects/${projectId}/locations/${location}/functions/${name}`;
16 |
17 | functions.logger.log(`Getting function URL for ${name} at ${url}`);
18 |
19 | const client = await auth.getClient();
20 | // eslint-disable-next-line @typescript-eslint/no-explicit-any
21 | const res = await client.request({ url });
22 | const uri = res.data?.serviceConfig?.uri;
23 | if (!uri) {
24 | throw new Error(`Unable to retreive uri for function at ${url}`);
25 | }
26 | return uri;
27 | }
28 |
--------------------------------------------------------------------------------
/functions/src/utils/queryLocation.ts:
--------------------------------------------------------------------------------
1 | import { z } from "genkit";
2 | import { genkitAI } from "./ai/ai";
3 | import { GroundingData, GroundingDataSchema } from "../entity/grounding";
4 | import { LocationSchema } from "../entity/location";
5 | import { GroundingChunk } from "@google/generative-ai";
6 |
7 | export const queryLocation = genkitAI.defineFlow(
8 | {
9 | name: "queryLocation",
10 | inputSchema: z.object({
11 | query: z.string(),
12 | }),
13 | outputSchema: z.object({
14 | aiTextResponse: z.string(),
15 | groundings: z.array(GroundingDataSchema),
16 | locations: z.array(LocationSchema),
17 | }),
18 | },
19 | async (input) => {
20 | console.log(`#queryLocation: ${JSON.stringify({ input }, null, 2)}`);
21 | const response = await genkitAI.generate({
22 | prompt:
23 | input.query +
24 | "\n" +
25 | "場所の名称,郵便番号,住所(郵便番号を除く),メールアドレス,電話番号。これらの情報を提供してください。見つからない場合はその旨も書いてください",
26 | });
27 | const aiTextResponse = response.text ?? "";
28 | const responseCustom = response.custom as any;
29 | const candidates = responseCustom.candidates;
30 | const groundings: GroundingData[] = [];
31 | if (candidates) {
32 | for (const candidate of candidates) {
33 | const groudingMetadata = candidate?.groundingMetadata;
34 | const index = candidate?.index;
35 | const anyGroudingMetadata = groudingMetadata as any;
36 | // typo: https://github.com/google-gemini/generative-ai-js/issues/323
37 | const groundingChunks = anyGroudingMetadata[
38 | "groundingChunks"
39 | ] as GroundingChunk[];
40 | const web = groundingChunks?.[0].web;
41 | const title = web?.title;
42 | const url = web?.uri;
43 | groundings.push({ title, url, index });
44 | }
45 | }
46 |
47 | const jsonResponse = await genkitAI.generate({
48 | prompt: `
49 | 次の文章から場所に関する情報を取り出してください
50 | ----
51 | ${aiTextResponse}
52 | ----
53 | `,
54 | output: {
55 | schema: z.array(LocationSchema),
56 | },
57 | });
58 |
59 | const locations = jsonResponse.output ?? [];
60 |
61 | return {
62 | aiTextResponse,
63 | groundings,
64 | locations,
65 | };
66 | }
67 | );
68 |
--------------------------------------------------------------------------------
/functions/src/utils/stdlib/nullable.ts:
--------------------------------------------------------------------------------
1 | import * as z from "zod";
2 |
3 | // NOTE: .partial() だと undefined にしかならない
4 | export function nullable(schema: TSchema) {
5 | const entries = Object.entries(schema.shape) as [
6 | keyof TSchema["shape"],
7 | z.ZodTypeAny,
8 | ][];
9 |
10 | const newProps = entries.reduce(
11 | (acc, [key, value]) => {
12 | acc[key] = value.nullable();
13 | return acc;
14 | },
15 | {} as {
16 | [key in keyof TSchema["shape"]]: z.ZodNullable;
17 | }
18 | );
19 |
20 | return z.object(newProps);
21 | }
22 |
23 | export const nonNullable = (value: T): value is NonNullable =>
24 | value != null;
25 |
--------------------------------------------------------------------------------
/functions/src/utils/stdlib/type_guard.ts:
--------------------------------------------------------------------------------
1 | import type z from "zod";
2 |
3 | // 実際に遭遇したケース。safeParseでは、指定した型の新しいparseされたインスタンスが返却されるのでtype guardを用意する
4 | //
5 | // export const TaskPreparedSchema = z
6 | // .object({
7 | // status: z.literal("prepared"),
8 | // preparedDateTime: FirestoreTimestampSchema,
9 | // })
10 | // .....
11 | // const updateTask: z.infer = {
12 | // status: "prepared",
13 | // preparedDateTime: Timestamp.now(),
14 | // serverUpdatedDateTime: Timestamp.now(),
15 | // };
16 |
17 | // const taskSnapshot = await database
18 | // .doc(`/users/${userID}/tasks/${taskID}`)
19 | // .get();
20 | // const task = TaskPreparedSchema.safeParse({
21 | // ...(taskSnapshot.data() ?? {}),
22 | // ...updateTask,
23 | // }).data;
24 | // if (!task) {
25 | // // 用意できなかったプロパティがあると判断するのでRetryする
26 | // functions.logger.info("task not fullfilled", {
27 | // taskLoading,
28 | // });
29 | // throw new Error("task not fullfilled");
30 | // }
31 |
32 | export function zodTypeGuard(
33 | schema: T,
34 | data: unknown
35 | ): data is z.infer {
36 | const result = schema.safeParse(data);
37 | if (process.env.APP_ENV === "local") {
38 | if (!result.success) {
39 | console.error(result.error);
40 | }
41 | }
42 | return result.success;
43 | }
44 |
--------------------------------------------------------------------------------
/functions/tsconfig.dev.json:
--------------------------------------------------------------------------------
1 | {
2 | "include": [
3 | ".eslintrc.js"
4 | ]
5 | }
6 |
--------------------------------------------------------------------------------
/functions/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "allowJs": true,
4 | "target": "ES2020",
5 | "module": "commonjs",
6 | "sourceMap": true,
7 | "outDir": "./lib",
8 | "strict": true,
9 | "strictNullChecks": true,
10 | "alwaysStrict": true,
11 | "moduleResolution": "node",
12 | "esModuleInterop": true,
13 | "skipLibCheck": true,
14 | "forceConsistentCasingInFileNames": true
15 | },
16 | "include": ["src/**/*"]
17 | }
18 |
--------------------------------------------------------------------------------
/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 |
36 | build
37 |
--------------------------------------------------------------------------------
/ios/Firebase/.gitignore:
--------------------------------------------------------------------------------
1 |
2 | GoogleService-Info.plist
3 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | platform :ios, '16.0'
3 |
4 | # Source Code: https://github.com/FirebaseExtended/flutterfire/blob/efdcc16aa2f3d45a3835f0eb73801ba88e808e07/packages/firebase_core/firebase_core/ios/firebase_core.podspec#L6-L8
5 | # It should be with https://github.com/invertase/firestore-ios-sdk-frameworks.git :tag
6 | $FirebaseSDKVersion='11.6.0'
7 |
8 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
9 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
10 |
11 | project 'Runner', {
12 | 'Debug' => :debug,
13 | 'Profile' => :release,
14 | 'Release' => :release,
15 | }
16 |
17 | def flutter_root
18 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
19 | unless File.exist?(generated_xcode_build_settings_path)
20 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
21 | end
22 |
23 | File.foreach(generated_xcode_build_settings_path) do |line|
24 | matches = line.match(/FLUTTER_ROOT\=(.*)/)
25 | return matches[1].strip if matches
26 | end
27 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
28 | end
29 |
30 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
31 |
32 | flutter_ios_podfile_setup
33 |
34 | target 'Runner' do
35 | use_frameworks!
36 | use_modular_headers!
37 |
38 | # Ref: https://github.dev/FirebaseExtended/flutterfire/tree/master/packages/firebase_core/firebase_core/ios/firebase_sdk_version.rb#L1-L2
39 | pod 'FirebaseFirestore', :git => 'https://github.com/invertase/firestore-ios-sdk-frameworks.git', :tag => $FirebaseSDKVersion
40 |
41 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
42 | target 'RunnerTests' do
43 | inherit! :search_paths
44 | end
45 | end
46 |
47 | post_install do |installer|
48 | installer.pods_project.targets.each do |target|
49 | flutter_additional_ios_build_settings(target)
50 |
51 | if target.name == 'BoringSSL-GRPC'
52 | target.source_build_phase.files.each do |file|
53 | if file.settings && file.settings['COMPILER_FLAGS']
54 | flags = file.settings['COMPILER_FLAGS'].split
55 | flags.reject! { |flag| flag == '-GCC_WARN_INHIBIT_ALL_WARNINGS' }
56 | file.settings['COMPILER_FLAGS'] = flags.join(' ')
57 | end
58 | end
59 | end
60 |
61 | if target.name == "geolocator_apple"
62 | target.build_configurations.each do |config|
63 | config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= ['$(inherited)', 'BYPASS_PERMISSION_LOCATION_ALWAYS=1']
64 | end
65 | end
66 |
67 | target.build_configurations.each do |config|
68 | # Reference: https://github.com/flutter/flutter/issues/64502#issuecomment-759165857
69 | config.build_settings["EXCLUDED_ARCHS[sdk=iphonesimulator*]"] = "arm64 i386"
70 | config.build_settings['ENABLE_BITCODE'] = 'NO'
71 |
72 | # workaround: https://github.com/CocoaPods/CocoaPods/issues/11196#issuecomment-1058716826
73 | if config.build_settings['WRAPPER_EXTENSION'] == 'bundle'
74 | config.build_settings['DEVELOPMENT_TEAM'] = 'TQPN82UBBY'
75 | end
76 | end
77 | end
78 | end
79 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
37 |
38 |
39 |
40 |
43 |
49 |
50 |
51 |
52 |
53 |
63 |
65 |
71 |
72 |
73 |
74 |
80 |
82 |
88 |
89 |
90 |
91 |
93 |
94 |
97 |
98 |
99 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/bannzai/zenn_ai_hackathon/81d4f33f4337a554ac9792d0ac880c45f573cabf/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleDisplayName
8 | Zenn Ai Hackathon
9 | CFBundleExecutable
10 | $(EXECUTABLE_NAME)
11 | CFBundleIdentifier
12 | $(PRODUCT_BUNDLE_IDENTIFIER)
13 | CFBundleInfoDictionaryVersion
14 | 6.0
15 | CFBundleName
16 | zenn_ai_hackathon
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 | NSLocationWhenInUseUsageDescription
49 | This app needs access to location when open.
50 | NSCalendarsUsageDescription
51 | カレンダーの予定の取得・書き込みにカレンダーを使用します
52 | NSContactsUsageDescription
53 | カレンダーのイベントへアクセスします
54 | NSCalendarsFullAccessUsageDescription
55 | カレンダーの予定の取得・書き込みにカレンダーを使用します
56 |
57 |
58 |
--------------------------------------------------------------------------------
/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/lib/app.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:todomaker/features/root/page.dart';
3 | import 'package:todomaker/style/color.dart';
4 |
5 | class App extends StatelessWidget {
6 | const App({super.key});
7 |
8 | // This widget is the root of your application.
9 | @override
10 | Widget build(BuildContext context) {
11 | final colorScheme = ColorScheme.fromSeed(
12 | seedColor: AppColors.primary,
13 | primary: AppColors.primary,
14 | );
15 |
16 | return MaterialApp(
17 | theme: ThemeData(
18 | colorScheme: colorScheme,
19 | dividerColor: Colors.black,
20 | bottomSheetTheme: const BottomSheetThemeData(
21 | backgroundColor: AppColors.formBackground,
22 | ),
23 | textSelectionTheme: const TextSelectionThemeData(
24 | cursorColor: AppColors.primary,
25 | ),
26 | inputDecorationTheme: InputDecorationTheme(
27 | filled: true,
28 | fillColor: Colors.white,
29 | enabledBorder: OutlineInputBorder(
30 | borderSide: const BorderSide(color: AppColors.border),
31 | borderRadius: BorderRadius.circular(8),
32 | ),
33 | focusedBorder: const OutlineInputBorder(
34 | borderSide: BorderSide(color: AppColors.primary),
35 | ),
36 | contentPadding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 20.0),
37 | ),
38 | appBarTheme: const AppBarTheme(
39 | elevation: 1,
40 | titleTextStyle: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
41 | ),
42 | textButtonTheme: TextButtonThemeData(
43 | style: TextButton.styleFrom(
44 | foregroundColor: colorScheme.primary,
45 | textStyle: const TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold),
46 | ),
47 | ),
48 | elevatedButtonTheme: ElevatedButtonThemeData(
49 | style: ElevatedButton.styleFrom(
50 | shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
51 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
52 | minimumSize: const Size(double.infinity, 48.0),
53 | textStyle: const TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold),
54 | disabledBackgroundColor: AppColors.disabled,
55 | ),
56 | ),
57 | floatingActionButtonTheme: const FloatingActionButtonThemeData(
58 | extendedTextStyle: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold),
59 | iconSize: 32.0,
60 | ),
61 | outlinedButtonTheme: OutlinedButtonThemeData(
62 | style: OutlinedButton.styleFrom(
63 | shape: RoundedRectangleBorder(
64 | borderRadius: BorderRadius.circular(10),
65 | ),
66 | side: const BorderSide(),
67 | ),
68 | ),
69 | useMaterial3: false,
70 | ),
71 | home: const RootPage(),
72 | );
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/components/alert/discard.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:todomaker/style/color.dart';
3 |
4 | class DiscardDialog extends StatelessWidget {
5 | final String title;
6 | final Widget message;
7 | final List actions;
8 |
9 | const DiscardDialog({
10 | super.key,
11 | required this.title,
12 | required this.message,
13 | required this.actions,
14 | });
15 | @override
16 | Widget build(BuildContext context) {
17 | return AlertDialog(
18 | title: const Icon(Icons.warning, color: TextColor.danger),
19 | content: Column(
20 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
21 | mainAxisSize: MainAxisSize.min,
22 | children: [
23 | if (title.isNotEmpty) ...[
24 | Text(
25 | title,
26 | style: const TextStyle(
27 | fontWeight: FontWeight.w600,
28 | fontSize: 16,
29 | ),
30 | textAlign: TextAlign.center,
31 | ),
32 | const SizedBox(
33 | height: 15,
34 | ),
35 | ],
36 | message,
37 | ],
38 | ),
39 | actions: actions,
40 | );
41 | }
42 | }
43 |
44 | void showDiscardDialog(
45 | BuildContext context, {
46 | required String title,
47 | required String message,
48 | required List actions,
49 | }) {
50 | showDialog(
51 | context: context,
52 | builder: (context) => DiscardDialog(
53 | title: title,
54 | message: Text(
55 | message,
56 | style: const TextStyle(
57 | fontWeight: FontWeight.w300,
58 | fontSize: 14,
59 | ),
60 | ),
61 | actions: actions,
62 | ),
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/lib/components/alert/help.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_markdown/flutter_markdown.dart';
3 | import 'package:todomaker/components/grounding_data/list.dart';
4 | import 'package:todomaker/entity/grounding_data.dart';
5 | import 'package:todomaker/style/color.dart';
6 |
7 | // NOTE: 困ったら Widget を渡すようにするか、リソースごとの個別コンポーネントにする
8 | class HelpAlertLayout extends StatelessWidget {
9 | final String title;
10 | final String? subtitle;
11 | final String detailMarkdown;
12 | final List groundings;
13 |
14 | const HelpAlertLayout({
15 | super.key,
16 | required this.title,
17 | required this.subtitle,
18 | required this.detailMarkdown,
19 | required this.groundings,
20 | });
21 |
22 | @override
23 | Widget build(BuildContext context) {
24 | final subtitle = this.subtitle;
25 |
26 | final primaryColor = Theme.of(context).colorScheme.primary;
27 |
28 | return AlertDialog(
29 | title: Column(
30 | children: [
31 | Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
32 | const SizedBox(height: 10),
33 | if (subtitle != null && subtitle.isNotEmpty) ...[
34 | Text(subtitle, style: const TextStyle(fontSize: 14, color: TextColor.darkGray)),
35 | const SizedBox(height: 10),
36 | ],
37 | ],
38 | ),
39 | content: Column(
40 | crossAxisAlignment: CrossAxisAlignment.start,
41 | children: [
42 | Expanded(
43 | child: SingleChildScrollView(
44 | padding: const EdgeInsets.only(bottom: 20),
45 | child: Column(
46 | crossAxisAlignment: CrossAxisAlignment.start,
47 | children: [
48 | Text('詳細', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: primaryColor)),
49 | const SizedBox(height: 10),
50 | MarkdownBody(data: detailMarkdown),
51 | ],
52 | ),
53 | ),
54 | ),
55 | const Divider(
56 | height: 1,
57 | color: Colors.black,
58 | ),
59 | const SizedBox(height: 10),
60 | for (final grounding in groundings) GroundingDataList(groundings: [grounding]),
61 | ],
62 | ),
63 | // Default padding is 20;
64 | contentPadding: const EdgeInsets.only(top: 20, left: 20, right: 20, bottom: 0),
65 | actionsPadding: const EdgeInsets.only(bottom: 16, left: 16, right: 16),
66 | actions: [
67 | TextButton(
68 | onPressed: () {
69 | Navigator.of(context).pop();
70 | },
71 | child: const Text('閉じる')),
72 | ],
73 | );
74 | }
75 | }
76 |
--------------------------------------------------------------------------------
/lib/components/alert/image_picker.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_hooks/flutter_hooks.dart';
3 | import 'package:hooks_riverpod/hooks_riverpod.dart';
4 | import 'package:image_picker/image_picker.dart';
5 | import 'package:todomaker/components/loading/loading.dart';
6 |
7 | class ImagePickerDialog extends HookConsumerWidget {
8 | const ImagePickerDialog({super.key});
9 |
10 | @override
11 | Widget build(BuildContext context, WidgetRef ref) {
12 | final isLoading = useState(false);
13 | final imagePicker = ImagePicker();
14 |
15 | return AlertDialog(
16 | content: Loading(
17 | isLoading: isLoading.value,
18 | child: Column(
19 | mainAxisSize: MainAxisSize.min,
20 | children: [
21 | ListTile(
22 | title: const Text('カメラ'),
23 | leading: const Icon(Icons.photo_camera),
24 | onTap: () async {
25 | final XFile? photo = await imagePicker.pickImage(source: ImageSource.camera);
26 |
27 | if (photo != null) {
28 | if (context.mounted) {
29 | Navigator.of(context).pop(photo);
30 | }
31 | }
32 | },
33 | ),
34 | ListTile(
35 | title: const Text('フォトライブラリ'),
36 | leading: const Icon(Icons.photo_album),
37 | onTap: () async {
38 | final XFile? photo = await imagePicker.pickImage(source: ImageSource.gallery);
39 | if (photo != null) {
40 | if (context.mounted) {
41 | Navigator.of(context).pop(photo);
42 | }
43 | }
44 | },
45 | ),
46 | ],
47 | ),
48 | ),
49 | actions: [
50 | TextButton(
51 | onPressed: () {
52 | Navigator.pop(context, null);
53 | },
54 | child: const Text('閉じる'),
55 | ),
56 | ],
57 | );
58 | }
59 | }
60 |
61 | Future showImagePickerDialog(BuildContext context) async {
62 | return await showDialog(
63 | context: context,
64 | builder: (context) => const ImagePickerDialog(),
65 | );
66 | }
67 |
--------------------------------------------------------------------------------
/lib/components/alert/ok.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:todomaker/components/loading/loading.dart';
3 |
4 | class OKDialog extends StatelessWidget {
5 | final IconData icon;
6 | final String title;
7 | final String message;
8 | final Future Function()? ok;
9 |
10 | const OKDialog({
11 | super.key,
12 | required this.icon,
13 | required this.title,
14 | required this.message,
15 | required this.ok,
16 | });
17 |
18 | @override
19 | Widget build(BuildContext context) {
20 | return AlertDialog(
21 | title: Icon(
22 | icon,
23 | color: Theme.of(context).primaryColor,
24 | ),
25 | content: Column(
26 | mainAxisAlignment: MainAxisAlignment.spaceBetween,
27 | mainAxisSize: MainAxisSize.min,
28 | children: [
29 | if (title.isNotEmpty) ...[
30 | Text(title,
31 | style: const TextStyle(
32 | fontWeight: FontWeight.w600,
33 | fontSize: 16,
34 | )),
35 | const SizedBox(
36 | height: 15,
37 | ),
38 | ],
39 | Text(message,
40 | style: const TextStyle(
41 | fontWeight: FontWeight.w300,
42 | fontSize: 14,
43 | )),
44 | ],
45 | ),
46 | actions: [
47 | LoadingAction(
48 | action: () async {
49 | final ok = this.ok;
50 | if (ok != null) {
51 | ok();
52 | } else {
53 | Navigator.of(context).pop();
54 | }
55 | },
56 | builder: (action) => TextButton(
57 | onPressed: action,
58 | child: const Text('OK'),
59 | ),
60 | ),
61 | ],
62 | );
63 | }
64 | }
65 |
66 | Future showOKDialog(
67 | BuildContext context, {
68 | required IconData icon,
69 | required String title,
70 | required String message,
71 | Future Function()? ok,
72 | }) async {
73 | return showDialog(
74 | context: context,
75 | builder: (context) => OKDialog(
76 | icon: icon,
77 | title: title,
78 | message: message,
79 | ok: ok,
80 | ),
81 | );
82 | }
83 |
--------------------------------------------------------------------------------
/lib/components/error/error_alert.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:todomaker/components/loading/loading.dart';
3 | import 'package:url_launcher/url_launcher.dart';
4 |
5 | void showErrorAlert(BuildContext? context, Object error) {
6 | if (context == null) {
7 | return;
8 | }
9 | final String title;
10 | final String message;
11 | final String? faqLinkURL;
12 | if (error is FormatException) {
13 | title = '不明なエラーです';
14 | message = error.message;
15 | faqLinkURL = null;
16 | } else if (error is String) {
17 | title = 'エラーが発生しました';
18 | message = error;
19 | faqLinkURL = null;
20 | } else {
21 | title = '予期せぬエラーが発生しました';
22 | message = error.toString();
23 | faqLinkURL = null;
24 | }
25 | showDialog(
26 | context: context,
27 | builder: (_) {
28 | return ErrorAlert(
29 | title: title,
30 | errorMessage: message,
31 | faqLinkURL: faqLinkURL,
32 | );
33 | },
34 | );
35 | }
36 |
37 | class ErrorAlert extends StatelessWidget {
38 | final String? title;
39 | final String errorMessage;
40 | final String? faqLinkURL;
41 |
42 | const ErrorAlert({super.key, this.title, this.faqLinkURL, required this.errorMessage});
43 |
44 | @override
45 | Widget build(BuildContext context) {
46 | final faq = faqLinkURL;
47 | return AlertDialog(
48 | title: Text(
49 | title ?? 'エラーが発生しました',
50 | style: const TextStyle(
51 | fontWeight: FontWeight.w600,
52 | fontSize: 16,
53 | color: Colors.black,
54 | ),
55 | ),
56 | content: Text(errorMessage,
57 | style: const TextStyle(
58 | fontWeight: FontWeight.w300,
59 | fontSize: 14,
60 | color: Colors.black,
61 | )),
62 | actions: [
63 | if (faq != null)
64 | LoadingAction(
65 | action: () async {
66 | launchUrl(Uri.parse(faq));
67 | },
68 | builder: (action) => TextButton(
69 | onPressed: action,
70 | child: const Text('FAQを見る'),
71 | ),
72 | ),
73 | LoadingAction(
74 | action: () async {
75 | Navigator.of(context).pop();
76 | },
77 | builder: (action) => TextButton(
78 | onPressed: action,
79 | child: const Text('閉じる'),
80 | ),
81 | ),
82 | ],
83 | );
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/lib/components/form/question_form.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_hooks/flutter_hooks.dart';
3 |
4 | class QuestionFormSheet extends HookWidget {
5 | const QuestionFormSheet({super.key});
6 |
7 | @override
8 | Widget build(BuildContext context) {
9 | final question = useState('');
10 | final focusNode = useFocusNode();
11 |
12 | WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
13 | focusNode.requestFocus();
14 | });
15 |
16 | return DraggableScrollableSheet(
17 | maxChildSize: 1,
18 | initialChildSize: 0.7,
19 | builder: (context, scrollController) {
20 | return Container(
21 | decoration: const BoxDecoration(
22 | color: Colors.white,
23 | borderRadius: BorderRadius.only(topLeft: Radius.circular(16), topRight: Radius.circular(16)),
24 | ),
25 | child: Stack(
26 | children: [
27 | SingleChildScrollView(
28 | controller: scrollController,
29 | child: Padding(
30 | padding: const EdgeInsets.only(bottom: 20, top: 40, left: 16, right: 16),
31 | child: TextFormField(
32 | focusNode: focusNode,
33 | initialValue: question.value,
34 | minLines: 1,
35 | maxLines: 10,
36 | decoration: const InputDecoration(
37 | border: UnderlineInputBorder(),
38 | enabledBorder: UnderlineInputBorder(),
39 | focusedBorder: UnderlineInputBorder(),
40 | hintText: '例)確定申告の方法,結婚の手続き',
41 | labelText: '手順がわからないものをご記入ください',
42 | ),
43 | onChanged: (value) {
44 | question.value = value;
45 | },
46 | ),
47 | ),
48 | ),
49 | Positioned(
50 | top: 10,
51 | right: 10,
52 | child: TextButton(
53 | onPressed: () {
54 | Navigator.of(context).pop(question.value);
55 | },
56 | child: const Text('🤖に聞く'),
57 | ),
58 | ),
59 | ],
60 | ),
61 | );
62 | },
63 | );
64 | }
65 | }
66 |
67 | Future showQuestionFormSheet(BuildContext context) async {
68 | return await showModalBottomSheet(
69 | useSafeArea: true,
70 | backgroundColor: Colors.transparent,
71 | isScrollControlled: true,
72 | context: context,
73 | builder: (context) => const QuestionFormSheet(),
74 | );
75 | }
76 |
--------------------------------------------------------------------------------
/lib/components/grounding_data/list.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:todomaker/entity/grounding_data.dart';
3 |
4 | import 'package:url_launcher/link.dart';
5 |
6 | class GroundingDataList extends StatelessWidget {
7 | final List groundings;
8 | const GroundingDataList({super.key, required this.groundings});
9 |
10 | @override
11 | Widget build(BuildContext context) {
12 | return Column(
13 | crossAxisAlignment: CrossAxisAlignment.start,
14 | children: [
15 | const Text('情報元', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
16 | for (final grounding in groundings) ...[
17 | GroundingDataRow(grounding: grounding),
18 | ],
19 | ],
20 | );
21 | }
22 | }
23 |
24 | class GroundingDataRow extends StatelessWidget {
25 | final GroundingData grounding;
26 | const GroundingDataRow({super.key, required this.grounding});
27 |
28 | @override
29 | Widget build(BuildContext context) {
30 | final title = grounding.title;
31 | final url = grounding.url;
32 | if (title == null || url == null) {
33 | return const SizedBox.shrink();
34 | }
35 | return Link(
36 | uri: Uri.parse(url),
37 | builder: (BuildContext ctx, FollowLink? openLink) {
38 | return TextButton(
39 | onPressed: openLink,
40 | style: ButtonStyle(
41 | tapTargetSize: MaterialTapTargetSize.shrinkWrap,
42 | padding: WidgetStateProperty.all(const EdgeInsets.symmetric(horizontal: 2, vertical: 0)),
43 | ),
44 | child: Text(
45 | title,
46 | style: const TextStyle(fontSize: 12),
47 | ),
48 | );
49 | },
50 | );
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/lib/components/loading/bot.dart:
--------------------------------------------------------------------------------
1 | import 'package:animated_text_kit/animated_text_kit.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_hooks/flutter_hooks.dart';
4 | import 'package:url_launcher/url_launcher.dart';
5 |
6 | class BotLoading extends StatelessWidget {
7 | final List messages;
8 | final VoidCallback onStop;
9 |
10 | const BotLoading({super.key, required this.messages, required this.onStop});
11 |
12 | @override
13 | Widget build(BuildContext context) {
14 | return Positioned.fill(
15 | child: Container(
16 | padding: const EdgeInsets.symmetric(vertical: 12),
17 | decoration: BoxDecoration(
18 | color: Colors.white.withOpacity(0.78),
19 | ),
20 | child: Center(
21 | child: Column(
22 | children: [
23 | BotChat(messages: messages),
24 | const SizedBox(height: 12),
25 | const Padding(
26 | padding: EdgeInsets.symmetric(horizontal: 20.0),
27 | child: Text('※ 時間がかかり過ぎているようであれば、\n一度停止して再度実行してください。', style: TextStyle(fontSize: 12)),
28 | ),
29 | Padding(
30 | padding: const EdgeInsets.symmetric(horizontal: 12.0),
31 | child: TextButton.icon(
32 | onPressed: () {
33 | onStop();
34 | },
35 | icon: const Icon(Icons.stop),
36 | label: const Text('停止'),
37 | ),
38 | ),
39 | ],
40 | ),
41 | ),
42 | ),
43 | );
44 | }
45 | }
46 |
47 | class BotChat extends HookWidget {
48 | final List messages;
49 |
50 | const BotChat({super.key, required this.messages});
51 |
52 | @override
53 | Widget build(BuildContext context) {
54 | final tapCount = useState(0);
55 | final showEasterEgg = useState(false);
56 | tapCount.addListener(() {
57 | const threshold = 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1;
58 | if (tapCount.value >= threshold) {
59 | showEasterEgg.value = true;
60 | }
61 | });
62 |
63 | return GestureDetector(
64 | onTap: () => tapCount.value++,
65 | child: SizedBox(
66 | width: double.infinity,
67 | child: DefaultTextStyle(
68 | style: const TextStyle(
69 | fontSize: 22,
70 | fontWeight: FontWeight.w500,
71 | color: Colors.black,
72 | shadows: [
73 | Shadow(color: Colors.blueGrey, blurRadius: 2),
74 | ],
75 | ),
76 | child: Row(
77 | children: [
78 | if (!showEasterEgg.value) ...[
79 | const Spacer(),
80 | const Text('🤖'),
81 | const SizedBox(width: 2),
82 | Container(
83 | constraints: const BoxConstraints(maxWidth: 310),
84 | child: AnimatedTextKit(
85 | repeatForever: true,
86 | animatedTexts: [
87 | for (var message in messages) TyperAnimatedText(message, speed: const Duration(milliseconds: 60)),
88 | ],
89 | ),
90 | ),
91 | const Spacer(),
92 | ] else ...[
93 | const Spacer(),
94 | GestureDetector(
95 | onTap: () async {
96 | final uri = Uri(scheme: 'https', host: 'github.com', path: 'bannzai/zenn_ai_hackathon');
97 | await launchUrl(uri);
98 | },
99 | child: Row(children: [
100 | Image.asset('assets/bannzai.programmer.png', width: 40, height: 40),
101 | const SizedBox(width: 2),
102 | const Text('このリポジトリにスターください⭐️'),
103 | ]),
104 | ),
105 | const Spacer(),
106 | ],
107 | ],
108 | ),
109 | ),
110 | ),
111 | );
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/lib/components/loading/indicator.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | class Indicator extends StatelessWidget {
4 | const Indicator({
5 | super.key,
6 | });
7 |
8 | @override
9 | Widget build(BuildContext context) {
10 | return Center(
11 | child: CircularProgressIndicator(
12 | valueColor: AlwaysStoppedAnimation(
13 | Theme.of(context).primaryColor,
14 | ),
15 | ),
16 | );
17 | }
18 | }
19 |
20 | class IndicatorPage extends StatelessWidget {
21 | const IndicatorPage({super.key});
22 |
23 | @override
24 | Widget build(BuildContext context) {
25 | return const Scaffold(
26 | backgroundColor: Colors.white,
27 | body: Indicator(),
28 | );
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/lib/components/loading/loading.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_hooks/flutter_hooks.dart';
3 | import 'package:todomaker/components/loading/indicator.dart';
4 |
5 | class Loading extends StatelessWidget {
6 | final bool isLoading;
7 | final Widget child;
8 |
9 | const Loading({super.key, required this.isLoading, required this.child});
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | return Stack(
14 | alignment: Alignment.center,
15 | children: [
16 | child,
17 | if (isLoading) ...[
18 | const SizedBox(
19 | width: 20,
20 | height: 20,
21 | child: Indicator(),
22 | ),
23 | ],
24 | ],
25 | );
26 | }
27 | }
28 |
29 | class LoadingAction extends HookWidget {
30 | final Future Function() action;
31 | final Widget Function(VoidCallback?) builder;
32 |
33 | const LoadingAction({
34 | super.key,
35 | required this.action,
36 | required this.builder,
37 | });
38 |
39 | @override
40 | Widget build(BuildContext context) {
41 | final isLoading = useState(false);
42 | final future = useMemoized(() => action(), []);
43 | final asyncSnapshot = useFuture(future);
44 |
45 | useEffect(() {
46 | isLoading.value = asyncSnapshot.connectionState == ConnectionState.waiting;
47 | return null;
48 | }, [asyncSnapshot]);
49 |
50 | return Loading(
51 | isLoading: isLoading.value,
52 | child: builder(isLoading.value
53 | ? null
54 | : () async {
55 | await future;
56 | }),
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/lib/components/retry/button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:hooks_riverpod/hooks_riverpod.dart';
3 | import 'package:todomaker/components/retry/page.dart';
4 |
5 | class RetryButton extends HookConsumerWidget {
6 | final Object exception;
7 | final StackTrace? stackTrace;
8 |
9 | const RetryButton({
10 | super.key,
11 | required this.exception,
12 | required this.stackTrace,
13 | });
14 |
15 | @override
16 | Widget build(BuildContext context, WidgetRef ref) {
17 | debugPrint('Retry: $exception');
18 | debugPrint('StackTrace: $stackTrace');
19 | return Padding(
20 | padding: const EdgeInsets.symmetric(horizontal: 12.0),
21 | child: ElevatedButton(
22 | onPressed: () {
23 | Retry.of(context).retry();
24 | },
25 | child: const Text('再試行'),
26 | ),
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/components/retry/page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:hooks_riverpod/hooks_riverpod.dart';
3 |
4 | class Retry extends InheritedWidget {
5 | final VoidCallback retry;
6 |
7 | const Retry({
8 | super.key,
9 | required this.retry,
10 | required super.child,
11 | });
12 |
13 | static Retry of(BuildContext context) {
14 | final result = context.dependOnInheritedWidgetOfExactType();
15 | assert(result != null, 'No Retry found in context');
16 | return result!;
17 | }
18 |
19 | @override
20 | bool updateShouldNotify(Retry oldWidget) {
21 | return retry != oldWidget.retry;
22 | }
23 | }
24 |
25 | class RetryPage extends HookConsumerWidget {
26 | final Object exception;
27 | final StackTrace? stackTrace;
28 |
29 | const RetryPage({
30 | super.key,
31 | required this.exception,
32 | required this.stackTrace,
33 | });
34 |
35 | @override
36 | Widget build(BuildContext context, WidgetRef ref) {
37 | debugPrint('Retry: $exception');
38 | debugPrint('StackTrace: $stackTrace');
39 | return Scaffold(
40 | body: Center(
41 | child: Column(
42 | mainAxisAlignment: MainAxisAlignment.center,
43 | children: [
44 | Text(
45 | exception.toString(),
46 | style: Theme.of(context).textTheme.bodyMedium,
47 | ),
48 | const SizedBox(height: 16),
49 | Padding(
50 | padding: const EdgeInsets.symmetric(horizontal: 12.0),
51 | child: ElevatedButton(
52 | onPressed: () {
53 | Retry.of(context).retry();
54 | },
55 | child: const Text('再試行'),
56 | ),
57 | ),
58 | ],
59 | ),
60 | ),
61 | );
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/lib/components/todo/help.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:todomaker/components/alert/help.dart';
3 | import 'package:todomaker/entity/todo.dart';
4 |
5 | class TodoHelpButton extends StatelessWidget {
6 | final Todo todo;
7 | const TodoHelpButton({super.key, required this.todo});
8 |
9 | @override
10 | Widget build(BuildContext context) {
11 | final aiTextResponseMarkdown = todo.aiTextResponseMarkdown;
12 | final groundings = todo.groundings;
13 | if (aiTextResponseMarkdown == null || groundings == null) {
14 | return const SizedBox.shrink();
15 | }
16 | return IconButton(
17 | onPressed: () {
18 | showDialog(
19 | context: context,
20 | builder: (context) => HelpAlertLayout(
21 | title: todo.content,
22 | subtitle: todo.supplement,
23 | detailMarkdown: aiTextResponseMarkdown,
24 | groundings: groundings,
25 | ),
26 | );
27 | },
28 | icon: const Icon(Icons.help_outline, size: 20),
29 | // IconButtonは内部でBoxConstraints(minWidth: 48, minHeight: 48)が設定されているので、これをオーバーライドする
30 | constraints: const BoxConstraints(minWidth: 20, minHeight: 20),
31 | padding: EdgeInsets.zero,
32 | style: ButtonStyle(
33 | padding: WidgetStateProperty.all(EdgeInsets.zero),
34 | ),
35 | );
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/lib/components/todo_locations/ask.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:hooks_riverpod/hooks_riverpod.dart';
3 | import 'package:todomaker/components/error/error_alert.dart';
4 | import 'package:todomaker/components/location/form.dart';
5 | import 'package:todomaker/entity/todo.dart';
6 | import 'package:todomaker/provider/todo.dart';
7 | import 'package:todomaker/utils/functions/firebase_functions.dart';
8 |
9 | class TodoLocationAskAI extends HookConsumerWidget {
10 | final Todo todo;
11 | const TodoLocationAskAI({super.key, required this.todo});
12 |
13 | @override
14 | Widget build(BuildContext context, WidgetRef ref) {
15 | final todoFillLocation = ref.watch(todoFillLocationProvider);
16 | final locations = todo.locations;
17 |
18 | return TextButton(
19 | style: TextButton.styleFrom(
20 | padding: const EdgeInsets.symmetric(vertical: 0),
21 | tapTargetSize: MaterialTapTargetSize.shrinkWrap,
22 | minimumSize: Size.zero,
23 | ),
24 | onPressed: () async {
25 | showDialog(
26 | context: context,
27 | builder: (context) => LocationForm(
28 | onSubmit: (userLocation) async {
29 | try {
30 | await todoFillLocation(
31 | taskID: todo.taskID,
32 | todoID: todo.id,
33 | userLocation: userLocation,
34 | );
35 | await functions.fillTodoLocation(
36 | taskID: todo.taskID,
37 | todoID: todo.id,
38 | userLocation: userLocation,
39 | );
40 | } catch (e) {
41 | if (context.mounted) {
42 | showErrorAlert(context, e.toString());
43 | }
44 | }
45 | },
46 | ),
47 | );
48 | },
49 | child: Row(
50 | crossAxisAlignment: CrossAxisAlignment.baseline,
51 | textBaseline: TextBaseline.ideographic,
52 | children: [
53 | const Text('🤖'),
54 | const SizedBox(width: 2),
55 | if (locations != null && locations.isNotEmpty) ...[
56 | const Expanded(child: Text('関連する位置情報・会場・場所をAIに聞き直す')),
57 | ] else ...[
58 | const Expanded(child: Text('関連する位置情報・会場・場所をAIに聞く')),
59 | ],
60 | ],
61 | ),
62 | );
63 | }
64 | }
65 |
--------------------------------------------------------------------------------
/lib/components/todo_locations/list.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:todomaker/components/todo_locations/row.dart';
3 | import 'package:todomaker/entity/todo.dart';
4 | import 'package:todomaker/style/color.dart';
5 |
6 | class TodoLocationList extends StatelessWidget {
7 | final List todos;
8 | const TodoLocationList({super.key, required this.todos});
9 |
10 | @override
11 | Widget build(BuildContext context) {
12 | return Padding(
13 | padding: const EdgeInsets.symmetric(horizontal: 20),
14 | child: Column(
15 | crossAxisAlignment: CrossAxisAlignment.start,
16 | children: [
17 | for (final todo in todos) ...[
18 | TodoLocationsSection(todo: todo),
19 | const Divider(),
20 | ],
21 | ],
22 | ),
23 | );
24 | }
25 | }
26 |
27 | class TodoLocationsSection extends StatelessWidget {
28 | final Todo todo;
29 | const TodoLocationsSection({super.key, required this.todo});
30 |
31 | @override
32 | Widget build(BuildContext context) {
33 | final supplement = todo.supplement;
34 |
35 | return Padding(
36 | padding: const EdgeInsets.symmetric(vertical: 10),
37 | child: Column(
38 | crossAxisAlignment: CrossAxisAlignment.start,
39 | children: [
40 | Text(todo.content, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
41 | if (supplement != null && supplement.isNotEmpty) ...[
42 | Text(
43 | supplement,
44 | style: const TextStyle(fontSize: 14, color: TextColor.darkGray),
45 | maxLines: 2,
46 | overflow: TextOverflow.ellipsis,
47 | ),
48 | ],
49 | TodoLocationsRow(todo: todo),
50 | ],
51 | ),
52 | );
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/lib/components/todo_time_required/functions.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:todomaker/entity/todo.dart';
4 | import 'package:device_calendar/device_calendar.dart';
5 |
6 | Future grandPermission({required DeviceCalendarPlugin deviceCalendarPlugin}) async {
7 | var permissionsGranted = await deviceCalendarPlugin.requestPermissions();
8 | return permissionsGranted.isSuccess && permissionsGranted.data == true;
9 | }
10 |
11 | Future> retrieveCalendars({
12 | required DeviceCalendarPlugin deviceCalendarPlugin,
13 | }) async {
14 | var calendarsResult = await deviceCalendarPlugin.retrieveCalendars();
15 | if (calendarsResult.isSuccess && calendarsResult.data != null) {
16 | return calendarsResult.data!;
17 | }
18 | return [];
19 | }
20 |
21 | /// 予定の読み出し(例:過去30日~未来30日)
22 | Future> calendarEvents({
23 | required Todo todo,
24 | required DeviceCalendarPlugin deviceCalendarPlugin,
25 | required Calendar calendar,
26 | }) async {
27 | final eventIDs = todo.calendarSchedules.map((e) => e.calendarEventID).toList();
28 | var eventsResult = await deviceCalendarPlugin.retrieveEvents(
29 | calendar.id,
30 | RetrieveEventsParams(eventIds: eventIDs),
31 | );
32 | final eventResultData = eventsResult.data;
33 | if (eventsResult.isSuccess && eventResultData != null) {
34 | return eventResultData;
35 | }
36 | return [];
37 | }
38 |
--------------------------------------------------------------------------------
/lib/entity/app_user.dart:
--------------------------------------------------------------------------------
1 | import 'package:cloud_firestore/cloud_firestore.dart';
2 | import 'package:freezed_annotation/freezed_annotation.dart';
3 | import 'package:todomaker/entity/timestamp.dart';
4 |
5 | part 'app_user.g.dart';
6 | part 'app_user.freezed.dart';
7 |
8 | @freezed
9 | class AppUser with _$AppUser {
10 | const AppUser._();
11 | @JsonSerializable(explicitToJson: true)
12 | const factory AppUser({
13 | required String id,
14 | @ClientCreatedTimestamp() DateTime? createdDateTime,
15 | @ClientUpdatedTimestamp() DateTime? updatedDateTime,
16 | @ServerCreatedTimestamp() DateTime? serverCreatedDateTime,
17 | @ServerUpdatedTimestamp() DateTime? serverUpdatedDateTime,
18 | }) = _AppUser;
19 |
20 | factory AppUser.fromJson(Map json) => _$AppUserFromJson(json);
21 | }
22 |
--------------------------------------------------------------------------------
/lib/entity/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) => _$AppUserImpl(
10 | id: json['id'] as String,
11 | createdDateTime: const ClientCreatedTimestamp().fromJson(json['createdDateTime'] as Timestamp?),
12 | updatedDateTime: const ClientUpdatedTimestamp().fromJson(json['updatedDateTime'] as Timestamp?),
13 | serverCreatedDateTime: const ServerCreatedTimestamp().fromJson(json['serverCreatedDateTime']),
14 | serverUpdatedDateTime: const ServerUpdatedTimestamp().fromJson(json['serverUpdatedDateTime']),
15 | );
16 |
17 | Map _$$AppUserImplToJson(_$AppUserImpl instance) => {
18 | 'id': instance.id,
19 | 'createdDateTime': const ClientCreatedTimestamp().toJson(instance.createdDateTime),
20 | 'updatedDateTime': const ClientUpdatedTimestamp().toJson(instance.updatedDateTime),
21 | 'serverCreatedDateTime': const ServerCreatedTimestamp().toJson(instance.serverCreatedDateTime),
22 | 'serverUpdatedDateTime': const ServerUpdatedTimestamp().toJson(instance.serverUpdatedDateTime),
23 | };
24 |
--------------------------------------------------------------------------------
/lib/entity/grounding_data.dart:
--------------------------------------------------------------------------------
1 | import 'package:cloud_firestore/cloud_firestore.dart';
2 | import 'package:freezed_annotation/freezed_annotation.dart';
3 | import 'package:todomaker/entity/timestamp.dart';
4 |
5 | part 'grounding_data.g.dart';
6 | part 'grounding_data.freezed.dart';
7 |
8 | @freezed
9 | class GroundingData with _$GroundingData {
10 | const GroundingData._();
11 | @JsonSerializable(explicitToJson: true)
12 | const factory GroundingData({
13 | required int? index,
14 | required String? url,
15 | required String? title,
16 | @ClientCreatedTimestamp() DateTime? createdDateTime,
17 | @ClientUpdatedTimestamp() DateTime? updatedDateTime,
18 | @ServerCreatedTimestamp() DateTime? serverCreatedDateTime,
19 | @ServerUpdatedTimestamp() DateTime? serverUpdatedDateTime,
20 | }) = _GroundingData;
21 |
22 | factory GroundingData.fromJson(Map json) => _$GroundingDataFromJson(json);
23 | }
24 |
25 | /*
26 | import { z } from "zod";
27 |
28 | export const GroundingDataSchema = z.object({
29 | url: z.string().optional(),
30 | title: z.string().optional(),
31 | index: z.number().optional(),
32 | });
33 |
34 | export type GroundingData = z.infer;
35 | */
36 |
--------------------------------------------------------------------------------
/lib/entity/grounding_data.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'grounding_data.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | _$GroundingDataImpl _$$GroundingDataImplFromJson(Map json) => _$GroundingDataImpl(
10 | index: (json['index'] as num?)?.toInt(),
11 | url: json['url'] as String?,
12 | title: json['title'] as String?,
13 | createdDateTime: const ClientCreatedTimestamp().fromJson(json['createdDateTime'] as Timestamp?),
14 | updatedDateTime: const ClientUpdatedTimestamp().fromJson(json['updatedDateTime'] as Timestamp?),
15 | serverCreatedDateTime: const ServerCreatedTimestamp().fromJson(json['serverCreatedDateTime']),
16 | serverUpdatedDateTime: const ServerUpdatedTimestamp().fromJson(json['serverUpdatedDateTime']),
17 | );
18 |
19 | Map _$$GroundingDataImplToJson(_$GroundingDataImpl instance) => {
20 | 'index': instance.index,
21 | 'url': instance.url,
22 | 'title': instance.title,
23 | 'createdDateTime': const ClientCreatedTimestamp().toJson(instance.createdDateTime),
24 | 'updatedDateTime': const ClientUpdatedTimestamp().toJson(instance.updatedDateTime),
25 | 'serverCreatedDateTime': const ServerCreatedTimestamp().toJson(instance.serverCreatedDateTime),
26 | 'serverUpdatedDateTime': const ServerUpdatedTimestamp().toJson(instance.serverUpdatedDateTime),
27 | };
28 |
--------------------------------------------------------------------------------
/lib/entity/location.dart:
--------------------------------------------------------------------------------
1 | import 'package:freezed_annotation/freezed_annotation.dart';
2 |
3 | part 'location.g.dart';
4 | part 'location.freezed.dart';
5 |
6 | @freezed
7 | class AppLocation with _$AppLocation {
8 | const AppLocation._();
9 | @JsonSerializable(explicitToJson: true)
10 | const factory AppLocation({
11 | required String? name,
12 | required String? postalCode,
13 | required String? address,
14 | required String? tel,
15 | required String? email,
16 | }) = _AppLocation;
17 |
18 | factory AppLocation.fromJson(Map json) => _$AppLocationFromJson(json);
19 | }
20 |
21 | /*
22 | import { z } from "zod";
23 |
24 | export const LocationSchema = z.object({
25 | name: z.string().describe("場所の名称"),
26 | postalCode: z.string().nullable().describe("郵便番号"),
27 | address: z.string().nullable().describe("住所。郵便番号を除く"),
28 | tel: z.string().nullable().describe("電話番号"),
29 | email: z.string().nullable().describe("メールアドレス"),
30 | });
31 | */
32 |
--------------------------------------------------------------------------------
/lib/entity/location.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'location.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | _$AppLocationImpl _$$AppLocationImplFromJson(Map json) => _$AppLocationImpl(
10 | name: json['name'] as String?,
11 | postalCode: json['postalCode'] as String?,
12 | address: json['address'] as String?,
13 | tel: json['tel'] as String?,
14 | email: json['email'] as String?,
15 | );
16 |
17 | Map _$$AppLocationImplToJson(_$AppLocationImpl instance) => {
18 | 'name': instance.name,
19 | 'postalCode': instance.postalCode,
20 | 'address': instance.address,
21 | 'tel': instance.tel,
22 | 'email': instance.email,
23 | };
24 |
--------------------------------------------------------------------------------
/lib/entity/location_form.dart:
--------------------------------------------------------------------------------
1 | import 'package:freezed_annotation/freezed_annotation.dart';
2 |
3 | part 'location_form.g.dart';
4 | part 'location_form.freezed.dart';
5 |
6 | @freezed
7 | class LocationFormInfo with _$LocationFormInfo {
8 | const factory LocationFormInfo({
9 | required String name,
10 | required double? latitude,
11 | required double? longitude,
12 | }) = _LocationFormInfo;
13 |
14 | factory LocationFormInfo.fromJson(Map json) => _$LocationFormInfoFromJson(json);
15 | }
16 |
--------------------------------------------------------------------------------
/lib/entity/location_form.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'location_form.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | _$LocationFormInfoImpl _$$LocationFormInfoImplFromJson(Map json) => _$LocationFormInfoImpl(
10 | name: json['name'] as String,
11 | latitude: (json['latitude'] as num?)?.toDouble(),
12 | longitude: (json['longitude'] as num?)?.toDouble(),
13 | );
14 |
15 | Map _$$LocationFormInfoImplToJson(_$LocationFormInfoImpl instance) => {
16 | 'name': instance.name,
17 | 'latitude': instance.latitude,
18 | 'longitude': instance.longitude,
19 | };
20 |
--------------------------------------------------------------------------------
/lib/entity/remote_config_parameter.dart:
--------------------------------------------------------------------------------
1 | import 'package:freezed_annotation/freezed_annotation.dart';
2 |
3 | part 'remote_config_parameter.freezed.dart';
4 | part 'remote_config_parameter.g.dart';
5 |
6 | abstract class RemoteConfigKeys {
7 | static const minimumAppVersion = 'minimumAppVersion';
8 | }
9 |
10 | abstract class RemoteConfigParameterDefaultValues {
11 | static const minimumAppVersion = '1.0.0';
12 | }
13 |
14 | @freezed
15 | class RemoteConfigParameter with _$RemoteConfigParameter {
16 | factory RemoteConfigParameter({
17 | @Default(RemoteConfigParameterDefaultValues.minimumAppVersion) String minimumAppVersion,
18 | }) = _RemoteConfigParameter;
19 |
20 | RemoteConfigParameter._();
21 |
22 | factory RemoteConfigParameter.fromJson(Map json) => _$RemoteConfigParameterFromJson(json);
23 | }
24 |
--------------------------------------------------------------------------------
/lib/entity/remote_config_parameter.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'remote_config_parameter.dart';
4 |
5 | // **************************************************************************
6 | // JsonSerializableGenerator
7 | // **************************************************************************
8 |
9 | _$RemoteConfigParameterImpl _$$RemoteConfigParameterImplFromJson(Map json) => _$RemoteConfigParameterImpl(
10 | minimumAppVersion: json['minimumAppVersion'] as String? ?? RemoteConfigParameterDefaultValues.minimumAppVersion,
11 | );
12 |
13 | Map _$$RemoteConfigParameterImplToJson(_$RemoteConfigParameterImpl instance) => {
14 | 'minimumAppVersion': instance.minimumAppVersion,
15 | };
16 |
--------------------------------------------------------------------------------
/lib/entity/timestamp.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 json) {
9 | return json.toDate();
10 | }
11 |
12 | @override
13 | Timestamp toJson(DateTime dateTime) {
14 | return Timestamp.fromDate(dateTime);
15 | }
16 | }
17 |
18 | class NullableTimestampConverter implements JsonConverter {
19 | const NullableTimestampConverter();
20 |
21 | @override
22 | DateTime? fromJson(Timestamp? json) {
23 | return json?.toDate();
24 | }
25 |
26 | @override
27 | Timestamp? toJson(DateTime? dateTime) {
28 | if (dateTime == null) {
29 | return null;
30 | }
31 | return Timestamp.fromDate(dateTime);
32 | }
33 | }
34 |
35 | class ClientCreatedTimestamp implements JsonConverter {
36 | const ClientCreatedTimestamp();
37 |
38 | @override
39 | DateTime? fromJson(Timestamp? timestamp) {
40 | return timestamp?.toDate();
41 | }
42 |
43 | @override
44 | Timestamp toJson(DateTime? dateTime) {
45 | if (dateTime == null) return Timestamp.fromDate(DateTime.now());
46 | return Timestamp.fromDate(dateTime);
47 | }
48 | }
49 |
50 | class ClientUpdatedTimestamp implements JsonConverter {
51 | const ClientUpdatedTimestamp();
52 |
53 | @override
54 | DateTime? fromJson(Timestamp? timestamp) {
55 | return timestamp?.toDate();
56 | }
57 |
58 | @override
59 | Timestamp? toJson(DateTime? date) {
60 | if (date == null) return null;
61 | return Timestamp.fromDate(date);
62 | }
63 | }
64 |
65 | class ServerCreatedTimestamp implements JsonConverter {
66 | const ServerCreatedTimestamp();
67 |
68 | @override
69 | DateTime? fromJson(dynamic timestamp) {
70 | timestamp as Timestamp?;
71 | return timestamp?.toDate();
72 | }
73 |
74 | @override
75 | dynamic toJson(DateTime? dateTime) {
76 | if (dateTime == null) return FieldValue.serverTimestamp();
77 | return dateTime;
78 | }
79 | }
80 |
81 | class ServerUpdatedTimestamp implements JsonConverter {
82 | const ServerUpdatedTimestamp();
83 |
84 | @override
85 | DateTime? fromJson(dynamic timestamp) {
86 | timestamp as Timestamp?;
87 | return timestamp?.toDate();
88 | }
89 |
90 | @override
91 | dynamic toJson(DateTime? date) {
92 | return FieldValue.serverTimestamp();
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/lib/features/home/page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:hooks_riverpod/hooks_riverpod.dart';
3 | import 'package:todomaker/features/tasks/page.dart';
4 |
5 | enum HomePageTabType { tasks, profile }
6 |
7 | class HomePage extends HookConsumerWidget {
8 | const HomePage({super.key});
9 |
10 | @override
11 | Widget build(BuildContext context, WidgetRef ref) {
12 | return const TasksPage();
13 | }
14 | }
15 |
--------------------------------------------------------------------------------
/lib/features/onboarding/components/body_1.dart:
--------------------------------------------------------------------------------
1 | import 'package:animated_text_kit/animated_text_kit.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_hooks/flutter_hooks.dart';
4 | import 'package:todomaker/features/onboarding/components/event.dart';
5 |
6 | class OnboardingBody1 extends HookWidget {
7 | const OnboardingBody1({
8 | super.key,
9 | required this.index,
10 | });
11 |
12 | final ValueNotifier index;
13 |
14 | @override
15 | Widget build(BuildContext context) {
16 | final animationController = useAnimationController(duration: const Duration(milliseconds: 400));
17 | index.addListener(() {
18 | if (index.value == OnboardingEvent.showTapLabel) {
19 | animationController.forward();
20 | }
21 | });
22 | return GestureDetector(
23 | onTap: () => index.value = OnboardingEvent.secondPage,
24 | child: Stack(
25 | alignment: Alignment.bottomCenter,
26 | children: [
27 | Column(
28 | crossAxisAlignment: CrossAxisAlignment.center,
29 | children: [
30 | const Spacer(),
31 | OnboardingBot1(
32 | messages: const [
33 | 'こんにちは!',
34 | 'TODOMakerへようこそ',
35 | ],
36 | onFinished: () => index.value = OnboardingEvent.showTapLabel,
37 | ),
38 | const Spacer(),
39 | ],
40 | ),
41 | if (index.value.index <= OnboardingEvent.showTapLabel.index) ...[
42 | Positioned(
43 | bottom: 20,
44 | child: SizedBox(
45 | height: 100,
46 | child: FadeTransition(
47 | opacity: Tween(begin: 0, end: 1).animate(
48 | CurvedAnimation(
49 | parent: animationController,
50 | curve: Curves.easeInOut,
51 | ),
52 | ),
53 | child: const Text(
54 | 'タップして次へ',
55 | style: TextStyle(
56 | fontSize: 24,
57 | fontWeight: FontWeight.w500,
58 | color: Colors.black,
59 | shadows: [Shadow(color: Colors.blueGrey, blurRadius: 2)],
60 | ),
61 | ),
62 | ),
63 | ),
64 | ),
65 | ],
66 | ],
67 | ),
68 | );
69 | }
70 | }
71 |
72 | class OnboardingBot1 extends StatelessWidget {
73 | final List messages;
74 | final VoidCallback onFinished;
75 |
76 | const OnboardingBot1({super.key, required this.messages, required this.onFinished});
77 |
78 | @override
79 | Widget build(BuildContext context) {
80 | return SizedBox(
81 | width: double.infinity,
82 | height: 100,
83 | child: Row(
84 | mainAxisAlignment: MainAxisAlignment.center,
85 | mainAxisSize: MainAxisSize.min,
86 | children: [
87 | const Text('🤖'),
88 | const SizedBox(width: 2),
89 | Container(
90 | constraints: const BoxConstraints(maxWidth: 300),
91 | child: AnimatedTextKit(
92 | isRepeatingAnimation: false,
93 | animatedTexts: [
94 | for (var message in messages) TyperAnimatedText(message, speed: const Duration(milliseconds: 90)),
95 | ],
96 | onNext: (index, isLast) {
97 | debugPrint('index: $index, isLast: $isLast');
98 | if (isLast) {
99 | onFinished();
100 | }
101 | }),
102 | ),
103 | ],
104 | ),
105 | );
106 | }
107 | }
108 |
--------------------------------------------------------------------------------
/lib/features/onboarding/components/body_2.dart:
--------------------------------------------------------------------------------
1 | import 'package:animated_text_kit/animated_text_kit.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_hooks/flutter_hooks.dart';
4 | import 'package:todomaker/features/onboarding/components/event.dart';
5 |
6 | class OnboardingBody2 extends HookWidget {
7 | const OnboardingBody2({
8 | super.key,
9 | required this.index,
10 | });
11 |
12 | final ValueNotifier index;
13 |
14 | @override
15 | Widget build(BuildContext context) {
16 | final animationController = useAnimationController(duration: const Duration(milliseconds: 400));
17 | index.addListener(() {
18 | if (index.value == OnboardingEvent.showTapLabel) {
19 | animationController.forward();
20 | }
21 | });
22 | return GestureDetector(
23 | onTap: () => index.value = OnboardingEvent.secondPage,
24 | child: Stack(
25 | alignment: Alignment.bottomCenter,
26 | children: [
27 | Column(
28 | crossAxisAlignment: CrossAxisAlignment.center,
29 | children: [
30 | const Spacer(),
31 | OnboardingBot1(
32 | messages: const [
33 | 'こんにちは!',
34 | 'TODOMakerへようこそ',
35 | ],
36 | onFinished: () => index.value = OnboardingEvent.showTapLabel,
37 | ),
38 | const Spacer(),
39 | ],
40 | ),
41 | if (index.value.index <= OnboardingEvent.showTapLabel.index) ...[
42 | Positioned(
43 | bottom: 20,
44 | child: SizedBox(
45 | height: 100,
46 | child: FadeTransition(
47 | opacity: Tween(begin: 0, end: 1).animate(
48 | CurvedAnimation(
49 | parent: animationController,
50 | curve: Curves.easeInOut,
51 | ),
52 | ),
53 | child: const Text(
54 | 'タップして次へ',
55 | style: TextStyle(
56 | fontSize: 24,
57 | fontWeight: FontWeight.w500,
58 | color: Colors.black,
59 | shadows: [Shadow(color: Colors.blueGrey, blurRadius: 2)],
60 | ),
61 | ),
62 | ),
63 | ),
64 | ),
65 | ],
66 | ],
67 | ),
68 | );
69 | }
70 | }
71 |
72 | class OnboardingBot1 extends StatelessWidget {
73 | final List messages;
74 | final VoidCallback onFinished;
75 |
76 | const OnboardingBot1({super.key, required this.messages, required this.onFinished});
77 |
78 | @override
79 | Widget build(BuildContext context) {
80 | return SizedBox(
81 | width: double.infinity,
82 | height: 100,
83 | child: Row(
84 | mainAxisAlignment: MainAxisAlignment.center,
85 | mainAxisSize: MainAxisSize.min,
86 | children: [
87 | const Text('🤖'),
88 | const SizedBox(width: 2),
89 | Container(
90 | constraints: const BoxConstraints(maxWidth: 300),
91 | child: AnimatedTextKit(
92 | isRepeatingAnimation: false,
93 | animatedTexts: [
94 | for (var message in messages) ...[
95 | TyperAnimatedText(
96 | message,
97 | speed: const Duration(milliseconds: 90),
98 | ),
99 | ],
100 | ],
101 | onNext: (index, isLast) {
102 | debugPrint('index: $index, isLast: $isLast');
103 | if (isLast) {
104 | onFinished();
105 | }
106 | }),
107 | ),
108 | ],
109 | ),
110 | );
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/lib/features/onboarding/components/event.dart:
--------------------------------------------------------------------------------
1 | enum OnboardingEvent {
2 | initial,
3 | showTapLabel,
4 | secondPage,
5 | }
6 |
--------------------------------------------------------------------------------
/lib/features/onboarding/const.dart:
--------------------------------------------------------------------------------
1 | const onboardingResolvedKey = 'onboardingResolvedKey';
2 |
--------------------------------------------------------------------------------
/lib/features/onboarding/page.dart:
--------------------------------------------------------------------------------
1 | import 'package:animated_text_kit/animated_text_kit.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_hooks/flutter_hooks.dart';
4 | import 'package:todomaker/features/onboarding/components/body_1.dart';
5 | import 'package:todomaker/features/onboarding/components/event.dart';
6 |
7 | class OnboardingPage extends HookWidget {
8 | final ValueNotifier isOnboardingResolved;
9 | const OnboardingPage({super.key, required this.isOnboardingResolved});
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | final index = useState(OnboardingEvent.initial);
14 |
15 | return Scaffold(
16 | body: DefaultTextStyle(
17 | style: const TextStyle(
18 | fontSize: 24,
19 | fontWeight: FontWeight.w500,
20 | color: Colors.black,
21 | shadows: [
22 | Shadow(color: Colors.blueGrey, blurRadius: 2),
23 | ],
24 | ),
25 | child: SafeArea(
26 | child: Stack(
27 | children: [
28 | if (index.value.index <= OnboardingEvent.showTapLabel.index) ...[
29 | OnboardingBody1(index: index),
30 | ],
31 | ],
32 | ),
33 | ),
34 | ),
35 | );
36 | }
37 | }
38 |
39 | class OnboardingBot extends StatelessWidget {
40 | final List messages;
41 |
42 | const OnboardingBot({super.key, required this.messages});
43 |
44 | @override
45 | Widget build(BuildContext context) {
46 | return SizedBox(
47 | width: double.infinity,
48 | child: Row(
49 | mainAxisSize: MainAxisSize.min,
50 | children: [
51 | const Text('🤖'),
52 | const SizedBox(width: 2),
53 | Container(
54 | constraints: const BoxConstraints(maxWidth: 300),
55 | child: AnimatedTextKit(
56 | repeatForever: true,
57 | animatedTexts: [
58 | for (var message in messages) TyperAnimatedText(message, speed: const Duration(milliseconds: 60)),
59 | ],
60 | ),
61 | ),
62 | ],
63 | ),
64 | );
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/lib/features/onboarding/resolver.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_hooks/flutter_hooks.dart';
3 | import 'package:hooks_riverpod/hooks_riverpod.dart';
4 | import 'package:todomaker/features/onboarding/const.dart';
5 | import 'package:todomaker/features/onboarding/page.dart';
6 | import 'package:todomaker/provider/shared_preferences.dart';
7 |
8 | class OnboardingResolver extends HookConsumerWidget {
9 | final WidgetBuilder builder;
10 |
11 | const OnboardingResolver({super.key, required this.builder});
12 |
13 | @override
14 | Widget build(BuildContext context, WidgetRef ref) {
15 | final isOnboardingResolved = ref.watch(sharedPreferencesProvider).getBool(onboardingResolvedKey);
16 | final isOnboardingResolvedState = useState(isOnboardingResolved ?? false);
17 | isOnboardingResolvedState.addListener(() {
18 | if (isOnboardingResolvedState.value) {
19 | return;
20 | }
21 | });
22 |
23 | if (isOnboardingResolvedState.value) {
24 | return builder(context);
25 | }
26 |
27 | return OnboardingPage(isOnboardingResolved: isOnboardingResolvedState);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/features/root/page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:hooks_riverpod/hooks_riverpod.dart';
3 | import 'package:todomaker/features/home/page.dart';
4 | import 'package:todomaker/features/root/resolver/app_user_create.dart';
5 | import 'package:todomaker/features/root/resolver/auth.dart';
6 | import 'package:todomaker/features/root/resolver/database.dart';
7 | import 'package:todomaker/features/root/resolver/force_update.dart';
8 |
9 | class RootPage extends HookConsumerWidget {
10 | const RootPage({super.key});
11 |
12 | @override
13 | Widget build(BuildContext context, WidgetRef ref) {
14 | return MediaQuery(
15 | data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true),
16 | child: AuthResolver(builder: (context, user) {
17 | return UserDatabaseResolver(builder: (context) {
18 | return AppUserCreateResolver(builder: (context) {
19 | return ForceUpdateResolver(builder: (context) {
20 | // return OnboardingResolver(builder: (context) {
21 | return const HomePage();
22 | // });
23 | });
24 | });
25 | });
26 | }),
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/lib/features/root/resolver/app_user_create.dart:
--------------------------------------------------------------------------------
1 | // ignore: implementation_imports
2 | import 'package:cloud_firestore/cloud_firestore.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:flutter_hooks/flutter_hooks.dart';
5 | import 'package:hooks_riverpod/hooks_riverpod.dart';
6 | import 'package:todomaker/components/loading/indicator.dart';
7 | import 'package:todomaker/components/retry/page.dart';
8 | import 'package:todomaker/entity/app_user.dart';
9 | import 'package:todomaker/features/root/resolver/database.dart';
10 | import 'package:riverpod_annotation/riverpod_annotation.dart';
11 | import 'package:todomaker/provider/app_user.dart';
12 |
13 | part 'app_user_create.g.dart';
14 |
15 | class AppUserCreate {
16 | final UserDatabase userDatabase;
17 |
18 | AppUserCreate({required this.userDatabase});
19 |
20 | Future call() async {
21 | final docRef = userDatabase.userReference();
22 | final docData = await docRef.get();
23 | if (docData.exists) {
24 | return;
25 | }
26 | await docRef.set(
27 | AppUser(id: docRef.id),
28 | SetOptions(merge: true),
29 | );
30 | }
31 | }
32 |
33 | @Riverpod(dependencies: [userDatabase])
34 | AppUserCreate appUserCreate(AppUserCreateRef ref) {
35 | return AppUserCreate(userDatabase: ref.watch(userDatabaseProvider));
36 | }
37 |
38 | class AppUserCreateResolver extends HookConsumerWidget {
39 | final Widget Function(BuildContext) builder;
40 |
41 | const AppUserCreateResolver({super.key, required this.builder});
42 |
43 | @override
44 | Widget build(BuildContext context, WidgetRef ref) {
45 | final appUser = ref.watch(appUserProvider);
46 | final appUserCreate = ref.watch(appUserCreateProvider);
47 |
48 | useEffect(() {
49 | appUserCreate();
50 | return null;
51 | }, []);
52 |
53 | return Retry(
54 | retry: () => ref.invalidate(appUserProvider),
55 | child: () {
56 | return appUser.when(
57 | data: (appUser) => builder(context),
58 | error: (e, st) => RetryPage(
59 | exception: e,
60 | stackTrace: st,
61 | ),
62 | loading: () => const IndicatorPage(),
63 | );
64 | }(),
65 | );
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/lib/features/root/resolver/app_user_create.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'app_user_create.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$appUserCreateHash() => r'5347e10fddf40fb2c852b6fe39caccd7d8b96058';
10 |
11 | /// See also [appUserCreate].
12 | @ProviderFor(appUserCreate)
13 | final appUserCreateProvider = AutoDisposeProvider.internal(
14 | appUserCreate,
15 | name: r'appUserCreateProvider',
16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$appUserCreateHash,
17 | dependencies: [userDatabaseProvider],
18 | allTransitiveDependencies: {userDatabaseProvider, ...?userDatabaseProvider.allTransitiveDependencies},
19 | );
20 |
21 | typedef AppUserCreateRef = AutoDisposeProviderRef;
22 | // ignore_for_file: type=lint
23 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
24 |
--------------------------------------------------------------------------------
/lib/features/root/resolver/auth.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_auth/firebase_auth.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:flutter_hooks/flutter_hooks.dart';
4 | import 'package:hooks_riverpod/hooks_riverpod.dart';
5 | import 'package:todomaker/components/loading/indicator.dart';
6 | import 'package:todomaker/components/retry/page.dart';
7 | import 'package:riverpod_annotation/riverpod_annotation.dart';
8 |
9 | part 'auth.g.dart';
10 |
11 | @Riverpod(keepAlive: true, dependencies: [])
12 | Stream firebaseUserChanges(FirebaseUserChangesRef ref) {
13 | return FirebaseAuth.instance.userChanges();
14 | }
15 |
16 | class AuthResolver extends HookConsumerWidget {
17 | final Widget Function(BuildContext, User) builder;
18 |
19 | const AuthResolver({super.key, required this.builder});
20 |
21 | @override
22 | Widget build(BuildContext context, WidgetRef ref) {
23 | final firebaseUserChanges = ref.watch(firebaseUserChangesProvider);
24 |
25 | return Retry(
26 | retry: () => ref.invalidate(firebaseUserChangesProvider),
27 | child: () {
28 | return firebaseUserChanges.when(
29 | data: (user) {
30 | debugPrint('user.uid: ${user?.uid}');
31 | if (user == null) {
32 | return SignInResolver(
33 | builder: (context, user) => builder(context, user),
34 | );
35 | } else {
36 | return builder(context, user);
37 | }
38 | },
39 | error: (e, st) => RetryPage(exception: e, stackTrace: st),
40 | loading: () => const IndicatorPage(),
41 | );
42 | }(),
43 | );
44 | }
45 | }
46 |
47 | class SignInResolver extends HookConsumerWidget {
48 | final Widget Function(BuildContext, User) builder;
49 |
50 | const SignInResolver({super.key, required this.builder});
51 |
52 | @override
53 | Widget build(BuildContext context, WidgetRef ref) {
54 | final user = useState(FirebaseAuth.instance.currentUser);
55 | final userValue = user.value;
56 |
57 | useEffect(() {
58 | void f() async {
59 | if (userValue == null) {
60 | await FirebaseAuth.instance.signInAnonymously();
61 | }
62 | }
63 |
64 | f();
65 | return null;
66 | }, [user.value]);
67 |
68 | if (userValue == null) {
69 | return const IndicatorPage();
70 | } else {
71 | return builder(context, userValue);
72 | }
73 | }
74 | }
75 |
--------------------------------------------------------------------------------
/lib/features/root/resolver/auth.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'auth.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$firebaseUserChangesHash() => r'7640fb519be2781b52dfbaa306b26ede6d0fc3cc';
10 |
11 | /// See also [firebaseUserChanges].
12 | @ProviderFor(firebaseUserChanges)
13 | final firebaseUserChangesProvider = StreamProvider.internal(
14 | firebaseUserChanges,
15 | name: r'firebaseUserChangesProvider',
16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$firebaseUserChangesHash,
17 | dependencies: const [],
18 | allTransitiveDependencies: const {},
19 | );
20 |
21 | typedef FirebaseUserChangesRef = StreamProviderRef;
22 | // ignore_for_file: type=lint
23 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
24 |
--------------------------------------------------------------------------------
/lib/features/root/resolver/database.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'database.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$userDatabaseHash() => r'f303f1ceb234a43ec9b0dcdeace28917c9f85c14';
10 |
11 | /// See also [userDatabase].
12 | @ProviderFor(userDatabase)
13 | final userDatabaseProvider = Provider.internal(
14 | userDatabase,
15 | name: r'userDatabaseProvider',
16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$userDatabaseHash,
17 | dependencies: [firebaseUserChangesProvider],
18 | allTransitiveDependencies: {firebaseUserChangesProvider, ...?firebaseUserChangesProvider.allTransitiveDependencies},
19 | );
20 |
21 | typedef UserDatabaseRef = ProviderRef;
22 | // ignore_for_file: type=lint
23 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
24 |
--------------------------------------------------------------------------------
/lib/features/root/resolver/force_update.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:flutter_hooks/flutter_hooks.dart';
3 | import 'package:hooks_riverpod/hooks_riverpod.dart';
4 | import 'package:todomaker/components/alert/ok.dart';
5 | import 'package:todomaker/components/loading/indicator.dart';
6 | import 'package:todomaker/provider/force_update.dart';
7 | import 'package:todomaker/utils/analytics/error.dart';
8 | import 'package:todomaker/utils/config/platform.dart';
9 | import 'package:url_launcher/url_launcher.dart';
10 |
11 | class ForceUpdateResolver extends HookConsumerWidget {
12 | final Widget Function(BuildContext) builder;
13 |
14 | const ForceUpdateResolver({
15 | super.key,
16 | required this.builder,
17 | });
18 |
19 | @override
20 | Widget build(BuildContext context, WidgetRef ref) {
21 | final checkForceUpdate = ref.watch(checkForceUpdateProvider);
22 | final shouldForceUpdate = useState(false);
23 | useEffect(() {
24 | void f() async {
25 | try {
26 | // 多少データが変更される可能性があるが、それは許容する。ほとんど問題はないはず
27 | // 起動時間を優先する
28 | if (await checkForceUpdate()) {
29 | shouldForceUpdate.value = true;
30 | }
31 | } catch (e, st) {
32 | errorLogger.recordError(e, st);
33 | }
34 | }
35 |
36 | f();
37 | return null;
38 | }, [checkForceUpdate]);
39 |
40 | if (shouldForceUpdate.value) {
41 | Future.microtask(() async {
42 | if (context.mounted) {
43 | await showOKDialog(context,
44 | icon: Icons.error, title: 'アプリをアップデートしてください', message: 'お使いのアプリのバージョンのアップデートをお願いしております。$storeNameから最新バージョンにアップデートしてください', ok: () async {
45 | await launchUrl(
46 | Uri.parse(forceUpdateStoreURL),
47 | mode: LaunchMode.externalApplication,
48 | );
49 | });
50 | }
51 | });
52 | return const IndicatorPage();
53 | }
54 |
55 | return builder(context);
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/lib/features/task/components/location/ask.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:hooks_riverpod/hooks_riverpod.dart';
3 | import 'package:todomaker/components/error/error_alert.dart';
4 | import 'package:todomaker/components/location/form.dart';
5 | import 'package:todomaker/entity/task.dart';
6 | import 'package:todomaker/provider/task.dart';
7 | import 'package:todomaker/utils/functions/firebase_functions.dart';
8 |
9 | class TaskLocationAskAI extends HookConsumerWidget {
10 | final TaskPrepared task;
11 | const TaskLocationAskAI({super.key, required this.task});
12 |
13 | @override
14 | Widget build(BuildContext context, WidgetRef ref) {
15 | final taskFillLocation = ref.watch(taskFillLocationProvider);
16 | final locations = task.locations;
17 |
18 | return TextButton(
19 | style: TextButton.styleFrom(
20 | padding: const EdgeInsets.symmetric(vertical: 0),
21 | tapTargetSize: MaterialTapTargetSize.shrinkWrap,
22 | minimumSize: Size.zero,
23 | ),
24 | onPressed: () async {
25 | showDialog(
26 | context: context,
27 | builder: (context) => LocationForm(
28 | onSubmit: (userLocation) async {
29 | try {
30 | await taskFillLocation(taskID: task.id, userLocation: userLocation);
31 | await functions.fillLocation(
32 | taskID: task.id,
33 | userLocation: userLocation,
34 | );
35 | } catch (e) {
36 | if (context.mounted) {
37 | showErrorAlert(context, e.toString());
38 | }
39 | }
40 | },
41 | ),
42 | );
43 | },
44 | child: Row(
45 | crossAxisAlignment: CrossAxisAlignment.baseline,
46 | textBaseline: TextBaseline.ideographic,
47 | children: [
48 | const Text('🤖'),
49 | const SizedBox(width: 2),
50 | if (locations != null && locations.isNotEmpty) ...[
51 | const Expanded(child: Text('関連する位置情報・会場・場所をAIに聞き直す')),
52 | ] else ...[
53 | const Expanded(child: Text('関連する位置情報・会場・場所をAIに聞く')),
54 | ],
55 | ],
56 | ),
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/lib/features/todo/components/memo.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:todomaker/entity/todo.dart';
3 |
4 | class TodoListMemoTile extends StatelessWidget {
5 | final Todo todo;
6 | const TodoListMemoTile({
7 | super.key,
8 | required this.todo,
9 | });
10 |
11 | @override
12 | Widget build(BuildContext context) {
13 | return ListTile(
14 | leading: const Icon(Icons.edit),
15 | title: const Text('メモ'),
16 | onTap: () async {},
17 | );
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/lib/features/todo_locations/page.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:todomaker/components/todo_locations/list.dart';
3 | import 'package:todomaker/entity/todo.dart';
4 |
5 | class TodoLocationsPage extends StatelessWidget {
6 | final List todos;
7 | const TodoLocationsPage({super.key, required this.todos});
8 |
9 | @override
10 | Widget build(BuildContext context) {
11 | return Scaffold(
12 | appBar: AppBar(
13 | title: const Text('関連場所一覧'),
14 | ),
15 | body: SafeArea(
16 | child: SingleChildScrollView(
17 | padding: const EdgeInsets.symmetric(vertical: 20),
18 | child: TodoLocationList(todos: todos),
19 | ),
20 | ),
21 | );
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:firebase_core/firebase_core.dart';
4 | import 'package:firebase_crashlytics/firebase_crashlytics.dart';
5 | import 'package:flutter/foundation.dart';
6 | import 'package:flutter/material.dart';
7 | import 'package:geocoding/geocoding.dart';
8 | import 'package:hooks_riverpod/hooks_riverpod.dart';
9 | import 'package:shared_preferences/shared_preferences.dart';
10 | import 'package:todomaker/app.dart';
11 | import 'package:todomaker/provider/shared_preferences.dart';
12 | import 'package:todomaker/utils/config/remote_config.dart';
13 | import 'package:todomaker/utils/log/debug_print.dart';
14 |
15 | void main() async {
16 | runZonedGuarded(() async {
17 | if (kDebugMode) {
18 | overrideDebugPrint();
19 | }
20 | WidgetsFlutterBinding.ensureInitialized();
21 | await Firebase.initializeApp();
22 |
23 | final (sharedPreferences, _) = await (
24 | SharedPreferences.getInstance(),
25 | setupRemoteConfig(),
26 | ).wait;
27 |
28 | // MEMO: FirebaseCrashlytics#recordFlutterError called dumpErrorToConsole in function.
29 | FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError;
30 |
31 | setLocaleIdentifier('ja');
32 |
33 | runApp(
34 | ProviderScope(
35 | overrides: [
36 | sharedPreferencesProvider.overrideWith((ref) => sharedPreferences),
37 | ],
38 | child: const App(),
39 | ),
40 | );
41 | }, (error, stack) => FirebaseCrashlytics.instance.recordError(error, stack));
42 | }
43 |
--------------------------------------------------------------------------------
/lib/provider/app_user.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_auth/firebase_auth.dart';
2 | import 'package:riverpod_annotation/riverpod_annotation.dart';
3 | import 'package:todomaker/entity/app_user.dart';
4 | import 'package:todomaker/features/root/resolver/database.dart';
5 |
6 | part 'app_user.g.dart';
7 |
8 | @Riverpod(dependencies: [appUser])
9 | String appUserID(AppUserIDRef ref) {
10 | return ref.watch(appUserProvider).asData?.value.id ?? FirebaseAuth.instance.currentUser!.uid;
11 | }
12 |
13 | @Riverpod(dependencies: [userDatabase])
14 | Stream appUser(AppUserRef ref) {
15 | final database = ref.watch(userDatabaseProvider);
16 | return database.userReference().snapshots().map((event) => event.data()!);
17 | }
18 |
--------------------------------------------------------------------------------
/lib/provider/app_user.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'app_user.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$appUserIDHash() => r'4f06a266d5801bd41331f035ac4916bc2380ea3f';
10 |
11 | /// See also [appUserID].
12 | @ProviderFor(appUserID)
13 | final appUserIDProvider = AutoDisposeProvider.internal(
14 | appUserID,
15 | name: r'appUserIDProvider',
16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$appUserIDHash,
17 | dependencies: [appUserProvider],
18 | allTransitiveDependencies: {appUserProvider, ...?appUserProvider.allTransitiveDependencies},
19 | );
20 |
21 | typedef AppUserIDRef = AutoDisposeProviderRef;
22 | String _$appUserHash() => r'd4db768e8c250fd8a19d1bb85ead8d5df365a312';
23 |
24 | /// See also [appUser].
25 | @ProviderFor(appUser)
26 | final appUserProvider = AutoDisposeStreamProvider.internal(
27 | appUser,
28 | name: r'appUserProvider',
29 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$appUserHash,
30 | dependencies: [userDatabaseProvider],
31 | allTransitiveDependencies: {userDatabaseProvider, ...?userDatabaseProvider.allTransitiveDependencies},
32 | );
33 |
34 | typedef AppUserRef = AutoDisposeStreamProviderRef;
35 | // ignore_for_file: type=lint
36 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
37 |
--------------------------------------------------------------------------------
/lib/provider/force_update.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:hooks_riverpod/hooks_riverpod.dart';
4 | import 'package:todomaker/entity/remote_config_parameter.dart';
5 | import 'package:todomaker/provider/remote_config_parameter.dart';
6 | import 'package:todomaker/utils/analytics/analytics.dart';
7 | import 'package:todomaker/utils/config/version.dart';
8 |
9 | final checkForceUpdateProvider = Provider.autoDispose(
10 | (ref) => CheckForceUpdate(
11 | remoteConfigParameter: ref.watch(
12 | remoteConfigParameterProvider,
13 | ),
14 | ),
15 | );
16 |
17 | class CheckForceUpdate {
18 | final RemoteConfigParameter remoteConfigParameter;
19 |
20 | CheckForceUpdate({required this.remoteConfigParameter});
21 |
22 | // Return false: should not force update
23 | // Return true: should force update
24 | Future call() async {
25 | final config = remoteConfigParameter;
26 | final packageVersion = await Version.fromPackage();
27 |
28 | final forceUpdate = packageVersion.isLessThan(Version.parse(config.minimumAppVersion));
29 | if (forceUpdate) {
30 | analytics.logEvent(
31 | name: 'screen_type_force_update',
32 | parameters: {
33 | 'package_version': packageVersion.toString(),
34 | 'minimum_app_version': config.minimumAppVersion,
35 | },
36 | );
37 | }
38 | return forceUpdate;
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/lib/provider/remote_config_parameter.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_remote_config/firebase_remote_config.dart';
2 | import 'package:todomaker/entity/remote_config_parameter.dart';
3 | import 'package:todomaker/utils/config/remote_config.dart';
4 | import 'package:riverpod_annotation/riverpod_annotation.dart';
5 |
6 | part 'remote_config_parameter.g.dart';
7 |
8 | @Riverpod()
9 | RemoteConfigParameter remoteConfigParameter(RemoteConfigParameterRef ref) {
10 | // fetchAndActiveをentrypointで完了しているので値が取れる想定
11 | return RemoteConfigParameter(
12 | minimumAppVersion: remoteConfig.getStringOrDefault(
13 | RemoteConfigKeys.minimumAppVersion,
14 | RemoteConfigParameterDefaultValues.minimumAppVersion,
15 | ),
16 | );
17 | }
18 |
19 | extension RemoteConfigExt on FirebaseRemoteConfig {
20 | bool getBoolOrDefault(String key, bool defaultValue) {
21 | try {
22 | return getAll().containsKey(key) ? getBool(key) : defaultValue;
23 | } catch (error) {
24 | return defaultValue;
25 | }
26 | }
27 |
28 | int getIntOrDefault(String key, int defaultValue) {
29 | try {
30 | return getAll().containsKey(key) ? getInt(key) : defaultValue;
31 | } catch (error) {
32 | return defaultValue;
33 | }
34 | }
35 |
36 | String getStringOrDefault(String key, String defaultValue) {
37 | try {
38 | return getAll().containsKey(key) ? getString(key) : defaultValue;
39 | } catch (error) {
40 | return defaultValue;
41 | }
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/lib/provider/remote_config_parameter.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'remote_config_parameter.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$remoteConfigParameterHash() => r'ad696ca938d52e5a3c3c4b9f3ab9be996f5fcaac';
10 |
11 | /// See also [remoteConfigParameter].
12 | @ProviderFor(remoteConfigParameter)
13 | final remoteConfigParameterProvider = AutoDisposeProvider.internal(
14 | remoteConfigParameter,
15 | name: r'remoteConfigParameterProvider',
16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$remoteConfigParameterHash,
17 | dependencies: null,
18 | allTransitiveDependencies: null,
19 | );
20 |
21 | typedef RemoteConfigParameterRef = AutoDisposeProviderRef;
22 | // ignore_for_file: type=lint
23 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
24 |
--------------------------------------------------------------------------------
/lib/provider/shared_preferences.dart:
--------------------------------------------------------------------------------
1 | import 'package:riverpod_annotation/riverpod_annotation.dart';
2 | import 'package:shared_preferences/shared_preferences.dart';
3 |
4 | part 'shared_preferences.g.dart';
5 |
6 | // overrideする前提なので.autoDisposeはつけない=(keeyAlive: trueにする)。またこれに依存したProviderもkeepAlive: trueにする必要がある
7 | @Riverpod(keepAlive: true, dependencies: [])
8 | SharedPreferences sharedPreferences(SharedPreferencesRef ref) {
9 | throw UnimplementedError('sharedPreferencesProvider is not implemented');
10 | }
11 |
--------------------------------------------------------------------------------
/lib/provider/shared_preferences.g.dart:
--------------------------------------------------------------------------------
1 | // GENERATED CODE - DO NOT MODIFY BY HAND
2 |
3 | part of 'shared_preferences.dart';
4 |
5 | // **************************************************************************
6 | // RiverpodGenerator
7 | // **************************************************************************
8 |
9 | String _$sharedPreferencesHash() => r'9bef4c6887ebe74597e857ffe72ade05522602aa';
10 |
11 | /// See also [sharedPreferences].
12 | @ProviderFor(sharedPreferences)
13 | final sharedPreferencesProvider = Provider.internal(
14 | sharedPreferences,
15 | name: r'sharedPreferencesProvider',
16 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null : _$sharedPreferencesHash,
17 | dependencies: const [],
18 | allTransitiveDependencies: const {},
19 | );
20 |
21 | typedef SharedPreferencesRef = ProviderRef;
22 | // ignore_for_file: type=lint
23 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member
24 |
--------------------------------------------------------------------------------
/lib/provider/task.dart:
--------------------------------------------------------------------------------
1 | import 'package:cloud_firestore/cloud_firestore.dart';
2 | import 'package:riverpod_annotation/riverpod_annotation.dart';
3 | import 'package:todomaker/entity/location_form.dart';
4 | import 'package:todomaker/entity/task.dart';
5 | import 'package:todomaker/features/root/resolver/database.dart';
6 |
7 | part 'task.g.dart';
8 |
9 | @Riverpod(dependencies: [userDatabase])
10 | Stream> tasks(TasksRef ref) {
11 | final database = ref.watch(userDatabaseProvider);
12 | return database
13 | .tasksReference()
14 | .orderBy('serverCreatedDateTime', descending: true)
15 | .snapshots()
16 | .map((event) => event.docs.map((doc) => doc.data()).toList());
17 | }
18 |
19 | @Riverpod(dependencies: [userDatabase])
20 | Stream task(TaskRef ref, {required String taskID}) {
21 | final database = ref.watch(userDatabaseProvider);
22 | return database.taskReference(taskID: taskID).snapshots().map((event) => event.data()!);
23 | }
24 |
25 | class TaskCreate {
26 | final UserDatabase database;
27 |
28 | TaskCreate({required this.database});
29 |
30 | Future call({required String question}) async {
31 | final docRef = database.tasksReference().doc();
32 | final task = TaskPreparing(
33 | id: docRef.id,
34 | userID: database.userID,
35 | question: question,
36 | );
37 | await database.taskReference(taskID: task.id).set(task, SetOptions(merge: true));
38 | return task;
39 | }
40 | }
41 |
42 | @Riverpod(dependencies: [userDatabase])
43 | TaskCreate taskCreate(TaskCreateRef ref) {
44 | final database = ref.watch(userDatabaseProvider);
45 | return TaskCreate(database: database);
46 | }
47 |
48 | class TaskDelete {
49 | final UserDatabase database;
50 |
51 | TaskDelete({required this.database});
52 |
53 | Future call({required String taskID}) {
54 | return database.taskReference(taskID: taskID).delete();
55 | }
56 | }
57 |
58 | @Riverpod(dependencies: [userDatabase])
59 | TaskDelete taskDelete(TaskDeleteRef ref) {
60 | final database = ref.watch(userDatabaseProvider);
61 | return TaskDelete(database: database);
62 | }
63 |
64 | class TaskComplete {
65 | final UserDatabase database;
66 |
67 | TaskComplete({required this.database});
68 |
69 | Future call({required String taskID}) {
70 | return database.taskReference(taskID: taskID).update({'completedDateTime': DateTime.now()});
71 | }
72 | }
73 |
74 | @Riverpod(dependencies: [userDatabase])
75 | TaskComplete taskComplete(TaskCompleteRef ref) {
76 | final database = ref.watch(userDatabaseProvider);
77 | return TaskComplete(database: database);
78 | }
79 |
80 | class TaskRevertComplete {
81 | final UserDatabase database;
82 |
83 | TaskRevertComplete({required this.database});
84 |
85 | Future call({required String taskID}) {
86 | return database.taskReference(taskID: taskID).update({'completedDateTime': null});
87 | }
88 | }
89 |
90 | @Riverpod(dependencies: [userDatabase])
91 | TaskRevertComplete taskRevertComplete(TaskRevertCompleteRef ref) {
92 | final database = ref.watch(userDatabaseProvider);
93 | return TaskRevertComplete(database: database);
94 | }
95 |
96 | class TaskFillLocation {
97 | final UserDatabase database;
98 |
99 | TaskFillLocation({required this.database});
100 |
101 | Future call({required String taskID, required LocationFormInfo userLocation}) {
102 | return database.taskReference(taskID: taskID).update(
103 | {
104 | 'userLocation': userLocation.toJson(),
105 | 'locations': null,
106 | 'locationsAITextResponse': null,
107 | 'locationsGroundings': null,
108 | },
109 | );
110 | }
111 | }
112 |
113 | @Riverpod(dependencies: [userDatabase])
114 | TaskFillLocation taskFillLocation(TaskFillLocationRef ref) {
115 | final database = ref.watch(userDatabaseProvider);
116 | return TaskFillLocation(database: database);
117 | }
118 |
--------------------------------------------------------------------------------
/lib/style/button.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:todomaker/style/color.dart';
3 |
4 | ButtonStyle get filledWidthButtonStyle => ButtonStyle(
5 | minimumSize: WidgetStateProperty.all(const Size(double.infinity, 48.0)),
6 | );
7 |
8 | ButtonStyle capsuleTextButtonStyle(BuildContext context) {
9 | final foregroundColor = Theme.of(context).colorScheme.primary;
10 | return TextButton.styleFrom(
11 | foregroundColor: foregroundColor,
12 | backgroundColor: Colors.white,
13 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
14 | shape: const StadiumBorder(),
15 | elevation: 1,
16 | );
17 | }
18 |
19 | ButtonStyle alertButtonStyle(BuildContext context) {
20 | final foregroundColor = Theme.of(context).colorScheme.primary;
21 | return TextButton.styleFrom(
22 | foregroundColor: foregroundColor,
23 | disabledForegroundColor: TextColor.disabled(TextColor.gray),
24 | backgroundColor: Colors.white,
25 | padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
26 | shape: const StadiumBorder(),
27 | textStyle: const TextStyle(
28 | fontWeight: FontWeight.w600,
29 | fontSize: 14,
30 | ),
31 | elevation: 1,
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/lib/style/color.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 |
3 | abstract class AppColors {
4 | static const Color primary = Color(0xFF4072B3);
5 | static const Color secondary = Color(0xFF6088C6);
6 | static const Color tertiary = Color(0xFF80A4D9);
7 | static const Color accent = Color(0xFFEB8686);
8 | static const Color background = Color(0xFFFAFAFA);
9 | static const Color formBackground = Color(0xFFC0C0C0);
10 | static const Color border = Color(0xFFE0E0E0);
11 | static const Color disabled = Color(0xFFE0E0E0);
12 |
13 | static const Color sunday = Color(0xFFE17F7F);
14 | static const Color saturday = Color(0xFF7FB9E1);
15 | static const Color weekday = Color(0xFF7E7E7E);
16 | }
17 |
18 | abstract class TextColor {
19 | static const Color main = AppColors.primary;
20 | static const Color darkGray = Color(0xFF404040);
21 | static const Color gray = Color(0xFF928484);
22 | static const Color black = Colors.black;
23 | static const Color white = Colors.white;
24 | static const Color link = Colors.blueAccent;
25 | static const Color secondaryLink = darkGray;
26 | static const Color danger = Color(0xFFB00020);
27 | static const Color discount = Color(0xFFB00020);
28 |
29 | static Color highEmphasis(Color color) => color.withOpacity(0.87);
30 | static Color mediumEmphasis(Color color) => color.withOpacity(0.6);
31 | static Color disabled(Color color) => color.withOpacity(0.37);
32 | }
33 |
--------------------------------------------------------------------------------
/lib/utils/analytics/analytics.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:firebase_analytics/firebase_analytics.dart';
4 | import 'package:flutter/foundation.dart';
5 |
6 | final firebaseAnalytics = FirebaseAnalytics.instance;
7 |
8 | var analyticsDebugIsEnabled = false;
9 |
10 | class Analytics {
11 | void debug({required String name, Map? parameters}) async {
12 | if (analyticsDebugIsEnabled || kDebugMode) {
13 | logEvent(name: name, parameters: parameters);
14 | }
15 | }
16 |
17 | void logEvent({required String name, Map? parameters}) async {
18 | assert(name.length <= 40, 'firebase analytics log event name limit length up to 40');
19 | if (kDebugMode) {
20 | print('[INFO] logEvent name: $name, parameters: $parameters');
21 | }
22 |
23 | if (parameters != null) {
24 | for (final key in parameters.keys) {
25 | final param = parameters[key];
26 | if (param is DateTime) {
27 | parameters[key] = param.toIso8601String();
28 | }
29 | if (param is bool) {
30 | parameters[key] = param ? 'true' : 'false';
31 | }
32 | }
33 | }
34 | try {
35 | await firebaseAnalytics.logEvent(name: name, parameters: parameters);
36 | } catch (e) {
37 | debugPrint('analytics error: $e');
38 | }
39 | }
40 |
41 | void setCurrentScreen({required String screenName, String screenClassOverride = 'Flutter'}) async {
42 | unawaited(firebaseAnalytics.logEvent(name: 'screen_$screenName'));
43 | return firebaseAnalytics.logScreenView(screenName: screenName);
44 | }
45 |
46 | /// Up to 25 user property names are supported.
47 | // The "firebase_" prefix is reserved and should not be used for
48 | /// user property names.
49 | void setUserProperties(String name, value) {
50 | assert(name.toLowerCase() != 'age');
51 | assert(name.toLowerCase() != 'gender');
52 | assert(name.toLowerCase() != 'interest');
53 | assert(name.length < 25, 'firebase setUserProperties name limit length up to 25');
54 | assert(!name.startsWith('firebase_'));
55 |
56 | if (kDebugMode) {
57 | print('[INFO] setUserProperties name: $name, value: $value');
58 | }
59 | firebaseAnalytics.setUserProperty(name: name, value: value);
60 | }
61 | }
62 |
63 | Analytics analytics = Analytics();
64 |
--------------------------------------------------------------------------------
/lib/utils/analytics/error.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 |
3 | import 'package:firebase_crashlytics/firebase_crashlytics.dart';
4 |
5 | ErrorLogger errorLogger = ErrorLogger();
6 |
7 | class ErrorLogger {
8 | void recordError(
9 | dynamic exception,
10 | StackTrace? stack,
11 | ) {
12 | unawaited(FirebaseCrashlytics.instance.recordError(exception, stack));
13 | }
14 |
15 | void log(String message) {
16 | unawaited(FirebaseCrashlytics.instance.log(message));
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/lib/utils/config/platform.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | String get storeName {
4 | return Platform.isIOS ? 'App Store' : 'Google Play';
5 | }
6 |
7 | String get accountName {
8 | return Platform.isIOS ? 'Apple ID' : 'Google アカウント';
9 | }
10 |
11 | String get forceUpdateStoreURL {
12 | return Platform.isIOS ? 'https://apps.apple.com/app/apple-store/id6741352387?pt=97327896&ct=force_update&mt=8' : '';
13 | }
14 |
--------------------------------------------------------------------------------
/lib/utils/config/remote_config.dart:
--------------------------------------------------------------------------------
1 | import 'package:firebase_remote_config/firebase_remote_config.dart';
2 | import 'package:flutter/widgets.dart';
3 | import 'package:todomaker/entity/remote_config_parameter.dart';
4 | import 'package:todomaker/utils/analytics/error.dart';
5 |
6 | final remoteConfig = FirebaseRemoteConfig.instance;
7 |
8 | Future setupRemoteConfig() async {
9 | try {
10 | await (
11 | remoteConfig.setConfigSettings(RemoteConfigSettings(
12 | fetchTimeout: const Duration(minutes: 1),
13 | minimumFetchInterval: const Duration(hours: 1),
14 | )),
15 | remoteConfig.setDefaults({
16 | RemoteConfigKeys.minimumAppVersion: RemoteConfigParameterDefaultValues.minimumAppVersion,
17 | }),
18 | remoteConfig.fetchAndActivate()
19 | ).wait;
20 |
21 | debugPrintRemoteConfig();
22 |
23 | remoteConfig.onConfigUpdated.listen((event) {
24 | remoteConfig.activate();
25 | });
26 | } catch (error, st) {
27 | // ignore error
28 | // ParallelWaitErrorとentrypointでRemoteConfigを導入してからエラーが出るようになった。RemoteConfigの設定は失敗しても最悪どうにかなるだろう。ということでエラーは無視する
29 | debugPrint(error.toString());
30 | errorLogger.recordError(error, st);
31 | }
32 | }
33 |
34 | void debugPrintRemoteConfig() {
35 | for (final entry in remoteConfig.getAll().entries) {
36 | debugPrint('RemoteConfig: ${entry.key} ${entry.value.asString()}');
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/lib/utils/config/version.dart:
--------------------------------------------------------------------------------
1 | import 'package:package_info_plus/package_info_plus.dart';
2 | import 'package:todomaker/utils/platform/environment.dart';
3 |
4 | class Version {
5 | final int major;
6 | final int minor;
7 | final int patch;
8 |
9 | Version({
10 | required this.major,
11 | required this.minor,
12 | required this.patch,
13 | });
14 |
15 | factory Version.parse(String str) {
16 | final splited = str.split('.');
17 | if (Environment.isDevelopment) {
18 | assert(splited.length <= 3 || (splited.last == 'dev' && splited.length == 4), 'unexpected version format $str');
19 | }
20 |
21 | final versions = List.filled(3, 0);
22 | for (int i = 0; i < splited.length; i++) {
23 | if (splited[i] == 'dev') {
24 | break;
25 | }
26 | versions[i] = int.parse(splited[i]);
27 | }
28 | return Version(
29 | major: versions[0],
30 | minor: versions[1],
31 | patch: versions[2],
32 | );
33 | }
34 |
35 | static Future fromPackage() async {
36 | final package = await PackageInfo.fromPlatform();
37 | return Version.parse(package.version);
38 | }
39 |
40 | bool isLessThan(Version other) =>
41 | major < other.major || (major <= other.major && minor < other.minor) || (major <= other.major && minor <= other.minor && patch < other.patch);
42 | bool isGreaterThan(Version other) =>
43 | major > other.major || (major >= other.major && minor > other.minor) || (major >= other.major && minor >= other.minor && patch > other.patch);
44 |
45 | String get version {
46 | return '$major.$minor.$patch';
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/lib/utils/date_time/date_time_ext.dart:
--------------------------------------------------------------------------------
1 | import 'package:todomaker/utils/date_time/weekday.dart';
2 |
3 | // dateTime.addDays(n) だと n * 24 * 60 * 59 * 1000 が足されるので、サマータイムの国ではずれる
4 | extension DateTimeAdd on DateTime {
5 | DateTime addDays(int offset) {
6 | return DateTime(
7 | year,
8 | month,
9 | day + offset,
10 | hour,
11 | minute,
12 | second,
13 | millisecond,
14 | microsecond,
15 | );
16 | }
17 | }
18 |
19 | extension Date on DateTime {
20 | DateTime date() {
21 | return DateTime(year, month, day);
22 | }
23 | }
24 |
25 | bool isSameDay(DateTime lhs, DateTime rhs) => lhs.year == rhs.year && lhs.month == rhs.month && lhs.day == rhs.day;
26 |
27 | bool isSameMonth(DateTime lhs, DateTime rhs) => lhs.year == rhs.year && lhs.month == rhs.month;
28 |
29 | extension DateTimeMonth on DateTime {
30 | bool isPreviousMonth(DateTime date) {
31 | if (isSameMonth(date, this)) {
32 | return false;
33 | }
34 | return isBefore(date);
35 | }
36 | }
37 |
38 | DateTime today() {
39 | return DateTime.now().date();
40 | }
41 |
42 | DateTime now() {
43 | return DateTime.now();
44 | }
45 |
46 | // Reference: https://stackoverflow.com/questions/52713115/flutter-find-the-number-of-days-between-two-dates/67679455#67679455
47 | // 同じ日だと0を返す
48 | int daysBetween(DateTime from, DateTime to) {
49 | from = DateTime(from.year, from.month, from.day);
50 | to = DateTime(to.year, to.month, to.day);
51 | return (to.difference(from).inHours / 24).round();
52 | }
53 |
54 | DateTime firstDayOfWeekday(DateTime day) {
55 | return day.subtract(Duration(days: day.weekday == 7 ? 0 : day.weekday));
56 | }
57 |
58 | DateTime endDayOfWeekday(DateTime day) {
59 | return day.subtract(Duration(days: day.weekday == 7 ? 0 : day.weekday)).addDays(Weekday.values.length - 1);
60 | }
61 |
--------------------------------------------------------------------------------
/lib/utils/date_time/date_time_range_ext.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/material.dart';
2 | import 'package:todomaker/utils/date_time/date_time_ext.dart';
3 |
4 | extension DateTimeRangeExtension on DateTimeRange {
5 | bool containsInDay(DateTime date) {
6 | final isBefore = start.isBefore(date) || isSameDay(start, date);
7 | final isAfter = end.isAfter(date) || isSameDay(end, date);
8 | return isBefore && isAfter;
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/lib/utils/date_time/weekday.dart:
--------------------------------------------------------------------------------
1 | // ignore_for_file: constant_identifier_names
2 |
3 | import 'dart:io';
4 |
5 | import 'package:flutter/material.dart';
6 | import 'package:freezed_annotation/freezed_annotation.dart';
7 | import 'package:intl/intl.dart';
8 | import 'package:todomaker/style/color.dart';
9 |
10 | enum Weekday {
11 | @JsonKey(name: 'Sunday')
12 | Sunday,
13 | @JsonKey(name: 'Monday')
14 | Monday,
15 | @JsonKey(name: 'Tuesday')
16 | Tuesday,
17 | @JsonKey(name: 'Wednesday')
18 | Wednesday,
19 | @JsonKey(name: 'Thursday')
20 | Thursday,
21 | @JsonKey(name: 'Friday')
22 | Friday,
23 | @JsonKey(name: 'Saturday')
24 | Saturday,
25 | }
26 |
27 | extension WeekdayFunctions on Weekday {
28 | static Weekday weekdayFromDate(DateTime date) {
29 | var weekdayIndex = date.weekday;
30 | var weekdays = Weekday.values;
31 | var sunday = weekdays.first;
32 | weekdays = weekdays.sublist(1)
33 | ..addAll(weekdays.sublist(0, weekdays.length))
34 | ..insert(0, sunday);
35 | return weekdays[weekdayIndex];
36 | }
37 |
38 | static List weekdaysForFirstWeekday(Weekday firstWeekday) {
39 | return Weekday.values.sublist(firstWeekday.index)..addAll(Weekday.values.sublist(0, firstWeekday.index));
40 | }
41 |
42 | static List weekdays() {
43 | return DateFormat.EEEE(Platform.localeName).dateSymbols.WEEKDAYS;
44 | }
45 |
46 | String weekdayString() {
47 | return weekdays()[index];
48 | }
49 |
50 | // [日月火水木金土]
51 | static List shortWeekdays() {
52 | return DateFormat.E(Platform.localeName).dateSymbols.SHORTWEEKDAYS;
53 | }
54 |
55 | String weekdayShortString() {
56 | return shortWeekdays()[index];
57 | }
58 |
59 | Color weekdayColor() {
60 | switch (this) {
61 | case Weekday.Sunday:
62 | return AppColors.sunday;
63 | case Weekday.Monday:
64 | return AppColors.weekday;
65 | case Weekday.Tuesday:
66 | return AppColors.weekday;
67 | case Weekday.Wednesday:
68 | return AppColors.weekday;
69 | case Weekday.Thursday:
70 | return AppColors.weekday;
71 | case Weekday.Friday:
72 | return AppColors.weekday;
73 | case Weekday.Saturday:
74 | return AppColors.saturday;
75 | default:
76 | throw ArgumentError.notNull('');
77 | }
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/lib/utils/format/time_of_day.dart:
--------------------------------------------------------------------------------
1 | import 'package:todomaker/utils/picker/time.dart';
2 |
3 | class TimeOfDayFormatter {
4 | static String format(AppTimeOfDay timeOfDay) {
5 | final hour = timeOfDay.hour;
6 | final minute = timeOfDay.minute;
7 | final String hourString;
8 | final String minuteString;
9 | if (hour < 10) {
10 | hourString = '0$hour';
11 | } else {
12 | hourString = hour.toString();
13 | }
14 | if (minute < 10) {
15 | minuteString = '0$minute';
16 | } else {
17 | minuteString = minute.toString();
18 | }
19 | return '$hourString:$minuteString';
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/lib/utils/functions/firebase_functions.dart:
--------------------------------------------------------------------------------
1 | import 'package:cloud_functions/cloud_functions.dart';
2 | import 'package:firebase_auth/firebase_auth.dart';
3 | import 'package:flutter/material.dart';
4 | import 'package:todomaker/entity/location_form.dart';
5 |
6 | // GenKitがus-central1のサポートになる
7 | final functions = FirebaseFunctions.instanceFor(region: 'us-central1');
8 |
9 | extension FirebaseFunctionsExt on FirebaseFunctions {
10 | Future enqueueTaskCreate({
11 | required String taskID,
12 | required String question,
13 | }) async {
14 | // {
15 | // "question": "",
16 | // "userRequest": {
17 | // "userID": "",
18 | // }
19 | // }
20 | final result = await httpsCallable('enqueueTaskCreate').call(
21 | {
22 | 'taskID': taskID,
23 | 'question': question,
24 | 'userRequest': {
25 | 'userID': FirebaseAuth.instance.currentUser?.uid,
26 | },
27 | },
28 | );
29 |
30 | final response = mapToJSON(result.data);
31 | if (response['result'] != 'OK') {
32 | throw Exception(response['error']['message']);
33 | }
34 | return;
35 | }
36 |
37 | Future fillLocation({
38 | required String taskID,
39 | required LocationFormInfo userLocation,
40 | }) async {
41 | final result = await httpsCallable('enqueueFillLocation').call(
42 | {
43 | 'taskID': taskID,
44 | 'userLocation': userLocation.toJson(),
45 | 'userRequest': {
46 | 'userID': FirebaseAuth.instance.currentUser?.uid,
47 | },
48 | },
49 | );
50 | final response = mapToJSON(result.data);
51 | debugPrint('enqueueFillLocation response: ${response.toString()}');
52 | if (response['result'] != 'OK') {
53 | throw Exception(response['error']['message']);
54 | }
55 |
56 | return;
57 | }
58 |
59 | Future fillTodoLocation({
60 | required String taskID,
61 | required String todoID,
62 | required LocationFormInfo userLocation,
63 | }) async {
64 | final result = await httpsCallable('enqueueFillTODOLocation').call(
65 | {
66 | 'taskID': taskID,
67 | 'todoID': todoID,
68 | 'userLocation': userLocation.toJson(),
69 | 'userRequest': {
70 | 'userID': FirebaseAuth.instance.currentUser?.uid,
71 | },
72 | },
73 | );
74 | final response = mapToJSON(result.data);
75 | debugPrint('enqueueFillTODOLocation response: ${response.toString()}');
76 | if (response['result'] != 'OK') {
77 | throw Exception(response['error']['message']);
78 | }
79 |
80 | return;
81 | }
82 | }
83 |
84 | // Map.fromだけだとネストした子要素が_Map