├── firebase ├── build ├── .firebaserc ├── functions │ ├── tsconfig.dev.json │ ├── src │ │ ├── collection.ts │ │ ├── model │ │ │ ├── user.ts │ │ │ └── post.ts │ │ ├── script │ │ │ ├── deleteAllAuthUsers.ts │ │ │ └── initializeApp.ts │ │ ├── index.ts │ │ └── test │ │ │ └── rules │ │ │ ├── common.ts │ │ │ ├── utils.ts │ │ │ ├── users.test.ts │ │ │ └── posts.test.ts │ ├── .gitignore │ ├── jest.config.ts │ ├── tsconfig.json │ ├── .eslintrc.js │ └── package.json ├── firestore.rules.default ├── firebase.json ├── firestore.indexes.json ├── .gitignore ├── firestore.rules └── public │ ├── 404.html │ └── index.html ├── .fvm ├── flutter_sdk └── fvm_config.json ├── .vscode ├── settings.json └── launch.json ├── lib ├── common │ ├── extensions │ │ ├── extensions.dart │ │ └── date_time.dart │ ├── providers │ │ ├── providers.dart │ │ ├── app_info_provider.g.dart │ │ └── app_info_provider.dart │ ├── common.dart │ └── widgets │ │ ├── widgets.dart │ │ ├── centered_circular_progress_indicator.dart │ │ ├── app_logo.dart │ │ └── cached_circle_avatar.dart ├── features │ ├── post │ │ ├── widgets │ │ │ ├── widgets.dart │ │ │ ├── user_avatar.dart │ │ │ └── post_list_view.dart │ │ ├── form │ │ │ ├── title_form_field.dart │ │ │ ├── description_form_field.dart │ │ │ ├── form_validator.dart │ │ │ └── post_form_page.dart │ │ ├── model │ │ │ ├── post_converter.dart │ │ │ ├── post.dart │ │ │ └── post.g.dart │ │ ├── post_list_page.dart │ │ ├── detail │ │ │ ├── post_action_handler.g.dart │ │ │ ├── post_page.dart │ │ │ ├── post_action_handler.dart │ │ │ ├── post_detail_view.dart │ │ │ └── post_action_menu_button.dart │ │ ├── post_repository.dart │ │ ├── post_repository.g.dart │ │ └── post_provider.dart │ ├── user │ │ ├── model │ │ │ ├── user.dart │ │ │ └── user.g.dart │ │ ├── user_provider.dart │ │ ├── widgets │ │ │ └── profile.dart │ │ └── user_page.dart │ ├── signin │ │ └── signin_page.dart │ └── setting │ │ └── setting_page.dart ├── core │ ├── logger.dart │ ├── const.dart │ ├── navigation │ │ ├── navigation_item.dart │ │ └── scaffold_with_navigation.dart │ ├── authentication │ │ ├── auth_provider.dart │ │ ├── auth_repository.g.dart │ │ ├── auth_provider.g.dart │ │ └── auth_repository.dart │ ├── theme │ │ └── theme.dart │ └── router │ │ └── router.dart ├── app.dart ├── main.dart └── firebase_options.dart ├── ios ├── Runner │ ├── Runner-Bridging-Header.h │ ├── Assets.xcassets │ │ ├── LaunchImage.imageset │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ ├── README.md │ │ │ └── Contents.json │ │ └── AppIcon.appiconset │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-38x38@2x.png │ │ │ ├── Icon-App-38x38@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-64x64@2x.png │ │ │ ├── Icon-App-64x64@3x.png │ │ │ ├── Icon-App-68x68@2x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── GoogleService-Info.plist │ ├── Base.lproj │ │ ├── Main.storyboard │ │ └── LaunchScreen.storyboard │ └── Info.plist ├── Flutter │ ├── Debug.xcconfig │ ├── Release.xcconfig │ └── AppFrameworkInfo.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── firebase_app_id_file.json ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ ├── WorkspaceSettings.xcsettings │ │ └── IDEWorkspaceChecks.plist ├── RunnerTests │ └── RunnerTests.swift ├── .gitignore └── Podfile ├── web ├── favicon.png ├── icons │ ├── Icon-192.png │ ├── Icon-512.png │ ├── Icon-maskable-192.png │ └── Icon-maskable-512.png ├── manifest.json └── index.html ├── .firebaserc ├── assets └── images │ ├── app_icon.png │ ├── app_adaptive_background.png │ ├── app_adaptive_foreground.png │ ├── app_adaptive_monochrome.png │ └── logo │ ├── logo_yoko_dark.svg │ └── logo_yoko_light.svg ├── pubspec_overrides.yaml ├── macos ├── Runner │ ├── Configs │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ ├── Warnings.xcconfig │ │ └── AppInfo.xcconfig │ ├── Assets.xcassets │ │ └── AppIcon.appiconset │ │ │ ├── app_icon_128.png │ │ │ ├── app_icon_16.png │ │ │ ├── app_icon_256.png │ │ │ ├── app_icon_32.png │ │ │ ├── app_icon_512.png │ │ │ ├── app_icon_64.png │ │ │ ├── app_icon_1024.png │ │ │ └── Contents.json │ ├── AppDelegate.swift │ ├── Release.entitlements │ ├── MainFlutterWindow.swift │ ├── DebugProfile.entitlements │ ├── Info.plist │ └── GoogleService-Info.plist ├── .gitignore ├── Flutter │ ├── Flutter-Debug.xcconfig │ ├── Flutter-Release.xcconfig │ └── GeneratedPluginRegistrant.swift ├── firebase_app_id_file.json ├── Runner.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist ├── Runner.xcodeproj │ ├── project.xcworkspace │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── xcshareddata │ │ └── xcschemes │ │ └── Runner.xcscheme ├── RunnerTests │ └── RunnerTests.swift └── Podfile ├── .github ├── images │ └── nippo_readme_eyecatch.png ├── scripts │ └── validate-formatting.sh └── workflows │ ├── functions-test.yml │ └── flutter.yml ├── android ├── app │ ├── src │ │ ├── main │ │ │ ├── ic_launcher-playstore.png │ │ │ ├── res │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ ├── ic_launcher.png │ │ │ │ │ ├── ic_launcher_background.png │ │ │ │ │ ├── ic_launcher_foreground.png │ │ │ │ │ └── ic_launcher_monochrome.png │ │ │ │ ├── drawable-hdpi │ │ │ │ │ └── ic_notification.png │ │ │ │ ├── drawable-mdpi │ │ │ │ │ └── ic_notification.png │ │ │ │ ├── drawable-xhdpi │ │ │ │ │ └── ic_notification.png │ │ │ │ ├── drawable-xxhdpi │ │ │ │ │ └── ic_notification.png │ │ │ │ ├── drawable-xxxhdpi │ │ │ │ │ └── ic_notification.png │ │ │ │ ├── mipmap-anydpi-v26 │ │ │ │ │ └── ic_launcher.xml │ │ │ │ ├── drawable │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable-v21 │ │ │ │ │ └── launch_background.xml │ │ │ │ ├── values │ │ │ │ │ └── styles.xml │ │ │ │ └── values-night │ │ │ │ │ └── styles.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── htsuruo │ │ │ │ │ └── nippo │ │ │ │ │ └── MainActivity.kt │ │ │ └── AndroidManifest.xml │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── google-services.json │ └── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ └── gradle-wrapper.properties ├── .gitignore ├── build.gradle └── settings.gradle ├── analysis_options.yaml ├── .gitignore ├── firebase.json ├── melos.yaml ├── .metadata ├── pubspec.yaml └── README.md /firebase/build: -------------------------------------------------------------------------------- 1 | ../build/ -------------------------------------------------------------------------------- /.fvm/flutter_sdk: -------------------------------------------------------------------------------- 1 | /Users/tsuruoka/fvm/versions/stable -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": ["Signout"] 3 | } 4 | -------------------------------------------------------------------------------- /lib/common/extensions/extensions.dart: -------------------------------------------------------------------------------- 1 | export 'date_time.dart'; 2 | -------------------------------------------------------------------------------- /lib/common/providers/providers.dart: -------------------------------------------------------------------------------- 1 | export 'app_info_provider.dart'; 2 | -------------------------------------------------------------------------------- /ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/web/favicon.png -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "nippo-e8922" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "stable", 3 | "flavors": {} 4 | } -------------------------------------------------------------------------------- /firebase/.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "nippo-e8922" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/web/icons/Icon-192.png -------------------------------------------------------------------------------- /web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/web/icons/Icon-512.png -------------------------------------------------------------------------------- /firebase/functions/tsconfig.dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": [ 3 | ".eslintrc.js" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /assets/images/app_icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/assets/images/app_icon.png -------------------------------------------------------------------------------- /lib/features/post/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'post_list_view.dart'; 2 | export 'user_avatar.dart'; 3 | -------------------------------------------------------------------------------- /web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /pubspec_overrides.yaml: -------------------------------------------------------------------------------- 1 | # melos_managed_dependency_overrides: http 2 | dependency_overrides: 3 | http: ^1.0.0 4 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /.github/images/nippo_readme_eyecatch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/.github/images/nippo_readme_eyecatch.png -------------------------------------------------------------------------------- /assets/images/app_adaptive_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/assets/images/app_adaptive_background.png -------------------------------------------------------------------------------- /assets/images/app_adaptive_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/assets/images/app_adaptive_foreground.png -------------------------------------------------------------------------------- /assets/images/app_adaptive_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/assets/images/app_adaptive_monochrome.png -------------------------------------------------------------------------------- /macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /lib/common/common.dart: -------------------------------------------------------------------------------- 1 | export 'extensions/extensions.dart'; 2 | export 'providers/providers.dart'; 3 | export 'widgets/widgets.dart'; 4 | -------------------------------------------------------------------------------- /android/app/src/main/ic_launcher-playstore.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/ic_launcher-playstore.png -------------------------------------------------------------------------------- /ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /firebase/functions/src/collection.ts: -------------------------------------------------------------------------------- 1 | export class Collection { 2 | static readonly users = 'users' 3 | static readonly posts = 'posts' 4 | } 5 | -------------------------------------------------------------------------------- /ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /lib/common/widgets/widgets.dart: -------------------------------------------------------------------------------- 1 | export 'app_logo.dart'; 2 | export 'cached_circle_avatar.dart'; 3 | export 'centered_circular_progress_indicator.dart'; 4 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-hdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/drawable-hdpi/ic_notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-mdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/drawable-mdpi/ic_notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/drawable-xhdpi/ic_notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/drawable-xxhdpi/ic_notification.png -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-xxxhdpi/ic_notification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/drawable-xxxhdpi/ic_notification.png -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-hdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-mdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-xhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_background.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_foreground.png -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_monochrome.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/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/htsuruo/nippo/HEAD/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/htsuruo/nippo/HEAD/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/htsuruo/nippo/HEAD/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/htsuruo/nippo/HEAD/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/htsuruo/nippo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-38x38@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/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/htsuruo/nippo/HEAD/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/htsuruo/nippo/HEAD/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/htsuruo/nippo/HEAD/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/htsuruo/nippo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-64x64@3x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-68x68@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-68x68@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/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/htsuruo/nippo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/htsuruo/nippo/HEAD/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /lib/core/logger.dart: -------------------------------------------------------------------------------- 1 | import 'package:simple_logger/simple_logger.dart'; 2 | 3 | final logger = SimpleLogger() 4 | ..setLevel( 5 | Level.INFO, 6 | includeCallerInfo: true, 7 | ); 8 | -------------------------------------------------------------------------------- /android/app/src/main/kotlin/com/htsuruo/nippo/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.htsuruo.nippo 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() 6 | -------------------------------------------------------------------------------- /firebase/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 | -------------------------------------------------------------------------------- /firebase/firestore.rules.default: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | match /{document=**} { 5 | allow read, write: if true; 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/core/const.dart: -------------------------------------------------------------------------------- 1 | class CollectionName { 2 | static const users = 'users'; 3 | static const posts = 'posts'; 4 | } 5 | 6 | class FieldName { 7 | static const postId = 'postId'; 8 | static const createdAt = 'createdAt'; 9 | } 10 | -------------------------------------------------------------------------------- /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.7-all.zip 6 | -------------------------------------------------------------------------------- /macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /firebase/functions/jest.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: 'ts-jest', 3 | roots: ['/src'], 4 | // https://github.com/facebook/jest/issues/7780#issuecomment-645989788 5 | testEnvironment: 'node', 6 | transform: { 7 | '^.+\\.m?[tj]sx?$': ['ts-jest'], 8 | }, 9 | } 10 | -------------------------------------------------------------------------------- /ios/firebase_app_id_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "file_generated_by": "FlutterFire CLI", 3 | "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", 4 | "GOOGLE_APP_ID": "1:554602506203:ios:28faf2e71f77493b3ee9fe", 5 | "FIREBASE_PROJECT_ID": "nippo-e8922", 6 | "GCM_SENDER_ID": "554602506203" 7 | } -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/firebase_app_id_file.json: -------------------------------------------------------------------------------- 1 | { 2 | "file_generated_by": "FlutterFire CLI", 3 | "purpose": "FirebaseAppID & ProjectID for this Firebase app in this directory", 4 | "GOOGLE_APP_ID": "1:554602506203:ios:82ca5bda76750c973ee9fe", 5 | "FIREBASE_PROJECT_ID": "nippo-e8922", 6 | "GCM_SENDER_ID": "554602506203" 7 | } -------------------------------------------------------------------------------- /macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.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/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /firebase/functions/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "noImplicitReturns": true, 5 | "noUnusedLocals": true, 6 | "outDir": "lib", 7 | "sourceMap": true, 8 | "strict": true, 9 | "target": "es2022", 10 | "resolveJsonModule": true 11 | }, 12 | "compileOnSave": true, 13 | "include": ["src"] 14 | } 15 | -------------------------------------------------------------------------------- /macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import FlutterMacOS 2 | import Cocoa 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 | -------------------------------------------------------------------------------- /android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /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. -------------------------------------------------------------------------------- /lib/common/widgets/centered_circular_progress_indicator.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | class CenteredCircularProgressIndicator extends StatelessWidget { 4 | const CenteredCircularProgressIndicator({super.key}); 5 | 6 | @override 7 | Widget build(BuildContext context) { 8 | return const Center( 9 | child: CircularProgressIndicator(), 10 | ); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/core/navigation/navigation_item.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | enum NavigationItem { 4 | timeline(iconData: Icons.view_timeline_outlined, label: 'みんなの日報'), 5 | profile(iconData: Icons.person_outline, label: 'プロフィール'), 6 | ; 7 | 8 | const NavigationItem({required this.iconData, required this.label}); 9 | final IconData iconData; 10 | final String label; 11 | } 12 | -------------------------------------------------------------------------------- /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/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/common/extensions/date_time.dart: -------------------------------------------------------------------------------- 1 | import 'package:intl/intl.dart'; 2 | 3 | // ref. 4 | // https://github.com/dart-lang/intl/blob/eb4ab704c4a3c48d957b2a188c6b452051a093a7/lib/date_symbol_data_local.dart#L9237 5 | // https://github.com/dart-lang/intl/blob/eb4ab704c4a3c48d957b2a188c6b452051a093a7/lib/date_time_patterns.dart#L2707 6 | extension DateTimeEx on DateTime { 7 | String get formatted => DateFormat.yMMMd().add_Hms().format(this); 8 | } 9 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:pedantic_mono/analysis_options.yaml 2 | analyzer: 3 | plugins: 4 | - custom_lint 5 | errors: 6 | # @serializableやJsonKeyを無視する 7 | # https://github.com/rrousselGit/freezed/issues/488 8 | invalid_annotation_target: ignore 9 | exclude: 10 | - lib/**/*.g.dart 11 | - lib/**/*.freezed.dart 12 | - lib/firebase_options.dart 13 | linter: 14 | rules: 15 | use_build_context_synchronously: false 16 | -------------------------------------------------------------------------------- /ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 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 | -------------------------------------------------------------------------------- /macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.client 10 | 11 | com.apple.security.network.server 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /firebase/functions/src/model/user.ts: -------------------------------------------------------------------------------- 1 | export interface User { 2 | readonly name?: string 3 | readonly email?: string 4 | readonly photoUrl?: string 5 | 6 | readonly updatedAt: FirebaseFirestore.Timestamp 7 | readonly createdAt: FirebaseFirestore.Timestamp 8 | } 9 | 10 | export const userConverter: FirebaseFirestore.FirestoreDataConverter = { 11 | toFirestore(user) { 12 | return user 13 | }, 14 | fromFirestore(snapshot) { 15 | return snapshot.data() as User 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /firebase/functions/src/model/post.ts: -------------------------------------------------------------------------------- 1 | export interface Post { 2 | readonly postId: string 3 | readonly title: string 4 | readonly description: string 5 | 6 | readonly updatedAt: FirebaseFirestore.Timestamp 7 | readonly createdAt: FirebaseFirestore.Timestamp 8 | } 9 | 10 | export const postConverter: FirebaseFirestore.FirestoreDataConverter = { 11 | toFirestore(post) { 12 | return post 13 | }, 14 | fromFirestore(snapshot) { 15 | return snapshot.data() as Post 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /lib/core/authentication/auth_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_auth/firebase_auth.dart'; 2 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 3 | 4 | part 'auth_provider.g.dart'; 5 | 6 | @Riverpod(keepAlive: true) 7 | Future isSignedIn(IsSignedInRef ref) { 8 | return ref.watch(firUserProvider.future).then((user) => user != null); 9 | } 10 | 11 | @Riverpod(keepAlive: true) 12 | Stream firUser(FirUserRef ref) { 13 | return FirebaseAuth.instance.authStateChanges(); 14 | } 15 | -------------------------------------------------------------------------------- /firebase/functions/src/script/deleteAllAuthUsers.ts: -------------------------------------------------------------------------------- 1 | import * as admin from 'firebase-admin' 2 | import { initializeApp } from './initializeApp' 3 | 4 | initializeApp() 5 | 6 | // How to run: 7 | // $ npm run build 8 | // $ node -e 'require("./lib/script/deleteAllAuthUsers").deleteAllAuthUsers()' 9 | export const deleteAllAuthUsers = async () => { 10 | const listUsersResult = await admin.auth().listUsers() 11 | for (const user of listUsersResult.users) { 12 | await admin.auth().deleteUser(user.uid) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "nippo", 6 | "request": "launch", 7 | "type": "dart" 8 | }, 9 | { 10 | "name": "nippo (release mode)", 11 | "request": "launch", 12 | "type": "dart", 13 | "flutterMode": "release" 14 | }, 15 | { 16 | "name": "nippo(web)", 17 | "request": "launch", 18 | "type": "dart", 19 | // Google OAuthのクライアントIDホワイトリストに固定するため5000ポートを指定 20 | "args": ["-d", "chrome", "--web-port=5000"] 21 | } 22 | ] 23 | } 24 | -------------------------------------------------------------------------------- /lib/common/widgets/app_logo.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:nippo/gen/assets.gen.dart'; 3 | 4 | class AppLogo extends StatelessWidget { 5 | const AppLogo({super.key, this.height = 48}); 6 | 7 | final double height; 8 | 9 | @override 10 | Widget build(BuildContext context) { 11 | final brightness = Theme.of(context).brightness; 12 | return brightness == Brightness.light 13 | ? Assets.images.logo.logoYokoLight.svg( 14 | height: height, 15 | ) 16 | : Assets.images.logo.logoYokoDark.svg( 17 | height: height, 18 | ); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = nippo 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.htsuruo.nippo 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2023 com.htsuruo. All rights reserved. 15 | -------------------------------------------------------------------------------- /lib/features/user/model/user.dart: -------------------------------------------------------------------------------- 1 | import 'package:freezed_annotation/freezed_annotation.dart'; 2 | import 'package:json_converter_helper/json_converter_helper.dart'; 3 | 4 | part 'user.freezed.dart'; 5 | part 'user.g.dart'; 6 | 7 | @freezed 8 | class User with _$User { 9 | @allJsonConvertersSerializable 10 | const factory User({ 11 | @Default('名無し') String name, 12 | required String email, 13 | required String? photoUrl, 14 | @Default(UnionTimestamp.serverTimestamp()) UnionTimestamp updatedAt, 15 | @Default(UnionTimestamp.serverTimestamp()) UnionTimestamp createdAt, 16 | }) = _User; 17 | 18 | factory User.fromJson(Map json) => _$UserFromJson(json); 19 | } 20 | -------------------------------------------------------------------------------- /firebase/functions/src/script/initializeApp.ts: -------------------------------------------------------------------------------- 1 | import * as admin from 'firebase-admin' 2 | 3 | // admin.initializeAppの引数に統一的に何かを与えたい場合を想定した 4 | // 初期化処理をラップしたメソッド 5 | // script実行時には必ず最初に呼ぶ 6 | export function initializeApp(params?: { useEmulator?: boolean }) { 7 | process.env.GCLOUD_PROJECT = 'nippo-e8922' 8 | if (params?.useEmulator) { 9 | // 向き先をエミュレータにする 10 | // ref. https://firebase.google.com/docs/emulator-suite/connect_firestore?hl=ja#admin_sdks 11 | process.env.FIRESTORE_EMULATOR_HOST = 'localhost:8080' 12 | } else { 13 | delete process.env.FIRESTORE_EMULATOR_HOST 14 | } 15 | admin.initializeApp() 16 | admin.firestore().settings({ ignoreUndefinedProperties: true }) 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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | 12 | # IntelliJ related 13 | *.iml 14 | *.ipr 15 | *.iws 16 | .idea/ 17 | 18 | # The .vscode folder contains launch configuration and tasks you configure in 19 | # VS Code which you may wish to be included in version control, so this line 20 | # is commented out by default. 21 | #.vscode/ 22 | 23 | # Flutter/Dart/Pub related 24 | **/doc/api/ 25 | .dart_tool/ 26 | .flutter-plugins 27 | .flutter-plugins-dependencies 28 | .packages 29 | .pub-cache/ 30 | .pub/ 31 | /build/ 32 | 33 | # Web related 34 | 35 | # Exceptions to above rules. 36 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages 37 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | {"flutter":{"platforms":{"android":{"default":{"projectId":"nippo-e8922","appId":"1:554602506203:android:052255c36e16460d3ee9fe","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"nippo-e8922","appId":"1:554602506203:ios:28faf2e71f77493b3ee9fe","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"macos":{"default":{"projectId":"nippo-e8922","appId":"1:554602506203:ios:28faf2e71f77493b3ee9fe","uploadDebugSymbols":false,"fileOutput":"macos/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"nippo-e8922","configurations":{"android":"1:554602506203:android:052255c36e16460d3ee9fe","ios":"1:554602506203:ios:28faf2e71f77493b3ee9fe","macos":"1:554602506203:ios:28faf2e71f77493b3ee9fe","web":"1:554602506203:web:bcb0eae5ca2da51e3ee9fe"}}}}}} -------------------------------------------------------------------------------- /lib/features/post/form/title_form_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | 4 | import 'form_validator.dart'; 5 | 6 | class TitleFormField extends HookWidget { 7 | const TitleFormField({super.key, required this.controller}); 8 | 9 | final TextEditingController controller; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final theme = Theme.of(context); 14 | 15 | return TextFormField( 16 | controller: controller, 17 | style: theme.textTheme.bodyMedium, 18 | textInputAction: TextInputAction.next, 19 | autofocus: true, 20 | decoration: const InputDecoration( 21 | label: Text('件名'), 22 | hintText: '今日を一言で表現しましょう', 23 | ), 24 | validator: FormValidator.validateTitle, 25 | ); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /lib/features/post/form/description_form_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_hooks/flutter_hooks.dart'; 3 | 4 | import 'form_validator.dart'; 5 | 6 | class DescriptionFormField extends HookWidget { 7 | const DescriptionFormField({super.key, required this.controller}); 8 | 9 | final TextEditingController controller; 10 | 11 | @override 12 | Widget build(BuildContext context) { 13 | final theme = Theme.of(context); 14 | 15 | return TextFormField( 16 | controller: controller, 17 | style: theme.textTheme.bodyMedium, 18 | autofocus: true, 19 | expands: true, 20 | maxLines: null, 21 | textAlignVertical: TextAlignVertical.top, 22 | decoration: const InputDecoration( 23 | hintText: '今日起きたたくさんの出来事を記録しましょう', 24 | ), 25 | validator: FormValidator.validateDescription, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/features/post/form/form_validator.dart: -------------------------------------------------------------------------------- 1 | class FormValidator { 2 | FormValidator._(); 3 | 4 | /// title: 20文字以内 5 | static String? validateTitle(String? value) { 6 | final emptyText = _validateNotEmptyText(value); 7 | if (emptyText != null) { 8 | return emptyText; 9 | } 10 | if (value!.length > 20) { 11 | return '20文字以内で入力しましょう'; 12 | } 13 | return null; 14 | } 15 | 16 | /// description: 200文字以内 17 | static String? validateDescription(String? value) { 18 | final emptyText = _validateNotEmptyText(value); 19 | if (emptyText != null) { 20 | return emptyText; 21 | } 22 | if (value!.length > 200) { 23 | return '200文字以内で入力しましょう'; 24 | } 25 | return null; 26 | } 27 | 28 | static String? _validateNotEmptyText(String? value) { 29 | if (value!.isEmpty) { 30 | return 'テキストを入力しましょう'; 31 | } 32 | return null; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /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.5.1' apply false 22 | // START: FlutterFire Configuration 23 | id "com.google.gms.google-services" version "4.3.15" apply false 24 | // END: FlutterFire Configuration 25 | id "org.jetbrains.kotlin.android" version "1.8.22" apply false 26 | } 27 | 28 | include ":app" 29 | -------------------------------------------------------------------------------- /firebase/functions/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as logger from 'firebase-functions/logger' 2 | import { auth } from 'firebase-functions' 3 | 4 | import * as admin from 'firebase-admin' 5 | import { FieldValue } from 'firebase-admin/firestore' 6 | import { userConverter } from './model/user' 7 | import { Collection } from './collection' 8 | 9 | admin.initializeApp() 10 | admin.firestore().settings({ ignoreUndefinedProperties: true }) 11 | 12 | const firestore = admin.firestore() 13 | 14 | // Firebase Authユーザーが新規で作成された際に、そのユーザーのuidのドキュメントを作成し、 15 | // publicなプロフィール情報を同期します。 16 | export const onAuthUserCreate = auth.user().onCreate((user) => { 17 | logger.info(`New user created: ${user.uid}`) 18 | firestore 19 | .collection(Collection.users) 20 | .doc(user.uid) 21 | .withConverter(userConverter) 22 | .set({ 23 | email: user.email, 24 | name: user.displayName, 25 | photoUrl: user.photoURL, 26 | createdAt: FieldValue.serverTimestamp(), 27 | updatedAt: FieldValue.serverTimestamp(), 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /melos.yaml: -------------------------------------------------------------------------------- 1 | name: nippo 2 | 3 | packages: 4 | - ./ 5 | 6 | sdkPath: '.fvm/flutter_sdk' 7 | 8 | scripts: 9 | pub:upgrade: 10 | exec: flutter pub upgrade 11 | packageFilters: 12 | flutter: true 13 | 14 | # ref. https://github.com/firebase/flutterfire/blob/27a7f44e02f2ed533e0249622afdd0a421261385/melos.yaml#L27-L32 15 | analyze: 16 | run: | 17 | melos exec -c 1 -- \ 18 | dart analyze . --fatal-infos 19 | 20 | run:custom_lint: 21 | run: | 22 | melos exec --fail-fast --depends-on=custom_lint \ 23 | -- "dart run custom_lint" 24 | 25 | fix:format: 26 | exec: melos run fix && melos run format 27 | 28 | fix: 29 | exec: dart fix --apply . 30 | 31 | format: 32 | exec: dart format . 33 | 34 | test: 35 | run: | 36 | melos exec --ignore="*example*" --dir-exists=test \ 37 | --fail-fast -- \ 38 | "flutter test --no-pub" 39 | 40 | deploy:web: 41 | run: | 42 | flutter build web 43 | cd ./firebase 44 | firebase deploy --only hosting 45 | -------------------------------------------------------------------------------- /web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nippo", 3 | "short_name": "nippo", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /lib/features/post/widgets/user_avatar.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | import 'package:nippo/common/common.dart'; 5 | import 'package:nippo/core/router/router.dart'; 6 | import 'package:nippo/features/post/model/post.dart'; 7 | import 'package:nippo/features/user/user_provider.dart'; 8 | 9 | class UserAvatar extends ConsumerWidget { 10 | const UserAvatar({super.key, required this.postRef}); 11 | 12 | final DocumentReference postRef; 13 | 14 | @override 15 | Widget build(BuildContext context, WidgetRef ref) { 16 | // users/[uid]/posts/[postId] 17 | final user = ref.watch(userProvider(uid: postRef.uid)).value; 18 | return CachedCircleAvatar( 19 | // TODO(htsuruo): `applyUnlessNull`に書き換える 20 | onTap: user == null 21 | ? null 22 | : () { 23 | UserPageRoute(uid: user.id).push(context); 24 | }, 25 | imageUrl: user?.data()?.photoUrl, 26 | ); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /firebase/functions/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | es6: true, 5 | node: true, 6 | }, 7 | extends: [ 8 | 'plugin:import/errors', 9 | 'plugin:import/warnings', 10 | 'plugin:import/typescript', 11 | 'google', 12 | 'plugin:prettier/recommended', 13 | ], 14 | parser: '@typescript-eslint/parser', 15 | parserOptions: { 16 | project: ['tsconfig.json', 'tsconfig.dev.json'], 17 | sourceType: 'module', 18 | }, 19 | ignorePatterns: [ 20 | '/lib/**/*', // Ignore built files. 21 | ], 22 | plugins: ['@typescript-eslint', 'import'], 23 | rules: { 24 | 'prettier/prettier': ['error', { singleQuote: true, semi: false }], 25 | 'import/no-unresolved': 0, 26 | 'require-jsdoc': [ 27 | 'error', 28 | { 29 | require: { 30 | FunctionDeclaration: false, 31 | MethodDefinition: false, 32 | ClassDeclaration: false, 33 | ArrowFunctionExpression: false, 34 | FunctionExpression: false, 35 | }, 36 | }, 37 | ], 38 | }, 39 | } 40 | -------------------------------------------------------------------------------- /lib/common/providers/app_info_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ignore_for_file: type=lint, implicit_dynamic_parameter, implicit_dynamic_type, implicit_dynamic_method, strict_raw_type 4 | 5 | part of 'app_info_provider.dart'; 6 | 7 | // ************************************************************************** 8 | // RiverpodGenerator 9 | // ************************************************************************** 10 | 11 | String _$appInfoHash() => r'2476c12ab36a7b9e00c26e1c81d54fee92d1f0ad'; 12 | 13 | /// See also [appInfo]. 14 | @ProviderFor(appInfo) 15 | final appInfoProvider = Provider.internal( 16 | appInfo, 17 | name: r'appInfoProvider', 18 | debugGetCreateSourceHash: 19 | const bool.fromEnvironment('dart.vm.product') ? null : _$appInfoHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef AppInfoRef = ProviderRef; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 27 | -------------------------------------------------------------------------------- /.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: "2663184aa79047d0a33a14a3b607954f8fdd8730" 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: 2663184aa79047d0a33a14a3b607954f8fdd8730 17 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 18 | - platform: android 19 | create_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 20 | base_revision: 2663184aa79047d0a33a14a3b607954f8fdd8730 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /firebase/firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "firestore": { 3 | "rules": "firestore.rules", 4 | "indexes": "firestore.indexes.json" 5 | }, 6 | "functions": [ 7 | { 8 | "source": "functions", 9 | "codebase": "default", 10 | "ignore": [ 11 | "node_modules", 12 | ".git", 13 | "firebase-debug.log", 14 | "firebase-debug.*.log" 15 | ], 16 | "predeploy": [ 17 | "npm --prefix \"$RESOURCE_DIR\" run lint", 18 | "npm --prefix \"$RESOURCE_DIR\" run build" 19 | ] 20 | } 21 | ], 22 | "emulators": { 23 | "auth": { 24 | "port": 9099 25 | }, 26 | "functions": { 27 | "port": 5001 28 | }, 29 | "firestore": { 30 | "port": 8080 31 | }, 32 | "ui": { 33 | "enabled": true 34 | }, 35 | "singleProjectMode": true 36 | }, 37 | "hosting": { 38 | "public": "build/web", 39 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 40 | "rewrites": [ 41 | { 42 | "source": "**", 43 | "destination": "/index.html" 44 | } 45 | ] 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /lib/features/post/model/post_converter.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | 3 | import 'post.dart'; 4 | 5 | Post _from( 6 | DocumentSnapshot> snapshot, 7 | SnapshotOptions? options, 8 | ) => 9 | Post.fromJson(snapshot.data()!); 10 | 11 | Map _to( 12 | Post post, 13 | SetOptions? options, 14 | ) => 15 | post.toJson(); 16 | 17 | extension DocumentReferencePostConverter on DocumentReference { 18 | DocumentReference withPostConverter() { 19 | return withConverter( 20 | fromFirestore: _from, 21 | toFirestore: _to, 22 | ); 23 | } 24 | } 25 | 26 | extension CollectionReferencePostConverter on CollectionReference { 27 | CollectionReference withPostConverter() { 28 | return withConverter( 29 | fromFirestore: _from, 30 | toFirestore: _to, 31 | ); 32 | } 33 | } 34 | 35 | extension QueryPostConverter on Query { 36 | Query withPostConverter() { 37 | return withConverter( 38 | fromFirestore: _from, 39 | toFirestore: _to, 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /lib/features/post/post_list_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | import 'package:nippo/common/common.dart'; 4 | import 'package:nippo/core/router/router.dart'; 5 | import 'package:nippo/features/post/post_provider.dart'; 6 | 7 | import 'widgets/widgets.dart'; 8 | 9 | class PostListPage extends ConsumerWidget { 10 | const PostListPage({super.key}); 11 | 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | final postsSnapshots = ref.watch(postsProvider).value; 15 | 16 | return Scaffold( 17 | appBar: AppBar( 18 | title: const AppLogo(height: 28), 19 | ), 20 | body: PostListView( 21 | snapshots: postsSnapshots, 22 | postSelected: (postId) { 23 | PostPageRoute(pid: postId).push(context); 24 | }, 25 | ), 26 | floatingActionButton: FloatingActionButton( 27 | onPressed: () { 28 | PostCreatePageRoute().go(context); 29 | }, 30 | child: const Icon(Icons.mode_edit), 31 | ), 32 | ); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /lib/features/post/detail/post_action_handler.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ignore_for_file: type=lint, implicit_dynamic_parameter, implicit_dynamic_type, implicit_dynamic_method, strict_raw_type 4 | 5 | part of 'post_action_handler.dart'; 6 | 7 | // ************************************************************************** 8 | // RiverpodGenerator 9 | // ************************************************************************** 10 | 11 | String _$postActionHash() => r'5cb345468f2cff919c6ed57ca69804f08cb371b8'; 12 | 13 | /// See also [postAction]. 14 | @ProviderFor(postAction) 15 | final postActionProvider = Provider.internal( 16 | postAction, 17 | name: r'postActionProvider', 18 | debugGetCreateSourceHash: 19 | const bool.fromEnvironment('dart.vm.product') ? null : _$postActionHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef PostActionRef = ProviderRef; 25 | // ignore_for_file: type=lint 26 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 27 | -------------------------------------------------------------------------------- /firebase/functions/src/test/rules/common.ts: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/compat/app' 2 | import { assertFails } from '@firebase/rules-unit-testing' 3 | 4 | export function assertUnauthenticatedAccessFails( 5 | collectionRef: () => firebase.firestore.CollectionReference, 6 | documentRef: () => firebase.firestore.DocumentReference, 7 | query?: () => firebase.firestore.Query 8 | ): void { 9 | describe('未認証の時', () => { 10 | test('list: できない', async () => { 11 | await assertFails(collectionRef().get()) 12 | }) 13 | test('get: できない', async () => { 14 | await assertFails(documentRef().get()) 15 | }) 16 | test('create: できない', async () => { 17 | await assertFails(documentRef().set({} as T)) 18 | }) 19 | test('update: できない', async () => { 20 | await assertFails(documentRef().update({})) 21 | }) 22 | test('delete: できない', async () => { 23 | await assertFails(documentRef().delete()) 24 | }) 25 | if (query != null) { 26 | test('list: CollectionGroupで取得できない', async () => { 27 | await assertFails(documentRef().delete()) 28 | }) 29 | } 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /lib/features/post/post_repository.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | import 'package:json_converter_helper/json_converter_helper.dart'; 4 | import 'package:nippo/features/post/post_provider.dart'; 5 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 6 | 7 | import 'model/post.dart'; 8 | 9 | part 'post_repository.g.dart'; 10 | 11 | @riverpod 12 | PostRepository postRepository(PostRepositoryRef ref) => PostRepository(ref); 13 | 14 | class PostRepository { 15 | PostRepository(this._ref); 16 | final Ref _ref; 17 | 18 | void create({required Post post}) { 19 | final doc = _ref.read(selfPostRefProvider).doc(); 20 | doc.set(post.copyWith(nullablePostId: doc.id)); 21 | } 22 | 23 | void update({ 24 | required DocumentReference reference, 25 | required Post post, 26 | }) => 27 | reference.update( 28 | post 29 | .copyWith(updatedAt: const UnionTimestamp.serverTimestamp()) 30 | .toJson(), 31 | ); 32 | 33 | void delete({required DocumentReference postRef}) => postRef.delete(); 34 | } 35 | -------------------------------------------------------------------------------- /lib/features/user/user_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:nippo/core/authentication/auth_provider.dart'; 3 | import 'package:nippo/core/const.dart'; 4 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 5 | 6 | import 'model/user.dart'; 7 | 8 | part 'user_provider.g.dart'; 9 | 10 | @Riverpod(keepAlive: true) 11 | DocumentReference authUserRef(AuthUserRefRef ref) { 12 | final firUser = ref.watch(firUserProvider).value; 13 | return ref.watch(userRefProvider(firUser?.uid)); 14 | } 15 | 16 | @Riverpod(keepAlive: true) 17 | DocumentReference userRef(UserRefRef ref, String? uid) { 18 | final firUser = ref.watch(firUserProvider).value; 19 | return FirebaseFirestore.instance 20 | .collection(CollectionName.users) 21 | .doc(firUser?.uid) 22 | .withConverter( 23 | fromFirestore: (snapshot, _) => User.fromJson(snapshot.data()!), 24 | toFirestore: (user, _) => user.toJson(), 25 | ); 26 | } 27 | 28 | @riverpod 29 | Stream> user(UserRef ref, {required String? uid}) => 30 | ref.watch(userRefProvider(uid)).snapshots(); 31 | -------------------------------------------------------------------------------- /lib/features/post/post_repository.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ignore_for_file: type=lint, implicit_dynamic_parameter, implicit_dynamic_type, implicit_dynamic_method, strict_raw_type 4 | 5 | part of 'post_repository.dart'; 6 | 7 | // ************************************************************************** 8 | // RiverpodGenerator 9 | // ************************************************************************** 10 | 11 | String _$postRepositoryHash() => r'd63a4af0fad0065e60376bc6ca23b5985c2a39ff'; 12 | 13 | /// See also [postRepository]. 14 | @ProviderFor(postRepository) 15 | final postRepositoryProvider = AutoDisposeProvider.internal( 16 | postRepository, 17 | name: r'postRepositoryProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$postRepositoryHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef PostRepositoryRef = AutoDisposeProviderRef; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 28 | -------------------------------------------------------------------------------- /lib/core/authentication/auth_repository.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ignore_for_file: type=lint, implicit_dynamic_parameter, implicit_dynamic_type, implicit_dynamic_method, strict_raw_type 4 | 5 | part of 'auth_repository.dart'; 6 | 7 | // ************************************************************************** 8 | // RiverpodGenerator 9 | // ************************************************************************** 10 | 11 | String _$authRepositoryHash() => r'827d48289fca9ff7300a551270d89129c3b7693f'; 12 | 13 | /// See also [authRepository]. 14 | @ProviderFor(authRepository) 15 | final authRepositoryProvider = AutoDisposeProvider.internal( 16 | authRepository, 17 | name: r'authRepositoryProvider', 18 | debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') 19 | ? null 20 | : _$authRepositoryHash, 21 | dependencies: null, 22 | allTransitiveDependencies: null, 23 | ); 24 | 25 | typedef AuthRepositoryRef = AutoDisposeProviderRef; 26 | // ignore_for_file: type=lint 27 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 28 | -------------------------------------------------------------------------------- /macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /lib/common/widgets/cached_circle_avatar.dart: -------------------------------------------------------------------------------- 1 | import 'package:cached_network_image/cached_network_image.dart'; 2 | import 'package:flutter/material.dart'; 3 | 4 | class CachedCircleAvatar extends StatelessWidget { 5 | const CachedCircleAvatar({ 6 | super.key, 7 | required this.imageUrl, 8 | this.radius, 9 | this.onTap, 10 | }); 11 | 12 | final String? imageUrl; 13 | final double? radius; 14 | final VoidCallback? onTap; 15 | 16 | @override 17 | Widget build(BuildContext context) { 18 | final placeHolder = CircleAvatar(radius: radius); 19 | return imageUrl == null 20 | ? placeHolder 21 | : CachedNetworkImage( 22 | imageUrl: imageUrl ?? '', 23 | placeholder: (context, _) => placeHolder, 24 | imageBuilder: (context, imageProvider) { 25 | return CircleAvatar( 26 | backgroundImage: imageProvider, 27 | radius: radius, 28 | child: ClipOval( 29 | child: Material( 30 | type: MaterialType.transparency, 31 | child: InkWell(onTap: onTap), 32 | ), 33 | ), 34 | ); 35 | }, 36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /lib/app.dart: -------------------------------------------------------------------------------- 1 | import 'package:adaptive_theme/adaptive_theme.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:flutter_localizations/flutter_localizations.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | import 'package:tsuruo_kit/tsuruo_kit.dart'; 6 | 7 | import 'core/router/router.dart'; 8 | import 'core/theme/theme.dart'; 9 | 10 | class MyApp extends ConsumerWidget { 11 | const MyApp({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context, WidgetRef ref) { 15 | return AdaptiveTheme( 16 | light: AppTheme.light, 17 | dark: AppTheme.dark, 18 | initial: AdaptiveThemeMode.system, 19 | builder: (light, dark) => MaterialApp.router( 20 | title: 'nippo', 21 | theme: light, 22 | darkTheme: dark, 23 | routerConfig: ref.watch(routerProvider), 24 | scaffoldMessengerKey: ref.watch(scaffoldMessengerKey), 25 | localizationsDelegates: const [ 26 | GlobalMaterialLocalizations.delegate, 27 | GlobalWidgetsLocalizations.delegate, 28 | GlobalCupertinoLocalizations.delegate, 29 | ], 30 | supportedLocales: const [ 31 | Locale('ja'), 32 | ], 33 | ), 34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /firebase/firestore.indexes.json: -------------------------------------------------------------------------------- 1 | { 2 | "indexes": [], 3 | "fieldOverrides": [ 4 | { 5 | "collectionGroup": "posts", 6 | "fieldPath": "createdAt", 7 | "ttl": false, 8 | "indexes": [ 9 | { 10 | "order": "ASCENDING", 11 | "queryScope": "COLLECTION" 12 | }, 13 | { 14 | "order": "DESCENDING", 15 | "queryScope": "COLLECTION" 16 | }, 17 | { 18 | "arrayConfig": "CONTAINS", 19 | "queryScope": "COLLECTION" 20 | }, 21 | { 22 | "order": "DESCENDING", 23 | "queryScope": "COLLECTION_GROUP" 24 | } 25 | ] 26 | }, 27 | { 28 | "collectionGroup": "posts", 29 | "fieldPath": "postId", 30 | "ttl": false, 31 | "indexes": [ 32 | { 33 | "order": "ASCENDING", 34 | "queryScope": "COLLECTION" 35 | }, 36 | { 37 | "order": "DESCENDING", 38 | "queryScope": "COLLECTION" 39 | }, 40 | { 41 | "arrayConfig": "CONTAINS", 42 | "queryScope": "COLLECTION" 43 | }, 44 | { 45 | "order": "ASCENDING", 46 | "queryScope": "COLLECTION_GROUP" 47 | } 48 | ] 49 | } 50 | ] 51 | } 52 | -------------------------------------------------------------------------------- /lib/features/post/model/post.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:freezed_annotation/freezed_annotation.dart'; 3 | import 'package:json_converter_helper/json_converter_helper.dart'; 4 | import 'package:nippo/core/const.dart'; 5 | 6 | part 'post.freezed.dart'; 7 | part 'post.g.dart'; 8 | 9 | @freezed 10 | class Post with _$Post { 11 | @allJsonConvertersSerializable 12 | factory Post({ 13 | // コレクショングループで引くためにフィールドにドキュメントIDをもたせる必要がある 14 | // フォームからインプットする時点ではドキュメントIDが決まらないのでnullableにしておく 15 | // 利用時には`late final`の非null版を利用すること 16 | @Deprecated('Use late field postId instead') 17 | @JsonKey(name: FieldName.postId) 18 | String? nullablePostId, 19 | required String title, 20 | required String description, 21 | @Default(UnionTimestamp.serverTimestamp()) UnionTimestamp updatedAt, 22 | @Default(UnionTimestamp.serverTimestamp()) UnionTimestamp createdAt, 23 | }) = _Post; 24 | 25 | factory Post.fromJson(Map json) => _$PostFromJson(json); 26 | Post._(); 27 | 28 | // ignore: deprecated_member_use_from_same_package 29 | late final String postId = nullablePostId!; 30 | } 31 | 32 | extension PostDocumentReferenceX on DocumentReference { 33 | String? get uid => parent.parent?.id; 34 | } 35 | -------------------------------------------------------------------------------- /lib/features/user/widgets/profile.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 3 | import 'package:nippo/common/common.dart'; 4 | 5 | import '../user_provider.dart'; 6 | 7 | class Profile extends ConsumerWidget { 8 | const Profile({super.key, required this.uid}); 9 | 10 | final String uid; 11 | 12 | @override 13 | Widget build(BuildContext context, WidgetRef ref) { 14 | final userDoc = ref.watch(userProvider(uid: uid)).value; 15 | final user = userDoc?.data(); 16 | return Column( 17 | children: [ 18 | Padding( 19 | padding: const EdgeInsets.only(top: 8), 20 | child: CachedCircleAvatar( 21 | imageUrl: user?.photoUrl, 22 | radius: 52, 23 | ), 24 | ), 25 | Padding( 26 | padding: const EdgeInsets.all(8), 27 | child: Text( 28 | user?.name ?? '', 29 | style: Theme.of(context).textTheme.headlineSmall!.copyWith( 30 | fontWeight: FontWeight.bold, 31 | ), 32 | ), 33 | ), 34 | Padding( 35 | padding: const EdgeInsets.symmetric(horizontal: 24), 36 | child: Text(userDoc?.id ?? ''), 37 | ), 38 | ], 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /.github/scripts/validate-formatting.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | if [[ $(git ls-files '*.dart' --modified) ]]; then 3 | echo "" 4 | echo "" 5 | echo "These files are not formatted correctly:" 6 | i=0 7 | for f in $(git ls-files '*.dart' --modified); do 8 | if [[ $f == *lib/firebase_options* ]] ; then 9 | continue 10 | fi 11 | if [[ $f == *.freezed.dart ]] ; then 12 | continue 13 | fi 14 | if [[ $f == *.g.dart ]] ; then 15 | continue 16 | fi 17 | if [[ $f == *.gen.dart ]] ; then 18 | continue 19 | fi 20 | i=$((i+1)) 21 | echo "" 22 | echo "" 23 | echo "-----------------------------------------------------------------" 24 | echo "$f" 25 | echo "-----------------------------------------------------------------" 26 | echo "" 27 | git --no-pager diff --unified=0 --minimal $f 28 | echo "" 29 | echo "-----------------------------------------------------------------" 30 | echo "" 31 | echo "" 32 | done 33 | if [[ $i == 0 ]]; then 34 | echo "" 35 | echo "✅ All files are formatted correctly." 36 | exit 0 37 | fi 38 | if [[ $GITHUB_WORKFLOW ]]; then 39 | git checkout . > /dev/null 2>&1 40 | fi 41 | echo "" 42 | echo "❌ Some files are incorrectly formatted, see above output." 43 | echo "" 44 | echo "To fix these locally, run Dart formatter." 45 | exit 1 46 | else 47 | echo "" 48 | echo "✅ All files are formatted correctly." 49 | fi -------------------------------------------------------------------------------- /firebase/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | firebase-debug.log* 8 | firebase-debug.*.log* 9 | 10 | # Firebase cache 11 | .firebase/ 12 | 13 | # Firebase config 14 | 15 | # Uncomment this if you'd like others to create their own Firebase project. 16 | # For a team working on the same Firebase project(s), it is recommended to leave 17 | # it commented so all members can deploy to the same project(s) in .firebaserc. 18 | # .firebaserc 19 | 20 | # Runtime data 21 | pids 22 | *.pid 23 | *.seed 24 | *.pid.lock 25 | 26 | # Directory for instrumented libs generated by jscoverage/JSCover 27 | lib-cov 28 | 29 | # Coverage directory used by tools like istanbul 30 | coverage 31 | 32 | # nyc test coverage 33 | .nyc_output 34 | 35 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 36 | .grunt 37 | 38 | # Bower dependency directory (https://bower.io/) 39 | bower_components 40 | 41 | # node-waf configuration 42 | .lock-wscript 43 | 44 | # Compiled binary addons (http://nodejs.org/api/addons.html) 45 | build/Release 46 | 47 | # Dependency directories 48 | node_modules/ 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Optional REPL history 57 | .node_repl_history 58 | 59 | # Output of 'npm pack' 60 | *.tgz 61 | 62 | # Yarn Integrity file 63 | .yarn-integrity 64 | 65 | # dotenv environment variables file 66 | .env 67 | -------------------------------------------------------------------------------- /ios/Runner/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 554602506203-2e3jkpfh6rabqmci2cck8k7ufkr9665e.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.554602506203-2e3jkpfh6rabqmci2cck8k7ufkr9665e 9 | ANDROID_CLIENT_ID 10 | 554602506203-cvqnsumv3q5c3mk5oj800tr7lugoo81u.apps.googleusercontent.com 11 | API_KEY 12 | AIzaSyALpm4NWeHM-PkaBkMaVuOWUJhRG0DHE-I 13 | GCM_SENDER_ID 14 | 554602506203 15 | PLIST_VERSION 16 | 1 17 | BUNDLE_ID 18 | com.htsuruo.nippo 19 | PROJECT_ID 20 | nippo-e8922 21 | STORAGE_BUCKET 22 | nippo-e8922.appspot.com 23 | IS_ADS_ENABLED 24 | 25 | IS_ANALYTICS_ENABLED 26 | 27 | IS_APPINVITE_ENABLED 28 | 29 | IS_GCM_ENABLED 30 | 31 | IS_SIGNIN_ENABLED 32 | 33 | GOOGLE_APP_ID 34 | 1:554602506203:ios:28faf2e71f77493b3ee9fe 35 | DATABASE_URL 36 | https://nippo-e8922.firebaseio.com 37 | 38 | -------------------------------------------------------------------------------- /macos/Runner/GoogleService-Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CLIENT_ID 6 | 554602506203-2e3jkpfh6rabqmci2cck8k7ufkr9665e.apps.googleusercontent.com 7 | REVERSED_CLIENT_ID 8 | com.googleusercontent.apps.554602506203-2e3jkpfh6rabqmci2cck8k7ufkr9665e 9 | ANDROID_CLIENT_ID 10 | 554602506203-cvqnsumv3q5c3mk5oj800tr7lugoo81u.apps.googleusercontent.com 11 | API_KEY 12 | AIzaSyALpm4NWeHM-PkaBkMaVuOWUJhRG0DHE-I 13 | GCM_SENDER_ID 14 | 554602506203 15 | PLIST_VERSION 16 | 1 17 | BUNDLE_ID 18 | com.htsuruo.nippo 19 | PROJECT_ID 20 | nippo-e8922 21 | STORAGE_BUCKET 22 | nippo-e8922.appspot.com 23 | IS_ADS_ENABLED 24 | 25 | IS_ANALYTICS_ENABLED 26 | 27 | IS_APPINVITE_ENABLED 28 | 29 | IS_GCM_ENABLED 30 | 31 | IS_SIGNIN_ENABLED 32 | 33 | GOOGLE_APP_ID 34 | 1:554602506203:ios:28faf2e71f77493b3ee9fe 35 | DATABASE_URL 36 | https://nippo-e8922.firebaseio.com 37 | 38 | 39 | -------------------------------------------------------------------------------- /android/app/google-services.json: -------------------------------------------------------------------------------- 1 | { 2 | "project_info": { 3 | "project_number": "554602506203", 4 | "firebase_url": "https://nippo-e8922.firebaseio.com", 5 | "project_id": "nippo-e8922", 6 | "storage_bucket": "nippo-e8922.appspot.com" 7 | }, 8 | "client": [ 9 | { 10 | "client_info": { 11 | "mobilesdk_app_id": "1:554602506203:android:052255c36e16460d3ee9fe", 12 | "android_client_info": { 13 | "package_name": "com.htsuruo.nippo" 14 | } 15 | }, 16 | "oauth_client": [ 17 | { 18 | "client_id": "554602506203-gv6vpvdndlppghdn3sv9cr2n3fc4d854.apps.googleusercontent.com", 19 | "client_type": 3 20 | } 21 | ], 22 | "api_key": [ 23 | { 24 | "current_key": "AIzaSyDSPm7SNLRYk1bBp5DFku-n-A4PrmjQFuU" 25 | } 26 | ], 27 | "services": { 28 | "appinvite_service": { 29 | "other_platform_oauth_client": [ 30 | { 31 | "client_id": "554602506203-gv6vpvdndlppghdn3sv9cr2n3fc4d854.apps.googleusercontent.com", 32 | "client_type": 3 33 | }, 34 | { 35 | "client_id": "554602506203-2e3jkpfh6rabqmci2cck8k7ufkr9665e.apps.googleusercontent.com", 36 | "client_type": 2, 37 | "ios_info": { 38 | "bundle_id": "com.htsuruo.nippo" 39 | } 40 | } 41 | ] 42 | } 43 | } 44 | } 45 | ], 46 | "configuration_version": "1" 47 | } -------------------------------------------------------------------------------- /firebase/functions/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "functions", 3 | "scripts": { 4 | "lint": "eslint --ext .js,.ts .", 5 | "build": "tsc", 6 | "build:watch": "tsc --watch", 7 | "serve": "npm run build && firebase emulators:start --only functions", 8 | "shell": "npm run build && firebase functions:shell", 9 | "start": "npm run shell", 10 | "deploy": "firebase deploy --only functions", 11 | "logs": "firebase functions:log", 12 | "test": "jest --silent --forceExit --detectOpenHandles", 13 | "test:watch": "jest --silent --watch", 14 | "test:instance": "npx firebase emulators:exec --only firestore,functions 'npm run test'" 15 | }, 16 | "engines": { 17 | "node": "18" 18 | }, 19 | "main": "lib/index.js", 20 | "dependencies": { 21 | "firebase-admin": "^11.11.0", 22 | "firebase-functions": "^4.4.1" 23 | }, 24 | "devDependencies": { 25 | "@firebase/rules-unit-testing": "^2.0.0", 26 | "@types/jest": "^29.5.6", 27 | "@typescript-eslint/eslint-plugin": "^6.9.0", 28 | "@typescript-eslint/parser": "^6.9.0", 29 | "eslint": "^8.52.0", 30 | "eslint-config-google": "^0.14.0", 31 | "eslint-config-prettier": "^9.0.0", 32 | "eslint-plugin-import": "^2.29.0", 33 | "eslint-plugin-prettier": "^5.0.1", 34 | "firebase": "^9.23.0", 35 | "firebase-functions-test": "^3.1.0", 36 | "firebase-tools": "^12.7.0", 37 | "prettier": "^3.0.3", 38 | "ts-jest": "^29.1.1", 39 | "ts-node": "^10.9.1", 40 | "typescript": "^5.2.2" 41 | }, 42 | "private": true 43 | } 44 | -------------------------------------------------------------------------------- /macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.15' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /lib/core/authentication/auth_provider.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ignore_for_file: type=lint, implicit_dynamic_parameter, implicit_dynamic_type, implicit_dynamic_method, strict_raw_type 4 | 5 | part of 'auth_provider.dart'; 6 | 7 | // ************************************************************************** 8 | // RiverpodGenerator 9 | // ************************************************************************** 10 | 11 | String _$isSignedInHash() => r'66951a5711a8cba0b0e18f9d1b39aa61a267f8d6'; 12 | 13 | /// See also [isSignedIn]. 14 | @ProviderFor(isSignedIn) 15 | final isSignedInProvider = FutureProvider.internal( 16 | isSignedIn, 17 | name: r'isSignedInProvider', 18 | debugGetCreateSourceHash: 19 | const bool.fromEnvironment('dart.vm.product') ? null : _$isSignedInHash, 20 | dependencies: null, 21 | allTransitiveDependencies: null, 22 | ); 23 | 24 | typedef IsSignedInRef = FutureProviderRef; 25 | String _$firUserHash() => r'e1a7491f8287541a42289c27ecbe613d5c37f7fd'; 26 | 27 | /// See also [firUser]. 28 | @ProviderFor(firUser) 29 | final firUserProvider = StreamProvider.internal( 30 | firUser, 31 | name: r'firUserProvider', 32 | debugGetCreateSourceHash: 33 | const bool.fromEnvironment('dart.vm.product') ? null : _$firUserHash, 34 | dependencies: null, 35 | allTransitiveDependencies: null, 36 | ); 37 | 38 | typedef FirUserRef = StreamProviderRef; 39 | // ignore_for_file: type=lint 40 | // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member 41 | -------------------------------------------------------------------------------- /ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | platform :ios, '16.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/features/post/detail/post_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gap/gap.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | import 'package:nippo/common/common.dart'; 5 | import 'package:nippo/features/post/detail/post_action_menu_button.dart'; 6 | import 'package:nippo/features/post/detail/post_detail_view.dart'; 7 | import 'package:nippo/features/post/post_provider.dart'; 8 | 9 | class PostPage extends ConsumerWidget { 10 | /// 投稿一覧画面からの遷移: [pid]のみでCollectionGroupで取得する 11 | const PostPage.fromAll({required String pid}) : this._(pid: pid); 12 | 13 | /// ユーザープロフィール画面からの遷移: [uid]と[pid]でDocumentReferenceから取得する 14 | const PostPage.fromProfile({required String pid, required String uid}) 15 | : this._(pid: pid, uid: uid); 16 | 17 | const PostPage._({required this.pid, this.uid}); 18 | 19 | final String pid; 20 | final String? uid; 21 | 22 | @override 23 | Widget build(BuildContext context, WidgetRef ref) { 24 | final uid = this.uid; 25 | final postSnapAsync = ref.watch(postProvider(postId: pid, uid: uid)); 26 | 27 | return Scaffold( 28 | appBar: AppBar( 29 | actions: [ 30 | PostActionMenuButton(postSnapAsync: postSnapAsync), 31 | const Gap(8), 32 | ], 33 | ), 34 | body: postSnapAsync.when( 35 | loading: CenteredCircularProgressIndicator.new, 36 | error: (error, stackTrace) { 37 | return Center( 38 | child: Text(error.toString()), 39 | ); 40 | }, 41 | data: (postSnap) => PostDetailView(postSnap: postSnap), 42 | ), 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /lib/features/post/detail/post_action_handler.dart: -------------------------------------------------------------------------------- 1 | import 'package:adaptive_dialog/adaptive_dialog.dart'; 2 | import 'package:cloud_firestore/cloud_firestore.dart'; 3 | import 'package:flutter/material.dart'; 4 | import 'package:go_router/go_router.dart'; 5 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 6 | import 'package:nippo/core/router/router.dart'; 7 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 8 | import 'package:tsuruo_kit/tsuruo_kit.dart'; 9 | 10 | import '../model/post.dart'; 11 | import '../post_repository.dart'; 12 | 13 | part 'post_action_handler.g.dart'; 14 | 15 | @Riverpod(keepAlive: true) 16 | PostActionHandler postAction(PostActionRef ref) => PostActionHandler(ref); 17 | 18 | class PostActionHandler { 19 | PostActionHandler(this._ref); 20 | final Ref _ref; 21 | 22 | BuildContext get _context => _ref.read(routerProvider).navigator.context; 23 | 24 | /// 編集 25 | Future edit({required DocumentSnapshot postSnap}) async => 26 | PostEditPageRoute(pid: postSnap.id).push(_context); 27 | 28 | /// 削除 29 | Future delete({required DocumentSnapshot postSnap}) async { 30 | final post = postSnap.data()!; 31 | if (OkCancelResult.ok == 32 | await showOkCancelAlertDialog( 33 | context: _context, 34 | title: '確認', 35 | message: '本当に削除しますか?', 36 | )) { 37 | _ref.read(postRepositoryProvider).delete(postRef: postSnap.reference); 38 | _ref 39 | .read(scaffoldMessengerKey) 40 | .currentState! 41 | .showMessage('[${post.title}]を削除しました'); 42 | _context.pop(); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/functions-test.yml: -------------------------------------------------------------------------------- 1 | name: Cloud Functions for Firebase 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'firebase/functions/**' 9 | - '!**.md' 10 | pull_request: 11 | paths: 12 | - 'firebase/functions/**' 13 | - '!**.md' 14 | 15 | jobs: 16 | test: 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 10 19 | defaults: 20 | run: 21 | working-directory: ./firebase/functions 22 | concurrency: 23 | group: ${{ github.workflow }}-${{ github.ref }} 24 | cancel-in-progress: true 25 | 26 | steps: 27 | - uses: actions/checkout@v3 28 | 29 | - name: setup Node.js 30 | uses: actions/setup-node@v3 31 | with: 32 | node-version: 18 33 | cache: 'npm' 34 | # ref. https://zenn.dev/nixieminton/articles/8b26a92feb26d8 35 | cache-dependency-path: ./firebase/functions/package-lock.json 36 | 37 | - name: Cache firebase emulators 38 | uses: actions/cache@v3 39 | with: 40 | path: ~/.cache/firebase/emulators 41 | key: ${{ runner.os }}-firebase-emulators-${{ hashFiles('~/.cache/firebase/emulators/**') }} 42 | 43 | - name: Install dependencies 44 | run: npm ci 45 | 46 | - name: Run tests 47 | # - --project: GitHub Actionsでは`firebase login`ができないため、プロジェクトの`default`指定が必要 48 | # `default`は`.firebaserc`の値に依存する 49 | # 50 | # - --forceExit: 実行されたjestが終了せずCIがタイムアウトしてしまうため`--forceExit`でテスト終了後強制終了させる 51 | # ref. https://jestjs.io/docs/cli#--forceexit 52 | run: npm run test:instance -- --project default 53 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | // START: FlutterFire Configuration 4 | id 'com.google.gms.google-services' 5 | // END: FlutterFire Configuration 6 | id "kotlin-android" 7 | // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. 8 | id "dev.flutter.flutter-gradle-plugin" 9 | } 10 | 11 | android { 12 | namespace = "com.htsuruo.nippo" 13 | compileSdk = flutter.compileSdkVersion 14 | // ndkVersion = flutter.ndkVersion 15 | ndkVersion = "26.1.10909125" 16 | 17 | compileOptions { 18 | sourceCompatibility = JavaVersion.VERSION_1_8 19 | targetCompatibility = JavaVersion.VERSION_1_8 20 | } 21 | 22 | kotlinOptions { 23 | jvmTarget = JavaVersion.VERSION_1_8 24 | } 25 | 26 | defaultConfig { 27 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 28 | applicationId = "com.htsuruo.nippo" 29 | // You can update the following values to match your application needs. 30 | // For more information, see: https://flutter.dev/to/review-gradle-config. 31 | // minSdk = flutter.minSdkVersion 32 | minSdk = 23 33 | targetSdk = flutter.targetSdkVersion 34 | versionCode = flutter.versionCode 35 | versionName = flutter.versionName 36 | } 37 | 38 | buildTypes { 39 | release { 40 | // TODO: Add your own signing config for the release build. 41 | // Signing with the debug keys for now, so `flutter run --release` works. 42 | signingConfig = signingConfigs.debug 43 | } 44 | } 45 | } 46 | 47 | flutter { 48 | source = "../.." 49 | } 50 | -------------------------------------------------------------------------------- /lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:firebase_app_check/firebase_app_check.dart'; 2 | import 'package:firebase_core/firebase_core.dart'; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:flutter_web_plugins/url_strategy.dart'; 6 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 7 | import 'package:intl/intl.dart'; 8 | import 'package:nippo/app.dart'; 9 | import 'package:nippo/firebase_options.dart'; 10 | import 'package:package_info_plus/package_info_plus.dart'; 11 | 12 | import 'common/common.dart'; 13 | 14 | Future main() async { 15 | WidgetsFlutterBinding.ensureInitialized(); 16 | usePathUrlStrategy(); 17 | 18 | final (_, appInfo) = await ( 19 | Firebase.initializeApp( 20 | options: DefaultFirebaseOptions.currentPlatform, 21 | ), 22 | PackageInfo.fromPlatform().then(AppInfo.fromPackageInfo), 23 | ).wait; 24 | 25 | await FirebaseAppCheck.instance.activate( 26 | webProvider: ReCaptchaEnterpriseProvider( 27 | '6Le3MeQoAAAAANvhE-K5ZL2F7jwuE0GNQz1Pka_x', 28 | ), 29 | // 一度デバッグトークンを登録しておけば、その後は優先的にデバッグトークンを使い続けるものと思っていたが、 30 | // activate時に`debug`指定しないとサーバ検証時にエラーになってしまトークンが返却されない 31 | // (未検証リクエスト判定)ので、一度登録した場合でもデバッグトークンを使いたい場合は`debug`指定が必要 32 | androidProvider: 33 | kDebugMode ? AndroidProvider.debug : AndroidProvider.playIntegrity, 34 | appleProvider: kDebugMode 35 | ? AppleProvider.debug 36 | : AppleProvider.appAttestWithDeviceCheckFallback, 37 | ); 38 | 39 | // DateTimeのdefaultLocaleを日本時間にする 40 | Intl.defaultLocale = 'ja'; 41 | runApp( 42 | ProviderScope( 43 | overrides: [ 44 | appInfoProvider.overrideWithValue(appInfo), 45 | ], 46 | child: const MyApp(), 47 | ), 48 | ); 49 | } 50 | -------------------------------------------------------------------------------- /lib/features/signin/signin_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:adaptive_dialog/adaptive_dialog.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:gap/gap.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | import 'package:nippo/common/common.dart'; 6 | import 'package:nippo/core/authentication/auth_repository.dart'; 7 | import 'package:nippo/core/logger.dart'; 8 | import 'package:tsuruo_kit/tsuruo_kit.dart'; 9 | 10 | class SigninPage extends ConsumerWidget { 11 | const SigninPage({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context, WidgetRef ref) { 15 | return Scaffold( 16 | body: Center( 17 | child: Column( 18 | mainAxisAlignment: MainAxisAlignment.center, 19 | children: [ 20 | const AppLogo(), 21 | const Gap(44), 22 | // 本来は`Sign in with Google`アイコンのレギュレーションに準拠するべきだが今回は割愛 23 | ElevatedButton( 24 | onPressed: () async { 25 | try { 26 | await ref 27 | .read(progressController.notifier) 28 | .executeWithProgress( 29 | () => 30 | ref.read(authRepositoryProvider).signInWithGoogle(), 31 | ); 32 | } on Exception catch (e) { 33 | logger.severe(e.toString()); 34 | await showOkAlertDialog( 35 | context: context, 36 | title: 'Google SignIn Failed', 37 | message: e.toString(), 38 | ); 39 | } 40 | }, 41 | child: const Text('Sign in with Google'), 42 | ), 43 | ], 44 | ), 45 | ), 46 | ); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /firebase/functions/src/test/rules/utils.ts: -------------------------------------------------------------------------------- 1 | import { 2 | initializeTestEnvironment, 3 | RulesTestEnvironment, 4 | TokenOptions, 5 | } from '@firebase/rules-unit-testing' 6 | import { readFileSync } from 'fs' 7 | import firebase from 'firebase/compat/app' 8 | 9 | export class Tester { 10 | #env!: RulesTestEnvironment 11 | static async init(projectId?: string): Promise { 12 | const tester = new Tester() 13 | process.env.FIRESTORE_EMULATOR_HOST = '127.0.0.1:8080' 14 | tester.#env = await initializeTestEnvironment({ 15 | // テスト用で利用するなら"demo-*"が推奨 16 | projectId: projectId ?? 'demo-project', 17 | firestore: { 18 | rules: readFileSync('../firestore.rules', 'utf8'), 19 | }, 20 | }) 21 | return tester 22 | } 23 | 24 | // How to Use: 25 | // await tester.withSecurityRulesDisabled(async (db) => { 26 | // await db.doc(_documentRef().path).set({}) 27 | // }) 28 | async withSecurityRulesDisabled( 29 | f: (db: firebase.firestore.Firestore) => Promise 30 | ): Promise { 31 | let result!: T 32 | await this.#env.withSecurityRulesDisabled(async (context) => { 33 | result = await f(context.firestore()) 34 | }) 35 | return result 36 | } 37 | 38 | db(auth?: Auth): firebase.firestore.Firestore { 39 | if (!auth?.userId) { 40 | return this.#env.unauthenticatedContext().firestore() 41 | } 42 | return this.#env.authenticatedContext(auth.userId, { ...auth }).firestore() 43 | } 44 | 45 | async afterEach(): Promise { 46 | await this.#env.clearFirestore() 47 | } 48 | 49 | async afterAll(): Promise { 50 | await this.#env.cleanup() 51 | } 52 | } 53 | 54 | // ToeknOptionsにneverのuid指定が存在し衝突するため、`userId`として定義 55 | export type Auth = TokenOptions & { readonly userId: string } 56 | -------------------------------------------------------------------------------- /.github/workflows/flutter.yml: -------------------------------------------------------------------------------- 1 | name: Flutter 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: 7 | - '*' 8 | - '!main' 9 | # ref. https://qiita.com/nacam403/items/3e2a5df5e88ba20aa76a 10 | paths: 11 | - 'lib/**' 12 | - '!**.md' 13 | 14 | jobs: 15 | test: 16 | runs-on: ubuntu-latest 17 | timeout-minutes: 10 18 | concurrency: 19 | group: ${{ github.workflow }}-${{ github.ref }} 20 | cancel-in-progress: true 21 | 22 | steps: 23 | - uses: actions/checkout@v3 24 | 25 | - name: Setup Java 26 | uses: actions/setup-java@v3 27 | with: 28 | distribution: 'temurin' 29 | java-version: '17' 30 | 31 | - name: Parse an FVM config file into environment variables 32 | uses: kuhnroyal/flutter-fvm-config-action@v1 33 | with: 34 | path: '.fvm/fvm_config.json' 35 | 36 | - name: Setup Flutter 37 | uses: subosito/flutter-action@v2 38 | with: 39 | channel: ${{ env.FLUTTER_CHANNEL }} 40 | cache: true 41 | # バージョン固定したい場合はこちら 42 | # flutter-version: ${{ env.FLUTTER_VERSION }} 43 | 44 | # fvmのsdkを参照するために`melos.yaml`に`sdkPath`を指定しているため、 45 | # より優先度の高い環境変数`MELOS_SDK_PATH`を指定します。 46 | - name: Set MELOS_SDK_PATH 47 | run: echo "MELOS_SDK_PATH=${{ env.FLUTTER_ROOT }}" >> $GITHUB_ENV 48 | 49 | - name: Setup Melos 50 | run: dart pub global activate melos && melos bs 51 | 52 | - name: Analyze 53 | run: melos run analyze 54 | 55 | - name: Run custom_lint 56 | run: melos run run:custom_lint 57 | 58 | - name: Check Format 59 | run: | 60 | melos run format 61 | .github/scripts/validate-formatting.sh 62 | 63 | - name: Test 64 | run: melos run test 65 | -------------------------------------------------------------------------------- /firebase/firestore.rules: -------------------------------------------------------------------------------- 1 | rules_version = '2'; 2 | service cloud.firestore { 3 | match /databases/{database}/documents { 4 | 5 | function isAuthenticated() { 6 | return request.auth != null; 7 | } 8 | function isUserAuthenticated(userId) { 9 | return request.auth.uid == userId; 10 | } 11 | function existingData() { 12 | return resource.data; 13 | } 14 | function incomingData() { 15 | return request.resource.data; 16 | } 17 | 18 | // コレクショングループ 19 | match /{path=**}/posts/{postId} { 20 | allow list: if isAuthenticated(); 21 | 22 | // 自分以外の投稿を閲覧できないようにする場合は以下などで書き換える。 23 | // userRef: "databases/(default)/documents/users/{userId}/posts/{postId}" 24 | // allow list: request.path.split('/')[4] == request.auth.uid; 25 | } 26 | 27 | match /users/{userId} { 28 | // ドキュメントはExtensionのAuthトリガーで作成・削除するためcreate,deleteは不要 29 | allow get: if isAuthenticated(); 30 | allow update: if isUserAuthenticated(userId); 31 | 32 | match /posts/{postId} { 33 | allow read: if isAuthenticated(); 34 | allow delete: if isUserAuthenticated(userId); 35 | allow create, update: if isUserAuthenticated(userId) 36 | && validatePostText() 37 | && incomingData().updatedAt == request.time 38 | && incomingData().keys().hasAll(['postId']); 39 | } 40 | 41 | function validatePostText() { 42 | let title = incomingData().title.size(); 43 | let description = incomingData().description.size(); 44 | return validateTextLength(title, 20) && validateTextLength(description, 200); 45 | } 46 | 47 | // テキストの文字数バリデーション 48 | // UTF16ベースのため絵文字絵文字を考慮し緩く制限を設ける必要あり 49 | function validateTextLength(size, length) { 50 | return size >= 1 && size <= length; 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/core/theme/theme.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:google_fonts/google_fonts.dart'; 3 | 4 | class AppTheme { 5 | AppTheme._(); 6 | 7 | static ThemeData get light => _themeData(Brightness.light); 8 | static ThemeData get dark => _themeData(Brightness.dark); 9 | 10 | static ThemeData _themeData(Brightness brightness) { 11 | final themeData = ThemeData.from( 12 | useMaterial3: true, 13 | colorScheme: ColorScheme.fromSeed( 14 | brightness: brightness, 15 | seedColor: Colors.deepPurple, 16 | ), 17 | ); 18 | final listTextStyle = themeData.textTheme.bodyMedium; 19 | 20 | return themeData.copyWith( 21 | splashFactory: InkSparkle.splashFactory, 22 | textTheme: themeData.textTheme.apply( 23 | fontFamily: GoogleFonts.lato().fontFamily, 24 | ), 25 | // スクロール時にAppBarのElevationがでカラーが切り替わる表現を無効化 26 | appBarTheme: const AppBarTheme(scrolledUnderElevation: 0), 27 | dividerTheme: const DividerThemeData(space: 0), 28 | listTileTheme: ListTileThemeData( 29 | titleTextStyle: listTextStyle, 30 | leadingAndTrailingTextStyle: listTextStyle, 31 | ), 32 | snackBarTheme: SnackBarThemeData( 33 | behavior: SnackBarBehavior.floating, 34 | shape: RoundedRectangleBorder( 35 | borderRadius: BorderRadius.circular(8), 36 | ), 37 | ), 38 | inputDecorationTheme: const InputDecorationTheme( 39 | filled: true, 40 | labelStyle: TextStyle(fontWeight: FontWeight.bold), 41 | floatingLabelBehavior: FloatingLabelBehavior.always, 42 | border: OutlineInputBorder( 43 | borderSide: BorderSide.none, 44 | borderRadius: BorderRadius.all(Radius.circular(8)), 45 | ), 46 | contentPadding: EdgeInsets.all(12), 47 | alignLabelWithHint: true, 48 | ), 49 | ); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /firebase/public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 23 | 24 | 25 |
26 |

404

27 |

Page Not Found

28 |

The specified file was not found on this website. Please check the URL for mistakes and try again.

29 |

Why am I seeing this?

30 |

This page was generated by the Firebase Command-Line Interface. To modify it, edit the 404.html file in your project's configured public directory.

31 |
32 | 33 | 34 | -------------------------------------------------------------------------------- /lib/common/providers/app_info_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:package_info_plus/package_info_plus.dart'; 3 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 4 | import 'package:version/version.dart'; 5 | 6 | part 'app_info_provider.g.dart'; 7 | 8 | @Riverpod(keepAlive: true) 9 | AppInfo appInfo(AppInfoRef ref) => throw UnimplementedError(); 10 | 11 | @immutable 12 | class AppInfo { 13 | const AppInfo._({ 14 | required this.appName, 15 | required this.packageName, 16 | required this.version, 17 | required this.buildNumber, 18 | required this.buildSignature, 19 | }); 20 | 21 | AppInfo.fromPackageInfo(PackageInfo info) 22 | : this._( 23 | appName: info.appName, 24 | packageName: info.packageName, 25 | version: Version.parse(info.version), 26 | buildNumber: info.buildNumber, 27 | buildSignature: info.buildSignature, 28 | ); 29 | 30 | final String appName; 31 | final String packageName; 32 | final Version version; 33 | final String buildNumber; 34 | final String buildSignature; 35 | 36 | @override 37 | String toString() { 38 | // ignore: lines_longer_than_80_chars 39 | return 'AppInfo{appName: $appName, packageName: $packageName, version: $version, buildNumber: $buildNumber, buildSignature: $buildSignature}'; 40 | } 41 | 42 | @override 43 | bool operator ==(Object other) => 44 | identical(this, other) || 45 | other is AppInfo && 46 | runtimeType == other.runtimeType && 47 | appName == other.appName && 48 | packageName == other.packageName && 49 | version == other.version && 50 | buildNumber == other.buildNumber && 51 | buildSignature == other.buildSignature; 52 | 53 | @override 54 | int get hashCode => 55 | appName.hashCode ^ 56 | packageName.hashCode ^ 57 | version.hashCode ^ 58 | buildNumber.hashCode ^ 59 | buildSignature.hashCode; 60 | } 61 | -------------------------------------------------------------------------------- /macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import appkit_ui_element_colors 9 | import cloud_firestore 10 | import desktop_webview_auth 11 | import dynamic_color 12 | import firebase_app_check 13 | import firebase_auth 14 | import firebase_core 15 | import google_sign_in_ios 16 | import macos_ui 17 | import macos_window_utils 18 | import package_info_plus 19 | import path_provider_foundation 20 | import shared_preferences_foundation 21 | import sqflite 22 | import url_launcher_macos 23 | 24 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 25 | AppkitUiElementColorsPlugin.register(with: registry.registrar(forPlugin: "AppkitUiElementColorsPlugin")) 26 | FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) 27 | DesktopWebviewAuthPlugin.register(with: registry.registrar(forPlugin: "DesktopWebviewAuthPlugin")) 28 | DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) 29 | FLTFirebaseAppCheckPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAppCheckPlugin")) 30 | FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) 31 | FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) 32 | FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin")) 33 | MacOSUiPlugin.register(with: registry.registrar(forPlugin: "MacOSUiPlugin")) 34 | MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin")) 35 | FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) 36 | PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) 37 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 38 | SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) 39 | UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) 40 | } 41 | -------------------------------------------------------------------------------- /ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CADisableMinimumFrameDurationOnPhone 6 | 7 | CFBundleDevelopmentRegion 8 | $(DEVELOPMENT_LANGUAGE) 9 | CFBundleDisplayName 10 | Nippo 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | nippo 19 | CFBundlePackageType 20 | APPL 21 | CFBundleShortVersionString 22 | $(FLUTTER_BUILD_NAME) 23 | CFBundleSignature 24 | ???? 25 | CFBundleURLTypes 26 | 27 | 28 | CFBundleTypeRole 29 | Editor 30 | CFBundleURLSchemes 31 | 32 | com.googleusercontent.apps.554602506203-2e3jkpfh6rabqmci2cck8k7ufkr9665e 33 | 34 | 35 | 36 | CFBundleVersion 37 | $(FLUTTER_BUILD_NUMBER) 38 | LSRequiresIPhoneOS 39 | 40 | UIApplicationSupportsIndirectInputEvents 41 | 42 | UILaunchStoryboardName 43 | LaunchScreen 44 | UIMainStoryboardFile 45 | Main 46 | UISupportedInterfaceOrientations 47 | 48 | UIInterfaceOrientationPortrait 49 | UIInterfaceOrientationLandscapeLeft 50 | UIInterfaceOrientationLandscapeRight 51 | 52 | UISupportedInterfaceOrientations~ipad 53 | 54 | UIInterfaceOrientationPortrait 55 | UIInterfaceOrientationPortraitUpsideDown 56 | UIInterfaceOrientationLandscapeLeft 57 | UIInterfaceOrientationLandscapeRight 58 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: nippo 2 | description: This is a daily report app, and it's a sample app using Flutter and Firebase. 3 | version: 1.0.0+1 4 | publish_to: none 5 | 6 | environment: 7 | sdk: '^3.5.0' 8 | 9 | dependencies: 10 | adaptive_dialog: 11 | adaptive_theme: 12 | cached_network_image: 13 | cloud_firestore: 14 | cupertino_icons: 15 | # 未マージのためgit指定 16 | # ref. https://github.com/invertase/flutter_desktop_webview_auth/pull/51 17 | desktop_webview_auth: 18 | git: https://github.com/andyshephard/flutter_desktop_webview_auth 19 | firebase_app_check: 20 | firebase_auth: 21 | firebase_core: 22 | flutter: 23 | sdk: flutter 24 | flutter_hooks: 25 | flutter_localizations: 26 | sdk: flutter 27 | flutter_svg: 28 | flutter_web_plugins: 29 | sdk: flutter 30 | freezed_annotation: 31 | gap: 32 | go_router: 33 | google_fonts: 34 | google_sign_in: 35 | hooks_riverpod: 36 | intersperse: 37 | intl: 38 | json_annotation: 39 | json_converter_helper: 40 | package_info_plus: 41 | provider: 42 | recase: 43 | riverpod_annotation: 44 | shared_preferences: 45 | simple_logger: 46 | smooth_highlight: 47 | tsuruo_kit: 48 | url_launcher: 49 | version: 50 | 51 | dev_dependencies: 52 | analyzer: 53 | build_runner: 54 | custom_lint: 55 | flutter_gen: 56 | flutter_gen_runner: 57 | freezed: 58 | go_router_builder: 59 | icons_launcher: 60 | json_serializable: 61 | melos: 62 | pedantic_mono: 63 | riverpod_generator: 64 | riverpod_lint: 65 | 66 | dependency_overrides: 67 | http: 68 | 69 | flutter_gen: 70 | integrations: 71 | flutter_svg: true 72 | 73 | flutter: 74 | uses-material-design: true 75 | assets: 76 | - assets/images/ 77 | - assets/images/logo/ 78 | 79 | icons_launcher: 80 | image_path: 'assets/images/app_icon.png' 81 | platforms: 82 | android: 83 | enable: true 84 | adaptive_foreground_image: 'assets/images/app_adaptive_foreground.png' 85 | adaptive_background_image: 'assets/images/app_adaptive_background.png' 86 | notification_image: 'assets/images/app_adaptive_foreground.png' 87 | adaptive_monochrome_image: 'assets/images/app_adaptive_monochrome.png' 88 | ios: 89 | enable: true 90 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | nippo 33 | 34 | 35 | 45 | 46 | 47 | 48 | 49 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /lib/features/post/post_provider.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:nippo/core/const.dart'; 3 | import 'package:nippo/features/post/model/post_converter.dart'; 4 | import 'package:nippo/features/user/user_provider.dart'; 5 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 6 | 7 | import 'model/post.dart'; 8 | 9 | part 'post_provider.g.dart'; 10 | 11 | // REVIEW(htsuruo): ドキュメントIDが取得できるようにQueryDocumentSnapshotから返すProviderにしてみた。 12 | // ただ、仮にデータソースをFirestoreからSupabaseやREST APIに差し替えたくなった場合は不都合が生じそう。 13 | // Listのデータモデルを返却する形にしておけば、差し替えは容易そうだがFirestoreを使う場合は割り切ったほうが良いのだろうか。 14 | @riverpod 15 | Stream>> posts(PostsRef ref) { 16 | return FirebaseFirestore.instance 17 | .collectionGroup(CollectionName.posts) 18 | .withPostConverter() 19 | .orderBy(FieldName.createdAt, descending: true) 20 | .snapshots() 21 | .map((snapshot) => snapshot.docs); 22 | } 23 | 24 | @riverpod 25 | Stream?> post( 26 | PostRef ref, { 27 | String? postId, 28 | String? uid, 29 | }) { 30 | if (uid == null) { 31 | // コレクショングループで取得 32 | return FirebaseFirestore.instance 33 | .collectionGroup(CollectionName.posts) 34 | .withPostConverter() 35 | .where(FieldName.postId, isEqualTo: postId) 36 | .snapshots() 37 | .map((s) => s.docs.firstOrNull); 38 | } 39 | 40 | // ドキュメントで取得 41 | return FirebaseFirestore.instance 42 | .collection(CollectionName.users) 43 | .doc(uid) 44 | .collection(CollectionName.posts) 45 | .doc(postId) 46 | .withPostConverter() 47 | .snapshots(); 48 | } 49 | 50 | @riverpod 51 | Stream>> userPosts( 52 | UserPostsRef ref, 53 | String uid, 54 | ) { 55 | return FirebaseFirestore.instance 56 | .collection(CollectionName.users) 57 | .doc(uid) 58 | .collection(CollectionName.posts) 59 | .withPostConverter() 60 | .orderBy(FieldName.createdAt, descending: true) 61 | .snapshots() 62 | .map( 63 | (snapshot) => snapshot.docs.toList(), 64 | ); 65 | } 66 | 67 | @riverpod 68 | CollectionReference selfPostRef(SelfPostRefRef ref) { 69 | return ref 70 | .watch(authUserRefProvider) 71 | .collection(CollectionName.posts) 72 | .withPostConverter(); 73 | } 74 | -------------------------------------------------------------------------------- /android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /lib/features/post/detail/post_detail_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:gap/gap.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | import 'package:nippo/common/common.dart'; 6 | import 'package:nippo/features/post/model/post.dart'; 7 | import 'package:smooth_highlight/smooth_highlight.dart'; 8 | 9 | class PostDetailView extends ConsumerWidget { 10 | const PostDetailView({super.key, required this.postSnap}); 11 | 12 | final DocumentSnapshot? postSnap; 13 | 14 | @override 15 | Widget build(BuildContext context, WidgetRef ref) { 16 | final theme = Theme.of(context); 17 | final post = postSnap?.data(); 18 | if (post == null) { 19 | return const Center( 20 | child: Text('データがありません'), 21 | ); 22 | } 23 | 24 | return SingleChildScrollView( 25 | padding: const EdgeInsets.all(16), 26 | child: Column( 27 | crossAxisAlignment: CrossAxisAlignment.start, 28 | children: [ 29 | _Highlight( 30 | value: post.title, 31 | child: Text( 32 | post.title, 33 | style: theme.textTheme.titleLarge!.copyWith( 34 | fontWeight: FontWeight.bold, 35 | ), 36 | ), 37 | ), 38 | const Gap(4), 39 | Text('作成日: ${post.createdAt.date!.formatted}'), 40 | const Padding( 41 | padding: EdgeInsets.symmetric(vertical: 16), 42 | child: Divider(), 43 | ), 44 | _Highlight( 45 | value: post.description, 46 | child: Text( 47 | post.description, 48 | style: theme.textTheme.bodyLarge, 49 | ), 50 | ), 51 | ], 52 | ), 53 | ); 54 | } 55 | } 56 | 57 | // TODO(htsuruo): アニメーションの開始が早くてチラついてしまうので、ディレイを設定できるようにする 58 | class _Highlight extends StatelessWidget { 59 | const _Highlight({ 60 | super.key, 61 | required this.value, 62 | required this.child, 63 | }); 64 | 65 | final T? value; 66 | final Widget child; 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | final theme = Theme.of(context); 71 | final colorScheme = theme.colorScheme; 72 | 73 | return ValueChangeHighlight( 74 | value: value, 75 | color: colorScheme.primary.withOpacity(.4), 76 | child: child, 77 | ); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /firebase/functions/src/test/rules/users.test.ts: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/compat/app' 2 | import { Auth, Tester } from './utils' 3 | import { Collection } from '../../collection' 4 | import { assertUnauthenticatedAccessFails } from './common' 5 | import { assertFails, assertSucceeds } from '@firebase/rules-unit-testing' 6 | 7 | const auth = { 8 | userId: 'alice', 9 | email: 'alice@example.com', 10 | } as Auth 11 | 12 | describe('users security rule', () => { 13 | let tester: Tester 14 | let db: firebase.firestore.Firestore 15 | 16 | beforeEach(async () => { 17 | tester = await Tester.init() 18 | db = tester.db() 19 | }) 20 | afterEach(async () => await tester.afterEach()) 21 | afterAll(async () => await tester.afterAll()) 22 | 23 | const collectionRef = () => db.collection(Collection.users) 24 | const documentRef = () => collectionRef().doc(auth.userId) 25 | assertUnauthenticatedAccessFails(collectionRef, documentRef) 26 | 27 | describe('認証済みの時', () => { 28 | beforeEach(async () => { 29 | db = tester.db(auth) 30 | }) 31 | test('list: できない', async () => { 32 | await assertFails(collectionRef().get()) 33 | }) 34 | test('get: できる', async () => { 35 | await assertSucceeds(documentRef().get()) 36 | }) 37 | test('create: できない', async () => { 38 | await assertFails(collectionRef().add({})) 39 | await assertFails(documentRef().set({})) 40 | }) 41 | test('update: できる', async () => { 42 | await tester.withSecurityRulesDisabled(async (db) => { 43 | // authenticatedContextを通したdbでsetする必要があるので、`documentRef().set()`ではNG 44 | await db.doc(documentRef().path).set({}) 45 | }) 46 | await assertSucceeds(documentRef().update({})) 47 | }) 48 | test('delete: できない', async () => { 49 | await assertFails(documentRef().delete()) 50 | }) 51 | 52 | describe('他人のデータに対して', () => { 53 | let otherUserRef: firebase.firestore.DocumentReference 54 | beforeEach(() => { 55 | otherUserRef = collectionRef().doc('bob') 56 | }) 57 | 58 | test('get: できる', async () => { 59 | // 他ユーザーのプロフィール表示などを想定 60 | await assertSucceeds(otherUserRef.get()) 61 | }) 62 | test('update: できない', async () => { 63 | await tester.withSecurityRulesDisabled(async (db) => { 64 | await db.doc(otherUserRef.path).set({}) 65 | }) 66 | await assertFails(otherUserRef.update({})) 67 | }) 68 | }) 69 | }) 70 | }) 71 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/features/post/detail/post_action_menu_button.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:gap/gap.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | import 'package:nippo/core/authentication/auth_provider.dart'; 6 | import 'package:nippo/features/post/detail/post_action_handler.dart'; 7 | 8 | import '../model/post.dart'; 9 | 10 | class PostActionMenuButton extends ConsumerWidget { 11 | const PostActionMenuButton({super.key, required this.postSnapAsync}); 12 | 13 | final AsyncValue?> postSnapAsync; 14 | 15 | @override 16 | Widget build(BuildContext context, WidgetRef ref) { 17 | final theme = Theme.of(context); 18 | final colorScheme = theme.colorScheme; 19 | final postSnap = postSnapAsync.value; 20 | final isMine = ref.watch( 21 | firUserProvider.select( 22 | (s) => s.value?.uid == postSnap?.reference.uid, 23 | ), 24 | ); 25 | 26 | return !isMine 27 | ? const SizedBox.shrink() 28 | : PopupMenuButton<_MenuItem>( 29 | icon: const Icon(Icons.more_horiz_outlined), 30 | // TODO(htsuruo): 書き換える 31 | onSelected: postSnap == null 32 | ? null 33 | : (item) async { 34 | final handler = ref.read(postActionProvider); 35 | switch (item) { 36 | case _MenuItem.edit: 37 | await handler.edit(postSnap: postSnap); 38 | case _MenuItem.delete: 39 | await handler.delete(postSnap: postSnap); 40 | } 41 | }, 42 | itemBuilder: (context) => _MenuItem.values.map((item) { 43 | final color = item == _MenuItem.delete ? colorScheme.error : null; 44 | return PopupMenuItem( 45 | value: item, 46 | child: Row( 47 | children: [ 48 | Icon(item.iconData, color: color), 49 | const Gap(8), 50 | Text(item.title, style: TextStyle(color: color)), 51 | ], 52 | ), 53 | ); 54 | }).toList(), 55 | ); 56 | } 57 | } 58 | 59 | enum _MenuItem { 60 | edit(title: '編集', iconData: Icons.edit_outlined), 61 | delete(title: '削除', iconData: Icons.delete_outline), 62 | ; 63 | 64 | const _MenuItem({required this.title, required this.iconData}); 65 | final String title; 66 | final IconData iconData; 67 | } 68 | -------------------------------------------------------------------------------- /lib/features/user/model/user.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ignore_for_file: type=lint, implicit_dynamic_parameter, implicit_dynamic_type, implicit_dynamic_method, strict_raw_type 4 | 5 | part of 'user.dart'; 6 | 7 | // ************************************************************************** 8 | // JsonSerializableGenerator 9 | // ************************************************************************** 10 | 11 | _$UserImpl _$$UserImplFromJson(Map json) => _$UserImpl( 12 | name: json['name'] as String? ?? '名無し', 13 | email: json['email'] as String, 14 | photoUrl: json['photoUrl'] as String?, 15 | updatedAt: json['updatedAt'] == null 16 | ? const UnionTimestamp.serverTimestamp() 17 | : const UnionTimestampConverter() 18 | .fromJson(json['updatedAt'] as Object), 19 | createdAt: json['createdAt'] == null 20 | ? const UnionTimestamp.serverTimestamp() 21 | : const UnionTimestampConverter() 22 | .fromJson(json['createdAt'] as Object), 23 | ); 24 | 25 | const _$$UserImplFieldMap = { 26 | 'name': 'name', 27 | 'email': 'email', 28 | 'photoUrl': 'photoUrl', 29 | 'updatedAt': 'updatedAt', 30 | 'createdAt': 'createdAt', 31 | }; 32 | 33 | abstract final class _$$UserImplJsonKeys { 34 | static const String name = 'name'; 35 | static const String email = 'email'; 36 | static const String photoUrl = 'photoUrl'; 37 | static const String updatedAt = 'updatedAt'; 38 | static const String createdAt = 'createdAt'; 39 | } 40 | 41 | // ignore: unused_element 42 | abstract class _$$UserImplPerFieldToJson { 43 | // ignore: unused_element 44 | static Object? name(String instance) => instance; 45 | // ignore: unused_element 46 | static Object? email(String instance) => instance; 47 | // ignore: unused_element 48 | static Object? photoUrl(String? instance) => instance; 49 | // ignore: unused_element 50 | static Object? updatedAt(UnionTimestamp instance) => 51 | const UnionTimestampConverter().toJson(instance); 52 | // ignore: unused_element 53 | static Object? createdAt(UnionTimestamp instance) => 54 | const UnionTimestampConverter().toJson(instance); 55 | } 56 | 57 | Map _$$UserImplToJson(_$UserImpl instance) => 58 | { 59 | 'name': instance.name, 60 | 'email': instance.email, 61 | 'photoUrl': instance.photoUrl, 62 | 'updatedAt': const UnionTimestampConverter().toJson(instance.updatedAt), 63 | 'createdAt': const UnionTimestampConverter().toJson(instance.createdAt), 64 | }; 65 | -------------------------------------------------------------------------------- /lib/features/post/model/post.g.dart: -------------------------------------------------------------------------------- 1 | // GENERATED CODE - DO NOT MODIFY BY HAND 2 | 3 | // ignore_for_file: type=lint, implicit_dynamic_parameter, implicit_dynamic_type, implicit_dynamic_method, strict_raw_type 4 | 5 | part of 'post.dart'; 6 | 7 | // ************************************************************************** 8 | // JsonSerializableGenerator 9 | // ************************************************************************** 10 | 11 | _$PostImpl _$$PostImplFromJson(Map json) => _$PostImpl( 12 | nullablePostId: json['postId'] as String?, 13 | title: json['title'] as String, 14 | description: json['description'] as String, 15 | updatedAt: json['updatedAt'] == null 16 | ? const UnionTimestamp.serverTimestamp() 17 | : const UnionTimestampConverter() 18 | .fromJson(json['updatedAt'] as Object), 19 | createdAt: json['createdAt'] == null 20 | ? const UnionTimestamp.serverTimestamp() 21 | : const UnionTimestampConverter() 22 | .fromJson(json['createdAt'] as Object), 23 | ); 24 | 25 | const _$$PostImplFieldMap = { 26 | 'nullablePostId': 'postId', 27 | 'title': 'title', 28 | 'description': 'description', 29 | 'updatedAt': 'updatedAt', 30 | 'createdAt': 'createdAt', 31 | }; 32 | 33 | abstract final class _$$PostImplJsonKeys { 34 | static const String nullablePostId = 'postId'; 35 | static const String title = 'title'; 36 | static const String description = 'description'; 37 | static const String updatedAt = 'updatedAt'; 38 | static const String createdAt = 'createdAt'; 39 | } 40 | 41 | // ignore: unused_element 42 | abstract class _$$PostImplPerFieldToJson { 43 | // ignore: unused_element 44 | static Object? nullablePostId(String? instance) => instance; 45 | // ignore: unused_element 46 | static Object? title(String instance) => instance; 47 | // ignore: unused_element 48 | static Object? description(String instance) => instance; 49 | // ignore: unused_element 50 | static Object? updatedAt(UnionTimestamp instance) => 51 | const UnionTimestampConverter().toJson(instance); 52 | // ignore: unused_element 53 | static Object? createdAt(UnionTimestamp instance) => 54 | const UnionTimestampConverter().toJson(instance); 55 | } 56 | 57 | Map _$$PostImplToJson(_$PostImpl instance) => 58 | { 59 | 'postId': instance.nullablePostId, 60 | 'title': instance.title, 61 | 'description': instance.description, 62 | 'updatedAt': const UnionTimestampConverter().toJson(instance.updatedAt), 63 | 'createdAt': const UnionTimestampConverter().toJson(instance.createdAt), 64 | }; 65 | -------------------------------------------------------------------------------- /lib/features/user/user_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | import 'package:nippo/core/authentication/auth_provider.dart'; 5 | import 'package:nippo/core/router/router.dart'; 6 | 7 | import '../post/post_provider.dart'; 8 | import '../post/widgets/widgets.dart'; 9 | import 'widgets/profile.dart'; 10 | 11 | class UserPage extends ConsumerWidget { 12 | const UserPage._({this.uid, required this.isMe}); 13 | 14 | // 投稿一覧からユーザーアバターを押下した場合はそのユーザーのプロフィールを表示する 15 | const UserPage.uid(String uid) : this._(uid: uid, isMe: false); 16 | 17 | // プロフィールタブを押下した場合は自分のプロフィールを表示する 18 | const UserPage.me() : this._(isMe: true); 19 | 20 | final String? uid; 21 | final bool isMe; 22 | 23 | @override 24 | Widget build(BuildContext context, WidgetRef ref) { 25 | final uid = isMe 26 | ? ref.watch(firUserProvider.select((s) => s.value?.uid)) 27 | : this.uid; 28 | 29 | return Scaffold( 30 | appBar: AppBar( 31 | actions: [ 32 | if (isMe) 33 | IconButton( 34 | icon: const Icon(Icons.settings_outlined), 35 | onPressed: () { 36 | SettingPageRoute().push(context); 37 | }, 38 | ), 39 | ], 40 | ), 41 | body: uid == null 42 | ? const _UserNotFound() 43 | : Column( 44 | crossAxisAlignment: CrossAxisAlignment.stretch, 45 | children: [ 46 | Profile(uid: uid), 47 | const Padding( 48 | padding: EdgeInsets.symmetric(vertical: 16), 49 | child: Divider(), 50 | ), 51 | Expanded( 52 | child: PostListView( 53 | snapshots: ref.watch(userPostsProvider(uid)).value, 54 | postSelected: (postId) { 55 | UserPostPageRoute(uid: uid, pid: postId) 56 | .push(context); 57 | }, 58 | ), 59 | ), 60 | ], 61 | ), 62 | ); 63 | } 64 | } 65 | 66 | class _UserNotFound extends StatelessWidget { 67 | const _UserNotFound(); 68 | 69 | @override 70 | Widget build(BuildContext context) { 71 | final theme = Theme.of(context); 72 | return Center( 73 | child: Column( 74 | mainAxisSize: MainAxisSize.min, 75 | children: [ 76 | Text( 77 | 'ユーザーIDが存在しません', 78 | style: theme.textTheme.bodyLarge, 79 | ), 80 | Text(GoRouterState.of(context).uri.toString()), 81 | ], 82 | ), 83 | ); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /assets/images/logo/logo_yoko_dark.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /assets/images/logo/logo_yoko_light.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images": [ 3 | { 4 | "filename": "Icon-App-20x20@2x.png", 5 | "idiom": "universal", 6 | "scale": "2x", 7 | "size": "20x20", 8 | "platform": "ios" 9 | }, 10 | { 11 | "filename": "Icon-App-20x20@3x.png", 12 | "idiom": "universal", 13 | "scale": "3x", 14 | "size": "20x20", 15 | "platform": "ios" 16 | }, 17 | { 18 | "filename": "Icon-App-29x29@2x.png", 19 | "idiom": "universal", 20 | "scale": "2x", 21 | "size": "29x29", 22 | "platform": "ios" 23 | }, 24 | { 25 | "filename": "Icon-App-29x29@3x.png", 26 | "idiom": "universal", 27 | "scale": "3x", 28 | "size": "29x29", 29 | "platform": "ios" 30 | }, 31 | { 32 | "filename": "Icon-App-38x38@2x.png", 33 | "idiom": "universal", 34 | "scale": "2x", 35 | "size": "38x38", 36 | "platform": "ios" 37 | }, 38 | { 39 | "filename": "Icon-App-38x38@3x.png", 40 | "idiom": "universal", 41 | "scale": "3x", 42 | "size": "38x38", 43 | "platform": "ios" 44 | }, 45 | { 46 | "filename": "Icon-App-40x40@2x.png", 47 | "idiom": "universal", 48 | "scale": "2x", 49 | "size": "40x40", 50 | "platform": "ios" 51 | }, 52 | { 53 | "filename": "Icon-App-40x40@3x.png", 54 | "idiom": "universal", 55 | "scale": "3x", 56 | "size": "40x40", 57 | "platform": "ios" 58 | }, 59 | { 60 | "filename": "Icon-App-60x60@2x.png", 61 | "idiom": "universal", 62 | "scale": "2x", 63 | "size": "60x60", 64 | "platform": "ios" 65 | }, 66 | { 67 | "filename": "Icon-App-60x60@3x.png", 68 | "idiom": "universal", 69 | "scale": "3x", 70 | "size": "60x60", 71 | "platform": "ios" 72 | }, 73 | { 74 | "filename": "Icon-App-64x64@2x.png", 75 | "idiom": "universal", 76 | "scale": "2x", 77 | "size": "64x64", 78 | "platform": "ios" 79 | }, 80 | { 81 | "filename": "Icon-App-64x64@3x.png", 82 | "idiom": "universal", 83 | "scale": "3x", 84 | "size": "64x64", 85 | "platform": "ios" 86 | }, 87 | { 88 | "filename": "Icon-App-68x68@2x.png", 89 | "idiom": "universal", 90 | "scale": "2x", 91 | "size": "68x68", 92 | "platform": "ios" 93 | }, 94 | { 95 | "filename": "Icon-App-76x76@2x.png", 96 | "idiom": "universal", 97 | "scale": "2x", 98 | "size": "76x76", 99 | "platform": "ios" 100 | }, 101 | { 102 | "filename": "Icon-App-83.5x83.5@2x.png", 103 | "idiom": "universal", 104 | "scale": "2x", 105 | "size": "83.5x83.5", 106 | "platform": "ios" 107 | }, 108 | { 109 | "filename": "Icon-App-1024x1024@1x.png", 110 | "idiom": "universal", 111 | "scale": "1x", 112 | "size": "1024x1024", 113 | "platform": "ios" 114 | } 115 | ], 116 | "info": { 117 | "author": "icons_launcher", 118 | "version": 1 119 | } 120 | } -------------------------------------------------------------------------------- /lib/core/authentication/auth_repository.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | 3 | import 'package:desktop_webview_auth/desktop_webview_auth.dart'; 4 | import 'package:desktop_webview_auth/google.dart'; 5 | import 'package:firebase_auth/firebase_auth.dart'; 6 | import 'package:flutter/foundation.dart'; 7 | import 'package:google_sign_in/google_sign_in.dart'; 8 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 9 | 10 | part 'auth_repository.g.dart'; 11 | 12 | @riverpod 13 | AuthRepository authRepository(AuthRepositoryRef ref) => AuthRepository(); 14 | 15 | class AuthRepository { 16 | final _auth = FirebaseAuth.instance; 17 | 18 | // `google_sign_in`プラグインパッケージでサポートされているプラットフォーム 19 | final _isPackageSupportPlatform = 20 | kIsWeb || Platform.isIOS || Platform.isAndroid; 21 | 22 | static const _clientId = 23 | // ignore: lines_longer_than_80_chars 24 | '554602506203-gv6vpvdndlppghdn3sv9cr2n3fc4d854.apps.googleusercontent.com'; 25 | static const _scopes = [ 26 | 'email', 27 | // Googleアカウント名やプロフィール画像の取得に必要 28 | // ref. https://developers.google.com/identity/protocols/oauth2/scopes?hl=ja#people 29 | 'https://www.googleapis.com/auth/userinfo.profile', 30 | ]; 31 | 32 | Future signOut() => _auth.signOut(); 33 | 34 | Future signInWithGoogle() async { 35 | final (:idToken, :accessToken) = await (_isPackageSupportPlatform 36 | ? _signInWithGoogleAccount() 37 | : _signInWithDesktop()); 38 | if (idToken == null || accessToken == null) { 39 | throw Exception( 40 | 'Google Signin Failed because idToken or accessToken is null', 41 | ); 42 | } 43 | await _auth.signInWithCredential( 44 | GoogleAuthProvider.credential( 45 | idToken: idToken, 46 | accessToken: accessToken, 47 | ), 48 | ); 49 | } 50 | 51 | // v0.12からGIS(Google Identity Services)方式に変更となっている 52 | // ref. https://pub.dev/packages/google_sign_in_web 53 | Future _signInWithWeb() async { 54 | final googleSignIn = GoogleSignIn(clientId: _clientId); 55 | final res = await googleSignIn.requestScopes(_scopes); 56 | return res ? googleSignIn.signInSilently() : null; 57 | } 58 | 59 | Future<({String? idToken, String? accessToken})> 60 | _signInWithGoogleAccount() async { 61 | final googleAccount = await (kIsWeb 62 | ? _signInWithWeb() 63 | : GoogleSignIn(scopes: _scopes).signIn()); 64 | if (googleAccount == null) { 65 | throw Exception('GoogleSignInAccount is null'); 66 | } 67 | final googleAuth = await googleAccount.authentication; 68 | return (idToken: googleAuth.idToken, accessToken: googleAuth.accessToken); 69 | } 70 | 71 | Future<({String? idToken, String? accessToken})> _signInWithDesktop() async { 72 | final args = GoogleSignInArgs( 73 | clientId: _clientId, 74 | redirectUri: 'https://nippo-e8922.firebaseapp.com/__/auth/handler', 75 | // scopeがListではなくStringで指定する必要があるが、半角スペース(カンマではNG)で複数指定できることを確認。 76 | // ref. https://github.com/invertase/flutter_desktop_webview_auth/issues/48 77 | scope: _scopes.join(' '), 78 | ); 79 | final res = await DesktopWebviewAuth.signIn(args); 80 | return (idToken: res?.idToken, accessToken: res?.accessToken); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /lib/core/navigation/scaffold_with_navigation.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:go_router/go_router.dart'; 3 | 4 | import 'navigation_item.dart'; 5 | 6 | class ScaffoldWithNavigation extends StatelessWidget { 7 | const ScaffoldWithNavigation({ 8 | super.key, 9 | required this.navigationShell, 10 | }); 11 | 12 | final StatefulNavigationShell navigationShell; 13 | 14 | @override 15 | Widget build(BuildContext context) { 16 | void onDestinationSelected(int index) { 17 | navigationShell.goBranch( 18 | index, 19 | initialLocation: index == navigationShell.currentIndex, 20 | ); 21 | } 22 | 23 | return LayoutBuilder( 24 | builder: (context, constraints) { 25 | if (constraints.maxWidth < 450) { 26 | return _ScaffoldWithNavigationBar( 27 | navigationShell: navigationShell, 28 | onDestinationSelected: onDestinationSelected, 29 | ); 30 | } else { 31 | return _ScaffoldWithNavigationRail( 32 | navigationShell: navigationShell, 33 | onDestinationSelected: onDestinationSelected, 34 | ); 35 | } 36 | }, 37 | ); 38 | } 39 | } 40 | 41 | class _ScaffoldWithNavigationBar extends StatelessWidget { 42 | const _ScaffoldWithNavigationBar({ 43 | required this.navigationShell, 44 | required this.onDestinationSelected, 45 | }); 46 | 47 | final StatefulNavigationShell navigationShell; 48 | final void Function(int)? onDestinationSelected; 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | return Scaffold( 53 | body: navigationShell, 54 | bottomNavigationBar: NavigationBar( 55 | selectedIndex: navigationShell.currentIndex, 56 | onDestinationSelected: onDestinationSelected, 57 | destinations: [ 58 | for (final item in NavigationItem.values) 59 | NavigationDestination( 60 | icon: Icon(item.iconData), 61 | label: item.label, 62 | ), 63 | ], 64 | ), 65 | ); 66 | } 67 | } 68 | 69 | class _ScaffoldWithNavigationRail extends StatelessWidget { 70 | const _ScaffoldWithNavigationRail({ 71 | required this.navigationShell, 72 | required this.onDestinationSelected, 73 | }); 74 | 75 | final StatefulNavigationShell navigationShell; 76 | final void Function(int)? onDestinationSelected; 77 | 78 | @override 79 | Widget build(BuildContext context) { 80 | return Scaffold( 81 | body: Row( 82 | children: [ 83 | NavigationRail( 84 | selectedIndex: navigationShell.currentIndex, 85 | onDestinationSelected: onDestinationSelected, 86 | labelType: NavigationRailLabelType.all, 87 | destinations: [ 88 | for (final item in NavigationItem.values) 89 | NavigationRailDestination( 90 | icon: Icon(item.iconData), 91 | label: Text(item.label), 92 | ), 93 | ], 94 | ), 95 | const VerticalDivider( 96 | thickness: 1, 97 | width: 1, 98 | ), 99 | Expanded(child: navigationShell), 100 | ], 101 | ), 102 | ); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /lib/firebase_options.dart: -------------------------------------------------------------------------------- 1 | // File generated by FlutterFire CLI. 2 | // ignore_for_file: type=lint 3 | import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; 4 | import 'package:flutter/foundation.dart' 5 | show defaultTargetPlatform, kIsWeb, TargetPlatform; 6 | 7 | /// Default [FirebaseOptions] for use with your Firebase apps. 8 | /// 9 | /// Example: 10 | /// ```dart 11 | /// import 'firebase_options.dart'; 12 | /// // ... 13 | /// await Firebase.initializeApp( 14 | /// options: DefaultFirebaseOptions.currentPlatform, 15 | /// ); 16 | /// ``` 17 | class DefaultFirebaseOptions { 18 | static FirebaseOptions get currentPlatform { 19 | if (kIsWeb) { 20 | return web; 21 | } 22 | switch (defaultTargetPlatform) { 23 | case TargetPlatform.android: 24 | return android; 25 | case TargetPlatform.iOS: 26 | return ios; 27 | case TargetPlatform.macOS: 28 | return macos; 29 | case TargetPlatform.windows: 30 | throw UnsupportedError( 31 | 'DefaultFirebaseOptions have not been configured for windows - ' 32 | 'you can reconfigure this by running the FlutterFire CLI again.', 33 | ); 34 | case TargetPlatform.linux: 35 | throw UnsupportedError( 36 | 'DefaultFirebaseOptions have not been configured for linux - ' 37 | 'you can reconfigure this by running the FlutterFire CLI again.', 38 | ); 39 | default: 40 | throw UnsupportedError( 41 | 'DefaultFirebaseOptions are not supported for this platform.', 42 | ); 43 | } 44 | } 45 | 46 | static const FirebaseOptions web = FirebaseOptions( 47 | apiKey: 'AIzaSyATQA1mQDP4r4KE2u5QoFEttPLvx8WK-rg', 48 | appId: '1:554602506203:web:bcb0eae5ca2da51e3ee9fe', 49 | messagingSenderId: '554602506203', 50 | projectId: 'nippo-e8922', 51 | authDomain: 'nippo-e8922.firebaseapp.com', 52 | databaseURL: 'https://nippo-e8922.firebaseio.com', 53 | storageBucket: 'nippo-e8922.appspot.com', 54 | measurementId: 'G-FXTJM2Q755', 55 | ); 56 | 57 | static const FirebaseOptions android = FirebaseOptions( 58 | apiKey: 'AIzaSyDSPm7SNLRYk1bBp5DFku-n-A4PrmjQFuU', 59 | appId: '1:554602506203:android:052255c36e16460d3ee9fe', 60 | messagingSenderId: '554602506203', 61 | projectId: 'nippo-e8922', 62 | databaseURL: 'https://nippo-e8922.firebaseio.com', 63 | storageBucket: 'nippo-e8922.appspot.com', 64 | ); 65 | 66 | static const FirebaseOptions ios = FirebaseOptions( 67 | apiKey: 'AIzaSyALpm4NWeHM-PkaBkMaVuOWUJhRG0DHE-I', 68 | appId: '1:554602506203:ios:28faf2e71f77493b3ee9fe', 69 | messagingSenderId: '554602506203', 70 | projectId: 'nippo-e8922', 71 | databaseURL: 'https://nippo-e8922.firebaseio.com', 72 | storageBucket: 'nippo-e8922.appspot.com', 73 | androidClientId: '554602506203-cvqnsumv3q5c3mk5oj800tr7lugoo81u.apps.googleusercontent.com', 74 | iosClientId: '554602506203-2e3jkpfh6rabqmci2cck8k7ufkr9665e.apps.googleusercontent.com', 75 | iosBundleId: 'com.htsuruo.nippo', 76 | ); 77 | 78 | static const FirebaseOptions macos = FirebaseOptions( 79 | apiKey: 'AIzaSyALpm4NWeHM-PkaBkMaVuOWUJhRG0DHE-I', 80 | appId: '1:554602506203:ios:28faf2e71f77493b3ee9fe', 81 | messagingSenderId: '554602506203', 82 | projectId: 'nippo-e8922', 83 | databaseURL: 'https://nippo-e8922.firebaseio.com', 84 | storageBucket: 'nippo-e8922.appspot.com', 85 | androidClientId: '554602506203-cvqnsumv3q5c3mk5oj800tr7lugoo81u.apps.googleusercontent.com', 86 | iosClientId: '554602506203-2e3jkpfh6rabqmci2cck8k7ufkr9665e.apps.googleusercontent.com', 87 | iosBundleId: 'com.htsuruo.nippo', 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![README](./.github/images/nippo_readme_eyecatch.png) 2 | 3 | [![Flutter](https://github.com/htsuruo/nippo/actions/workflows/flutter.yml/badge.svg)](https://github.com/htsuruo/nippo/actions/workflows/flutter.yml) 4 | 5 | # NIPPO 6 | 7 | - 毎日の学びを、もっと楽しく、わかりやすく。 8 | - シンプルな日報投稿型のSNS 9 | - Flutter + Firebaseの学習用プロジェクト 10 | 11 | **[View Demo](https://nippo-e8922.web.app)** 12 | 13 | ※デモは現在 [#32](https://github.com/htsuruo/nippo/issues/32) のため利用できません。 14 | 15 | ## Get started 16 | 17 | ### 1. Firebaseプロジェクトの作成 18 | 19 | 1.`.firebaserc`のデフォルトプロジェクトをご自身のFirebaseプロジェクトIDに変更してください。 20 | 21 | ```yaml 22 | { 23 | "projects": { 24 | "default": "nippo-e8922" 25 | } 26 | } 27 | ``` 28 | 29 | 2.Firebaseプロジェクトに関する情報はgit管理対象外のため、下記コマンドにてお手元のFirebaseプロジェクトにてセットアップしてください。 30 | - 事前に[flutterfire_cli | Dart Package](https://pub.dev/packages/flutterfire_cli)のインストールが必要です 31 | - 参考: [Add Firebase to your Flutter app](https://firebase.google.com/docs/flutter/setup?platform=ios) 32 | 33 | ```sh 34 | flutterfire configure 35 | ``` 36 | 37 | ### 2. App Checkのデバッグトークンを登録 38 | 39 | Firebase コンソールからApp Checkのデバッグトークンを登録します。デバッグトークンを自身のFirebaseプロジェクトに登録しておくことで、開発環境でリクエストが弾かれないようにします。 40 | 41 | ref. [Use App Check with the debug provider with Flutter  |  Firebase App Check](https://firebase.google.com/docs/app-check/flutter/debug-provider) 42 | 43 | #### Androidの場合 44 | 45 | `flutter run`でデバッグコンソールに以下のようにデバッグトークンが出力されますので、これをFirebaseコンソールに登録します。 46 | 47 | ```bash 48 | D/com.google.firebase.appcheck.debug.internal.DebugAppCheckProvider(10075): Enter this debug secret into the allow list in the Firebase Console for your project: xxxxxxx 49 | ``` 50 | 51 | #### iOSの場合 52 | 53 | - Apple Developer Programから「Device Check」を有効にした`.p8`の認証キーをFirebaseコンソールに登録 54 | - `-FIRDebugEnabled`をRunのArgumentsに登録 55 | 56 | 詳細: https://firebase.google.com/docs/app-check/flutter/debug-provider#apple_platforms 57 | 58 | ## 機能要件 59 | 60 | 1. **日報** 61 | - 投稿された全ての日報が一覧で表示できる 62 | - 日報詳細画面で件名と全文が表示できる 63 | - プロフィール画面では選択されたユーザーの日報のみが一覧で表示できる 64 | - 自分の日報を作成できる 65 | 66 | 2. **認証・ユーザー** 67 | - Google認証でユーザー登録・ログインができる 68 | - ログアウトができる 69 | - ログインしているユーザーの名前,プロフィール画像,uidが表示できる 70 | 71 | 3. **設定** 72 | - 認証プロバイダおよび最終ログインなどのログイン情報が閲覧できる 73 | - アプリケーション情報や来線図情報が閲覧できる 74 | 75 | ## セキュリティ周り 76 | 77 | 本レポジトリはFirebase APIキーを公開していますが、データリソースへのアクセスを筆頭に、APIの不正利用からの保護を行っているため、第三者からの意図しないリクエストで超過料金が請求されるなどのリスクが無いように配慮しています。また、第三者からの攻撃だけでなく開発者の実装ミス(ex. Cloud Functionsの無限ループミス)などによる事故に気づけるよう、[Cloud Billingの予算アラート](https://cloud.google.com/billing/docs/how-to/budgets?hl=ja)を設定し気付けるようにしています。 78 | 79 | - APIキーの制限 80 | - APIリソースの保護(App Check) 81 | - データの保護(Security Rule) 82 | 83 | ※本アプリケーションでは未設定ですが、[Using Cloud Monitoring to monitor App Check and Security Rules](https://firebase.blog/posts/2022/12/monitoring-app-check-and-rules/) のようにCloud Monitoringで閾値を設定し、Slackなどにレポートする形も良さそうです。 84 | 85 | ### APIキーの制限 86 | 87 | 各プラットフォームのAPIキーが利用可能なドメイン等をホワイトリストで管理し、アクセス元を制限しています。具体的には、Google Cloud コンソールの認証情報によりAPIキーを制限を設定しています。これにより、ホワイトリスト以外のクライアントからAPIキーが利用されるのを防ぎます。 88 | 89 | | プラットフォーム | 制限内容 | 90 | | --- | --- | 91 | | iOS+,Android | `com.htsuruo.nippo`のバンドルID,パッケージ名のみ利用を制限 | 92 | | Web | Firebase Hostingのデフォルトサイトおよび独自のドメインに制限 | 93 | 94 | ### APIリソースの保護 95 | 96 | Firebase App Checkを有効化(Enforcement)し、本アプリケーションで有効化しているAPIリソースに対する不正なAPIアクセスをブロックしています。APIキーの制限ではドメインなりすましやシミュレータ実行で迂回されてしまう可能性がありますが、App Checkでは検証された実機以外からのアクセスを受け付けません。 97 | 98 | - ref. [Flutter アプリで App Check を使ってみる  |  Firebase ドキュメント](https://firebase.google.com/docs/app-check/flutter/default-providers?hl=ja) 99 | 100 | ### データの保護 101 | 102 | 本アプリケーションはCloud Firestore を利用しているため、セキュリティルールによって保護しています。 103 | サービス提供において最も重要なインシデントである、ユーザー情報の漏洩やサービスの深刻な破壊がされるリスクを防ぎます。 104 | また、アプリケーションの利用者が他人のデータを勝手に編集・削除するような、想定されない操作を制限しアプリケーションのデータを健全に保ちます。また、[単体テスト](https://github.com/htsuruo/nippo/tree/main/firebase/functions/src/test/rules)にて意図通りのルール設定になっていることを検証済です。 105 | 106 | - ref. https://github.com/htsuruo/nippo/blob/main/firebase/firestore.rules 107 | -------------------------------------------------------------------------------- /lib/features/post/widgets/post_list_view.dart: -------------------------------------------------------------------------------- 1 | import 'package:cloud_firestore/cloud_firestore.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:gap/gap.dart'; 4 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 5 | import 'package:nippo/common/common.dart'; 6 | 7 | import '../model/post.dart'; 8 | import 'user_avatar.dart'; 9 | 10 | /// Post一覧からの遷移とUserPageからの遷移で共通して利用されるListView 11 | /// StatefulShellRouteの影響でパスを同一に出来ないため、 12 | /// それぞれの遷移元に合わせて[postSelected]にて遷移先を指定する。 13 | class PostListView extends ConsumerWidget { 14 | const PostListView({ 15 | super.key, 16 | required this.snapshots, 17 | required this.postSelected, 18 | }); 19 | 20 | final List>? snapshots; 21 | final ValueChanged? postSelected; 22 | 23 | @override 24 | Widget build(BuildContext context, WidgetRef ref) { 25 | final snapshots = this.snapshots; 26 | 27 | return snapshots == null 28 | ? const CenteredCircularProgressIndicator() 29 | : snapshots.isEmpty 30 | ? const Center( 31 | child: Text('データがありません'), 32 | ) 33 | : ListView.builder( 34 | padding: const EdgeInsets.all(8), 35 | itemCount: snapshots.length, 36 | itemBuilder: (context, index) => _PostCard( 37 | snapshots[index], 38 | postSelected: postSelected, 39 | ), 40 | ); 41 | } 42 | } 43 | 44 | class _PostCard extends StatelessWidget { 45 | const _PostCard(this.postSnapshot, {required this.postSelected}); 46 | 47 | final QueryDocumentSnapshot postSnapshot; 48 | final ValueChanged? postSelected; 49 | 50 | @override 51 | Widget build(BuildContext context) { 52 | final theme = Theme.of(context); 53 | final colorScheme = theme.colorScheme; 54 | final post = postSnapshot.data(); 55 | 56 | return Card( 57 | clipBehavior: Clip.antiAlias, 58 | child: InkWell( 59 | onTap: () { 60 | postSelected?.call(postSnapshot.id); 61 | }, 62 | child: Padding( 63 | padding: const EdgeInsets.all(16), 64 | child: IntrinsicHeight( 65 | child: Row( 66 | crossAxisAlignment: CrossAxisAlignment.stretch, 67 | children: [ 68 | Align( 69 | alignment: Alignment.topCenter, 70 | child: UserAvatar(postRef: postSnapshot.reference), 71 | ), 72 | const Gap(12), 73 | Expanded( 74 | child: Column( 75 | crossAxisAlignment: CrossAxisAlignment.start, 76 | children: [ 77 | Text( 78 | post.title, 79 | style: theme.textTheme.titleMedium!.copyWith( 80 | fontWeight: FontWeight.bold, 81 | ), 82 | maxLines: 1, 83 | overflow: TextOverflow.ellipsis, 84 | ), 85 | Text( 86 | post.description, 87 | style: theme.textTheme.bodyMedium, 88 | maxLines: 2, 89 | overflow: TextOverflow.ellipsis, 90 | ), 91 | const Gap(2), 92 | Text( 93 | // ServerTimestamp確定まで微妙にラグがあるため暫定的に空文字でごまかす 94 | post.createdAt.date?.formatted ?? '', 95 | style: theme.textTheme.labelSmall! 96 | .copyWith(color: theme.hintColor), 97 | ), 98 | ], 99 | ), 100 | ), 101 | Icon(Icons.navigate_next, color: colorScheme.primary), 102 | ], 103 | ), 104 | ), 105 | ), 106 | ), 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /lib/features/setting/setting_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:gap/gap.dart'; 3 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 4 | import 'package:intersperse/intersperse.dart'; 5 | import 'package:nippo/common/common.dart'; 6 | import 'package:nippo/core/authentication/auth_provider.dart'; 7 | import 'package:nippo/core/authentication/auth_repository.dart'; 8 | import 'package:url_launcher/url_launcher.dart'; 9 | 10 | class SettingPage extends StatelessWidget { 11 | const SettingPage({super.key}); 12 | 13 | @override 14 | Widget build(BuildContext context) { 15 | return Scaffold( 16 | appBar: AppBar( 17 | title: const Text( 18 | '設定', 19 | ), 20 | ), 21 | body: SingleChildScrollView( 22 | child: Column( 23 | children: [ 24 | const _FirUserSection(), 25 | const _AboutSection(), 26 | const _SignoutButton(), 27 | ].intersperseOuter(const Gap(16)).toList(), 28 | ), 29 | ), 30 | ); 31 | } 32 | } 33 | 34 | const _dividerWithIndent = Divider(indent: 16); 35 | 36 | class _FirUserSection extends ConsumerWidget { 37 | const _FirUserSection(); 38 | 39 | @override 40 | Widget build(BuildContext context, WidgetRef ref) { 41 | final firUser = ref.watch(firUserProvider).value; 42 | final providerData = firUser?.providerData.map((p) => p.providerId); 43 | final lastSignInTime = firUser?.metadata.lastSignInTime; 44 | return Column( 45 | crossAxisAlignment: CrossAxisAlignment.start, 46 | children: [ 47 | const _SectionText(label: 'ログイン情報'), 48 | ListTile( 49 | title: const Text('認証プロバイダ'), 50 | trailing: providerData == null 51 | ? null 52 | : Text( 53 | providerData.join(','), 54 | ), 55 | ), 56 | _dividerWithIndent, 57 | ListTile( 58 | title: const Text('最終ログイン日時'), 59 | trailing: lastSignInTime == null 60 | ? null 61 | : Text(lastSignInTime.toLocal().formatted), 62 | ), 63 | ], 64 | ); 65 | } 66 | } 67 | 68 | class _AboutSection extends ConsumerWidget { 69 | const _AboutSection(); 70 | 71 | @override 72 | Widget build(BuildContext context, WidgetRef ref) { 73 | final appInfo = ref.watch(appInfoProvider); 74 | return Column( 75 | crossAxisAlignment: CrossAxisAlignment.start, 76 | children: [ 77 | const _SectionText(label: 'このアプリについて'), 78 | ListTile( 79 | title: const Text('ソースコード'), 80 | trailing: const Icon(Icons.open_in_new_outlined), 81 | onTap: () => launchUrl( 82 | Uri.parse('https://github.com/htsuruo/nippo'), 83 | ), 84 | ), 85 | _dividerWithIndent, 86 | AboutListTile( 87 | applicationVersion: appInfo.version.toString(), 88 | aboutBoxChildren: [ 89 | Padding( 90 | // AboutDialogのpaddingに合わせる 91 | padding: const EdgeInsets.symmetric(horizontal: 24), 92 | child: Text(appInfo.packageName), 93 | ), 94 | ], 95 | child: const Text('アプリケーション情報'), 96 | ), 97 | ], 98 | ); 99 | } 100 | } 101 | 102 | class _SignoutButton extends ConsumerWidget { 103 | const _SignoutButton(); 104 | 105 | @override 106 | Widget build(BuildContext context, WidgetRef ref) { 107 | return ElevatedButton( 108 | onPressed: () { 109 | ref.read(authRepositoryProvider).signOut(); 110 | }, 111 | child: const Text('Sign out'), 112 | ); 113 | } 114 | } 115 | 116 | class _SectionText extends StatelessWidget { 117 | const _SectionText({required this.label}); 118 | 119 | final String label; 120 | 121 | @override 122 | Widget build(BuildContext context) { 123 | final theme = Theme.of(context); 124 | final colorScheme = theme.colorScheme; 125 | return Padding( 126 | padding: const EdgeInsets.symmetric(horizontal: 16), 127 | child: Text( 128 | label, 129 | style: TextStyle(color: colorScheme.primary), 130 | ), 131 | ); 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /firebase/functions/src/test/rules/posts.test.ts: -------------------------------------------------------------------------------- 1 | import firebase from 'firebase/compat/app' 2 | import { Auth, Tester } from './utils' 3 | import { Collection } from '../../collection' 4 | import { assertUnauthenticatedAccessFails } from './common' 5 | import { assertFails, assertSucceeds } from '@firebase/rules-unit-testing' 6 | import { Post } from '../../model/post' 7 | 8 | const auth = { 9 | userId: 'alice', 10 | email: 'alice@example.com', 11 | } as Auth 12 | 13 | const post = { 14 | postId: 'postId', 15 | title: 'title', 16 | description: 'description', 17 | updatedAt: firebase.firestore.FieldValue.serverTimestamp(), 18 | createdAt: firebase.firestore.FieldValue.serverTimestamp(), 19 | } as Post 20 | 21 | describe('posts security rule', () => { 22 | let tester: Tester 23 | let db: firebase.firestore.Firestore 24 | 25 | beforeEach(async () => { 26 | tester = await Tester.init() 27 | db = tester.db() 28 | }) 29 | afterEach(async () => await tester.afterEach()) 30 | afterAll(async () => await tester.afterAll()) 31 | 32 | const userRef = () => db.collection(Collection.users).doc(auth.userId) 33 | const postsCollectionRef = () => userRef().collection(Collection.posts) 34 | const postRef = () => postsCollectionRef().doc() 35 | const postsQuery = () => 36 | db.collectionGroup(Collection.posts).where('postId', '==', post.postId) 37 | 38 | assertUnauthenticatedAccessFails(postsCollectionRef, postRef) 39 | 40 | describe('認証済みの時', () => { 41 | beforeEach(async () => { 42 | db = tester.db(auth) 43 | }) 44 | 45 | test('read: できる', async () => { 46 | await assertSucceeds(postsCollectionRef().get()) 47 | await assertSucceeds(postRef().get()) 48 | }) 49 | test('create: できる', async () => { 50 | await assertSucceeds(postsCollectionRef().add(post)) 51 | await assertSucceeds(postRef().set(post)) 52 | }) 53 | test('update: できる', async () => { 54 | await tester.withSecurityRulesDisabled(async (db) => { 55 | await db.doc(postRef().path).set(post) 56 | }) 57 | await assertFails(postRef().update({ title: 'updatedTitle' } as Post)) 58 | }) 59 | test('delete: できる', async () => { 60 | await assertSucceeds(postRef().delete()) 61 | }) 62 | 63 | test('collectionGroup: できる', async () => { 64 | await assertSucceeds(postsQuery().get()) 65 | }) 66 | 67 | describe('タイトルが20文字を超える時', () => { 68 | const overCharactersTitle = '日報の件名が20文字を超える場合の文章です' 69 | const postWithOverCharacters = { ...post } 70 | postWithOverCharacters.title = overCharactersTitle 71 | 72 | test('create: できない', async () => { 73 | await assertFails(postsCollectionRef().add(postWithOverCharacters)) 74 | await assertFails(postRef().set(postWithOverCharacters)) 75 | }) 76 | test('update: できない', async () => { 77 | await tester.withSecurityRulesDisabled(async (db) => { 78 | await db.doc(postRef().path).set(post) 79 | }) 80 | await assertFails(postRef().update({ title: overCharactersTitle })) 81 | }) 82 | }) 83 | 84 | describe('他人のデータに対して', () => { 85 | let otherCollectionRef: firebase.firestore.CollectionReference 86 | let otherDocumentRef: firebase.firestore.DocumentReference 87 | beforeEach(() => { 88 | otherCollectionRef = db 89 | .collection(Collection.users) 90 | .doc('bob') 91 | .collection(Collection.posts) 92 | otherDocumentRef = otherCollectionRef.doc('otherPostId') 93 | }) 94 | 95 | test('read: できる', async () => { 96 | await assertSucceeds(otherCollectionRef.get()) 97 | await assertSucceeds(otherDocumentRef.get()) 98 | }) 99 | test('create: できない', async () => { 100 | await assertFails(otherCollectionRef.add(post)) 101 | await assertFails(otherDocumentRef.set(post)) 102 | }) 103 | test('update: できない', async () => { 104 | await tester.withSecurityRulesDisabled(async (db) => { 105 | await db.doc(postRef().path).set(post) 106 | }) 107 | await assertFails( 108 | otherDocumentRef.update({ title: 'updatedTitle' } as Post) 109 | ) 110 | }) 111 | }) 112 | }) 113 | }) 114 | -------------------------------------------------------------------------------- /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 | 77 | 78 | 79 | 80 | 86 | 88 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /macos/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 | 77 | 78 | 79 | 80 | 86 | 88 | 94 | 95 | 96 | 97 | 99 | 100 | 103 | 104 | 105 | -------------------------------------------------------------------------------- /firebase/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Welcome to Firebase Hosting 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 40 | 41 | 42 |
43 |

Welcome

44 |

Firebase Hosting Setup Complete

45 |

You're seeing this because you've successfully setup Firebase Hosting. Now it's time to go build something extraordinary!

46 | Open Hosting Documentation 47 |
48 |

Firebase SDK Loading…

49 | 50 | 88 | 89 | 90 | -------------------------------------------------------------------------------- /lib/core/router/router.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:go_router/go_router.dart'; 4 | import 'package:nippo/features/post/detail/post_page.dart'; 5 | import 'package:nippo/features/post/form/post_form_page.dart'; 6 | import 'package:nippo/features/post/post_list_page.dart'; 7 | import 'package:nippo/features/setting/setting_page.dart'; 8 | import 'package:nippo/features/signin/signin_page.dart'; 9 | import 'package:nippo/features/user/user_page.dart'; 10 | import 'package:riverpod_annotation/riverpod_annotation.dart'; 11 | import 'package:tsuruo_kit/tsuruo_kit.dart'; 12 | 13 | import '../authentication/auth_provider.dart'; 14 | import '../navigation/scaffold_with_navigation.dart'; 15 | 16 | part 'router.g.dart'; 17 | 18 | final _rootNavigatorKey = GlobalKey(debugLabel: 'root'); 19 | 20 | extension GoRouterX on GoRouter { 21 | NavigatorState get navigator => routerDelegate.navigatorKey.currentState!; 22 | } 23 | 24 | @riverpod 25 | GoRouter router(RouterRef ref) { 26 | return GoRouter( 27 | routes: $appRoutes, 28 | debugLogDiagnostics: kDebugMode, 29 | initialLocation: const PostsPageRoute().location, 30 | navigatorKey: _rootNavigatorKey, 31 | redirect: (context, state) async { 32 | final signedIn = await ref.watch(isSignedInProvider.future); 33 | final location = state.uri.toString(); 34 | final isSigninLocation = location == SigninPageRoute().location; 35 | if (!signedIn) { 36 | return isSigninLocation ? null : SigninPageRoute().location; 37 | } 38 | if (isSigninLocation || location == const PostsPageRoute().location) { 39 | return const PostsPageRoute().location; 40 | } 41 | return null; 42 | }, 43 | ); 44 | } 45 | 46 | @TypedStatefulShellRoute( 47 | branches: [ 48 | TypedStatefulShellBranch( 49 | routes: [ 50 | TypedGoRoute( 51 | path: '/posts', 52 | // `:pid`を先頭にすると`create`がIDとして解釈されてしまいcreateページに遷移できなくなるので注意 53 | // ベタ指定されるパスを先に書く必要がある。 54 | routes: [ 55 | TypedGoRoute( 56 | // NOTE(htsuruo): 本来はZennのように`[uid]/posts/[pid]`のようなパスにしたいが、 57 | // StatefulShellBranchでは親ルートのpostsが頭にきてしまうため実現できなさそう? 58 | // PostのドキュメントIDを取得するためにはコレクショングループで引くしかない。 59 | path: ':pid', 60 | ), 61 | TypedGoRoute( 62 | path: 'create', 63 | ), 64 | TypedGoRoute( 65 | path: 'edit/:pid', 66 | ), 67 | ], 68 | ), 69 | ], 70 | ), 71 | TypedStatefulShellBranch( 72 | routes: [ 73 | TypedGoRoute( 74 | path: '/profile', 75 | routes: [ 76 | TypedGoRoute( 77 | path: 'setting', 78 | ), 79 | ], 80 | ), 81 | ], 82 | ), 83 | ], 84 | ) 85 | class ShellRouteData extends StatefulShellRouteData { 86 | const ShellRouteData(); 87 | 88 | @override 89 | Widget builder( 90 | BuildContext context, 91 | GoRouterState state, 92 | StatefulNavigationShell navigationShell, 93 | ) { 94 | return _Root( 95 | child: ScaffoldWithNavigation( 96 | navigationShell: navigationShell, 97 | ), 98 | ); 99 | } 100 | } 101 | 102 | @TypedGoRoute(path: '/signin') 103 | class SigninPageRoute extends GoRouteData { 104 | @override 105 | Widget build(BuildContext context, GoRouterState state) => 106 | const _Root(child: SigninPage()); 107 | } 108 | 109 | class PostsPageRoute extends GoRouteData { 110 | const PostsPageRoute(); 111 | 112 | @override 113 | Widget build(BuildContext context, GoRouterState state) { 114 | return const PostListPage(); 115 | } 116 | } 117 | 118 | class PostPageRoute extends GoRouteData { 119 | const PostPageRoute({required this.pid}); 120 | 121 | final String pid; 122 | 123 | @override 124 | Widget build(BuildContext context, GoRouterState state) { 125 | return PostPage.fromAll(pid: pid); 126 | } 127 | } 128 | 129 | class PostCreatePageRoute extends GoRouteData { 130 | @override 131 | Widget build(BuildContext context, GoRouterState state) => 132 | const PostFormPage.create(); 133 | } 134 | 135 | class PostEditPageRoute extends GoRouteData { 136 | const PostEditPageRoute({required this.pid}); 137 | final String pid; 138 | 139 | @override 140 | Widget build(BuildContext context, GoRouterState state) => 141 | PostFormPage.edit(postId: pid); 142 | } 143 | 144 | class ProfilePageRoute extends GoRouteData { 145 | const ProfilePageRoute(); 146 | 147 | @override 148 | Widget build(BuildContext context, GoRouterState state) { 149 | return const UserPage.me(); 150 | } 151 | } 152 | 153 | class SettingPageRoute extends GoRouteData { 154 | // ref. https://github.com/flutter/packages/tree/main/packages/go_router_builder#typedshellroute-and-navigator-keys 155 | static final GlobalKey $parentNavigatorKey = 156 | _rootNavigatorKey; 157 | 158 | @override 159 | Page buildPage(BuildContext context, GoRouterState state) => 160 | const MaterialPage( 161 | fullscreenDialog: true, 162 | child: SettingPage(), 163 | ); 164 | } 165 | 166 | @TypedGoRoute(path: '/user/:uid') 167 | class UserPageRoute extends GoRouteData { 168 | const UserPageRoute({required this.uid}); 169 | 170 | final String uid; 171 | 172 | @override 173 | Page buildPage(BuildContext context, GoRouterState state) => 174 | MaterialPage( 175 | child: _Root(child: UserPage.uid(uid)), 176 | ); 177 | } 178 | 179 | @TypedGoRoute(path: '/user/:uid/posts/:pid') 180 | class UserPostPageRoute extends GoRouteData { 181 | const UserPostPageRoute({required this.uid, required this.pid}); 182 | 183 | final String uid; 184 | final String pid; 185 | 186 | @override 187 | Widget build(BuildContext context, GoRouterState state) { 188 | return PostPage.fromProfile(uid: uid, pid: pid); 189 | } 190 | } 191 | 192 | class _Root extends StatelessWidget { 193 | const _Root({required this.child}); 194 | 195 | final Widget child; 196 | 197 | @override 198 | Widget build(BuildContext context) { 199 | return ProgressHUD(child: child); 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /lib/features/post/form/post_form_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:adaptive_dialog/adaptive_dialog.dart'; 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:gap/gap.dart'; 6 | import 'package:go_router/go_router.dart'; 7 | import 'package:hooks_riverpod/hooks_riverpod.dart'; 8 | import 'package:nippo/common/common.dart'; 9 | import 'package:nippo/features/post/model/post.dart'; 10 | import 'package:nippo/features/post/post_repository.dart'; 11 | import 'package:tsuruo_kit/tsuruo_kit.dart'; 12 | 13 | import '../post_provider.dart'; 14 | import 'description_form_field.dart'; 15 | import 'title_form_field.dart'; 16 | 17 | class PostFormPage extends HookConsumerWidget { 18 | const PostFormPage._({this.postId}); 19 | 20 | const PostFormPage.create() : this._(); 21 | const PostFormPage.edit({required String postId}) : this._(postId: postId); 22 | 23 | final String? postId; 24 | 25 | @override 26 | Widget build(BuildContext context, WidgetRef ref) { 27 | // TODO(htsuruo): 他デバイスからデータの更新があったときに入力途中でも破棄されて強制的に更新されてしまう問題。 28 | // `ref.listen`を使うようにコメントがあるが対象がAutoDisposeなProviderなので使えない。 29 | // ref. https://github.com/rrousselGit/riverpod/discussions/1069#discussioncomment-1919829 30 | final postSnap = ref.watch(postProvider(postId: postId)).value; 31 | final post = postSnap?.data(); 32 | final isLoading = postId != null && postSnap == null; 33 | 34 | final formKey = useMemoized(GlobalKey.new); 35 | final titleEditController = useTextEditingController( 36 | text: post?.title, 37 | keys: [post?.title], 38 | ); 39 | final descriptionEditController = useTextEditingController( 40 | text: post?.description, 41 | keys: [post?.description], 42 | ); 43 | 44 | return Scaffold( 45 | appBar: AppBar( 46 | automaticallyImplyLeading: false, 47 | centerTitle: false, 48 | actions: [ 49 | Expanded( 50 | child: Padding( 51 | padding: const EdgeInsets.symmetric(horizontal: 16), 52 | child: Row( 53 | children: [ 54 | _CancelButton( 55 | post: post, 56 | titleController: titleEditController, 57 | descriptionController: descriptionEditController, 58 | ), 59 | const Spacer(), 60 | if (!isLoading) 61 | _SubmitButton( 62 | formKey: formKey, 63 | titleController: titleEditController, 64 | descriptionController: descriptionEditController, 65 | postSnap: postSnap, 66 | ), 67 | ], 68 | ), 69 | ), 70 | ), 71 | ], 72 | ), 73 | body: Padding( 74 | padding: const EdgeInsets.all(16), 75 | child: isLoading 76 | // TODO(htsuruo): 不自然なので1秒未満は非表示にする 77 | ? const CenteredCircularProgressIndicator() 78 | : Form( 79 | key: formKey, 80 | child: Column( 81 | crossAxisAlignment: CrossAxisAlignment.stretch, 82 | children: [ 83 | TitleFormField(controller: titleEditController), 84 | const Gap(12), 85 | Expanded( 86 | child: DescriptionFormField( 87 | controller: descriptionEditController, 88 | ), 89 | ), 90 | ], 91 | ), 92 | ), 93 | ), 94 | ); 95 | } 96 | } 97 | 98 | class _CancelButton extends StatelessWidget { 99 | const _CancelButton({ 100 | required this.post, 101 | required this.titleController, 102 | required this.descriptionController, 103 | }); 104 | 105 | final Post? post; 106 | final TextEditingController titleController; 107 | final TextEditingController descriptionController; 108 | 109 | @override 110 | Widget build(BuildContext context) { 111 | return TextButton( 112 | style: TextButton.styleFrom( 113 | visualDensity: VisualDensity.compact, 114 | ), 115 | onPressed: () async { 116 | final skipConfirm = post == 117 | post?.copyWith( 118 | title: titleController.text, 119 | description: descriptionController.text, 120 | ); 121 | if (skipConfirm) { 122 | context.pop(); 123 | return; 124 | } 125 | if (OkCancelResult.ok == 126 | await showOkCancelAlertDialog( 127 | context: context, 128 | title: '確認', 129 | message: '変更内容は破棄されますがよろしいですか?', 130 | )) { 131 | context.pop(); 132 | } 133 | }, 134 | child: const Text('キャンセル'), 135 | ); 136 | } 137 | } 138 | 139 | class _SubmitButton extends ConsumerWidget { 140 | const _SubmitButton({ 141 | required this.formKey, 142 | required this.titleController, 143 | required this.descriptionController, 144 | this.postSnap, 145 | }); 146 | 147 | final GlobalKey formKey; 148 | final TextEditingController titleController; 149 | final TextEditingController descriptionController; 150 | final DocumentSnapshot? postSnap; 151 | 152 | @override 153 | Widget build(BuildContext context, WidgetRef ref) { 154 | return FilledButton( 155 | child: const Text('投稿する'), 156 | onPressed: () { 157 | if (formKey.currentState!.validate()) { 158 | final title = titleController.text; 159 | final description = descriptionController.text; 160 | 161 | final postSnap = this.postSnap; 162 | if (postSnap == null) { 163 | ref 164 | ..read(postRepositoryProvider).create( 165 | post: Post( 166 | title: title, 167 | description: description, 168 | ), 169 | ) 170 | ..read(scaffoldMessengerKey).currentState!.showMessage( 171 | '[$title]を投稿しました', 172 | ); 173 | } else { 174 | ref.read(postRepositoryProvider).update( 175 | reference: postSnap.reference, 176 | post: postSnap.data()!.copyWith( 177 | title: title, 178 | description: description, 179 | ), 180 | ); 181 | } 182 | context.pop(); 183 | } 184 | }, 185 | ); 186 | } 187 | } 188 | --------------------------------------------------------------------------------