diff --git a/.github/workflows/_build-android-app.yml b/.github/workflows/_build-android-app.yml index 5299d72f..bf72a262 100644 --- a/.github/workflows/_build-android-app.yml +++ b/.github/workflows/_build-android-app.yml @@ -2,27 +2,30 @@ name: Android Build workflow on: workflow_call: inputs: - pubspec-filename: + pubspec_filename: required: true type: string - environment-flag: + environment_flag: required: true type: string versionNumber: required: true type: string - build-flag: + build_flag: required: true type: boolean - build-artifact: + build_artifact: required: true type: string + build_appbundle: + required: true + type: boolean jobs: - buildAndroid: - name: Build Android - if: ${{ inputs.build-flag == true }} + testAnalyzeBuildAndroid: + name: Android - Test, Analyze, Build runs-on: ubuntu-latest + steps: - name: Checkout repository uses: actions/checkout@v4 @@ -32,34 +35,74 @@ jobs: - name: Set up Flutter and Java uses: ./.github/actions/setup-java-flutter + - name: melos test all + run: melos test:all + + - run: echo "Starting analyze" + + - name: ⚠️ℹ️ Run Dart analysis + uses: zgosalvez/github-actions-analyze-dart@v3 + with: + working-directory: "${{github.workspace}}/" + + - run: echo "Starting build process" + - name: Get pubspec.yaml + if: ${{ inputs.build_flag == true }} && success() uses: actions/download-artifact@v4 with: - name: ${{ inputs.pubspec-filename }} + name: ${{ inputs.pubspec_filename }} path: ${{ github.workspace }}/apps/multichoice/ - name: Download Android keystore id: android_keystore + if: success() uses: timheuer/base64-to-file@v1.2.4 with: fileName: upload-keystore.jks encodedString: ${{ secrets.ANDROID_KEYSTORE_BASE64 }} - name: Create key.properties - if: ${{ inputs.environment-flag }} == 'release' + if: ${{ inputs.environment_flag }} == 'release' run: | echo "storeFile=${{ steps.android_keystore.outputs.filePath }}" > key.properties echo "storePassword=${{ secrets.STORE_PASSWORD }}" >> key.properties echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> key.properties echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> key.properties - working-directory: apps/multichoice/android + working-directory: apps/multichoice/android/ + + - name: Create secrets.dart + run: | + mkdir auth + echo "String webApiKey = '${{ secrets.WEB_API_KEY }}';" > auth/secrets.dart + echo "String webAppId = '${{ secrets.WEB_APP_ID }}';" >> auth/secrets.dart + echo "String androidApiKey = '${{ secrets.ANDROID_API_KEY }}';" >> auth/secrets.dart + echo "String androidAppId = '${{ secrets.ANDROID_APP_ID }}';" >> auth/secrets.dart + echo "String iosApiKey = '${{ secrets.IOS_API_KEY }}';" >> auth/secrets.dart + echo "String iosAppId = '${{ secrets.IOS_APP_ID }}';" >> auth/secrets.dart + working-directory: apps/multichoice/lib/ - - name: Start ${{ inputs.environment-flag }} build - run: flutter build appbundle --${{ inputs.environment-flag }} - working-directory: ./apps/multichoice + - name: Start appbundle ${{ inputs.environment_flag }} build + if: ${{ inputs.build_appbundle == true }} && success() + run: flutter build appbundle --${{ inputs.environment_flag }} + working-directory: apps/multichoice/ - - name: Upload Android ${{ inputs.environment-flag }} + - name: Start apk ${{ inputs.environment_flag }} build + if: success() + run: flutter build apk --${{ inputs.environment_flag }} + working-directory: apps/multichoice/ + + - name: Upload Android ${{ inputs.environment_flag }} appbundle uses: actions/upload-artifact@v4 with: - name: ${{ inputs.build-artifact }} - path: ./apps/multichoice/build/app/outputs/bundle/release/app-release.aab + name: ${{ inputs.build_artifact }} + path: ./apps/multichoice/build/app/outputs/bundle/${{ inputs.environment_flag }}/app-${{ inputs.environment_flag }}.aab + + - name: Upload artifact to Firebase App Distribution + if: success() + uses: wzieba/Firebase-Distribution-Github-Action@v1 + with: + appId: ${{secrets.APP_ID}} + serviceCredentialsFileContent: ${{secrets.CREDENTIAL_FILE_CONTENT}} + groups: testers + file: ./apps/multichoice/build/app/outputs/flutter-apk/app-${{ inputs.environment_flag }}.apk diff --git a/.github/workflows/_build-env-apps.yml b/.github/workflows/_build-env-apps.yml index 8fb85832..9ee81018 100644 --- a/.github/workflows/_build-env-apps.yml +++ b/.github/workflows/_build-env-apps.yml @@ -2,31 +2,34 @@ name: Build Android, Web, & Windows Apps on: workflow_call: inputs: - pubspec-filename: + pubspec_filename: required: true type: string - web-build-flag: + web_build_flag: required: true type: boolean - web-environment-flag: + web_environment_flag: required: true type: string - android-build-flag: + android_build_flag: required: true type: boolean - android-environment-flag: + android_environment_flag: required: true type: string - android-versionNumber: + android_versionNumber: required: true type: string - android-build-artifact: + android_build_artifact: required: true type: string - windows-build-flag: + android_build_appbundle: required: true type: boolean - windows-environment-flag: + windows_build_flag: + required: true + type: boolean + windows_environment_flag: required: true type: string @@ -35,23 +38,24 @@ jobs: name: "Build Web app" uses: ./.github/workflows/_build-web-app.yml with: - pubspec-filename: ${{ inputs.pubspec-filename }} - environment-flag: ${{ inputs.web-environment-flag }} - build-flag: ${{ inputs.web-build-flag }} + pubspec_filename: ${{ inputs.pubspec_filename }} + environment_flag: ${{ inputs.web_environment_flag }} + build_flag: ${{ inputs.web_build_flag }} buildAndroid: name: "Build Android app" uses: ./.github/workflows/_build-android-app.yml secrets: inherit with: - pubspec-filename: ${{ inputs.pubspec-filename }} - environment-flag: ${{ inputs.android-environment-flag }} - versionNumber: ${{ inputs.android-versionNumber }} - build-flag: ${{ inputs.android-build-flag }} - build-artifact: ${{ inputs.android-build-artifact }} + pubspec_filename: ${{ inputs.pubspec_filename }} + environment_flag: ${{ inputs.android_environment_flag }} + versionNumber: ${{ inputs.android_versionNumber }} + build_flag: ${{ inputs.android_build_flag }} + build_artifact: ${{ inputs.android_build_artifact }} + build_appbundle: ${{ inputs.android_build_appbundle }} buildWindows: name: "Build Windows app" uses: ./.github/workflows/_build-windows-app.yml with: - pubspec-filename: ${{ inputs.pubspec-filename }} - environment-flag: ${{ inputs.windows-environment-flag }} - build-flag: ${{ inputs.windows-build-flag }} + pubspec_filename: ${{ inputs.pubspec_filename }} + environment_flag: ${{ inputs.windows_environment_flag }} + build_flag: ${{ inputs.windows_build_flag }} diff --git a/.github/workflows/_build-web-app.yml b/.github/workflows/_build-web-app.yml index 24cf517e..76f59c8d 100644 --- a/.github/workflows/_build-web-app.yml +++ b/.github/workflows/_build-web-app.yml @@ -2,20 +2,20 @@ name: Web Build workflow on: workflow_call: inputs: - pubspec-filename: + pubspec_filename: required: true type: string - environment-flag: + environment_flag: required: true type: string - build-flag: + build_flag: required: true type: boolean jobs: buildWeb: name: Build Web - if: inputs.build-flag == true + if: ${{ inputs.build_flag == true }} runs-on: ubuntu-latest steps: - name: Checkout repository @@ -25,8 +25,8 @@ jobs: uses: ./.github/actions/setup-flutter - name: Start Web Release Build - run: flutter build web --base-href='/multichoice/' --${{ inputs.environment-flag }} - working-directory: ./apps/multichoice + run: flutter build web --base-href='/multichoice/' --${{ inputs.environment_flag }} + working-directory: apps/multichoice/ - name: Upload Web Build Files uses: actions/upload-artifact@v4 diff --git a/.github/workflows/_build-windows-app.yml b/.github/workflows/_build-windows-app.yml index 291768a3..dc82bec1 100644 --- a/.github/workflows/_build-windows-app.yml +++ b/.github/workflows/_build-windows-app.yml @@ -2,20 +2,20 @@ name: Windows Build workflow on: workflow_call: inputs: - pubspec-filename: + pubspec_filename: required: true type: string - environment-flag: + environment_flag: required: true type: string - build-flag: + build_flag: required: true type: boolean jobs: buildWindows: name: Build Windows - if: ${{ inputs.build-flag == true }} + if: ${{ inputs.build_flag == true }} runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/.github/workflows/_deploy-android-app.yml b/.github/workflows/_deploy-android-app.yml index 8ce41c0a..3aea8aa9 100644 --- a/.github/workflows/_deploy-android-app.yml +++ b/.github/workflows/_deploy-android-app.yml @@ -6,19 +6,19 @@ on: deploy_flag: required: true type: boolean - environment-flag: + environment_flag: required: true type: string - package-name: + package_name: required: true type: string track: required: true type: string - release-name: + release_name: required: true type: string - deploy-status: + deploy_status: required: true type: string @@ -47,9 +47,9 @@ jobs: with: releaseFiles: app-release.aab serviceAccountJsonPlainText: ${{ secrets.SERVICE_ACCOUNT_JSON }} - packageName: ${{ inputs.package-name }} + packageName: ${{ inputs.package_name }} track: ${{ inputs.track }} - releaseName: ${{ inputs.release-name }} + releaseName: ${{ inputs.release_name }} # inAppUpdatePriority: 2 # userFraction: 0.5 - status: ${{ inputs.deploy-status }} + status: ${{ inputs.deploy_status }} diff --git a/.github/workflows/_deploy-env-apps.yml b/.github/workflows/_deploy-env-apps.yml index 104a3605..3d332b6a 100644 --- a/.github/workflows/_deploy-env-apps.yml +++ b/.github/workflows/_deploy-env-apps.yml @@ -5,33 +5,33 @@ on: web_deploy_flag: required: true type: boolean - web-environment-flag: + web_environment_flag: required: true type: string android_deploy_flag: required: true type: boolean - android-environment-flag: + android_environment_flag: required: true type: string - android-package-name: + android_package_name: required: true type: string - android-track: + android_track: required: true type: string - android-release-name: + android_release_name: required: true type: string - android-deploy-status: + android_deploy_status: required: true type: string windows_deploy_flag: required: true type: boolean - windows-environment-flag: + windows_environment_flag: required: true type: string @@ -46,21 +46,21 @@ jobs: secrets: inherit with: deploy_flag: ${{ inputs.web_deploy_flag }} - environment-flag: ${{ inputs.web-environment-flag }} + environment_flag: ${{ inputs.web_environment_flag }} deployAndroid: name: "Deploy Android app" uses: ./.github/workflows/_deploy-android-app.yml secrets: inherit with: deploy_flag: ${{ inputs.android_deploy_flag }} - environment-flag: ${{ inputs.android-environment-flag }} - package-name: ${{ inputs.android-package-name }} - track: ${{ inputs.android-track }} - release-name: ${{ inputs.android-release-name }} - deploy-status: ${{ inputs.android-deploy-status }} + environment_flag: ${{ inputs.android_environment_flag }} + package_name: ${{ inputs.android_package_name }} + track: ${{ inputs.android_track }} + release_name: ${{ inputs.android_release_name }} + deploy_status: ${{ inputs.android_deploy_status }} deployWindows: name: "Deploy Windows app" uses: ./.github/workflows/_deploy-windows-app.yml with: deploy_flag: ${{ inputs.windows_deploy_flag }} - environment-flag: ${{ inputs.windows-environment-flag }} + environment_flag: ${{ inputs.windows_environment_flag }} diff --git a/.github/workflows/_deploy-web-app.yml b/.github/workflows/_deploy-web-app.yml index 1bb96ac2..f20a50f2 100644 --- a/.github/workflows/_deploy-web-app.yml +++ b/.github/workflows/_deploy-web-app.yml @@ -6,7 +6,7 @@ on: deploy_flag: required: true type: boolean - environment-flag: + environment_flag: required: true type: string workflow_dispatch: diff --git a/.github/workflows/_deploy-windows-app.yml b/.github/workflows/_deploy-windows-app.yml index 36a63cf4..5b32e7e5 100644 --- a/.github/workflows/_deploy-windows-app.yml +++ b/.github/workflows/_deploy-windows-app.yml @@ -6,7 +6,7 @@ on: deploy_flag: required: true type: boolean - environment-flag: + environment_flag: required: true type: string workflow_dispatch: diff --git a/.github/workflows/build_workflow.yml b/.github/workflows/build_workflow.yml index 2a1407d4..baa03540 100644 --- a/.github/workflows/build_workflow.yml +++ b/.github/workflows/build_workflow.yml @@ -20,60 +20,9 @@ concurrency: cancel-in-progress: true jobs: - test: - name: Test - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Flutter - uses: ./.github/actions/setup-flutter - - - name: melos test all - run: melos test:all - - analyze: - name: Analyze - if: github.event.pull_request.draft == false - runs-on: ubuntu-latest - needs: test - - permissions: - contents: read - packages: read - statuses: write - - steps: - - run: echo "Starting analyze" - - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Flutter - uses: ./.github/actions/setup-flutter - - - name: ⚠️ℹ️ Run Dart analysis - uses: zgosalvez/github-actions-analyze-dart@v3 - with: - working-directory: "${{github.workspace}}/" - - # - name: Super-linter - # uses: super-linter/super-linter@v6.0.0 - # env: - # DEFAULT_BRANCH: main - # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - # IGNORE_GITIGNORED_FILES: true - # FILTER_REGEX_EXCLUDE: "\\.freezed\\.dart$" - # CREATE_LOG_FILE: true - # DISABLE_ERRORS: true - preBuild: - name: Bump build number + name: Prebuild - Bump build number + if: github.event.pull_request.draft == false runs-on: ubuntu-latest concurrency: group: build-group @@ -83,6 +32,10 @@ jobs: web_build_flag: ${{ steps.id_out.outputs.web_build_flag }} android_build_flag: ${{ steps.id_out.outputs.android_build_flag }} windows_build_flag: ${{ steps.id_out.outputs.windows_build_flag }} + + web_environment_flag: ${{ steps.id_out.outputs.web_environment_flag }} + android_environment_flag: ${{ steps.id_out.outputs.android_environment_flag }} + windows_environment_flag: ${{ steps.id_out.outputs.windows_environment_flag }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -108,6 +61,10 @@ jobs: echo "android_build_flag=${{ steps.read_config_yaml.outputs.build__android_build_flag }}" >> $GITHUB_OUTPUT echo "windows_build_flag=${{ steps.read_config_yaml.outputs.build__windows_build_flag }}" >> $GITHUB_OUTPUT + echo "web_environment_flag=${{ steps.read_config_yaml.outputs.environment__web_environment_flag }}" >> $GITHUB_OUTPUT + echo "android_environment_flag=${{ steps.read_config_yaml.outputs.environment__android_environment_flag }}" >> $GITHUB_OUTPUT + echo "windows_environment_flag=${{ steps.read_config_yaml.outputs.environment__windows_environment_flag }}" >> $GITHUB_OUTPUT + - name: Get pubspec.yaml version uses: actions/download-artifact@v4 with: @@ -118,19 +75,23 @@ jobs: name: Build Apps if: github.event.pull_request.draft == false && success() uses: ./.github/workflows/_build-env-apps.yml - needs: [analyze, preBuild] + needs: [preBuild] secrets: inherit + permissions: + contents: read + packages: read + statuses: write with: - pubspec-filename: "pubspec-file" - web-build-flag: ${{ needs.preBuild.outputs.web_build_flag == 'true' }} - web-environment-flag: "release" - # profile, release - android-build-flag: ${{ needs.preBuild.outputs.android_build_flag == 'true' }} - android-environment-flag: "release" - android-versionNumber: ${{ needs.preBuild.outputs.version_number }} - android-build-artifact: "android-release" - windows-build-flag: ${{ needs.preBuild.outputs.windows_build_flag == 'true' }} - windows-environment-flag: "release" + pubspec_filename: "pubspec-file" + web_build_flag: ${{ needs.preBuild.outputs.web_build_flag == 'true' }} + web_environment_flag: ${{ needs.preBuild.outputs.web_environment_flag }} + android_build_flag: ${{ needs.preBuild.outputs.android_build_flag == 'true' }} + android_environment_flag: ${{ needs.preBuild.outputs.android_environment_flag }} + android_versionNumber: ${{ needs.preBuild.outputs.version_number }} + android_build_artifact: "android-release" + android_build_appbundle: ${{ 'false' == 'true' }} + windows_build_flag: ${{ needs.preBuild.outputs.windows_build_flag == 'true' }} + windows_environment_flag: ${{ needs.preBuild.outputs.windows_environment_flag }} post-build: name: Post Build diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 9f39e7e8..8b252305 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -41,13 +41,13 @@ jobs: run: flutter build web --base-href='/multichoice/' --release - name: Install GitVersion - uses: gittools/actions/gitversion/setup@v0.13.4 + uses: gittools/actions/gitversion/setup@v1.1.1 with: versionSpec: "5.x" - name: Use GitVersion id: gitversion - uses: gittools/actions/gitversion/execute@v0.13.4 + uses: gittools/actions/gitversion/execute@v1.1.1 - name: Create version.txt with nuGetVersion shell: bash @@ -69,7 +69,7 @@ jobs: - name: Update pubspec.yml version action id: update-pubspec - uses: stikkyapp/update-pubspec-version@v1 + uses: stikkyapp/update-pubspec-version@v2 with: strategy: "none" # bump-build: true diff --git a/.github/workflows/deploy_workflow.yml b/.github/workflows/deploy_workflow.yml index 72f44739..97bd5475 100644 --- a/.github/workflows/deploy_workflow.yml +++ b/.github/workflows/deploy_workflow.yml @@ -20,6 +20,7 @@ jobs: windows_deploy_flag: ${{ steps.id_out.outputs.windows_deploy_flag }} android_package_name: ${{ steps.id_out.outputs.android_package_name }} + android_environment_url: ${{ steps.id_out.outputs.android_environment_url }} android_track: ${{ steps.id_out.outputs.android_track }} android_release_name: ${{ steps.id_out.outputs.android_release_name }} android_deploy_status: ${{ steps.id_out.outputs.android_deploy_status }} @@ -40,6 +41,7 @@ jobs: echo "windows_deploy_flag=${{ steps.read_config_yaml.outputs.build__windows_build_flag }}" >> $GITHUB_OUTPUT echo "android_package_name=${{ steps.read_config_yaml.outputs.android_deploy__android_package_name }}" >> $GITHUB_OUTPUT + echo "android_environment_url=${{ steps.read_config_yaml.outputs.android_deploy__android_environment_url }}" >> $GITHUB_OUTPUT echo "android_track=${{ steps.read_config_yaml.outputs.android_deploy__android_track }}" >> $GITHUB_OUTPUT echo "android_release_name=${{ steps.read_config_yaml.outputs.android_deploy__android_release_name }}" >> $GITHUB_OUTPUT echo "android_deploy_status=${{ steps.read_config_yaml.outputs.android_deploy__android_deploy_status }}" >> $GITHUB_OUTPUT @@ -54,15 +56,15 @@ jobs: secrets: inherit with: web_deploy_flag: ${{ needs.preDeploy.outputs.web_deploy_flag == 'true' }} - web-environment-flag: "Web Prod" + web_environment_flag: "Web Prod" android_deploy_flag: ${{ needs.preDeploy.outputs.android_deploy_flag == 'true' }} - android-environment-flag: "Android Prod" - android-package-name: ${{ needs.preDeploy.outputs.android_package_name }} - # TODO(ZK): add android environment url - android-track: ${{ needs.preDeploy.outputs.android_track }} - android-release-name: ${{ needs.preDeploy.outputs.android_release_name }} - android-deploy-status: ${{ needs.preDeploy.outputs.android_deploy_status }} + android_environment_flag: "Android Prod" + android_package_name: ${{ needs.preDeploy.outputs.android_package_name }} + android_environment_url: ${{ needs.preDeploy.outputs.android_environment_url }} + android_track: ${{ needs.preDeploy.outputs.android_track }} + android_release_name: ${{ needs.preDeploy.outputs.android_release_name }} + android_deploy_status: ${{ needs.preDeploy.outputs.android_deploy_status }} windows_deploy_flag: ${{ needs.preDeploy.outputs.windows_deploy_flag == 'true' }} - windows-environment-flag: "Windows Prod" + windows_environment_flag: "Windows Prod" diff --git a/.github/workflows/release_workflow.yml b/.github/workflows/release_workflow.yml index bd0b6a2e..1239a5c3 100644 --- a/.github/workflows/release_workflow.yml +++ b/.github/workflows/release_workflow.yml @@ -109,23 +109,23 @@ jobs: name: pubspec-file-release path: ${{ github.workspace }}/apps/multichoice/ - build: - name: Build Apps + buildRelease: + name: Build Apps for Release if: github.event.pull_request.draft == false uses: ./.github/workflows/_build-env-apps.yml needs: [bumpVersion] secrets: inherit with: - pubspec-filename: "pubspec-file-release" - web-build-flag: ${{ needs.bumpVersion.outputs.web_build_flag == 'true' }} - web-environment-flag: "release" - # profile, release - android-build-flag: ${{ needs.bumpVersion.outputs.android_build_flag == 'true' }} - android-environment-flag: "release" - android-versionNumber: ${{ needs.bumpVersion.outputs.version_number }} - android-build-artifact: "android-release" - windows-build-flag: ${{ needs.bumpVersion.outputs.windows_build_flag == 'true' }} - windows-environment-flag: "release" + pubspec_filename: "pubspec-file-release" + web_build_flag: ${{ needs.bumpVersion.outputs.web_build_flag == 'true' }} + web_environment_flag: ${{ needs.bumpVersion.outputs.web_environment_flag }} + android_build_flag: ${{ needs.bumpVersion.outputs.android_build_flag == 'true' }} + android_environment_flag: ${{ needs.bumpVersion.outputs.android_environment_flag }} + android_versionNumber: ${{ needs.bumpVersion.outputs.version_number }} + android_build_artifact: "android-release" + android_build_appbundle: ${{ 'true' == 'true' }} + windows_build_flag: ${{ needs.bumpVersion.outputs.windows_build_flag == 'true' }} + windows_environment_flag: ${{ needs.bumpVersion.outputs.windows_environment_flag }} commitBump: name: Auto Commit Bumped Version diff --git a/.github/workflows/setup.yml b/.github/workflows/setup.yml deleted file mode 100644 index 391f9ae7..00000000 --- a/.github/workflows/setup.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Common Setup - -on: - # workflow_call: - # push: - # branches: - # - fix-workflows - workflow_dispatch: - -jobs: - setup: - name: Set up Java and Flutter - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: read-config-file - uses: pietrobolcato/action-read-yaml@1.1.0 - id: read_config_yaml - with: - config: ${{ github.workspace }}/config.yml - - - id: echo_config - run: | - echo "draft: ${{ steps.read_config_yaml.outputs['release.draft'] }}" - echo "prerelease: ${{ steps.read_config_yaml.outputs['release.prerelease'] }}" - - # - id: read_config - # uses: actions/github-script@v7 - # with: - # github-token: ${{ secrets.GITHUB_TOKEN }} - # script: | - # const yaml = require('js-yaml'); - # const fs = require('fs'); - # const path = require('path'); - - # const configFile = path.join(process.env.GITHUB_WORKSPACE, 'config.yml'); - # const config = yaml.load(fs.readFileSync(configFile, 'utf8')); - - # console.log(JSON.stringify(config)); - # const draft = process.env.CONFIG_RELEASE_DRAFT = config.release.draft ? 'true' : 'false'; - # const prerelease = process.env.CONFIG_RELEASE_PRERELEASE = config.release.prerelease ? 'true' : 'false'; - - # console.log("${CONFIG_RELEASE_DRAFT}, ${CONFIG_RELEASE_PRERELEASE}"); - - # return draft.concat(':', prerelease) - - # result-encoding: string diff --git a/.gitignore b/.gitignore index b059cbf9..8348339a 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,7 @@ pubspec.lock **/*.g.dart **/*.config.dart **/*.auto_mappr.dart +**/*.tailor.dart **/*.gr.dart **/*.isar **/*.isar.lock diff --git a/.metadata b/.metadata index 121df5a6..b7b051e6 100644 --- a/.metadata +++ b/.metadata @@ -35,5 +35,5 @@ migration: # # Files that are not part of the templates will be ignored by default. unmanaged_files: - - 'lib/main.dart' - - 'ios/Runner.xcodeproj/project.pbxproj' + - "lib/main.dart" + - "ios/Runner.xcodeproj/project.pbxproj" diff --git a/android/app/src/main/kotlin/co/za/zanderkotze/multichoice/MainActivity.kt b/android/app/src/main/kotlin/co/za/zanderkotze/multichoice/MainActivity.kt new file mode 100644 index 00000000..55b328f6 --- /dev/null +++ b/android/app/src/main/kotlin/co/za/zanderkotze/multichoice/MainActivity.kt @@ -0,0 +1,5 @@ +package co.za.zanderkotze.multichoice + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() diff --git a/apps/multichoice/.gitignore b/apps/multichoice/.gitignore index a09c194d..49e5a445 100644 --- a/apps/multichoice/.gitignore +++ b/apps/multichoice/.gitignore @@ -19,6 +19,7 @@ pubspec.lock **/*.isar.lock **/libisar.dylib **/coverage +**/firebase_options.dart # Keys and Secrets password.txt @@ -26,10 +27,11 @@ upload-keystore.* **/key.properties **/multichoice-*.json **/**/.ssh +**/lib/auth/secrets.dart # dotenv environment variables file .env* -**/tmp.* +**/tmp* **/tmp/ # Avoid committing generated Javascript files: diff --git a/apps/multichoice/analysis_options.yaml b/apps/multichoice/analysis_options.yaml index 0e469662..5b7264de 100644 --- a/apps/multichoice/analysis_options.yaml +++ b/apps/multichoice/analysis_options.yaml @@ -4,3 +4,7 @@ linter: rules: public_member_api_docs: false lines_longer_than_80_chars: false + +analyzer: + exclude: + - lib/firebase_options.dart diff --git a/apps/multichoice/android/app/build.gradle b/apps/multichoice/android/app/build.gradle index ed41a51b..51281b56 100644 --- a/apps/multichoice/android/app/build.gradle +++ b/apps/multichoice/android/app/build.gradle @@ -2,6 +2,7 @@ plugins { id "com.android.application" id "kotlin-android" id "dev.flutter.flutter-gradle-plugin" + // id 'com.google.gms.google-services' } def localProperties = new Properties() @@ -67,6 +68,8 @@ android { } buildTypes { release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.release ndk { debugSymbolLevel 'FULL' @@ -79,4 +82,7 @@ flutter { source '../..' } -dependencies {} +dependencies { + // implementation platform('com.google.firebase:firebase-bom:32.7.3') + // implementation 'com.google.firebase:firebase-analytics' +} diff --git a/apps/multichoice/android/app/src/main/AndroidManifest.xml b/apps/multichoice/android/app/src/main/AndroidManifest.xml index d6b14ab0..0e6806f5 100644 --- a/apps/multichoice/android/app/src/main/AndroidManifest.xml +++ b/apps/multichoice/android/app/src/main/AndroidManifest.xml @@ -1,28 +1,36 @@ - - + + - + - - + + - - - + + - + \ No newline at end of file diff --git a/apps/multichoice/ios/.gitignore b/apps/multichoice/ios/.gitignore index 7a7f9873..1e1726c9 100644 --- a/apps/multichoice/ios/.gitignore +++ b/apps/multichoice/ios/.gitignore @@ -32,3 +32,7 @@ Runner/GeneratedPluginRegistrant.* !default.mode2v3 !default.pbxuser !default.perspectivev3 + +# Generated code and files +**/firebase_app_id_file.json +**/GoogleService-Info.plist \ No newline at end of file diff --git a/apps/multichoice/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/apps/multichoice/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 919434a6..337ef66e 100644 --- a/apps/multichoice/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/apps/multichoice/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -1,7 +1,7 @@ + version="1.0"> + location="self:"> - + \ No newline at end of file diff --git a/apps/multichoice/lib/app/extensions/extension_getters.dart b/apps/multichoice/lib/app/extensions/extension_getters.dart new file mode 100644 index 00000000..a01b7111 --- /dev/null +++ b/apps/multichoice/lib/app/extensions/extension_getters.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +extension ThemeGetter on BuildContext { + // Usage example: `context.theme` + ThemeData get theme => Theme.of(this); +} + +// extension SizeGetter on BuildContext { +// Size get size => MediaQuery.of(this).size; +// } diff --git a/apps/multichoice/lib/app/view/app.dart b/apps/multichoice/lib/app/view/app.dart index cf6a50b8..c2175b7f 100644 --- a/apps/multichoice/lib/app/view/app.dart +++ b/apps/multichoice/lib/app/view/app.dart @@ -1,5 +1,9 @@ +import 'package:core/core.dart'; import 'package:flutter/material.dart'; import 'package:multichoice/app/engine/app_router.dart'; +import 'package:multichoice/app/view/theme/app_theme.dart'; +import 'package:provider/provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; class App extends StatelessWidget { App({super.key}); @@ -8,17 +12,24 @@ class App extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp.router( - title: 'Multichoice', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.deepPurple, - ), - useMaterial3: true, + final sharedPref = coreSl(); + final systemBrightness = MediaQuery.platformBrightnessOf(context); + final isDarkMode = systemBrightness == Brightness.dark; + + if (sharedPref.getString('theme') == 'light' && isDarkMode) { + sharedPref.setString('theme', 'dark'); + } + + return ChangeNotifierProvider( + create: (context) => AppTheme(), + builder: (context, child) => MaterialApp.router( + title: 'Multichoice', + theme: AppTheme.light, + darkTheme: AppTheme.dark, + themeMode: context.watch().themeMode, + debugShowCheckedModeBanner: false, + routerConfig: _appRouter.config(), ), - darkTheme: ThemeData.dark(), - debugShowCheckedModeBanner: false, - routerConfig: _appRouter.config(), ); } } diff --git a/apps/multichoice/lib/app/view/theme/app_palette.dart b/apps/multichoice/lib/app/view/theme/app_palette.dart new file mode 100644 index 00000000..c843bc1e --- /dev/null +++ b/apps/multichoice/lib/app/view/theme/app_palette.dart @@ -0,0 +1,66 @@ +import 'package:flutter/material.dart'; + +abstract class AppPalette { + static const white = Color(0xffffffff); + static const black = Color(0xff000000); + + static const red = Colors.red; + static const imperialRed = Color(0xFFE54B4B); + + static const seashell = Color(0xFFF7EBE8); + + static final green = _GreenColors(); + static final grey = _GreyColors(); + static final paletteOne = _PaletteOne(); + static final paletteTwo = _PaletteTwo(); +} + +class _GreenColors { + _GreenColors(); + + final greenPea = const Color(0xff26734d); + final chateauGreen = const Color(0xff37a06b); + final silverTree = const Color(0xff4eb18b); + final neptune = const Color(0xff81c1aa); + final jetStream = const Color(0xffa8d1c8); +} + +class _GreyColors { + _GreyColors(); + + final bigStone = const Color(0xff15203c); + final sanJuan = const Color(0xff385170); + final sanJuanLight = const Color(0x7f385170); + final slateGray = const Color(0xff697a91); + final towerGray = const Color(0xffa1baba); + final geyser = const Color(0xffd7e0e5); // primary + final geyserLight = const Color(0x7fd7e0e5); +} + +class _PaletteTwo { + _PaletteTwo(); + + final primary0 = const Color(0xff050a19); + final primary5 = const Color(0xff121625); + final primary10 = const Color(0xff8995C1); + final primary15 = const Color(0xff2A2F3B); + final primary20 = const Color(0xff373B47); + + final bigStone = const Color(0xff15203C); // primary 171d2c + final bigStoneLight = const Color(0xff313B54); + final sanJuan = const Color(0xffe7ecef); + final sanJuanLight = const Color(0x7f172435); + final slateGray = const Color(0xff79899D); // 555d68 + final slateGrayLight = const Color(0x7f555d68); + final towerGray = const Color(0xff4e5c5c); + final geyser = const Color(0xffDBE3E8); // 172435 + final geyserLight = const Color(0x7f172435); +} + +class _PaletteOne { + final darkSpringGreen = const Color(0xff226f54); + final pistachio = const Color(0xff87c38f); + final lemonChiffon = const Color(0xfff4f0bb); + final black = const Color(0xff000000); + final quinacridoneMagenta = const Color(0xff8f2d56); +} diff --git a/apps/multichoice/lib/app/view/theme/app_theme.dart b/apps/multichoice/lib/app/view/theme/app_theme.dart new file mode 100644 index 00000000..5279b5fb --- /dev/null +++ b/apps/multichoice/lib/app/view/theme/app_theme.dart @@ -0,0 +1,292 @@ +import 'package:flutter/material.dart'; +import 'package:multichoice/app/view/theme/app_palette.dart'; +import 'package:multichoice/app/view/theme/app_typography.dart'; +import 'package:multichoice/constants/export_constants.dart'; +import 'package:theme/theme.dart'; + +class AppTheme with ChangeNotifier { + ThemeMode _themeMode = ThemeMode.system; + + ThemeMode get themeMode => _themeMode; + + set themeMode(ThemeMode themeMode) { + _themeMode = themeMode; + notifyListeners(); + } + + static final light = () { + final defaultTheme = ThemeData.light(); + + return defaultTheme.copyWith( + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: _lightAppColors.primary, + textStyle: TextStyle(color: _lightAppColors.background), + side: BorderSide(color: _lightAppColors.primary ?? Colors.white), + shape: RoundedRectangleBorder(borderRadius: borderCircular12), + minimumSize: outlinedButtonMinimumSize, + ), + ), + textSelectionTheme: const TextSelectionThemeData( + cursorColor: Colors.white, + selectionHandleColor: Colors.grey, + ), + textButtonTheme: TextButtonThemeData( + style: ButtonStyle( + foregroundColor: MaterialStatePropertyAll( + _lightAppColors.black, + ), + backgroundColor: MaterialStatePropertyAll( + AppPalette.grey.geyserLight, + ), + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + foregroundColor: _lightAppColors.secondary, + shape: RoundedRectangleBorder(borderRadius: borderCircular12), + minimumSize: elevatedButtonMinimumSize, + ), + ), + dialogBackgroundColor: _lightAppColors.background, + dialogTheme: DialogTheme( + shape: RoundedRectangleBorder(borderRadius: borderCircular16), + alignment: Alignment.center, + titleTextStyle: AppTypography.titleMedium, + contentTextStyle: AppTypography.bodyMedium, + actionsPadding: allPadding12, + ), + scaffoldBackgroundColor: _lightAppColors.background, + appBarTheme: AppBarTheme( + titleTextStyle: AppTypography.titleMedium.copyWith( + color: AppPalette.grey.geyser, + ), + centerTitle: true, + color: _lightAppColors.foreground, + ), + cardTheme: CardTheme( + margin: vertical12horizontal4, + elevation: 7, + shadowColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: borderCircular12), + ), + textTheme: defaultTheme.textTheme.copyWith( + titleMedium: _lightTextTheme.titleMedium, + bodyMedium: _lightTextTheme.bodyMedium, + ), + inputDecorationTheme: InputDecorationTheme( + labelStyle: const TextStyle(color: AppPalette.white), + hintStyle: const TextStyle( + color: Colors.white, + fontSize: 18, + ), + floatingLabelStyle: const TextStyle(color: Colors.white), + enabledBorder: OutlineInputBorder( + borderRadius: borderCircular8, + borderSide: const BorderSide(color: Colors.white), + ), + focusedBorder: OutlineInputBorder( + borderRadius: borderCircular8, + borderSide: const BorderSide(color: Colors.white), + ), + ), + iconButtonTheme: IconButtonThemeData( + style: ButtonStyle( + foregroundColor: MaterialStatePropertyAll(AppPalette.grey.geyser), + padding: const MaterialStatePropertyAll(EdgeInsets.zero), + side: const MaterialStatePropertyAll(BorderSide.none), + ), + ), + iconTheme: IconThemeData( + size: 18, + color: _lightAppColors.primary, + ), + extensions: [ + _lightAppColors, + _lightTextTheme, + ], + ); + }(); + + static final _lightAppColors = AppColorsExtension( + primary: AppPalette.grey.geyser, + primaryLight: AppPalette.grey.geyserLight, + secondary: AppPalette.grey.sanJuan, + secondaryLight: AppPalette.grey.sanJuanLight, + ternary: AppPalette.grey.bigStone, + foreground: AppPalette.grey.bigStone, + background: AppPalette.grey.slateGray, + white: null, + black: AppPalette.black, + ); + + static final _lightTextTheme = AppTextExtension( + body1: AppTypography.body1.copyWith(color: _lightAppColors.background), + body2: AppTypography.body2, + h1: null, + titleLarge: null, + titleMedium: AppTypography.titleMedium.copyWith( + color: AppPalette.grey.bigStone, + ), + titleSmall: AppTypography.titleSmall.copyWith( + color: AppPalette.grey.geyser, + ), + subtitleLarge: null, + subtitleMedium: AppTypography.subtitleMedium.copyWith( + color: AppPalette.grey.bigStone, + ), + subtitleSmall: AppTypography.subtitleSmall.copyWith( + color: AppPalette.grey.geyser, + ), + bodyLarge: AppTypography.bodyLarge, + bodyMedium: AppTypography.bodyMedium.copyWith( + color: AppPalette.grey.geyser, + ), + bodySmall: null, + ); + + static final dark = () { + final defaultTheme = ThemeData.dark(); + + return defaultTheme.copyWith( + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: _darkAppColors.white, + textStyle: AppTypography.bodyLarge, + side: BorderSide( + color: _darkAppColors.white ?? Colors.white, + ), + shape: RoundedRectangleBorder(borderRadius: borderCircular12), + minimumSize: outlinedButtonMinimumSize, + ), + ), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + foregroundColor: AppPalette.paletteTwo.primary0, + backgroundColor: AppPalette.white, + shape: RoundedRectangleBorder(borderRadius: borderCircular12), + textStyle: AppTypography.bodyLarge, + minimumSize: elevatedButtonMinimumSize, + ), + ), + dialogBackgroundColor: _darkAppColors.background, + dialogTheme: DialogTheme( + surfaceTintColor: Colors.transparent, + shape: RoundedRectangleBorder(borderRadius: borderCircular16), + alignment: Alignment.center, + titleTextStyle: AppTypography.titleMedium, + contentTextStyle: AppTypography.bodyMedium, + actionsPadding: allPadding12, + ), + scaffoldBackgroundColor: _darkAppColors.background, + appBarTheme: AppBarTheme( + titleTextStyle: AppTypography.titleMedium.copyWith( + color: AppPalette.paletteTwo.sanJuan, + ), + centerTitle: true, + color: _darkAppColors.foreground, + ), + cardTheme: CardTheme( + margin: vertical12horizontal4, + elevation: 7, + shadowColor: _darkAppColors.black, + shape: RoundedRectangleBorder(borderRadius: borderCircular12), + ), + textTheme: defaultTheme.textTheme.copyWith( + titleMedium: _darkTextTheme.titleMedium, + bodyMedium: _darkTextTheme.bodyMedium, + ), + iconButtonTheme: IconButtonThemeData( + style: ButtonStyle( + foregroundColor: + MaterialStatePropertyAll(AppPalette.paletteTwo.sanJuan), + padding: const MaterialStatePropertyAll(EdgeInsets.zero), + side: const MaterialStatePropertyAll(BorderSide.none), + ), + ), + iconTheme: IconThemeData( + size: 18, + color: AppPalette.paletteTwo.sanJuan, + ), + inputDecorationTheme: InputDecorationTheme( + labelStyle: const TextStyle(color: AppPalette.white), + hintStyle: const TextStyle( + color: Colors.white, + fontSize: 18, + ), + floatingLabelStyle: const TextStyle(color: Colors.white), + enabledBorder: OutlineInputBorder( + borderRadius: borderCircular8, + borderSide: const BorderSide(color: Colors.white), + ), + focusedBorder: OutlineInputBorder( + borderRadius: borderCircular8, + borderSide: const BorderSide(color: Colors.white), + ), + ), + textSelectionTheme: const TextSelectionThemeData( + cursorColor: Colors.white, + selectionHandleColor: Colors.grey, + ), + textButtonTheme: TextButtonThemeData( + style: ButtonStyle( + backgroundColor: MaterialStatePropertyAll( + AppPalette.grey.geyserLight, + ), + ), + ), + extensions: [ + _darkAppColors, + _darkTextTheme, + ], + ); + }(); + + static final _darkAppColors = AppColorsExtension( + // primary: AppPalette.paletteTwo.geyser, + primary: AppPalette.paletteTwo.primary5, + primaryLight: AppPalette.grey.geyserLight.withOpacity(0.2), + secondary: AppPalette.paletteTwo.primary10, + secondaryLight: AppPalette.paletteTwo.slateGrayLight, + ternary: AppPalette.paletteTwo.sanJuan, + foreground: AppPalette.paletteTwo.primary15, + background: AppPalette.paletteTwo.primary0, + white: AppPalette.white, + black: AppPalette.black, + ); + + static final _darkTextTheme = AppTextExtension( + body1: AppTypography.body1.copyWith(color: _darkAppColors.background), + body2: AppTypography.body2, + h1: null, + titleLarge: null, + titleMedium: AppTypography.titleMedium.copyWith( + color: AppPalette.paletteTwo.geyser, + ), + titleSmall: AppTypography.titleSmall.copyWith( + color: AppPalette.paletteTwo.primary5, + ), + subtitleLarge: null, + subtitleMedium: AppTypography.subtitleMedium.copyWith( + color: AppPalette.paletteTwo.geyser, + ), + subtitleSmall: AppTypography.subtitleMedium.copyWith( + color: AppPalette.paletteTwo.primary5, + ), + bodyLarge: AppTypography.bodyLarge.copyWith( + color: AppPalette.paletteTwo.primary5, + ), + bodyMedium: AppTypography.bodyMedium.copyWith( + color: AppPalette.paletteTwo.geyser, + ), + bodySmall: null, + ); + + static AppColorsExtension get lightAppColors => _lightAppColors; + + static AppTextExtension get lightTextTheme => _lightTextTheme; + + static AppColorsExtension get darkAppColors => _darkAppColors; + + static AppTextExtension get darkTextTheme => _darkTextTheme; +} diff --git a/apps/multichoice/lib/app/view/theme/app_typography.dart b/apps/multichoice/lib/app/view/theme/app_typography.dart new file mode 100644 index 00000000..91c7dced --- /dev/null +++ b/apps/multichoice/lib/app/view/theme/app_typography.dart @@ -0,0 +1,62 @@ +import 'package:flutter/material.dart'; + +abstract class AppTypography { + static const body1 = TextStyle( + fontSize: 16, + fontWeight: FontWeight.normal, + ); + + static const body2 = TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + ); + + static const h1 = TextStyle( + fontSize: 96, + fontWeight: FontWeight.w300, + ); + + static const h2 = TextStyle( + fontSize: 72, + fontWeight: FontWeight.w500, + ); + + static const titleLarge = TextStyle( + fontSize: 36, + fontWeight: FontWeight.w400, + ); + static const titleMedium = TextStyle( + fontSize: 22, + fontWeight: FontWeight.w500, + ); + static const titleSmall = TextStyle( + fontSize: 18, + fontWeight: FontWeight.w500, + ); + static const subtitleLarge = TextStyle( + fontSize: 20, + fontWeight: FontWeight.w500, + ); + static const subtitleMedium = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w400, + ); + static const subtitleSmall = TextStyle( + fontSize: 14, + fontWeight: FontWeight.w300, + ); + static const bodyVeryLarge = TextStyle(); + static const bodyLarge = TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ); + static const bodyMedium = TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + ); + static const bodySmall = TextStyle( + fontSize: 12, + fontWeight: FontWeight.w300, + ); + static const bodyVerySmall = TextStyle(); +} diff --git a/apps/multichoice/lib/app/view/theme/theme_extension/app_theme_extension.dart b/apps/multichoice/lib/app/view/theme/theme_extension/app_theme_extension.dart new file mode 100644 index 00000000..da3c676d --- /dev/null +++ b/apps/multichoice/lib/app/view/theme/theme_extension/app_theme_extension.dart @@ -0,0 +1,12 @@ +import 'package:flutter/material.dart'; +import 'package:multichoice/app/view/theme/app_theme.dart'; +import 'package:theme/theme.dart'; + +extension AppThemeExtension on ThemeData { + /// Usage example: Theme.of(context).appColors; + AppColorsExtension get appColors => + extension() ?? AppTheme.lightAppColors; + + AppTextExtension get appTextTheme => + extension() ?? AppTheme.lightTextTheme; +} diff --git a/apps/multichoice/lib/bootstrap.dart b/apps/multichoice/lib/bootstrap.dart index f5a85a9d..7d01f017 100644 --- a/apps/multichoice/lib/bootstrap.dart +++ b/apps/multichoice/lib/bootstrap.dart @@ -3,7 +3,9 @@ import 'dart:developer'; import 'package:bloc/bloc.dart'; import 'package:core/core.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'package:flutter/material.dart'; +import 'package:multichoice/firebase_options.dart'; class SimpleBlocObserver extends BlocObserver { const SimpleBlocObserver(); @@ -40,5 +42,8 @@ class SimpleBlocObserver extends BlocObserver { Future bootstrap() async { WidgetsFlutterBinding.ensureInitialized(); await configureCoreDependencies(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); Bloc.observer = const SimpleBlocObserver(); } diff --git a/apps/multichoice/lib/constants/border_constants.dart b/apps/multichoice/lib/constants/border_constants.dart index 8908580e..9830c7b6 100644 --- a/apps/multichoice/lib/constants/border_constants.dart +++ b/apps/multichoice/lib/constants/border_constants.dart @@ -1,5 +1,9 @@ import 'package:flutter/material.dart'; -final circularBorder5 = BorderRadius.circular(5); +final borderCircular5 = BorderRadius.circular(5); -final circularBorder12 = BorderRadius.circular(12); +final borderCircular8 = BorderRadius.circular(8); + +final borderCircular12 = BorderRadius.circular(12); + +final borderCircular16 = BorderRadius.circular(16); diff --git a/apps/multichoice/lib/constants/export_constants.dart b/apps/multichoice/lib/constants/export_constants.dart index 50c75012..5632b19e 100644 --- a/apps/multichoice/lib/constants/export_constants.dart +++ b/apps/multichoice/lib/constants/export_constants.dart @@ -1,2 +1,3 @@ export 'border_constants.dart'; export 'spacing_constants.dart'; +export 'ui_constants.dart'; diff --git a/apps/multichoice/lib/constants/spacing_constants.dart b/apps/multichoice/lib/constants/spacing_constants.dart index 49174dd8..2c729b53 100644 --- a/apps/multichoice/lib/constants/spacing_constants.dart +++ b/apps/multichoice/lib/constants/spacing_constants.dart @@ -4,6 +4,8 @@ import 'package:gap/gap.dart'; // Gaps const gap4 = Gap(4); +const gap8 = Gap(8); + const gap10 = Gap(10); const gap12 = Gap(12); @@ -14,7 +16,13 @@ const gap20 = Gap(20); const gap24 = Gap(24); +const gap56 = Gap(56); + // Padding +const zeroPadding = EdgeInsets.zero; + +const allPadding2 = EdgeInsets.all(2); + const allPadding4 = EdgeInsets.all(4); const allPadding6 = EdgeInsets.all(6); @@ -29,6 +37,14 @@ const vertical8 = EdgeInsets.symmetric(vertical: 8); const left4 = EdgeInsets.only(left: 4); +const left4top2 = EdgeInsets.only(left: 4, top: 2); + +const left4top4 = EdgeInsets.only(left: 4, top: 4); + const right4 = EdgeInsets.only(right: 4); const top12 = EdgeInsets.only(top: 12); + +const vertical8horizontal4 = EdgeInsets.symmetric(vertical: 8, horizontal: 4); + +const vertical12horizontal4 = EdgeInsets.symmetric(horizontal: 4, vertical: 12); diff --git a/apps/multichoice/lib/constants/ui_constants.dart b/apps/multichoice/lib/constants/ui_constants.dart new file mode 100644 index 00000000..ebff4cfe --- /dev/null +++ b/apps/multichoice/lib/constants/ui_constants.dart @@ -0,0 +1,50 @@ +import 'package:flutter/material.dart'; + +const outlinedButtonMinimumSize = Size(96, 48); +const elevatedButtonMinimumSize = Size(96, 48); + +const entryCardMinimumSize = null; +const entryCardMinimumHeight = 90.0; +const entryCardMinimumWidth = 0; +const tabCardMinimumWidth = 120.0; +double tabsHeightConstant = 1.15; +double tabsWidthConstant = 3.65; + +const _mobileScreenWidth = 450; +// const _desktopScreenWidth = 1920; + +class UIConstants { + UIConstants(); + + static double? entryHeight(BuildContext context) { + final mediaHeight = MediaQuery.sizeOf(context).height / 12; + + if (mediaHeight < entryCardMinimumHeight) { + return entryCardMinimumHeight; + } + return mediaHeight; + } + + static double? tabHeight(BuildContext context) { + return MediaQuery.sizeOf(context).height / tabsHeightConstant; + } + + static double? tabWidth(BuildContext context) { + final mediaWidth = MediaQuery.sizeOf(context).width; + + if (mediaWidth > _mobileScreenWidth) { + tabsWidthConstant = 8; + } + + final tabsWidth = mediaWidth / tabsWidthConstant; + + if (tabsWidth < tabCardMinimumWidth) { + return tabCardMinimumWidth; + } + return tabsWidth; + } + + static double? newTabWidth(BuildContext context) { + return MediaQuery.sizeOf(context).width / 6; + } +} diff --git a/apps/multichoice/lib/firebase_options.dart b/apps/multichoice/lib/firebase_options.dart new file mode 100644 index 00000000..c38605c4 --- /dev/null +++ b/apps/multichoice/lib/firebase_options.dart @@ -0,0 +1,76 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: lines_longer_than_80_chars, avoid_classes_with_only_static_members +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; +import 'auth/secrets.dart'; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for macos - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.windows: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for windows - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static FirebaseOptions web = FirebaseOptions( + apiKey: '${webApiKey}', + appId: '${webAppId}', + messagingSenderId: '82796040762', + projectId: 'multichoice-412309', + authDomain: 'multichoice-412309.firebaseapp.com', + storageBucket: 'multichoice-412309.appspot.com', + measurementId: 'G-RKRDGDJMDK', + ); + + static FirebaseOptions android = FirebaseOptions( + apiKey: '${androidApiKey}', + appId: '${androidAppId}', + messagingSenderId: '82796040762', + projectId: 'multichoice-412309', + storageBucket: 'multichoice-412309.appspot.com', + ); + + static FirebaseOptions ios = FirebaseOptions( + apiKey: '${iosApiKey}', + appId: '${iosAppId}', + messagingSenderId: '82796040762', + projectId: 'multichoice-412309', + storageBucket: 'multichoice-412309.appspot.com', + iosBundleId: 'com.example.multichoice', + ); +} diff --git a/apps/multichoice/lib/presentation/edit/edit_entry_page.dart b/apps/multichoice/lib/presentation/edit/edit_entry_page.dart index 916dda3b..cad0df5e 100644 --- a/apps/multichoice/lib/presentation/edit/edit_entry_page.dart +++ b/apps/multichoice/lib/presentation/edit/edit_entry_page.dart @@ -20,9 +20,9 @@ class EditEntryPage extends StatelessWidget { child: BlocBuilder( builder: (context, state) { return Scaffold( - backgroundColor: Colors.blue[100], appBar: AppBar( title: const Text('Edit entry'), + centerTitle: false, leading: IconButton( onPressed: () { ctx.read().add(const HomeEvent.onPressedCancel()); @@ -57,7 +57,7 @@ class _EditEntryPage extends StatelessWidget { } return Padding( - padding: allPadding12, + padding: allPadding18, child: Form( key: formKey, child: Column( @@ -90,7 +90,16 @@ class _EditEntryPage extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - TextButton( + OutlinedButton( + onPressed: () { + context.read().add( + const HomeEvent.onPressedCancel(), + ); + context.router.popUntilRoot(); + }, + child: const Text('Cancel'), + ), + ElevatedButton( onPressed: state.isValid && state.entry.title.isNotEmpty ? () { context @@ -102,15 +111,6 @@ class _EditEntryPage extends StatelessWidget { : null, child: const Text('Ok'), ), - TextButton( - onPressed: () { - context.read().add( - const HomeEvent.onPressedCancel(), - ); - context.router.popUntilRoot(); - }, - child: const Text('Cancel'), - ), ], ), ], diff --git a/apps/multichoice/lib/presentation/edit/edit_tab_page.dart b/apps/multichoice/lib/presentation/edit/edit_tab_page.dart index 4243d756..9d059889 100644 --- a/apps/multichoice/lib/presentation/edit/edit_tab_page.dart +++ b/apps/multichoice/lib/presentation/edit/edit_tab_page.dart @@ -18,9 +18,9 @@ class EditTabPage extends StatelessWidget { return BlocProvider.value( value: ctx.read(), child: Scaffold( - backgroundColor: Colors.blue[100], appBar: AppBar( title: const Text('Edit Tab'), + centerTitle: false, leading: IconButton( onPressed: () { ctx.read().add(const HomeEvent.onPressedCancel()); @@ -53,7 +53,7 @@ class _EditPage extends StatelessWidget { } return Padding( - padding: allPadding12, + padding: allPadding18, child: Form( key: formKey, child: Column( @@ -86,7 +86,16 @@ class _EditPage extends StatelessWidget { Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - TextButton( + OutlinedButton( + onPressed: () { + context.read().add( + const HomeEvent.onPressedCancel(), + ); + context.router.popUntilRoot(); + }, + child: const Text('Cancel'), + ), + ElevatedButton( onPressed: state.isValid && state.tab.title.isNotEmpty ? () { context @@ -98,15 +107,6 @@ class _EditPage extends StatelessWidget { : null, child: const Text('Ok'), ), - TextButton( - onPressed: () { - context.read().add( - const HomeEvent.onPressedCancel(), - ); - context.router.popUntilRoot(); - }, - child: const Text('Cancel'), - ), ], ), ], diff --git a/apps/multichoice/lib/presentation/home/home_page.dart b/apps/multichoice/lib/presentation/home/home_page.dart index 0b550be1..9c5c6d93 100644 --- a/apps/multichoice/lib/presentation/home/home_page.dart +++ b/apps/multichoice/lib/presentation/home/home_page.dart @@ -5,14 +5,18 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:models/models.dart'; import 'package:multichoice/app/engine/app_router.gr.dart'; -import 'package:multichoice/constants/border_constants.dart'; -import 'package:multichoice/constants/spacing_constants.dart'; +import 'package:multichoice/app/extensions/extension_getters.dart'; +import 'package:multichoice/app/view/theme/app_theme.dart'; +import 'package:multichoice/app/view/theme/theme_extension/app_theme_extension.dart'; +import 'package:multichoice/constants/export_constants.dart'; import 'package:multichoice/presentation/shared/widgets/add_widgets/_base.dart'; import 'package:multichoice/utils/custom_dialog.dart'; import 'package:multichoice/utils/custom_scroll_behaviour.dart'; +import 'package:shared_preferences/shared_preferences.dart'; part 'widgets/cards.dart'; part 'widgets/entry_card.dart'; +part 'widgets/drawer.dart'; part 'widgets/menu_widget.dart'; part 'widgets/new_entry.dart'; part 'widgets/new_tab.dart'; @@ -25,27 +29,50 @@ class HomePage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => coreSl()..add(const HomeEvent.onGetTabs()), + create: (context) => coreSl() + ..add( + const HomeEvent.onGetTabs(), + ), child: BlocBuilder( builder: (context, state) { return Scaffold( appBar: AppBar( title: const Text('Multichoice'), - centerTitle: true, - backgroundColor: Colors.lightBlue, actions: [ IconButton( - onPressed: () { - context - .read() - .add(const HomeEvent.onPressedDeleteAll()); - }, + onPressed: state.tabs != null + ? () { + CustomDialog.show( + context: context, + title: const Text('Delete all tabs and entries?'), + content: const Text( + 'Are you sure you want to delete all tabs and their entries?', + ), + actions: [ + OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('No, cancel'), + ), + ElevatedButton( + onPressed: () { + context.read().add( + const HomeEvent.onPressedDeleteAll(), + ); + Navigator.of(context).pop(); + }, + child: const Text('Yes, delete'), + ), + ], + ); + } + : null, icon: const Icon( Icons.delete_sweep_rounded, ), ), ], ), + drawer: const _HomeDrawer(), body: const _HomePage(), ); }, @@ -63,11 +90,17 @@ class _HomePage extends StatelessWidget { builder: (context, state) { final tabs = state.tabs ?? []; + if (state.isLoading) { + return const Center( + child: CircularProgressIndicator.adaptive(), + ); + } + return Center( child: Padding( padding: allPadding12, child: SizedBox( - height: MediaQuery.sizeOf(context).height / 1.25, + height: UIConstants.tabHeight(context), child: CustomScrollView( scrollDirection: Axis.horizontal, controller: ScrollController(), diff --git a/apps/multichoice/lib/presentation/home/widgets/drawer.dart b/apps/multichoice/lib/presentation/home/widgets/drawer.dart new file mode 100644 index 00000000..79ac8a36 --- /dev/null +++ b/apps/multichoice/lib/presentation/home/widgets/drawer.dart @@ -0,0 +1,122 @@ +part of '../home_page.dart'; + +class _HomeDrawer extends StatelessWidget { + const _HomeDrawer(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final appVersion = coreSl().getAppVersion(); + final sharedPref = coreSl(); + + return Drawer( + width: MediaQuery.sizeOf(context).width, + backgroundColor: context.theme.appColors.background, + child: Padding( + padding: allPadding12, + child: Column( + children: [ + gap56, + Row( + children: [ + Expanded( + child: Text( + 'Settings', + style: context.theme.appTextTheme.titleMedium?.copyWith( + color: Colors.white, + ), + ), + ), + IconButton( + onPressed: () { + if (Navigator.canPop(context)) { + Navigator.pop(context); + } + }, + icon: const Icon( + Icons.close_outlined, + size: 28, + ), + ), + ], + ), + gap24, + Row( + children: [ + const Expanded( + child: Text('Light/Dark Mode'), + ), + _ThemeButton( + sharedPref: sharedPref, + state: state, + ), + ], + ), + const Expanded( + child: SizedBox.expand(), + ), + Padding( + padding: allPadding12, + child: FutureBuilder( + future: appVersion, + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done && + snapshot.hasData) { + return Text( + 'V${snapshot.data}', + style: context.theme.appTextTheme.bodyMedium, + ); + } + return const SizedBox.shrink(); + }, + ), + ), + ], + ), + ), + ); + }, + ); + } +} + +class _ThemeButton extends StatelessWidget { + const _ThemeButton({ + required this.sharedPref, + required this.state, + }); + + final SharedPreferences sharedPref; + final HomeState state; + + @override + Widget build(BuildContext context) { + if (sharedPref.getString('theme') == 'light') { + return IconButton( + onPressed: () { + _darkMode(context); + sharedPref.setString('theme', 'dark'); + }, + icon: const Icon(Icons.dark_mode_outlined), + ); + } else if (sharedPref.getString('theme') == 'dark') { + return IconButton( + onPressed: () { + _lightMode(context); + sharedPref.setString('theme', 'light'); + }, + icon: const Icon(Icons.light_mode_outlined), + ); + } + return const SizedBox.shrink(); + } + + void _lightMode(BuildContext context) { + context.read().themeMode = ThemeMode.light; + } + + void _darkMode(BuildContext context) { + context.read().themeMode = ThemeMode.dark; + } +} diff --git a/apps/multichoice/lib/presentation/home/widgets/entry_card.dart b/apps/multichoice/lib/presentation/home/widgets/entry_card.dart index be19be05..f5e53758 100644 --- a/apps/multichoice/lib/presentation/home/widgets/entry_card.dart +++ b/apps/multichoice/lib/presentation/home/widgets/entry_card.dart @@ -20,7 +20,9 @@ class _EntryCard extends HookWidget { menuChildren: [ MenuItemButton( onPressed: () { - context.read().add(HomeEvent.onUpdateEntry(entry.id)); + context.read().add( + HomeEvent.onUpdateEntry(entry.id), + ); context.router.push(EditEntryPageRoute(ctx: context)); }, child: Text(MenuItems.edit.name), @@ -78,6 +80,18 @@ class _EntryCard extends HookWidget { ), ], child: GestureDetector( + onTap: () { + CustomDialog.show( + context: context, + title: SizedBox( + width: 150, + child: Text( + entry.title, + ), + ), + content: Text(entry.subtitle), + ); + }, onDoubleTap: () { context.read().add(HomeEvent.onUpdateEntry(entry.id)); context.router.push(EditEntryPageRoute(ctx: context)); @@ -89,31 +103,38 @@ class _EntryCard extends HookWidget { menuController.open(); } }, - child: Card( - elevation: 7, - shadowColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: circularBorder5, - ), - color: const Color.fromARGB(255, 81, 153, 187), - child: Padding( - padding: allPadding4, - child: Column( - children: [ - Text( - entry.title, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.w500, - ), - ), - Text( - entry.subtitle, - style: const TextStyle( - fontSize: 14, - ), + child: Padding( + padding: allPadding4, + child: Card( + elevation: 3, + shadowColor: Colors.grey[400], + shape: RoundedRectangleBorder( + borderRadius: borderCircular5, + ), + margin: EdgeInsets.zero, + color: context.theme.appColors.secondary, + child: Padding( + padding: allPadding4, + child: SizedBox( + height: UIConstants.entryHeight(context), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text( + entry.title, + style: context.theme.appTextTheme.titleSmall, + overflow: TextOverflow.ellipsis, + maxLines: 1, + ), + Text( + entry.subtitle, + style: context.theme.appTextTheme.subtitleSmall, + overflow: TextOverflow.ellipsis, + maxLines: 2, + ), + ], ), - ], + ), ), ), ), diff --git a/apps/multichoice/lib/presentation/home/widgets/menu_widget.dart b/apps/multichoice/lib/presentation/home/widgets/menu_widget.dart index 97100e7a..1f193feb 100644 --- a/apps/multichoice/lib/presentation/home/widgets/menu_widget.dart +++ b/apps/multichoice/lib/presentation/home/widgets/menu_widget.dart @@ -1,9 +1,8 @@ part of '../home_page.dart'; -class MenuWidget extends StatelessWidget { - const MenuWidget({ +class _MenuWidget extends StatelessWidget { + const _MenuWidget({ required this.tab, - super.key, }); final TabsDTO tab; @@ -23,8 +22,11 @@ class MenuWidget extends StatelessWidget { menuController.open(); } }, + // visualDensity: VisualDensity.adaptivePlatformDensity, icon: const Icon(Icons.more_vert_outlined), - padding: EdgeInsets.zero, + iconSize: 20, + color: context.theme.appColors.ternary, + padding: zeroPadding, ); }, menuChildren: [ @@ -36,51 +38,53 @@ class MenuWidget extends StatelessWidget { child: Text(MenuItems.edit.name), ), MenuItemButton( - onPressed: () { - CustomDialog.show( - context: context, - title: RichText( - text: TextSpan( - text: 'Delete all entries of ', - style: DefaultTextStyle.of(context) - .style - .copyWith(fontSize: 24), - children: [ - TextSpan( - text: tab.title, - style: const TextStyle( - fontWeight: FontWeight.bold, + onPressed: tab.entries.isNotEmpty + ? () { + CustomDialog.show( + context: context, + title: RichText( + text: TextSpan( + text: 'Delete all entries of ', + style: DefaultTextStyle.of(context) + .style + .copyWith(fontSize: 24), + children: [ + TextSpan( + text: tab.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ), + TextSpan( + text: '?', + style: DefaultTextStyle.of(context) + .style + .copyWith(fontSize: 24), + ), + ], ), ), - TextSpan( - text: '?', - style: DefaultTextStyle.of(context) - .style - .copyWith(fontSize: 24), + content: Text( + 'Are you sure you want to delete all the entries of ${tab.title}?', ), - ], - ), - ), - content: Text( - 'Are you sure you want to delete all the entries of ${tab.title}?', - ), - actions: [ - OutlinedButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - ElevatedButton( - onPressed: () { - context.read().add( - HomeEvent.onPressedDeleteAllEntries(tab.id), - ); - Navigator.of(context).pop(); - }, - child: const Text('Delete Entries'), - ), - ], - ); - }, + actions: [ + OutlinedButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel'), + ), + ElevatedButton( + onPressed: () { + context.read().add( + HomeEvent.onPressedDeleteAllEntries(tab.id), + ); + Navigator.of(context).pop(); + }, + child: const Text('Delete Entries'), + ), + ], + ); + } + : null, child: Text(MenuItems.deleteEntries.name), ), MenuItemButton( diff --git a/apps/multichoice/lib/presentation/home/widgets/new_entry.dart b/apps/multichoice/lib/presentation/home/widgets/new_entry.dart index 3c1aa4bf..828f0d2d 100644 --- a/apps/multichoice/lib/presentation/home/widgets/new_entry.dart +++ b/apps/multichoice/lib/presentation/home/widgets/new_entry.dart @@ -16,15 +16,16 @@ class _NewEntry extends StatelessWidget { builder: (context, state) { final homeBloc = context.read(); return AddEntryCard( - padding: allPadding6, + padding: zeroPadding, onPressed: () { CustomDialog.show( context: context, title: RichText( text: TextSpan( text: 'Add New Entry', - style: - DefaultTextStyle.of(context).style.copyWith(fontSize: 24), + style: DefaultTextStyle.of(context).style.copyWith( + fontSize: 24, + ), ), ), content: BlocProvider.value( @@ -47,6 +48,7 @@ class _NewEntry extends StatelessWidget { hintText: 'Title', ), ), + gap10, TextFormField( controller: subtitleTextController, onChanged: (value) => context.read().add( diff --git a/apps/multichoice/lib/presentation/home/widgets/new_tab.dart b/apps/multichoice/lib/presentation/home/widgets/new_tab.dart index e74708f0..1bc08320 100644 --- a/apps/multichoice/lib/presentation/home/widgets/new_tab.dart +++ b/apps/multichoice/lib/presentation/home/widgets/new_tab.dart @@ -12,7 +12,7 @@ class _NewTab extends StatelessWidget { builder: (context, state) { final homeBloc = context.read(); return AddTabCard( - width: MediaQuery.sizeOf(context).width / 4, + width: UIConstants.newTabWidth(context), onPressed: () { CustomDialog.show( context: context, @@ -35,6 +35,7 @@ class _NewTab extends StatelessWidget { hintText: 'Title', ), ), + gap10, TextFormField( controller: subtitleTextController, onChanged: (value) => context diff --git a/apps/multichoice/lib/presentation/home/widgets/vertical_tab.dart b/apps/multichoice/lib/presentation/home/widgets/vertical_tab.dart index 1dafcd41..a1d2454a 100644 --- a/apps/multichoice/lib/presentation/home/widgets/vertical_tab.dart +++ b/apps/multichoice/lib/presentation/home/widgets/vertical_tab.dart @@ -18,7 +18,9 @@ class _VerticalTab extends StatelessWidget { title: RichText( text: TextSpan( text: 'Delete ', - style: DefaultTextStyle.of(context).style.copyWith(fontSize: 24), + style: DefaultTextStyle.of(context).style.copyWith( + fontSize: 24, + ), children: [ TextSpan( text: tab.title, @@ -28,8 +30,9 @@ class _VerticalTab extends StatelessWidget { ), TextSpan( text: '?', - style: - DefaultTextStyle.of(context).style.copyWith(fontSize: 24), + style: DefaultTextStyle.of(context).style.copyWith( + fontSize: 24, + ), ), ], ), @@ -54,54 +57,49 @@ class _VerticalTab extends StatelessWidget { ], ); }, - child: Padding( - padding: right4, - child: Card( - color: Colors.grey[200], - elevation: 5, - shadowColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: circularBorder12, - ), - child: Padding( - padding: allPadding4, - child: SizedBox( - width: MediaQuery.sizeOf(context).width / 4, - child: Column( - children: [ - Row( - children: [ - Expanded( - child: Padding( - padding: left4, - child: Text( - tab.title, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.w500, - color: Colors.black, - ), - ), + child: Card( + color: context.theme.appColors.primary, + child: Padding( + padding: allPadding2, + child: SizedBox( + width: UIConstants.tabWidth(context), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Padding( + padding: left4, + child: Text( + tab.title, + style: context.theme.appTextTheme.titleMedium, ), ), - MenuWidget(tab: tab), - ], - ), - const Divider( - indent: 4, - endIndent: 4, - ), + ), + _MenuWidget(tab: tab), + ], + ), + if (tab.subtitle.isEmpty) + const SizedBox.shrink() + else Padding( padding: left4, child: Text( tab.subtitle, - style: const TextStyle(color: Colors.black), + style: context.theme.appTextTheme.subtitleMedium, ), ), - gap10, - _Cards(id: tab.id, entries: entries), - ], - ), + Divider( + color: context.theme.appColors.secondaryLight, + thickness: 2, + indent: 4, + endIndent: 4, + ), + gap4, + _Cards(id: tab.id, entries: entries), + ], ), ), ), diff --git a/apps/multichoice/lib/presentation/shared/widgets/add_widgets/_base.dart b/apps/multichoice/lib/presentation/shared/widgets/add_widgets/_base.dart index 5293550f..87d360a0 100644 --- a/apps/multichoice/lib/presentation/shared/widgets/add_widgets/_base.dart +++ b/apps/multichoice/lib/presentation/shared/widgets/add_widgets/_base.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:multichoice/app/extensions/extension_getters.dart'; +import 'package:multichoice/app/view/theme/theme_extension/app_theme_extension.dart'; import 'package:multichoice/constants/export_constants.dart'; part 'entry.dart'; diff --git a/apps/multichoice/lib/presentation/shared/widgets/add_widgets/entry.dart b/apps/multichoice/lib/presentation/shared/widgets/add_widgets/entry.dart index aa8b67c0..209a7722 100644 --- a/apps/multichoice/lib/presentation/shared/widgets/add_widgets/entry.dart +++ b/apps/multichoice/lib/presentation/shared/widgets/add_widgets/entry.dart @@ -5,21 +5,23 @@ class AddEntryCard extends StatelessWidget { required this.onPressed, required this.padding, this.semanticLabel, + this.color, super.key, }); final String? semanticLabel; final VoidCallback onPressed; final EdgeInsetsGeometry padding; + final Color? color; @override Widget build(BuildContext context) { return _BaseCard( semanticLabel: semanticLabel ?? '', elevation: 5, - color: Colors.grey[600], + color: color ?? context.theme.appColors.secondaryLight, shape: RoundedRectangleBorder( - borderRadius: circularBorder5, + borderRadius: borderCircular5, ), padding: padding, icon: const Icon(Icons.add_outlined), diff --git a/apps/multichoice/lib/presentation/shared/widgets/add_widgets/tab.dart b/apps/multichoice/lib/presentation/shared/widgets/add_widgets/tab.dart index 52c8002f..5ed99de2 100644 --- a/apps/multichoice/lib/presentation/shared/widgets/add_widgets/tab.dart +++ b/apps/multichoice/lib/presentation/shared/widgets/add_widgets/tab.dart @@ -5,21 +5,23 @@ class AddTabCard extends StatelessWidget { required this.onPressed, this.semanticLabel, this.width, + this.color, super.key, }); final String? semanticLabel; final double? width; final VoidCallback onPressed; + final Color? color; @override Widget build(BuildContext context) { return _BaseCard( semanticLabel: semanticLabel ?? '', elevation: 5, - color: Colors.grey[600], + color: color ?? context.theme.appColors.primaryLight, shape: RoundedRectangleBorder( - borderRadius: circularBorder12, + borderRadius: borderCircular12, ), child: Padding( padding: allPadding6, diff --git a/apps/multichoice/pubspec.yaml b/apps/multichoice/pubspec.yaml index f1bb0f92..b4705c70 100644 --- a/apps/multichoice/pubspec.yaml +++ b/apps/multichoice/pubspec.yaml @@ -2,7 +2,7 @@ name: multichoice description: "The application for the Multichoice repo" publish_to: "none" -version: 0.1.4+108 +version: 0.1.4+119 environment: sdk: ">=3.3.0 <4.0.0" @@ -11,12 +11,16 @@ dependencies: auto_route: ^7.8.4 bloc: ^8.1.3 core: ^0.0.1 + firebase_core: ^2.27.2 flutter: sdk: flutter flutter_bloc: ^8.1.4 flutter_hooks: ^0.20.5 gap: ^3.0.1 models: ^0.0.1 + provider: ^6.1.1 + shared_preferences: ^2.2.2 + theme: ^0.0.1 window_size: git: url: https://github.com/google/flutter-desktop-embedding diff --git a/apps/multichoice/pubspec_overrides.yaml b/apps/multichoice/pubspec_overrides.yaml index dbebc64b..dcf73b58 100644 --- a/apps/multichoice/pubspec_overrides.yaml +++ b/apps/multichoice/pubspec_overrides.yaml @@ -1,6 +1,9 @@ +# melos_managed_dependency_overrides: theme # melos_managed_dependency_overrides: core,models dependency_overrides: core: path: ../../packages/core models: path: ../../packages/models + theme: + path: ../../packages/theme diff --git a/config.yml b/config.yml index 693297d2..626b26e7 100644 --- a/config.yml +++ b/config.yml @@ -7,6 +7,14 @@ build: android_build_flag: true windows_build_flag: false +# This can be used to determine the environment to build the app in. +# Note: This is used in both build_workflow and release_workflow +# Options: profile, release (debug is by default empty) +environment: + web_environment_flag: "release" + android_environment_flag: "release" + windows_environment_flag: "release" + # This can be used to determine the state of releases to Github. release: body: "Release notes here" @@ -21,6 +29,7 @@ deploy: android_deploy: android_package_name: "co.za.zanderkotze.multichoice" + android_environment_url: "https://play.google.com/console/u/0/developers/8783535225973670504/app/4976133683768209199/tracks/internal-testing" android_track: "alpha" android_release_name: "v1.0.0" android_deploy_status: "draft" diff --git a/docs/8-investigate-theming-of-app-as-well-as-colours-1.md b/docs/8-investigate-theming-of-app-as-well-as-colours-1.md new file mode 100644 index 00000000..eef227e2 --- /dev/null +++ b/docs/8-investigate-theming-of-app-as-well-as-colours-1.md @@ -0,0 +1,48 @@ +# [Investigate theming of app as well as colours](https://github.com/ZanderCowboy/multichoice/issues/8) + +## Ticket: [8](https://github.com/ZanderCowboy/multichoice/issues/8) + +### branch: `8-investigate-theming-of-app-as-well-as-colours-1` + +### Overview + +This ticket is primarily responsible for setting up theming in our app. I also implemented Light mode and Dark mode. + +### What was done + +- [X] Created a `Makefile` to avoid typing the commands in the terminal manually +```make +fb: + flutter pub get && dart run build_runner build --delete-conflicting-outputs + +db: + dart run build_runner build --delete-conflicting-outputs + +frb: + flutter clean && flutter pub get && dart run build_runner build --delete-conflicting-outputs +``` +- [X] Restructured new code +- [X] Created an app palette in `app_palette.dart` +- [X] Created typography for the app in `app_typography.dart` +- [X] Updated the UI with the new colors and text styles +- [X] Created a simple `Drawer` on the HomePage +- [X] Implemented `Light` mode and `Dark` mode +- [X] Created `app_colors_extension` and `app_text_extension`, along with `AppThemeExtension` +- [X] Created and updated main `AppTheme` in `app_theme.dart` +- [X] Created a `ThemeGetter` to enable us to use `context.theme` with `ThemeData get theme => Theme.of(this);` +- [X] Created `theme` package and added files + +### What needs to be done + +- [ ] Verify that our method of refreshing the theme button in the HomeBloc +- [ ] Remove `theme_service` and interface `i_theme_service` + +### Resources + +- [name](https://medium.com/@alexandersnotes/flutter-custom-theme-with-themeextension-792034106abc) +- [Don’t write Theme.of(context) ANYMORE](https://medium.com/@alexandersnotes/how-to-improve-flutter-code-with-extension-methods-99854a29692c) +- [`theme_tailor`](https://pub.dev/packages/theme_tailor) +- [`provider`](https://pub.dev/packages/provider) +- [`shared_preferences`](https://pub.dev/packages/shared_preferences) +- [Mobile Palette Generator](https://mobilepalette.colorion.co/) +- [colormagic](https://colormagic.app/) diff --git a/docs/94-add-firebase-app-distribution-to-the-app.md b/docs/94-add-firebase-app-distribution-to-the-app.md new file mode 100644 index 00000000..6036086a --- /dev/null +++ b/docs/94-add-firebase-app-distribution-to-the-app.md @@ -0,0 +1,30 @@ +# [Add Firebase App Distribution to the App](https://github.com/ZanderCowboy/multichoice/issues/94) + +## Ticket: [94](https://github.com/ZanderCowboy/multichoice/issues/94) + +### branch: `94-add-firebase-app-distribution-to-the-app` + +### Overview + +This ticket is about setting up `Firebase App Distribution` to our app + +### What was done + +- [X] Add `google-services.json` file with config +- [X] Update `build.gradle` files for Google Services +- [X] Set up firebase config +- [X] Update `_build-android-app.yml` file for firebase app distribution +- [X] Update workflows overall +- [X] Fix firebase_options.dart not found by GitHub Actions + +### What needs to be done + +### Resources + +- [Firebase CLI reference](https://firebase.google.com/docs/cli#install-cli-windows) +- [Add Firebase to your Flutter app](https://firebase.google.com/docs/flutter/setup?platform=android) +- [Service accounts for project 'Multichoice'](https://console.cloud.google.com/iam-admin/serviceaccounts?project=multichoice-412309) +- [Github Action and Firebase App Distribution — CI/CD Ways — Part 2](https://steveos.medium.com/github-action-and-firebase-app-distribution-ci-cd-ways-part-2-fcf9ba425c0) +- [Firebase-Distribution-Github-Action](https://github.com/wzieba/Firebase-Distribution-Github-Action) + +- [](https://play.google.com/console/u/0/developers/8783535225973670504/app/4976133683768209199/tracks/internal-testing) diff --git a/packages/core/analysis_options.yaml b/packages/core/analysis_options.yaml index 6fa6881c..273898fa 100644 --- a/packages/core/analysis_options.yaml +++ b/packages/core/analysis_options.yaml @@ -11,3 +11,5 @@ analyzer: - lib/generated/** - lib/**.g.dart - lib/**.freezed.dart + errors: + invalid_annotation_target: ignore diff --git a/packages/core/lib/src/application/home/home_bloc.dart b/packages/core/lib/src/application/home/home_bloc.dart index f40103c7..93fc38b3 100644 --- a/packages/core/lib/src/application/home/home_bloc.dart +++ b/packages/core/lib/src/application/home/home_bloc.dart @@ -34,14 +34,14 @@ class HomeBloc extends Bloc { ); }, onGetTab: (value) async { - emit(state.copyWith(isLoading: true)); + // emit(state.copyWith(isLoading: true)); final tab = await _tabsRepository.getTab(value.tabId); emit( state.copyWith( tab: tab, - isLoading: false, + // isLoading: false, ), ); }, @@ -171,7 +171,7 @@ class HomeBloc extends Bloc { emit( state.copyWith( tab: TabsDTO.empty(), - tabs: [], + tabs: null, entry: EntryDTO.empty(), entryCards: null, isLoading: false, @@ -283,12 +283,26 @@ class HomeBloc extends Bloc { ); }, onUpdateTabId: (value) async { + emit(state.copyWith(isLoading: true)); + final tab = await _tabsRepository.getTab(value.id); - emit(state.copyWith(tab: tab, isValid: false)); + + emit(state.copyWith( + tab: tab, + isValid: false, + isLoading: false, + )); }, onUpdateEntry: (value) async { + emit(state.copyWith(isLoading: true)); + final entry = await _entryRepository.getEntry(value.id); - emit(state.copyWith(entry: entry, isValid: false)); + + emit(state.copyWith( + entry: entry, + isValid: false, + isLoading: false, + )); }, ); }); diff --git a/packages/core/lib/src/injectable_module.dart b/packages/core/lib/src/injectable_module.dart index fed5e8c8..c2f235af 100644 --- a/packages/core/lib/src/injectable_module.dart +++ b/packages/core/lib/src/injectable_module.dart @@ -3,6 +3,7 @@ import 'package:isar/isar.dart'; import 'package:models/models.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:shared_preferences/shared_preferences.dart'; @module abstract class InjectableModule { @@ -20,4 +21,13 @@ abstract class InjectableModule { return isar; } + + @preResolve + Future get sharedPref async { + final sharedPref = await SharedPreferences.getInstance(); + + await sharedPref.setString('theme', 'light'); + + return sharedPref; + } } diff --git a/packages/core/lib/src/repositories/implementation/tabs/tabs_repository.dart b/packages/core/lib/src/repositories/implementation/tabs/tabs_repository.dart index 4edae2a3..0d5cd945 100644 --- a/packages/core/lib/src/repositories/implementation/tabs/tabs_repository.dart +++ b/packages/core/lib/src/repositories/implementation/tabs/tabs_repository.dart @@ -150,3 +150,40 @@ class TabsRepository implements ITabsRepository { } } } + + // List updateTabs(int oldIndex, int newIndex) { + // final List tempTabs = tabsData.keys.toList(); + // final int tempLength = tempTabs.length; + // final List> tempListEntry = tabsData.values.toList(); + + // final Tabs movedTab = tempTabs.removeAt(oldIndex); + // final List movedListEntry = tempListEntry.removeAt(oldIndex); + + // if (oldIndex < newIndex && newIndex != tempLength) { + // if (newIndex - oldIndex == 2) { + // tempTabs.insert(newIndex - 1, movedTab); + // tempListEntry.insert(newIndex - 1, movedListEntry); + // } else { + // tempTabs.insert(newIndex - 1, movedTab); + // tempListEntry.insert(newIndex - 1, movedListEntry); + // } + // } else if (oldIndex > newIndex) { + // tempTabs.insert(newIndex, movedTab); + // tempListEntry.insert(newIndex, movedListEntry); + // } else { + // tempTabs.add(movedTab); + // tempListEntry.add(movedListEntry); + // } + + // tabsData.clear(); + // if (tempTabs.length == tempListEntry.length) { + // for (var i = 0; i < tempTabs.length; i++) { + // final tab = tempTabs[i]; + // final listEntry = tempListEntry[i]; + + // tabsData[tab] = listEntry; + // } + // } + + // return tabsData.keys.toList(); + // } \ No newline at end of file diff --git a/packages/core/lib/src/services/implementations/login_service.dart b/packages/core/lib/src/services/implementations/login_service.dart new file mode 100644 index 00000000..d1990916 --- /dev/null +++ b/packages/core/lib/src/services/implementations/login_service.dart @@ -0,0 +1,36 @@ +import 'package:core/src/services/interfaces/i_login_service.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SessionImpl extends Session { + SessionImpl(this.sharedPref); + + SharedPreferences sharedPref; + + @override + void storeLoginInfo(String accessToken) { + sharedPref + ..setBool('login_status', true) + ..setString('access_token', accessToken); + } + + @override + bool isUserLoggedIn() { + final isLoggedIn = sharedPref.getBool('login_status') ?? false; + return isLoggedIn; + } + + @override + String getAccessToken() { + return sharedPref.getString('access_token') ?? ''; + } + + @override + void deleteLoginInfo() { + if (sharedPref.containsKey('login_status')) { + sharedPref.remove('login_status'); + } + if (sharedPref.containsKey('access_token')) { + sharedPref.remove('access_token'); + } + } +} diff --git a/packages/core/lib/src/services/interfaces/i_login_service.dart b/packages/core/lib/src/services/interfaces/i_login_service.dart new file mode 100644 index 00000000..4fd3d3d2 --- /dev/null +++ b/packages/core/lib/src/services/interfaces/i_login_service.dart @@ -0,0 +1,6 @@ +abstract class Session { + void storeLoginInfo(String accessToken); + bool isUserLoggedIn(); + String getAccessToken(); + void deleteLoginInfo(); +} diff --git a/packages/core/pubspec.yaml b/packages/core/pubspec.yaml index 3d6c5ef5..45016750 100644 --- a/packages/core/pubspec.yaml +++ b/packages/core/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: models: ^0.0.1 package_info_plus: ^4.2.0 path_provider: ^2.1.2 + shared_preferences: ^2.2.2 uuid: ^4.3.3 version: ^3.0.2 diff --git a/packages/theme/.gitignore b/packages/theme/.gitignore new file mode 100644 index 00000000..9ada86e1 --- /dev/null +++ b/packages/theme/.gitignore @@ -0,0 +1,26 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# Generated Code +**/*.tailor.dart + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Flutter/Dart/Pub related +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/packages/theme/.metadata b/packages/theme/.metadata new file mode 100644 index 00000000..48326e7c --- /dev/null +++ b/packages/theme/.metadata @@ -0,0 +1,10 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "bae5e49bc2a867403c43b2aae2de8f8c33b037e4" + channel: "stable" + +project_type: package diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md new file mode 100644 index 00000000..03425463 --- /dev/null +++ b/packages/theme/CHANGELOG.md @@ -0,0 +1,7 @@ +# CHANGELOG + +This is the changelog for the `theme` package used in the Multichoice repo + +## 0.0.1 + +* Initial set up of `theme` package diff --git a/packages/theme/README.md b/packages/theme/README.md new file mode 100644 index 00000000..ebcc9f2c --- /dev/null +++ b/packages/theme/README.md @@ -0,0 +1,24 @@ + +# Multichoice Theming + +This package contains all the theming used by the `multichoice` app. + +## Getting started and Installation + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/theme/analysis_options.yaml b/packages/theme/analysis_options.yaml new file mode 100644 index 00000000..b5cd8979 --- /dev/null +++ b/packages/theme/analysis_options.yaml @@ -0,0 +1,5 @@ +include: package:very_good_analysis/analysis_options.yaml + +linter: + rules: + public_member_api_docs: false diff --git a/packages/theme/lib/src/colors/app_colors_extension.dart b/packages/theme/lib/src/colors/app_colors_extension.dart new file mode 100644 index 00000000..366d8a7f --- /dev/null +++ b/packages/theme/lib/src/colors/app_colors_extension.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:theme_tailor_annotation/theme_tailor_annotation.dart'; + +part 'app_colors_extension.tailor.dart'; + +@TailorMixin() +class AppColorsExtension extends ThemeExtension + with _$AppColorsExtensionTailorMixin { + AppColorsExtension({ + required this.primary, + required this.primaryLight, + required this.secondary, + required this.secondaryLight, + required this.ternary, + required this.foreground, + required this.background, + required this.white, + required this.black, + }); + @override + final Color? primary; + @override + final Color? primaryLight; + @override + final Color? secondary; + @override + final Color? secondaryLight; + @override + final Color? ternary; + @override + final Color? foreground; + @override + final Color? background; + @override + final Color? white; + @override + final Color? black; +} diff --git a/packages/theme/lib/src/text/app_text_extension.dart b/packages/theme/lib/src/text/app_text_extension.dart new file mode 100644 index 00000000..d9b24d4e --- /dev/null +++ b/packages/theme/lib/src/text/app_text_extension.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:theme_tailor_annotation/theme_tailor_annotation.dart'; + +part 'app_text_extension.tailor.dart'; + +@TailorMixin() +class AppTextExtension extends ThemeExtension + with _$AppTextExtensionTailorMixin { + AppTextExtension({ + required this.body1, + required this.body2, + required this.h1, + required this.titleLarge, + required this.titleMedium, + required this.titleSmall, + required this.subtitleLarge, + required this.subtitleMedium, + required this.subtitleSmall, + required this.bodyLarge, + required this.bodyMedium, + required this.bodySmall, + }); + + @override + final TextStyle? h1; + @override + final TextStyle? body1; + @override + final TextStyle? body2; + @override + final TextStyle? titleLarge; + @override + final TextStyle? titleMedium; + @override + final TextStyle? titleSmall; + @override + final TextStyle? subtitleLarge; + @override + final TextStyle? subtitleMedium; + @override + final TextStyle? subtitleSmall; + @override + final TextStyle? bodyLarge; + @override + final TextStyle? bodyMedium; + @override + final TextStyle? bodySmall; +} diff --git a/packages/theme/lib/theme.dart b/packages/theme/lib/theme.dart new file mode 100644 index 00000000..0193c670 --- /dev/null +++ b/packages/theme/lib/theme.dart @@ -0,0 +1,5 @@ +/// Multichoice Theme +library theme; + +export 'src/colors/app_colors_extension.dart'; +export 'src/text/app_text_extension.dart'; diff --git a/packages/theme/pubspec.yaml b/packages/theme/pubspec.yaml new file mode 100644 index 00000000..54a96272 --- /dev/null +++ b/packages/theme/pubspec.yaml @@ -0,0 +1,22 @@ +name: theme +description: "The theme package used in the Multichoice repo." +version: 0.0.1 +publish_to: "none" + +environment: + sdk: ">=3.3.0 <4.0.0" + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + theme_tailor_annotation: ^2.1.0 + +dev_dependencies: + build_runner: ^2.4.8 + flutter_test: + sdk: flutter + theme_tailor: ^2.1.0 + very_good_analysis: ^5.1.0 + +flutter: diff --git a/packages/theme/test/theme_test.dart b/packages/theme/test/theme_test.dart new file mode 100644 index 00000000..ab73b3a2 --- /dev/null +++ b/packages/theme/test/theme_test.dart @@ -0,0 +1 @@ +void main() {}