diff --git a/.fvmrc b/.fvmrc new file mode 100644 index 000000000..906bbb349 --- /dev/null +++ b/.fvmrc @@ -0,0 +1,3 @@ +{ + "flutter": "3.24.3" +} \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..ff02ab40b --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +*.freezed.dart linguist-generated=true +*.g.dart linguist-generated=true \ No newline at end of file diff --git a/.github/workflows/aab_deploy.yml b/.github/workflows/aab_deploy.yml index 330605b2b..a621389a3 100644 --- a/.github/workflows/aab_deploy.yml +++ b/.github/workflows/aab_deploy.yml @@ -17,18 +17,21 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Get Flutter version from .fvmrc + run: echo "FLUTTER_FVM_VERSION=$(jq -r .flutter .fvmrc)" >> $GITHUB_ENV + - name: Install flutter uses: subosito/flutter-action@v2 with: - channel: 'stable' + flutter-version: ${{ env.FLUTTER_FVM_VERSION }} cache: true - uses: actions/setup-java@v4 with: distribution: 'zulu' - java-version: '11' + java-version: '17' cache: 'gradle' - + - name: Cache pubspec dependencies uses: actions/cache@v4 @@ -53,14 +56,13 @@ jobs: - name: Run flutter test with coverage run: flutter test --coverage --coverage-path=~/coverage/lcov.info - - name: Create aab file run: | echo -n "${{ secrets.ANDROID_KEYSTORE_BASE64 }}" | base64 -d > ./android/release.keystore export ANDROID_KEYSTORE_PASSWORD="${{ secrets.ANDROID_KEYSTORE_PASSWORD }}" export ANDROID_KEY_ALIAS="${{ secrets.ANDROID_KEY_ALIAS }}" - export ANDROID_KEY_PASSWORD="${{ secrets.ANDROID_KEY_PASSWORD }}" + export ANDROID_KEY_PASSWORD="${{ secrets.ANDROID_KEY_PASSWORD }}" flutter build appbundle --no-tree-shake-icons --release - name: Upload artifact uses: actions/upload-artifact@v4 @@ -69,4 +71,4 @@ jobs: # 保存するファイル path: ./build/app/outputs/bundle/release/app-release.aab # 保存期間(日) - retention-days: 3 \ No newline at end of file + retention-days: 3 diff --git a/.github/workflows/dart_test.yml b/.github/workflows/dart_test.yml index 789293c3e..af221756d 100644 --- a/.github/workflows/dart_test.yml +++ b/.github/workflows/dart_test.yml @@ -23,10 +23,15 @@ jobs: - uses: actions/checkout@v4 - uses: dart-lang/setup-dart@v1 + - run: dart format --output=none --set-exit-if-changed . + + - name: Get Flutter version from .fvmrc + run: echo "FLUTTER_FVM_VERSION=$(jq -r .flutter .fvmrc)" >> $GITHUB_ENV + - name: Install Flutter uses: subosito/flutter-action@v2 with: - channel: 'stable' + flutter-version: ${{ env.FLUTTER_FVM_VERSION }} cache: true - name: Run flutter version @@ -34,6 +39,9 @@ jobs: - name: Run flutter pub get run: flutter pub get + + # ゆくゆくは + # - run: flutter analyze --fatal-infos - name: Run flutter test with coverage run: flutter test --coverage --coverage-path=~/coverage/lcov.info @@ -41,4 +49,4 @@ jobs: - uses: codecov/codecov-action@v4 with: token: ${{secrets.CODECOV_TOKEN}} - file: ~/coverage/lcov.info \ No newline at end of file + file: ~/coverage/lcov.info diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0f540e0a0..5b0a40269 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -17,16 +17,19 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Get Flutter version from .fvmrc + run: echo "FLUTTER_FVM_VERSION=$(jq -r .flutter .fvmrc)" >> $GITHUB_ENV + - name: Install flutter uses: subosito/flutter-action@v2 with: - channel: 'stable' + flutter-version: ${{ env.FLUTTER_FVM_VERSION }} cache: true - uses: actions/setup-java@v4 with: distribution: 'zulu' - java-version: '11' + java-version: '17' cache: 'gradle' @@ -43,6 +46,9 @@ jobs: restore-keys: | build-pubspec- + - name: Create symbolic link + run: ln -s ${{ env.FLUTTER_ROOT }} $HOME/work/flutter + - name: Flutter pub get run: flutter pub get diff --git a/.github/workflows/snap_deploy.yml b/.github/workflows/snap_deploy.yml new file mode 100644 index 000000000..f956e9571 --- /dev/null +++ b/.github/workflows/snap_deploy.yml @@ -0,0 +1,54 @@ + +name: デプロイ(snap) +on: + workflow_dispatch: + release: + types: [published] + +permissions: + contents: write + +jobs: + build: + name: ビルド(Snap) + runs-on: ubuntu-latest + strategy: + matrix: + platform: [amd64, arm64] + outputs: + snap: ${{ steps.snapcraft.outputs.snap }} + steps: + - name: Checkout Code + uses: actions/checkout@v4 + + - name: Setup QEMU + uses: docker/setup-qemu-action@v3 + + - name: Build Snap + id: snapcraft + uses: diddlesnaps/snapcraft-multiarch-action@v1 + with: + architecture: ${{ matrix.platform }} + + - name: Get Build Version + run: | + echo "VERSION=$(yq -r '.version' pubspec.yaml)" >> $GITHUB_ENV + - name: Upload snap + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload v$VERSION ${{ steps.snapcraft.outputs.snap }} + # https://gihyo.jp/admin/serial/01/ubuntu-recipe/0660#sec3 : Snapパッケージアップロードまでの流れ + # https://github.com/snapcore/action-publish : Snap ActionのREADME.md + # Snap Storeでパッケージ名"miria"を予約($ snapcraft register miria)後、"SNAPCRAFT_STORE_CREDENTIALS"を登録し、 + # 以下をコメントアウトを解除することでSnap Storeへアップロードすることが可能です。 + # 通常、SnapファイルをそのままStore外で公開することはありません。 + # + #- name: Upload Snap Store + # uses: snapcore/action-publish@v1 + # env: + # SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.STORE_LOGIN }} + # with: + # snap: ${{ steps.snapcraft.outputs.snap }} + # release: stable + diff --git a/.github/workflows/windows_deploy.yml b/.github/workflows/windows_deploy.yml index 93d1727bd..cf75e9a65 100644 --- a/.github/workflows/windows_deploy.yml +++ b/.github/workflows/windows_deploy.yml @@ -15,10 +15,13 @@ jobs: steps: - uses: actions/checkout@v4 + - name: Get Flutter version from .fvmrc + run: echo "FLUTTER_FVM_VERSION=$(jq -r .flutter .fvmrc)" >> $env:GITHUB_ENV + - name: Install Flutter uses: subosito/flutter-action@v2 with: - channel: 'stable' + flutter-version: ${{ env.FLUTTER_FVM_VERSION }} cache: true - name: Flutter pub get @@ -35,16 +38,23 @@ jobs: run: | flutter build windows --release + - name: Delete unnecessary DLL + run: | + rm build\windows\x64\runner\Release\api-ms-*.dll + rm build\windows\x64\runner\Release\concrt140.dll + rm build\windows\x64\runner\Release\msvcp*.dll + rm build\windows\x64\runner\Release\ucrtbas*.dll + rm build\windows\x64\runner\Release\vc*.dll + - name: Get translation files for Inno Setup run: | - curl -o ${{ env.builddir }}\Korean.isl https://raw.githubusercontent.com/jrsoftware/issrc/main/Files/Languages/Korean.isl curl -o ${{ env.builddir }}\ChineseSimplified.isl https://raw.githubusercontent.com/jrsoftware/issrc/main/Files/Languages/Unofficial/ChineseSimplified.isl - name: Compile .ISS to .EXE Installer - uses: Minionguyjpro/Inno-Setup-Action@v1.2.4 + uses: Minionguyjpro/Inno-Setup-Action@v1.2.5 with: path: windows/innosetup.iss - options: /dMyAppVersion="${{ env.version }}" /dMyWorkDir="${{ env.builddir }}" + options: /dMyAppVersion="${{ env.version }}" /dMyWorkDir="${{ env.builddir }}" /Qp - name: Rename .EXE Installer run: mv miria-installer.exe miria-installer_${env:version}_x64.exe diff --git a/.gitignore b/.gitignore index 4ba6b0062..72263c827 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,9 @@ app.*.map.json **/build/ # Release -/private_keys/ \ No newline at end of file +/private_keys/ + +# Snap package related +*.snap +# FVM Version Cache +.fvm/ diff --git a/.vscode/launch.json b/.vscode/launch.json index 80d2e2790..a74d2be6a 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -21,6 +21,13 @@ "request": "launch", "type": "dart", "flutterMode": "release" + }, + { + "name": "debug current file", + "request": "launch", + "type": "dart", + "flutterMode": "debug", + "program": "${file}" } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..965328daf --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "dart.flutterSdkPath": ".fvm/versions/3.24.3" +} \ No newline at end of file diff --git a/README-ja.md b/README-ja.md index 10b268cd8..3fc5e32a2 100644 --- a/README-ja.md +++ b/README-ja.md @@ -31,7 +31,6 @@ MiriaはiOS, Android向けMisskeyクライアントです。Windowsでも動作 - 「ページ」機能(閲覧のみ) - サーバー情報の表示(オンラインユーザー数、ジョブキュー、広告、カスタム絵文字等) - ### 機能に対する制限 - Miriaはサーバー独自の機能に基本的には対応していません。 @@ -44,14 +43,15 @@ MiriaはiOS, Android向けMisskeyクライアントです。Windowsでも動作 - MFMの見た目がブラウザと異なる場合があります。 - カスタムCSSは実装できません。 -# コントリビュート - +## コントリビュート +### ローカライズ -# ライセンス +[Issue #164](https://github.com/shiosyakeyakini-info/miria/issues/164)を確認してください。 -## アイコンについて +## ライセンス +### アイコンについて ![Miriaのアイコン](/assets/images/icon.png) diff --git a/README.md b/README.md index 92e3691e5..b810356e0 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -EN | [日本語](https://github.com/shiosyakeyakini-info/miria/blob/develop/README-ja.md) +EN | [日本語](https://github.com/shiosyakeyakini-info/miria/blob/develop/README-ja.md) # Miria @@ -33,19 +33,25 @@ I'm planning to deploy from F-Droid. - Miria does not support forked server's unique features. - Miria supports only over Misskey v13 and forked servers. - - [Sharkey](https://joinsharkey.org/), [CherryPick](https://github.com/kokonect-link/cherrypick) and [mkkey.net](https://mkkey.net/) may be available but did not test. - - [Firefish](https://joinfirefish.org/ja/), [Catodon](https://catodon.social/), Mastodon will not support in the future too. -- Miria does not support AiScript related features. (Plugin, Play) -- Miria does not depend browser features. ¥ - - There are cases in which Miria's MFM appearance is different from browsers. + - [Sharkey](https://joinsharkey.org/), [CherryPick](https://github.com/kokonect-link/cherrypick) and [mkkey.net](https://mkkey.net/) may be available but did not test. + - [Firefish](https://joinfirefish.org/ja/), [Catodon](https://catodon.social/), Mastodon will not support in the future too. +- Miria does not support AiScript related features. (Plugin, Play) +- Miria does not depend browser features. ¥ + - There are cases in which Miria's MFM appearance is different from browsers. - Custom CSS didn't support. -# License +## Contribute -## About Miria Icon +### Localization + +See [Issue #164](https://github.com/shiosyakeyakini-info/miria/issues/164). + +## License + +### About Miria Icon ![Miria Icon](/assets/images/icon.png) Miria icons is avaiable `/assets/images/icon.png` -Miria icons is PD(Public Domain). you can use such as registering custom-emojis to your server. \ No newline at end of file +Miria icons is PD(Public Domain). you can use such as registering custom-emojis to your server. diff --git a/analysis_options.yaml b/analysis_options.yaml index 0b66c045b..88c06335a 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -2,8 +2,60 @@ include: package:flutter_lints/flutter.yaml linter: + rules: + - always_use_package_imports + - avoid_dynamic_calls + - cancel_subscriptions + - close_sinks + - discarded_futures + - literal_only_boolean_expressions + - no_self_assignments + - prefer_void_to_null + - unnecessary_statements + - always_declare_return_types + - always_put_required_named_parameters_first + - avoid_bool_literals_in_conditional_expressions + - avoid_classes_with_only_static_members + - avoid_private_typedef_functions + # - avoid_redundant_argument_values + - avoid_returning_this + - avoid_setters_without_getters + - avoid_types_on_closure_parameters + - avoid_unused_constructor_parameters + - avoid_void_async + - cascade_invocations + - cast_nullable_to_non_nullable + - directives_ordering + - eol_at_end_of_file + - join_return_with_assignment + - matching_super_parameters + - no_literal_bool_comparisons + - omit_local_variable_types + - one_member_abstracts + - parameter_assignments + - prefer_asserts_in_initializer_lists + - prefer_constructors_over_static_methods + - prefer_double_quotes + - prefer_final_in_for_each + - prefer_final_locals + - avoid_final_parameters + - prefer_if_elements_to_conditional_expressions + - prefer_null_aware_method_calls + - require_trailing_commas + - unawaited_futures + - unnecessary_breaks + - unnecessary_null_aware_operator_on_extension_on_nullable + - unnecessary_null_checks + - unnecessary_parenthesis + - unnecessary_raw_strings analyzer: exclude: - /**/*.freezed.dart - - /**/*.g.dart \ No newline at end of file + - /**/*.g.dart + plugins: + - custom_lint + language: + # strict-casts: true + # strict-inference: true + # strict-raw-types: true \ No newline at end of file diff --git a/android/app/build.gradle b/android/app/build.gradle index 15db463bb..bc677798c 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,3 +1,9 @@ +plugins { + id "com.android.application" + id "kotlin-android" + id "dev.flutter.flutter-gradle-plugin" +} + def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { @@ -6,11 +12,6 @@ if (localPropertiesFile.exists()) { } } -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' @@ -21,10 +22,6 @@ if (flutterVersionName == null) { flutterVersionName = '1.0' } -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - android { compileSdkVersion flutter.compileSdkVersion ndkVersion flutter.ndkVersion @@ -74,6 +71,4 @@ flutter { source '../..' } -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" -} +dependencies {} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7abda603d..08b7dbabf 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -14,7 +14,7 @@ + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4b7..42626eab6 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/android/app/src/main/res/mipmap-hdpi/launcher_icon.png deleted file mode 100644 index 42626eab6..000000000 Binary files a/android/app/src/main/res/mipmap-hdpi/launcher_icon.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b79b..384262c21 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/android/app/src/main/res/mipmap-mdpi/launcher_icon.png deleted file mode 100644 index 384262c21..000000000 Binary files a/android/app/src/main/res/mipmap-mdpi/launcher_icon.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d439148..05d83da6d 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png deleted file mode 100644 index 05d83da6d..000000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d34..85b456dca 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png deleted file mode 100644 index 85b456dca..000000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372eeb..c0b9250be 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png deleted file mode 100644 index c0b9250be..000000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png and /dev/null differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml index ab9832824..fd6dd1abb 100644 --- a/android/app/src/main/res/values/colors.xml +++ b/android/app/src/main/res/values/colors.xml @@ -1,4 +1,4 @@ - #ffffff + #ace601 \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 0a40a9865..bc157bd1a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,16 +1,3 @@ -buildscript { - ext.kotlin_version = '1.8.21' - repositories { - google() - mavenCentral() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.1.2' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - allprojects { repositories { google() diff --git a/android/settings.gradle b/android/settings.gradle index 44e62bcf0..6f2cdec82 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -1,11 +1,25 @@ -include ':app' +pluginManagement { + def flutterSdkPath = { + def properties = new Properties() + file("local.properties").withInputStream { properties.load(it) } + def flutterSdkPath = properties.getProperty("flutter.sdk") + assert flutterSdkPath != null, "flutter.sdk not set in local.properties" + return flutterSdkPath + }() -def localPropertiesFile = new File(rootProject.projectDir, "local.properties") -def properties = new Properties() + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") -assert localPropertiesFile.exists() -localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} -def flutterSdkPath = properties.getProperty("flutter.sdk") -assert flutterSdkPath != null, "flutter.sdk not set in local.properties" -apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" +plugins { + id "dev.flutter.flutter-plugin-loader" version "1.0.0" + id "com.android.application" version "7.1.2" apply false + id "org.jetbrains.kotlin.android" version "1.8.21" apply false +} + +include ":app" diff --git a/assets/emoji_list.json b/assets/emoji_list.json index 1dcad6e59..36c1c8c94 100644 --- a/assets/emoji_list.json +++ b/assets/emoji_list.json @@ -1 +1 @@ -[{"category":"face","char":"😀","name":"grinning","keywords":["face","smile","happy","joy",": D","grin","スマイル","にっこり","にっこりわらう","わらう","えがお","かお"]},{"category":"face","char":"😬","name":"grimacing","keywords":["face","grimace","teeth","いー","しかめっつら","しかめめん","しかめがお","かお"]},{"category":"face","char":"😁","name":"grin","keywords":["face","happy","smile","joy","kawaii","スマイル","にやっとわらう","わらう","えがお","かお"]},{"category":"face","char":"😂","name":"joy","keywords":["face","cry","tears","weep","happy","happytears","haha","スマイル","うれしなき","なみだ","わらう","えがお","かお"]},{"category":"face","char":"🤣","name":"rofl","keywords":["face","rolling","floor","laughing","lol","haha","スマイル","わらいころげる","わらう","えがお","かお"]},{"category":"face","char":"🥳","name":"partying","keywords":["face","celebration","woohoo","おいわい","パーティー","パーティーのかお","ぼうし","ふえ"]},{"category":"face","char":"😃","name":"smiley","keywords":["face","happy","joy","haha",": D",": )","smile","funny","スマイル","わーい","えがお","かお"]},{"category":"face","char":"😄","name":"smile","keywords":["face","happy","joy","funny","haha","laugh","like",": D",": )","スマイル","わーい","えがお","かお"]},{"category":"face","char":"😅","name":"sweat_smile","keywords":["face","hot","happy","laugh","sweat","smile","relief","スマイル","ひやあせ","ひやあせえがお","えがお","かお"]},{"category":"face","char":"🥲","name":"smiling_face_with_tear","keywords":["face","じーん","うれしなみだのかお","かんどう","なく","なみだ","かお"]},{"category":"face","char":"😆","name":"laughing","keywords":["happy","joy","lol","satisfied","haha","face","glad","XD","laugh","きゃー","スマイル","うれしい","まんぞく","えがお","かお"]},{"category":"face","char":"😇","name":"innocent","keywords":["face","angel","heaven","halo","スマイル","てんしのわ","てんしのわがついたえがお","えがお","かお"]},{"category":"face","char":"😉","name":"wink","keywords":["face","happy","mischievous","secret",";)","smile","eye","ウィンク","ウインク","かお"]},{"category":"face","char":"😊","name":"blush","keywords":["face","smile","happy","flushed","crush","embarrassed","shy","joy","スマイル","にこにこ","ほほえみ","めをほそめる","ほおをあからめる","かお"]},{"category":"face","char":"🙂","name":"slightly_smiling_face","keywords":["face","smile","スマイル","ほほえみ","ほほえむ","えがお","かお"]},{"category":"face","char":"🙃","name":"upside_down_face","keywords":["face","flipped","silly","smile","さかさま","さかさまのかお","かお"]},{"category":"face","char":"☺️","name":"relaxed","keywords":["face","blush","massage","happiness","スマイリー","スマイル","ほっ","えがお","かお"]},{"category":"face","char":"😋","name":"yum","keywords":["happy","joy","tongue","smile","face","silly","yummy","nom","delicious","savouring","うまい","おいしい","スマイル","にこにこぺろり","えがお","かお"]},{"category":"face","char":"😌","name":"relieved","keywords":["face","relaxed","phew","massage","happiness","ほっ","ほっとしたかお","あんしん","かお"]},{"category":"face","char":"😍","name":"heart_eyes","keywords":["face","love","like","affection","valentines","infatuation","crush","heart","スマイル","ハート","ラブ","めがハートのえがお","えがお","かお"]},{"category":"face","char":"🥰","name":"smiling_face_with_three_hearts","keywords":["face","love","like","affection","valentines","infatuation","crush","hearts","adore","ハートのえがお","メロメロ","ラブ","むちゅう","えがお","かお"]},{"category":"face","char":"😘","name":"kissing_heart","keywords":["face","love","like","affection","valentines","infatuation","kiss","キス","ちゅっ","ハート","なげキッス","かお"]},{"category":"face","char":"😗","name":"kissing","keywords":["love","like","face","3","valentines","infatuation","kiss","キス","ちゅっ","かお"]},{"category":"face","char":"😙","name":"kissing_smiling_eyes","keywords":["face","affection","valentines","infatuation","kiss","キス","スマイル","ちゅっ","にっこりキス","えがお","かお"]},{"category":"face","char":"😚","name":"kissing_closed_eyes","keywords":["face","love","like","affection","valentines","infatuation","kiss","キス","ちゅっ","めをとじる","かお"]},{"category":"face","char":"😜","name":"stuck_out_tongue_winking_eye","keywords":["face","prank","childish","playful","mischievous","smile","wink","tongue","あっかんべー","ジョーク","べー","じょうだん","した","かお"]},{"category":"face","char":"🤪","name":"zany","keywords":["face","goofy","crazy","おかしい","ジョーク","ふざけ","ふざけたかお","かお"]},{"category":"face","char":"🤨","name":"raised_eyebrow","keywords":["face","distrust","scepticism","disapproval","disbelief","surprise","うたがい","まゆをあげたかお","かお","おどろき"]},{"category":"face","char":"🧐","name":"monocle","keywords":["face","stuffy","wealthy","メガネ","モノクルをつけたかお","かためがね","かんがえる","かお"]},{"category":"face","char":"😝","name":"stuck_out_tongue_closed_eyes","keywords":["face","prank","playful","mischievous","smile","tongue","べー","べろ","わーい","めをとじてべー","した","かお"]},{"category":"face","char":"😛","name":"stuck_out_tongue","keywords":["face","prank","childish","playful","mischievous","smile","tongue","べー","べろ","した","したをだしたかお","かお"]},{"category":"face","char":"🤑","name":"money_mouth_face","keywords":["face","rich","dollar","money","おかねのかお","べー","べろ","した","かお"]},{"category":"face","char":"🤓","name":"nerd_face","keywords":["face","nerdy","geek","dork","オタク","マニア","メガネ","かお"]},{"category":"face","char":"🥸","name":"disguised_face","keywords":["face","nose","glasses","incognito","ひげ","メガネ","かそう","へんそう","へんそうしたかお","かお"]},{"category":"face","char":"😎","name":"sunglasses","keywords":["face","cool","smile","summer","beach","sunglass","クール","サングラスでえがお","スマイル","えがお","かお"]},{"category":"face","char":"🤩","name":"star_struck","keywords":["face","smile","starry","eyes","grinning","スマイル","ほし","めがほしのえがお","えがお","かお"]},{"category":"face","char":"🤡","name":"clown_face","keywords":["face","ピエロ","ピエロのかお","かお"]},{"category":"face","char":"🤠","name":"cowboy_hat_face","keywords":["face","cowgirl","hat","カウガール","カウボーイ","カウボーイのかお","かお"]},{"category":"face","char":"🤗","name":"hugs","keywords":["face","smile","hug","スマイル","ハグ","えがお","かお"]},{"category":"face","char":"😏","name":"smirk","keywords":["face","smile","mean","prank","smug","sarcasm","にやり","ふっ","うすわらいをするかお","かお"]},{"category":"face","char":"😶","name":"no_mouth","keywords":["face","hellokitty","だんまり","くちなし","くちのないかお","ちんもく","かお"]},{"category":"face","char":"😐","name":"neutral_face","keywords":["indifference","meh",": |","neutral","ポーカーフェイス","むひょうじょう","かお"]},{"category":"face","char":"😑","name":"expressionless","keywords":["face","indifferent","-_-","meh","deadpan","むかんじょう","むひょうじょう","かお"]},{"category":"face","char":"😒","name":"unamused","keywords":["indifference","bored","straight face","serious","sarcasm","unimpressed","skeptical","dubious","side_eye","ジトめ","しらけ","しらけた","しらける","かお"]},{"category":"face","char":"🙄","name":"roll_eyes","keywords":["face","eyeroll","frustrated","うえをみるかお","うわめ","かお"]},{"category":"face","char":"🤔","name":"thinking","keywords":["face","hmmm","think","consider","うーん","かんがえる","かんがえるかお","かんがえちゅう","かお"]},{"category":"face","char":"🤥","name":"lying_face","keywords":["face","lie","pinocchio","うそつきのかお","ピノキオのかお","うそつき","かお"]},{"category":"face","char":"🤭","name":"hand_over_mouth","keywords":["face","whoops","shock","surprise","クスクス","スマイル","ないしょ","くちにてをあてたかお","えがお","かお"]},{"category":"face","char":"🤫","name":"shushing","keywords":["face","quiet","shhh","しー","しーっ","しっ","しずかに","かお"]},{"category":"face","char":"🤬","name":"symbols_over_mouth","keywords":["face","swearing","cursing","cussing","profanity","expletive","ののしり","ののしる","いかり","かお"]},{"category":"face","char":"🤯","name":"exploding_head","keywords":["face","shocked","mind","blown","ショック","ぼんっ","ばくはつ","あたまばくはつ","かお","おどろき"]},{"category":"face","char":"😳","name":"flushed","keywords":["face","blush","shy","flattered","あかくなる","せきめん","かお"]},{"category":"face","char":"😞","name":"disappointed","keywords":["face","sad","upset","depressed",": (","がっかり","がっくり","しつぼうしたかお","かお"]},{"category":"face","char":"😟","name":"worried","keywords":["face","concern","nervous",": (","しんぱい","なやむかお","かお"]},{"category":"face","char":"😠","name":"angry","keywords":["mad","face","annoyed","frustrated","ぷんぷん","いかり","かお"]},{"category":"face","char":"😡","name":"rage","keywords":["angry","mad","hate","despise","かんかん","ふくれっつら","いかり","げきど","かお"]},{"category":"face","char":"😔","name":"pensive","keywords":["face","sad","depressed","upset","しょぼーん","しょんぼり","かお"]},{"category":"face","char":"😕","name":"confused","keywords":["face","indifference","huh","weird","hmmm",": /","こんわく","こんらん","かお"]},{"category":"face","char":"🙁","name":"slightly_frowning_face","keywords":["face","frowning","disappointed","sad","upset","しかめめん","こまった","すこしこまった","すこしこまったかお","かお"]},{"category":"face","char":"☹","name":"frowning_face","keywords":["face","sad","upset","frown","しかめめん","こまった","こまったかお","かお"]},{"category":"face","char":"😣","name":"persevere","keywords":["face","sick","no","upset","oops","がまん","かお"]},{"category":"face","char":"😖","name":"confounded","keywords":["face","confused","sick","unwell","oops",": S","こんわく","こんわくしたかお","こんらん","かお"]},{"category":"face","char":"😫","name":"tired_face","keywords":["sick","whine","upset","frustrated","つかれ","つかれた","げんかい","かお"]},{"category":"face","char":"😩","name":"weary","keywords":["face","tired","sleepy","sad","frustrated","upset","あきらめ","つかれ","かお"]},{"category":"face","char":"🥺","name":"pleading","keywords":["face","begging","mercy","こいぬのめ","なさけ","こんがん","うったえるようなかお","かお"]},{"category":"face","char":"😤","name":"triumph","keywords":["face","gas","phew","proud","pride","ふんっ","かちほこったかお","とくいげ","かお","はないき"]},{"category":"face","char":"😮","name":"open_mouth","keywords":["face","surprise","impressed","wow","whoa",": O","へー","きょうかん","くちがひらいたかお","かお"]},{"category":"face","char":"😱","name":"scream","keywords":["face","munch","scared","omg","がーん","ショック","さけび","きょうふ","かお"]},{"category":"face","char":"😨","name":"fearful","keywords":["face","scared","terrified","nervous","oops","huh","がーん","あおざめ","かお"]},{"category":"face","char":"😰","name":"cold_sweat","keywords":["face","nervous","sweat","ひやあせ","ひやあせあおざめ","あおざめ","かお"]},{"category":"face","char":"😯","name":"hushed","keywords":["face","woo","shh","びっくり","びっくりがお","ぽかーん","かお","おどろき"]},{"category":"face","char":"😦","name":"frowning","keywords":["face","aw","what","あきれがお","しかめめん","しかめがお","ふほんい","ふまん","かお"]},{"category":"face","char":"😧","name":"anguished","keywords":["face","stunned","nervous","くのう","くもん","かお"]},{"category":"face","char":"😢","name":"cry","keywords":["face","tears","sad","depressed","upset",": '(","かなしい","なきがお","なく","なみだ","かお"]},{"category":"face","char":"😥","name":"disappointed_relieved","keywords":["face","phew","sweat","nervous","どうしよう","こまった","あせ","かお"]},{"category":"face","char":"🤤","name":"drooling_face","keywords":["face","よだれ","よだれをたらしたかお","かお"]},{"category":"face","char":"😪","name":"sleepy","keywords":["face","tired","rest","nap","ねる","ねむい","ねむる","かお"]},{"category":"face","char":"😓","name":"sweat","keywords":["face","hot","sad","tired","exercise","ひやあせ","きもをひやした","かお"]},{"category":"face","char":"🥵","name":"hot","keywords":["face","feverish","heat","red","sweating","あついかお","あつさ","あせ","ねっちゅうしょう","はつねつ","あかいかお"]},{"category":"face","char":"🥶","name":"cold","keywords":["face","blue","freezing","frozen","frostbite","icicles","しもやけ","つらら","こごえ","さむいかお","さむさ","あおいかお"]},{"category":"face","char":"😭","name":"sob","keywords":["face","cry","tears","sad","upset","depressed","ごうきゅう","だいなき","かなしい","なく","なみだ","かお"]},{"category":"face","char":"😵","name":"dizzy_face","keywords":["spent","unconscious","xox","dizzy","ふらふら","めまい","かお"]},{"category":"face","char":"😲","name":"astonished","keywords":["face","xox","surprised","poisoned","びっくり","びっくりしたかお","かお","おどろき","きょうがく"]},{"category":"face","char":"🤐","name":"zipper_mouth_face","keywords":["face","sealed","zipper","secret","チャック","くち","くちチャック","かお"]},{"category":"face","char":"🤢","name":"nauseated_face","keywords":["face","vomit","gross","green","sick","throw up","ill","ぐあいのわるいかお","はきけをもよおしているかお","きもちわるい","びょうき","かお"]},{"category":"face","char":"🤧","name":"sneezing_face","keywords":["face","gesundheit","sneeze","sick","allergy","くしゃみ","くしゃみするかお","かふんしょう","かお","かぜ"]},{"category":"face","char":"🤮","name":"vomiting","keywords":["face","sick","げろ","おうとするかお","きもちわるい","びょうき","かお"]},{"category":"face","char":"😷","name":"mask","keywords":["face","sick","ill","disease","マスクがお","びょうき","かふんしょう","かお","かぜ"]},{"category":"face","char":"🤒","name":"face_with_thermometer","keywords":["sick","temperature","thermometer","cold","fever","たいおんけい","ねつがあるかお","ねつをはかる","びょうき","かお","かぜ"]},{"category":"face","char":"🤕","name":"face_with_head_bandage","keywords":["injured","clumsy","bandage","hurt","ケガ","ケガしてるかお","ほうたい","ほうたいをまいたかお","かお"]},{"category":"face","char":"🥴","name":"woozy","keywords":["face","dizzy","intoxicated","tipsy","wavy","うつろなめ","ふらふらのかお","べろんべろん","ほろよい","めまい","ちゅうどく"]},{"category":"face","char":"🥱","name":"yawning","keywords":["face","tired","yawning","あくびしたかお","ふあ~","つかれ","ねむい","たいくつ","かお"]},{"category":"face","char":"😴","name":"sleeping","keywords":["face","tired","sleepy","night","zzz","zzz","ねる","ねむい","ねむる","かお"]},{"category":"face","char":"💤","name":"zzz","keywords":["sleepy","tired","dream","zzz","いびき","グーグー","ねる"]},{"category":"face","char":"😶‍🌫️","name":"face_in_clouds","keywords":["うわのそら","ぼんやり","むちゅう","ほうしんじょうたい","くものなかのかお","かお"]},{"category":"face","char":"😮‍💨","name":"face_exhaling","keywords":["あきらめ","ためいき","ほっとした","あんしん","いきをはくかお","かお"]},{"category":"face","char":"😵‍💫","name":"face_with_spiral_eyes","keywords":["ふらふら","めまい","うずまき","めがぐるぐる","めがまわる","めをまわしたかお"]},{"category":"face","char":"🫠","name":"melting_face","keywords":["disappear","dissolve","liquid","melt","toketa","とろける","えきたい","とけそう","とけているかお","とける","かお"]},{"category":"face","char":"🫢","name":"face_with_open_eyes_and_hand_over_mouth","keywords":["amazement","awe","disbelief","embarrass","scared","surprise","ohoho","おそれ","おののき","びっくり","めをあけてくちにてをあてたかお","かお","おどろき"]},{"category":"face","char":"🫣","name":"face_with_peeking_eye","keywords":["captivated","peep","stare","chunibyo","こわごわ","のぞきみ","ゆびのまからのぞきみるかお","ぬすみみ","かお"]},{"category":"face","char":"🫡","name":"saluting_face","keywords":["ok","salute","sunny","troops","yes","raja","オーケー","りょうかい","けいれい","けいれいするかお","ぐんたい","かお"]},{"category":"face","char":"🫥","name":"dotted_line_face","keywords":["depressed","disappear","hide","introvert","invisible","tensen","ないこうてき","うちき","ゆううつ","てんせんのかお","らくたん","かお"]},{"category":"face","char":"🫤","name":"face_with_diagonal_mouth","keywords":["disappointed","meh","skeptical","unsure","くちがななめのかお","こんわく","むきりょく","むかんしん","かお"]},{"category":"face","char":"🥹","name":"face_holding_back_tears","keywords":["angry","cry","proud","resist","sad","うるうる","こんがん","なきがお","なみだ","なみだをこらえたかお","かお"]},{"category":"face","char":"💩","name":"poop","keywords":["hankey","shitface","fail","turd","shit","うんこ","うんち","かお"]},{"category":"face","char":"😈","name":"smiling_imp","keywords":["devil","horns","スマイル","デビル","あくま","わらったあくま","えがお","かお"]},{"category":"face","char":"👿","name":"imp","keywords":["devil","angry","horns","かんかん","デビル","おこったあくま","あくま","げきど","かお"]},{"category":"face","char":"👹","name":"japanese_ogre","keywords":["monster","red","mask","halloween","scary","creepy","devil","demon","japanese","ogre","おめん","なまはげ","ばけもの","おに"]},{"category":"face","char":"👺","name":"japanese_goblin","keywords":["red","evil","mask","monster","scary","creepy","japanese","goblin","おめん","てんぐ"]},{"category":"face","char":"💀","name":"skull","keywords":["dead","skeleton","creepy","death","スカル","ドクロ","し","かお","がいこつ"]},{"category":"face","char":"👻","name":"ghost","keywords":["halloween","spooky","scary","おばけ","ゴースト","ユーレイ","ゆうれい"]},{"category":"face","char":"👽","name":"alien","keywords":["UFO","paul","weird","outer_space","UFO","エイリアン","ユーフォー","うちゅうじん","いほしじん","かお"]},{"category":"face","char":"🤖","name":"robot","keywords":["computer","machine","bot","ロボット","かお"]},{"category":"face","char":"😺","name":"smiley_cat","keywords":["animal","cats","happy","smile","スマイル","にっこりわらうねこ","ねこ","えがお","かお"]},{"category":"face","char":"😸","name":"smile_cat","keywords":["animal","cats","smile","スマイル","にやっとわらうねこ","ねこ","わらう","えがお","かお"]},{"category":"face","char":"😹","name":"joy_cat","keywords":["animal","cats","haha","happy","tears","スマイル","うれしなきするねこ","なみだ","ねこ","えがお","かお"]},{"category":"face","char":"😻","name":"heart_eyes_cat","keywords":["animal","love","like","affection","cats","valentines","heart","スマイル","ハート","ねこ","めがハートのねこ","えがお","かお"]},{"category":"face","char":"😼","name":"smirk_cat","keywords":["animal","cats","smirk","にやり","にやりとするねこ","ふっ","ねこ","かお"]},{"category":"face","char":"😽","name":"kissing_cat","keywords":["animal","cats","kiss","キス","ちゅっ","ねこ","ねこのキス","かお"]},{"category":"face","char":"🙀","name":"scream_cat","keywords":["animal","cats","munch","scared","scream","がーん","ショック","びっくり","ねこ","ぜつぼうするねこ","かお"]},{"category":"face","char":"😿","name":"crying_cat_face","keywords":["animal","tears","weep","sad","cats","upset","cry","かなしい","ないているねこ","なみだ","ねこ","かお"]},{"category":"face","char":"😾","name":"pouting_cat","keywords":["animal","cats","ぷんぷん","ふきげん","ふきげんなねこ","ねこ","かお"]},{"category":"people","char":"🤲","name":"palms_up","keywords":["hands","gesture","cupped","prayer","りょうて","て","てのひらをそろえたりょうて","いのり"]},{"category":"people","char":"🙌","name":"raised_hands","keywords":["gesture","hooray","yea","celebration","hands","バンザイ","りょうて","て","てをあげる"]},{"category":"people","char":"👏","name":"clap","keywords":["hands","praise","applause","congrats","yay","パチパチ","て","はくしゅ"]},{"category":"people","char":"👋","name":"wave","keywords":["hands","gesture","goodbye","solong","farewell","hello","hi","palm","バイバイ","て","てをふる"]},{"category":"people","char":"🤙","name":"call_me_hand","keywords":["hands","gesture","て","でんわ","でんわのジェスチャー","でんわのあいず"]},{"category":"people","char":"👍","name":"+1","keywords":["thumbsup","yes","awesome","good","agree","accept","cool","hand","like","OK","オーケー","オッケー","グッド","サムズアップ","うえ","て","おやゆび"]},{"category":"people","char":"👎","name":"-1","keywords":["thumbsdown","no","dislike","hand","NG","サムズダウン","だめ","ブーイング","ボツ","した","て"]},{"category":"people","char":"👊","name":"facepunch","keywords":["angry","violence","fist","hit","attack","hand","グー","げんこつ","パンチ","て"]},{"category":"people","char":"✊","name":"fist","keywords":["fingers","hand","grasp","グー","げんこつ","て"]},{"category":"people","char":"🤛","name":"fist_left","keywords":["hand","fistbump","こぶし","パンチ","ひだり","ひだりむきのこぶし","て"]},{"category":"people","char":"🤜","name":"fist_right","keywords":["hand","fistbump","こぶし","パンチ","みぎ","みぎむきのこぶし","て"]},{"category":"people","char":"✌","name":"v","keywords":["fingers","ohyeah","hand","peace","victory","two","Vサイン","チョキ","ピース","て"]},{"category":"people","char":"👌","name":"ok_hand","keywords":["fingers","limbs","perfect","ok","okay","OK","OKのて","オーケー","オッケー","て"]},{"category":"people","char":"✋","name":"raised_hand","keywords":["fingers","stop","highfive","palm","ban","パー","て","きょしゅ"]},{"category":"people","char":"🤚","name":"raised_back_of_hand","keywords":["fingers","raised","backhand","て","てのこう"]},{"category":"people","char":"👐","name":"open_hands","keywords":["fingers","butterfly","hands","open","おっはー","りょうてのひら","て","てのひら"]},{"category":"people","char":"💪","name":"muscle","keywords":["arm","flex","hand","summer","strong","biceps","ムキムキ","ちからこぶ","すじトレ","きんにく"]},{"category":"people","char":"🦾","name":"mechanical_arm","keywords":["flex","hand","strong","biceps","アクセシビリティ","じんこうそうぐ","て","ぎしゅ","うで"]},{"category":"people","char":"🙏","name":"pray","keywords":["please","hope","wish","namaste","highfive","おねがい","ごめんなさい","がっしょう","いのり"]},{"category":"people","char":"🦶","name":"foot","keywords":["kick","stomp","キック","ダンス","あし","ふみつける"]},{"category":"people","char":"🦵","name":"leg","keywords":["kick","limb","キック","あし","あし"]},{"category":"people","char":"🦿","name":"mechanical_leg","keywords":["kick","limb","アクセシビリティ","じんこうそうぐ","ぎそく","あし","あし"]},{"category":"people","char":"🤝","name":"handshake","keywords":["agreement","shake","よろしく","ごうい","て","あくしゅ"]},{"category":"people","char":"☝","name":"point_up","keywords":["hand","fingers","direction","up","うえ","うえゆびさし","て","ゆびさし"]},{"category":"people","char":"👆","name":"point_up_2","keywords":["fingers","hand","direction","up","うえ","て","てのこうじょうゆびさし","ゆびさし"]},{"category":"people","char":"👇","name":"point_down","keywords":["fingers","hand","direction","down","した","しもゆびさし","て","ゆびさし"]},{"category":"people","char":"👈","name":"point_left","keywords":["direction","fingers","hand","left","ひだり","ひだりゆびさし","て","ゆびさし"]},{"category":"people","char":"👉","name":"point_right","keywords":["fingers","hand","direction","right","みぎ","みぎゆびさし","て","ゆびさし"]},{"category":"people","char":"🖕","name":"fu","keywords":["hand","fingers","rude","middle","flipping","うえ","なかゆび","て","ゆびさし","たてたなかゆび"]},{"category":"people","char":"🖐","name":"raised_hand_with_fingers_splayed","keywords":["hand","fingers","palm","パー","て","ひらいたて"]},{"category":"people","char":"🤟","name":"love_you","keywords":["hand","fingers","gesture","アイラブユー","あいしてる","て"]},{"category":"people","char":"🤘","name":"metal","keywords":["hand","fingers","evil_eye","sign_of_horns","rock_on","キツネ","て","かくのゆびサイン"]},{"category":"people","char":"🤞","name":"crossed_fingers","keywords":["good","lucky","て","ゆび","ゆびをクロス"]},{"category":"people","char":"🖖","name":"vulcan_salute","keywords":["hand","fingers","spock","star trek","スポック","バルカンのあいさつ","バルカンじん","て"]},{"category":"people","char":"✍","name":"writing_hand","keywords":["lower_left_ballpoint_pen","stationery","write","compose","て","てでかく","てがき","かいているて"]},{"category":"people","char":"🫰","name":"hand_with_index_finger_and_thumb_crossed","keywords":["て","ゆび","ゆびをクロス","おやゆびとひとさしゆびをクロス"]},{"category":"people","char":"🫱","name":"rightwards_hand","keywords":["みぎにむけたて","みぎがわ","て","ほうこう"]},{"category":"people","char":"🫲","name":"leftwards_hand","keywords":["ひだりにむけたて","ひだりがわ","て","ほうこう"]},{"category":"people","char":"🫳","name":"palm_down_hand","keywords":["した","したにむけたて","て","おとす"]},{"category":"people","char":"🫴","name":"palm_up_hand","keywords":["うえ","うえにむけたて","うける","て"]},{"category":"people","char":"🫵","name":"index_pointing_at_the_viewer","keywords":["おまえだ","ひとをゆびさしているて","て","ゆびさし"]},{"category":"people","char":"🫶","name":"heart_hands","keywords":["moemoekyun","ハートポーズ","ハートがたのて","ラブ","あい","て"]},{"category":"people","char":"🤏","name":"pinching_hand","keywords":["hand","fingers","ちょっと","つまんでいるゆび","すこしだけ","て","ゆび"]},{"category":"people","char":"🤌","name":"pinched_fingers","keywords":["hand","fingers","ジェスチャー","すぼめる","うわむきにすぼめたて","て","ゆび"]},{"category":"people","char":"🤳","name":"selfie","keywords":["camera","phone","スマホ","セルフィー","じぶんとり","じとり"]},{"category":"people","char":"💅","name":"nail_care","keywords":["beauty","manicure","finger","fashion","nail","ネイル","マニキュア","マニキュアをぬるて","つめ"]},{"category":"people","char":"👄","name":"lips","keywords":["mouth","kiss","キス","くち","くちびる"]},{"category":"people","char":"🫦","name":"biting_lip","keywords":["くち","くやしさ","くちびるをかんでいるくち","ゆうわく"]},{"category":"people","char":"🦷","name":"tooth","keywords":["teeth","dentist","は","はいしゃ"]},{"category":"people","char":"👅","name":"tongue","keywords":["mouth","playful","べー","べろ","した"]},{"category":"people","char":"👂","name":"ear","keywords":["face","hear","sound","listen","からだ","みみ"]},{"category":"people","char":"🦻","name":"ear_with_hearing_aid","keywords":["face","hear","sound","listen","アクセシビリティ","みみ","ちょうかく","ほちょうきをつけたみみ"]},{"category":"people","char":"👃","name":"nose","keywords":["smell","sniff","からだ","はな"]},{"category":"people","char":"👁","name":"eye","keywords":["face","look","see","watch","stare","ひとつめ","かため","め"]},{"category":"people","char":"👀","name":"eyes","keywords":["look","watch","stalk","peek","see","からだ","りょうめ","め"]},{"category":"people","char":"🧠","name":"brain","keywords":["smart","intelligent","ちしき","のう","のうみそ","あたま"]},{"category":"people","char":"🫀","name":"anatomical_heart","keywords":["しんぱく","しんぞう","みゃく","ぞうき","こどう"]},{"category":"people","char":"🫁","name":"lungs","keywords":["こきゅう","はい","ぞうき"]},{"category":"people","char":"👤","name":"bust_in_silhouette","keywords":["user","person","human","1にん","シルエット","じょうはんしん","ひとのシルエット"]},{"category":"people","char":"👥","name":"busts_in_silhouette","keywords":["user","person","human","group","team","2にん","2にんのシルエット","シルエット","じょうはんしん"]},{"category":"people","char":"🗣","name":"speaking_head","keywords":["user","person","human","sing","say","talk","シルエット","はなす","はなすひとのシルエット","かお"]},{"category":"people","char":"👶","name":"baby","keywords":["child","boy","girl","toddler","ベビー","あかちゃん","あかんぼう","かお"]},{"category":"people","char":"🧒","name":"child","keywords":["gender-neutral","young","こども","かお"]},{"category":"people","char":"👦","name":"boy","keywords":["man","male","guy","teenager","こども","しょうねん","おとこのこ","かお"]},{"category":"people","char":"👧","name":"girl","keywords":["female","woman","teenager","おんな","おんなのこ","こども","しょうじょ","かお"]},{"category":"people","char":"🧑","name":"adult","keywords":["gender-neutral","person","おとな","せいじん","かお"]},{"category":"people","char":"👨","name":"man","keywords":["mustache","father","dad","guy","classy","sir","moustache","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"👩","name":"woman","keywords":["female","girls","lady","おとな","おんな","じょせい","かお"]},{"category":"people","char":"🧑‍🦱","name":"curly_hair","keywords":["curly","afro","braids","ringlets","おとな","せいじん","かお"]},{"category":"people","char":"👩‍🦱","name":"curly_hair_woman","keywords":["woman","female","girl","curly","afro","braids","ringlets","おとな","おんな","じょせい","かお"]},{"category":"people","char":"👨‍🦱","name":"curly_hair_man","keywords":["man","male","boy","guy","curly","afro","braids","ringlets","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"🧑‍🦰","name":"red_hair","keywords":["redhead","おとな","せいじん","かお"]},{"category":"people","char":"👩‍🦰","name":"red_hair_woman","keywords":["woman","female","girl","ginger","redhead","おとな","おんな","じょせい","かお"]},{"category":"people","char":"👨‍🦰","name":"red_hair_man","keywords":["man","male","boy","guy","ginger","redhead","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"👱‍♀️","name":"blonde_woman","keywords":["woman","female","girl","blonde","person","ブロンド","おんな","じょせい","きんぱつ","きんぱつのじょせい"]},{"category":"people","char":"👱","name":"blonde_man","keywords":["man","male","boy","blonde","guy","person","ブロンド","ひと","きんぱつ","きんぱつのひと","かお"]},{"category":"people","char":"🧑‍🦳","name":"white_hair","keywords":["gray","old","white","おとな","せいじん","かお"]},{"category":"people","char":"👩‍🦳","name":"white_hair_woman","keywords":["woman","female","girl","gray","old","white","おとな","おんな","じょせい","かお"]},{"category":"people","char":"👨‍🦳","name":"white_hair_man","keywords":["man","male","boy","guy","gray","old","white","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"🧑‍🦲","name":"bald","keywords":["bald","chemotherapy","hairless","shaven","おとな","せいじん","かお"]},{"category":"people","char":"👩‍🦲","name":"bald_woman","keywords":["woman","female","girl","bald","chemotherapy","hairless","shaven","おとな","おんな","じょせい","かお"]},{"category":"people","char":"👨‍🦲","name":"bald_man","keywords":["man","male","boy","guy","bald","chemotherapy","hairless","shaven","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"🧔","name":"bearded_person","keywords":["person","bewhiskered","あごひげ","あごひげのひと","ひげ","ひと","かお"]},{"category":"people","char":"🧓","name":"older_adult","keywords":["human","elder","senior","gender-neutral","おとしより","ろうじん","かお","こうれいしゃ"]},{"category":"people","char":"👴","name":"older_man","keywords":["human","male","men","old","elder","senior","おじいさん","おとしより","だんせい","ろうじん","かお","こうれいしゃ"]},{"category":"people","char":"👵","name":"older_woman","keywords":["human","female","women","lady","old","elder","senior","おばあさん","おとしより","じょせい","ろうじん","かお","こうれいしゃ"]},{"category":"people","char":"👲","name":"man_with_gua_pi_mao","keywords":["male","boy","chinese","おわんぼう","ちゅうかぼうのだんせい","おとこ","だんせい"]},{"category":"people","char":"🧕","name":"woman_with_headscarf","keywords":["female","hijab","mantilla","tichel","スカーフ","スカーフのじょせい","ヒジャブ","ベール","おんな","じょせい"]},{"category":"people","char":"👳‍♀️","name":"woman_with_turban","keywords":["female","indian","hinduism","arabs","woman","ターバン","ターバンのじょせい","おんな","じょせい"]},{"category":"people","char":"👳","name":"man_with_turban","keywords":["male","indian","hinduism","arabs","ターバン","ターバンのひと"]},{"category":"people","char":"👮‍♀️","name":"policewoman","keywords":["woman","police","law","legal","enforcement","arrest","911","female","おまわりさん","じょせい","じょせいけいさつかん","けいかん","けいさつ","けいさつかん"]},{"category":"people","char":"👮","name":"policeman","keywords":["man","police","law","legal","enforcement","arrest","911","おまわりさん","けいかん","けいさつ","けいさつかん","かお"]},{"category":"people","char":"👷‍♀️","name":"construction_worker_woman","keywords":["female","human","wip","build","construction","worker","labor","woman","ハンマー","ヘルメット","じょせい","じょせいのけんせつさぎょういん","こうじ","けんせつさぎょういん"]},{"category":"people","char":"👷","name":"construction_worker_man","keywords":["male","human","wip","guy","build","construction","worker","labor","かなづち","ハンマー","ヘルメット","こうじ","けんせつさぎょういん"]},{"category":"people","char":"💂‍♀️","name":"guardswoman","keywords":["uk","gb","british","female","royal","woman","じょせい","じょせいのえいへい","しゅえい","えいへい","けいび","もんばん"]},{"category":"people","char":"💂","name":"guardsman","keywords":["uk","gb","british","male","guy","royal","しゅえい","えいへい","けいび","もんばん"]},{"category":"people","char":"🕵️‍♀️","name":"female_detective","keywords":["human","spy","detective","female","woman","スパイ","たんてい","むしめがね"]},{"category":"people","char":"🕵","name":"male_detective","keywords":["human","spy","detective","スパイ","たんてい","むしめがね"]},{"category":"people","char":"🧑‍⚕️","name":"health_worker","keywords":["doctor","nurse","therapist","healthcare","human","セラピスト","いし","いしゃ"]},{"category":"people","char":"👩‍⚕️","name":"woman_health_worker","keywords":["doctor","nurse","therapist","healthcare","woman","human","いし","いしゃ","おんな","じょせい","じょせいのいしゃ"]},{"category":"people","char":"👨‍⚕️","name":"man_health_worker","keywords":["doctor","nurse","therapist","healthcare","man","human","いし","いしゃ","おとこ","だんせい","だんせいのいしゃ"]},{"category":"people","char":"🧑‍🌾","name":"farmer","keywords":["rancher","gardener","human","ガーデナー","のうじょうぬし","のうか","のうぎょう"]},{"category":"people","char":"👩‍🌾","name":"woman_farmer","keywords":["rancher","gardener","woman","human","おんな","じょせい","のうか","のうかのじょせい"]},{"category":"people","char":"👨‍🌾","name":"man_farmer","keywords":["rancher","gardener","man","human","おとこ","だんせい","のうふ","のうか","のうかのだんせい"]},{"category":"people","char":"🧑‍🍳","name":"cook","keywords":["chef","human","コック","シェフ","りょうり"]},{"category":"people","char":"👩‍🍳","name":"woman_cook","keywords":["chef","woman","human","コック","シェフ","おんな","じょせい","じょせいのコック","りょうり"]},{"category":"people","char":"👨‍🍳","name":"man_cook","keywords":["chef","man","human","コック","シェフ","りょうり","おとこ","だんせい","だんせいのコック"]},{"category":"people","char":"🧑‍🎓","name":"student","keywords":["graduate","human","そつぎょう","がくせい","かくぼう"]},{"category":"people","char":"👩‍🎓","name":"woman_student","keywords":["graduate","woman","human","そつぎょう","おんな","じょしがくせい","じょせい","がくせい","かくぼう"]},{"category":"people","char":"👨‍🎓","name":"man_student","keywords":["graduate","man","human","そつぎょう","がくせい","おとこ","だんしがくせい","だんせい","かくぼう"]},{"category":"people","char":"🧑‍🎤","name":"singer","keywords":["rockstar","entertainer","human","アーティスト","シンガー","スター","ロッカー","かしゅ"]},{"category":"people","char":"👩‍🎤","name":"woman_singer","keywords":["rockstar","entertainer","woman","human","アーティスト","シンガー","おんな","じょせい","じょせいかしゅ","かしゅ"]},{"category":"people","char":"👨‍🎤","name":"man_singer","keywords":["rockstar","entertainer","man","human","アーティスト","シンガー","かしゅ","おとこ","だんせい","だんせいかしゅ"]},{"category":"people","char":"🧑‍🏫","name":"teacher","keywords":["instructor","professor","human","せんせい","たんにん","きょうし","きょうじゅ","こうし"]},{"category":"people","char":"👩‍🏫","name":"woman_teacher","keywords":["instructor","professor","woman","human","せんせい","おんな","じょせい","じょせいのきょうし","きょうし","きょうじゅ"]},{"category":"people","char":"👨‍🏫","name":"man_teacher","keywords":["instructor","professor","man","human","せんせい","きょうし","きょうじゅ","おとこ","だんせい","だんせいのきょうし"]},{"category":"people","char":"🧑‍🏭","name":"factory_worker","keywords":["assembly","industrial","human","モノづくり","こういん","こうじょう","ようせつこう"]},{"category":"people","char":"👩‍🏭","name":"woman_factory_worker","keywords":["assembly","industrial","woman","human","おんな","じょせい","じょせいのようせつこう","ようせつ"]},{"category":"people","char":"👨‍🏭","name":"man_factory_worker","keywords":["assembly","industrial","man","human","ようせつ","おとこ","だんせい","だんせいのようせつこう"]},{"category":"people","char":"🧑‍💻","name":"technologist","keywords":["coder","developer","engineer","programmer","software","human","laptop","computer","コンピュータ","パソコン","プログラマ","ぎじゅつしゃ","かいはつしゃ"]},{"category":"people","char":"👩‍💻","name":"woman_technologist","keywords":["coder","developer","engineer","programmer","software","woman","human","laptop","computer","コンピュータ","パソコン","プログラマ","じょせい","じょせいぎじゅつしゃ","かいはつしゃ"]},{"category":"people","char":"👨‍💻","name":"man_technologist","keywords":["coder","developer","engineer","programmer","software","man","human","laptop","computer","コンピュータ","パソコン","プログラマ","だんせい","だんせいぎじゅつしゃ","かいはつしゃ"]},{"category":"people","char":"🧑‍💼","name":"office_worker","keywords":["business","manager","human","サラリーマン","ビジネスパーソン","かいしゃいん"]},{"category":"people","char":"👩‍💼","name":"woman_office_worker","keywords":["business","manager","woman","human","OL","サラリーマン","ビジネスパーソン","かいしゃいん","じょせい","じょせいかいしゃいん"]},{"category":"people","char":"👨‍💼","name":"man_office_worker","keywords":["business","manager","man","human","サラリーマン","ビジネスパーソン","ビジネスマン","かいしゃいん","だんせい","だんせいかいしゃいん"]},{"category":"people","char":"🧑‍🔧","name":"mechanic","keywords":["plumber","human","wrench","スパナ","メカニック","さぎょういん","こうさく","せいびし"]},{"category":"people","char":"👩‍🔧","name":"woman_mechanic","keywords":["plumber","woman","human","wrench","スパナ","メカニック","おんな","じょせい","じょせいのせいびし","こうさく"]},{"category":"people","char":"👨‍🔧","name":"man_mechanic","keywords":["plumber","man","human","wrench","スパナ","メカニック","こうさく","おとこ","だんせい","だんせいのせいびし"]},{"category":"people","char":"🧑‍🔬","name":"scientist","keywords":["biologist","chemist","engineer","physicist","human","かがくしゃ","ぶつりがくしゃ","せいぶつがくしゃ","かがくしゃ"]},{"category":"people","char":"👩‍🔬","name":"woman_scientist","keywords":["biologist","chemist","engineer","physicist","woman","human","おんな","じょせい","じょせいかがくしゃ","かがくしゃ"]},{"category":"people","char":"👨‍🔬","name":"man_scientist","keywords":["biologist","chemist","engineer","physicist","man","human","おとこ","だんせい","だんせいかがくしゃ","かがくしゃ"]},{"category":"people","char":"🧑‍🎨","name":"artist","keywords":["painter","human","アーティスト","がか","えかき","げいじゅつか"]},{"category":"people","char":"👩‍🎨","name":"woman_artist","keywords":["painter","woman","human","アーティスト","おんな","じょせい","じょせいのげいじゅつか","がか","げいじゅつか"]},{"category":"people","char":"👨‍🎨","name":"man_artist","keywords":["painter","man","human","アーティスト","おとこ","だんせい","だんせいのげいじゅつか","がか","げいじゅつか"]},{"category":"people","char":"🧑‍🚒","name":"firefighter","keywords":["fireman","human","レスキュー","きゅうじょたい","しょうぼうし"]},{"category":"people","char":"👩‍🚒","name":"woman_firefighter","keywords":["fireman","woman","human","おんな","じょせい","じょせいしょうぼうし","しょうぼうし"]},{"category":"people","char":"👨‍🚒","name":"man_firefighter","keywords":["fireman","man","human","しょうぼうし","おとこ","だんせい","だんせいしょうぼうし"]},{"category":"people","char":"🧑‍✈️","name":"pilot","keywords":["aviator","plane","human","パイロット","そうじゅうし","きちょう"]},{"category":"people","char":"👩‍✈️","name":"woman_pilot","keywords":["aviator","plane","woman","human","おんな","じょせい","じょせいパイロット","そうじゅうし"]},{"category":"people","char":"👨‍✈️","name":"man_pilot","keywords":["aviator","plane","man","human","そうじゅうし","おとこ","だんせい","だんせいパイロット"]},{"category":"people","char":"🧑‍🚀","name":"astronaut","keywords":["space","rocket","human","ロケット","うちゅうひこうし"]},{"category":"people","char":"👩‍🚀","name":"woman_astronaut","keywords":["space","rocket","woman","human","おんな","じょせい","じょせいうちゅうひこうし","うちゅうひこうし"]},{"category":"people","char":"👨‍🚀","name":"man_astronaut","keywords":["space","rocket","man","human","うちゅうひこうし","おとこ","だんせい","だんせいうちゅうひこうし"]},{"category":"people","char":"🧑‍⚖️","name":"judge","keywords":["justice","court","human","さいばん","さいばんかん"]},{"category":"people","char":"👩‍⚖️","name":"woman_judge","keywords":["justice","court","woman","human","おんな","じょせい","じょせいのさいばんかん","さいばん","さいばんかん"]},{"category":"people","char":"👨‍⚖️","name":"man_judge","keywords":["justice","court","man","human","おとこ","だんせい","だんせいのさいばんかん","さいばん","さいばんかん"]},{"category":"people","char":"🦸‍♀️","name":"woman_superhero","keywords":["woman","female","good","heroine","superpowers","ヒーロー","ヒロイン","ぜん","じょせい","じょせいのスーパーヒーロー","ちょうじん"]},{"category":"people","char":"🦸‍♂️","name":"man_superhero","keywords":["man","male","good","hero","superpowers","ヒーロー","ぜん","だんせい","だんせいのスーパーヒーロー","ちょうじん"]},{"category":"people","char":"🦹‍♀️","name":"woman_supervillain","keywords":["woman","female","evil","bad","criminal","heroine","superpowers","ヴィラン","じょせい","じょせいのあくやく","わる","はんにん","ちょうじん"]},{"category":"people","char":"🦹‍♂️","name":"man_supervillain","keywords":["man","male","evil","bad","criminal","hero","superpowers","ヴィラン","わる","はんにん","だんせい","だんせいのあくやく","ちょうじん"]},{"category":"people","char":"🤶","name":"mrs_claus","keywords":["woman","female","xmas","mother christmas","クリスマス","サンタクロース","おんな","じょせい","じょせいのサンタ"]},{"category":"people","char":"🧑‍🎄","name":"mx_claus","keywords":["xmas","christmas","クリスマス","サンタクロース","サンタさん"]},{"category":"people","char":"🎅","name":"santa","keywords":["festival","man","male","xmas","father christmas","クリスマス","サンタ","サンタクロース","かお"]},{"category":"people","char":"🥷","name":"ninja","keywords":["スパイ","しのび","にんじゃ","にんじゅつ","くさのもの","おんみつ"]},{"category":"people","char":"🧙‍♀️","name":"sorceress","keywords":["woman","female","mage","witch","おんな","おんなのまほうつかい","じょせい","まじょ","まほうつかい"]},{"category":"people","char":"🧙‍♂️","name":"wizard","keywords":["man","male","mage","sorcerer","おとこ","おとこのまほうつかい","だんせい","まほうつかい"]},{"category":"people","char":"🧝‍♀️","name":"woman_elf","keywords":["woman","female","エルフ","おんな","おんなのエルフ","じょせい"]},{"category":"people","char":"🧝‍♂️","name":"man_elf","keywords":["man","male","エルフ","おとこ","おとこのエルフ","だんせい"]},{"category":"people","char":"🧛‍♀️","name":"woman_vampire","keywords":["woman","female","ドラキュラ","バンパイア","きゅうけつき","おんな","おんなのきゅうけつき","じょせい"]},{"category":"people","char":"🧛‍♂️","name":"man_vampire","keywords":["man","male","dracula","ドラキュラ","バンパイア","きゅうけつき","おとこ","おとこのきゅうけつき","だんせい"]},{"category":"people","char":"🧟‍♀️","name":"woman_zombie","keywords":["woman","female","undead","walking dead","ゾンビ","ホラー","おんな","おんなのゾンビ","じょせい"]},{"category":"people","char":"🧟‍♂️","name":"man_zombie","keywords":["man","male","dracula","undead","walking dead","ゾンビ","ホラー","おとこ","おとこのゾンビ","だんせい"]},{"category":"people","char":"🧞‍♀️","name":"woman_genie","keywords":["woman","female","おんな","おんなのせいれい","じょせい","せいれい","まじん"]},{"category":"people","char":"🧞‍♂️","name":"man_genie","keywords":["man","male","おとこ","おとこのせいれい","だんせい","せいれい","まじん"]},{"category":"people","char":"🧜‍♀️","name":"mermaid","keywords":["woman","female","merwoman","ariel","マーメイド","にんぎょ","おんな","じょせい"]},{"category":"people","char":"🧜‍♂️","name":"merman","keywords":["man","male","triton","マーマン","にんぎょ","おとこ","だんせい"]},{"category":"people","char":"🧚‍♀️","name":"woman_fairy","keywords":["woman","female","おんな","おんなのようせい","じょせい","ようせい"]},{"category":"people","char":"🧚‍♂️","name":"man_fairy","keywords":["man","male","ようせい","おとこ","おとこのようせい","だんせい"]},{"category":"people","char":"👼","name":"angel","keywords":["heaven","wings","halo","エンジェル","てんし","かお"]},{"category":"people","char":"🧌","name":"troll","keywords":["おとぎばなし","トロール","ファンタジー","モンスター"]},{"category":"people","char":"🤰","name":"pregnant_woman","keywords":["baby","おんな","じょせい","にんしん","にんぷ"]},{"category":"people","char":"🫃","name":"pregnant_man","keywords":["妊おっと","にんしん","おとこ","だんせい"]},{"category":"people","char":"🫄","name":"pregnant_person","keywords":["おなか","妊おっと","にんしん","にんしんしたひと","にんぷ"]},{"category":"people","char":"🫅","name":"person_with_crown","keywords":["くんしゅ","おうかんをかぶったひと","おうけ","おうぞく","おうさま"]},{"category":"people","char":"🤱","name":"breastfeeding","keywords":["nursing","baby","ミルク","おんな","じょせい","じゅにゅう","あかちゃん"]},{"category":"people","char":"👩‍🍼","name":"woman_feeding_baby","keywords":["おんな","じょせい","じゅにゅう","じゅにゅうするじょせい","あかちゃん"]},{"category":"people","char":"👨‍🍼","name":"man_feeding_baby","keywords":["じゅにゅう","じゅにゅうするだんせい","おとこ","だんせい","あかちゃん"]},{"category":"people","char":"🧑‍🍼","name":"person_feeding_baby","keywords":["ひと","じゅにゅう","じゅにゅうするひと","あかちゃん"]},{"category":"people","char":"👸","name":"princess","keywords":["girl","woman","female","blond","crown","royal","queen","おひめさま","プリンセス","おんな","じょせい","おうじょ"]},{"category":"people","char":"🤴","name":"prince","keywords":["boy","man","male","crown","royal","king","プリンス","おうじ","おとこ","だんせい"]},{"category":"people","char":"👰","name":"person_with_veil","keywords":["couple","marriage","wedding","woman","bride","ベールのひと","けっこん","はなよめ","かお"]},{"category":"people","char":"👰","name":"bride_with_veil","keywords":["couple","marriage","wedding","woman","bride","ベールのひと","けっこん","はなよめ","かお"]},{"category":"people","char":"🤵","name":"person_in_tuxedo","keywords":["couple","marriage","wedding","groom","タキシード","タキシードのひと","けっこん","はなむこ"]},{"category":"people","char":"🤵","name":"man_in_tuxedo","keywords":["couple","marriage","wedding","groom","タキシード","タキシードのひと","けっこん","はなむこ"]},{"category":"people","char":"🏃‍♀️","name":"running_woman","keywords":["woman","walking","exercise","race","running","female","ジョギング","マラソン","ランナー","ランニング","じょせい","はしるおんな"]},{"category":"people","char":"🏃","name":"running_man","keywords":["man","walking","exercise","race","running","ジョギング","マラソン","ランナー","ランニング","はしるひと"]},{"category":"people","char":"🚶‍♀️","name":"walking_woman","keywords":["human","feet","steps","woman","female","ウォーキング","じょせい","あるくおんな","ほこう","ほこうしゃ"]},{"category":"people","char":"🚶","name":"walking_man","keywords":["human","feet","steps","ウォーキング","あるくひと","ほこう","ほこうしゃ"]},{"category":"people","char":"💃","name":"dancer","keywords":["female","girl","woman","fun","ダンサー","ダンス","フラメンコ","じょせい","おどりこ","おどるおんな"]},{"category":"people","char":"🕺","name":"man_dancing","keywords":["male","boy","fun","dancer","ダンサー","ダンス","おとこ","だんせい","おどるおとこ"]},{"category":"people","char":"👯","name":"dancing_women","keywords":["female","bunny","women","girls","うさみみ","ダンサー","パーティー","バニー","バニーガール","バニーボーイ"]},{"category":"people","char":"👯‍♂️","name":"dancing_men","keywords":["male","bunny","men","boys","うさみみ","ダンサー","パーティー","バニーボーイ","おとこ","だんせい"]},{"category":"people","char":"👫","name":"couple","keywords":["pair","people","human","love","date","dating","like","affection","valentines","marriage","カップル","ともだち","こいびと","てをつなぐだんじょ","だんじょ","いせい"]},{"category":"people","char":"🧑‍🤝‍🧑","name":"people_holding_hands","keywords":["pair","couple","love","like","bromance","friendship","people","human","カップル","ひと","ともだち","こいびと","て","てをつなぐ","てをつなぐ2にん"]},{"category":"people","char":"👬","name":"two_men_holding_hands","keywords":["pair","couple","love","like","bromance","friendship","people","man","human","カップル","ともだち","どうせい","こいびと","てをつなぐだんせい","おとこ"]},{"category":"people","char":"👭","name":"two_women_holding_hands","keywords":["pair","couple","love","like","bromance","friendship","people","female","human","カップル","ともだち","どうせい","おんな","こいびと","てをつなぐじょせい"]},{"category":"people","char":"🫂","name":"people_hugging","keywords":["ありがとう","おわかれ","こんにちは","さようなら","ハグするひと","あいさつ"]},{"category":"people","char":"🙇‍♀️","name":"bowing_woman","keywords":["woman","female","girl","おじぎ","おじぎするおんな","ごめんなさい","どげざ","おんな","じょせい"]},{"category":"people","char":"🙇","name":"bowing_man","keywords":["man","male","boy","おじぎ","おじぎするひと","ごめんなさい","どげざ"]},{"category":"people","char":"🤦‍♂️","name":"man_facepalming","keywords":["man","male","boy","disbelief","あちゃー","ジェスチャー","ひたいにて","ひたいにてをあてるおとこ","おとこ","だんせい"]},{"category":"people","char":"🤦‍♀️","name":"woman_facepalming","keywords":["woman","female","girl","disbelief","あちゃー","ジェスチャー","ひたいにて","ひたいにてをあてるおんな","おんな","じょせい"]},{"category":"people","char":"🤷","name":"woman_shrugging","keywords":["woman","female","girl","confused","indifferent","doubt","おてあげ","おてあげするひと","ジェスチャー"]},{"category":"people","char":"🤷‍♂️","name":"man_shrugging","keywords":["man","male","boy","confused","indifferent","doubt","おてあげ","おてあげするおとこ","ジェスチャー","おとこ","だんせい"]},{"category":"people","char":"💁","name":"tipping_hand_woman","keywords":["female","girl","woman","human","information","うけつけ","うけつけじょう","あんない","あんないするひと","かお"]},{"category":"people","char":"💁‍♂️","name":"tipping_hand_man","keywords":["male","boy","man","human","information","ジェスチャー","あんない","あんないするおとこ","おとこ","だんせい"]},{"category":"people","char":"🙅","name":"no_good_woman","keywords":["female","girl","woman","nope","NG","ジェスチャー","だめ","ダメのポーズをするひと","バツ","ひと"]},{"category":"people","char":"🙅‍♂️","name":"no_good_man","keywords":["male","boy","man","nope","NG","ジェスチャー","ダメのポーズをするおとこ","バツ","おとこ","だんせい"]},{"category":"people","char":"🙆","name":"ok_woman","keywords":["women","girl","female","pink","human","woman","OK","OKのポーズをするひと","オーケー","ジェスチャー","まる","ひと"]},{"category":"people","char":"🙆‍♂️","name":"ok_man","keywords":["men","boy","male","blue","human","man","OKのポーズをするおとこ","オーケー","オッケー","ジェスチャー","まる","だんせい"]},{"category":"people","char":"🙋","name":"raising_hand_woman","keywords":["female","girl","woman","ひと","てをあげる","てをあげるひと","きょしゅ"]},{"category":"people","char":"🙋‍♂️","name":"raising_hand_man","keywords":["male","boy","man","てをあげる","てをあげるおとこ","きょしゅ","おとこ","だんせい"]},{"category":"people","char":"🙎","name":"pouting_woman","keywords":["female","girl","woman","ぷんぷん","ふきげん","ふきげんなひと","ひと"]},{"category":"people","char":"🙎‍♂️","name":"pouting_man","keywords":["male","boy","man","ぷんぷん","ふきげん","ふきげんなおとこ","おとこ","だんせい"]},{"category":"people","char":"🙍","name":"frowning_woman","keywords":["female","girl","woman","sad","depressed","discouraged","unhappy","しかめっつら","しかめめん","しかめめんのひと","しかめがお"]},{"category":"people","char":"🙍‍♂️","name":"frowning_man","keywords":["male","boy","man","sad","depressed","discouraged","unhappy","しかめっつら","しかめめんのおとこ","しかめがお","おとこ","だんせい"]},{"category":"people","char":"💇","name":"haircut_woman","keywords":["female","girl","woman","ヘアカット","とこや","さんぱつされるひと","びようしつ","びよういん"]},{"category":"people","char":"💇‍♂️","name":"haircut_man","keywords":["male","boy","man","ヘアカット","とこや","さんぱつされるおとこ","だんせい","びようしつ","びよういん"]},{"category":"people","char":"💆","name":"massage_woman","keywords":["female","girl","woman","head","エステ","フェイスマッサージ","フェイスマッサージちゅうのひと","マッサージ"]},{"category":"people","char":"💆‍♂️","name":"massage_man","keywords":["male","boy","man","head","エステ","フェイスマッサージ","フェイスマッサージちゅうのおとこ","マッサージ","おとこ","だんせい"]},{"category":"people","char":"🧖‍♀️","name":"woman_in_steamy_room","keywords":["female","woman","spa","steamroom","sauna","サウナ","サウナにはいるおんな","おんな","じょせい"]},{"category":"people","char":"🧖‍♂️","name":"man_in_steamy_room","keywords":["male","man","spa","steamroom","sauna","サウナ","サウナにはいるおとこ","おとこ","だんせい"]},{"category":"people","char":"🧏‍♀️","name":"woman_deaf","keywords":["woman","female","アクセシビリティ","じょせい","みみのふじゆうなじょせい","ちょうかく","さわがい"]},{"category":"people","char":"🧏‍♂️","name":"man_deaf","keywords":["man","male","アクセシビリティ","だんせい","みみのふじゆうなだんせい","ちょうかく","さわがい"]},{"category":"people","char":"🧍‍♀️","name":"woman_standing","keywords":["woman","female","じょせい","たつおんな","きりつ"]},{"category":"people","char":"🧍‍♂️","name":"man_standing","keywords":["man","male","だんせい","たつおとこ","きりつ"]},{"category":"people","char":"🧎‍♀️","name":"woman_kneeling","keywords":["woman","female","ひざまずく","じょせい","すわる","せいざするじょせい"]},{"category":"people","char":"🧎‍♂️","name":"man_kneeling","keywords":["man","male","ひざまずく","すわる","せいざするだんせい","だんせい"]},{"category":"people","char":"🧑‍🦯","name":"person_with_probing_cane","keywords":["accessibility","blind","アクセシビリティ","つえをついたひと","め","しかく","さわがい"]},{"category":"people","char":"👩‍🦯","name":"woman_with_probing_cane","keywords":["woman","female","accessibility","blind","アクセシビリティ","じょせい","つえをついたじょせい","め","しかく","さわがい"]},{"category":"people","char":"👨‍🦯","name":"man_with_probing_cane","keywords":["man","male","accessibility","blind","アクセシビリティ","つえをついただんせい","だんせい","め","しかく","さわがい"]},{"category":"people","char":"🧑‍🦼","name":"person_in_motorized_wheelchair","keywords":["accessibility","アクセシビリティ","くるまいす","さわがい","でんどうくるまいすのひと"]},{"category":"people","char":"👩‍🦼","name":"woman_in_motorized_wheelchair","keywords":["woman","female","accessibility","アクセシビリティ","じょせい","くるまいす","さわがい","でんどうくるまいすのじょせい"]},{"category":"people","char":"👨‍🦼","name":"man_in_motorized_wheelchair","keywords":["man","male","accessibility","アクセシビリティ","だんせい","くるまいす","さわがい","でんどうくるまいすのだんせい"]},{"category":"people","char":"🧑‍🦽","name":"person_in_manual_wheelchair","keywords":["accessibility","アクセシビリティ","しゅどうしきくるまいすのひと","くるまいす","さわがい"]},{"category":"people","char":"👩‍🦽","name":"woman_in_manual_wheelchair","keywords":["woman","female","accessibility","アクセシビリティ","じょせい","しゅどうしきくるまいすのじょせい","くるまいす","さわがい"]},{"category":"people","char":"👨‍🦽","name":"man_in_manual_wheelchair","keywords":["man","male","accessibility","アクセシビリティ","しゅどうしきくるまいすのだんせい","だんせい","くるまいす","さわがい"]},{"category":"people","char":"💑","name":"couple_with_heart_woman_man","keywords":["pair","love","like","affection","human","dating","valentines","marriage","カップル","カップルとハート","ハート","こいびと","あつあつ"]},{"category":"people","char":"👩‍❤️‍👩","name":"couple_with_heart_woman_woman","keywords":["pair","love","like","affection","human","dating","valentines","marriage","おとな","おんな","じょせい","かお"]},{"category":"people","char":"👨‍❤️‍👨","name":"couple_with_heart_man_man","keywords":["pair","love","like","affection","human","dating","valentines","marriage","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"💏","name":"couplekiss_man_woman","keywords":["pair","valentines","love","like","dating","marriage","2にんでキス","カップル","キス","ちゅっ","ハート"]},{"category":"people","char":"👩‍❤️‍💋‍👩","name":"couplekiss_woman_woman","keywords":["pair","valentines","love","like","dating","marriage","おとな","おんな","じょせい","かお"]},{"category":"people","char":"👨‍❤️‍💋‍👨","name":"couplekiss_man_man","keywords":["pair","valentines","love","like","dating","marriage","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"👪","name":"family_man_woman_boy","keywords":["home","parents","child","mom","dad","father","mother","people","human","かぞく","おやこ"]},{"category":"people","char":"👨‍👩‍👧","name":"family_man_woman_girl","keywords":["home","parents","people","human","child","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"👨‍👩‍👧‍👦","name":"family_man_woman_girl_boy","keywords":["home","parents","people","human","children","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"👨‍👩‍👦‍👦","name":"family_man_woman_boy_boy","keywords":["home","parents","people","human","children","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"👨‍👩‍👧‍👧","name":"family_man_woman_girl_girl","keywords":["home","parents","people","human","children","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"👩‍👩‍👦","name":"family_woman_woman_boy","keywords":["home","parents","people","human","children","おとな","おんな","じょせい","かお"]},{"category":"people","char":"👩‍👩‍👧","name":"family_woman_woman_girl","keywords":["home","parents","people","human","children","おとな","おんな","じょせい","かお"]},{"category":"people","char":"👩‍👩‍👧‍👦","name":"family_woman_woman_girl_boy","keywords":["home","parents","people","human","children","おとな","おんな","じょせい","かお"]},{"category":"people","char":"👩‍👩‍👦‍👦","name":"family_woman_woman_boy_boy","keywords":["home","parents","people","human","children","おとな","おんな","じょせい","かお"]},{"category":"people","char":"👩‍👩‍👧‍👧","name":"family_woman_woman_girl_girl","keywords":["home","parents","people","human","children","おとな","おんな","じょせい","かお"]},{"category":"people","char":"👨‍👨‍👦","name":"family_man_man_boy","keywords":["home","parents","people","human","children","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"👨‍👨‍👧","name":"family_man_man_girl","keywords":["home","parents","people","human","children","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"👨‍👨‍👧‍👦","name":"family_man_man_girl_boy","keywords":["home","parents","people","human","children","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"👨‍👨‍👦‍👦","name":"family_man_man_boy_boy","keywords":["home","parents","people","human","children","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"👨‍👨‍👧‍👧","name":"family_man_man_girl_girl","keywords":["home","parents","people","human","children","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"👩‍👦","name":"family_woman_boy","keywords":["home","parent","people","human","child","おとな","おんな","じょせい","かお"]},{"category":"people","char":"👩‍👧","name":"family_woman_girl","keywords":["home","parent","people","human","child","おとな","おんな","じょせい","かお"]},{"category":"people","char":"👩‍👧‍👦","name":"family_woman_girl_boy","keywords":["home","parent","people","human","children","おとな","おんな","じょせい","かお"]},{"category":"people","char":"👩‍👦‍👦","name":"family_woman_boy_boy","keywords":["home","parent","people","human","children","おとな","おんな","じょせい","かお"]},{"category":"people","char":"👩‍👧‍👧","name":"family_woman_girl_girl","keywords":["home","parent","people","human","children","おとな","おんな","じょせい","かお"]},{"category":"people","char":"👨‍👦","name":"family_man_boy","keywords":["home","parent","people","human","child","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"👨‍👧","name":"family_man_girl","keywords":["home","parent","people","human","child","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"👨‍👧‍👦","name":"family_man_girl_boy","keywords":["home","parent","people","human","children","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"👨‍👦‍👦","name":"family_man_boy_boy","keywords":["home","parent","people","human","children","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"👨‍👧‍👧","name":"family_man_girl_girl","keywords":["home","parent","people","human","children","おとな","おとこ","だんせい","かお"]},{"category":"people","char":"🧶","name":"yarn","keywords":["ball","crochet","knit","かぎばり","ニット","けいと","けいとだま","あみもの"]},{"category":"people","char":"🧵","name":"thread","keywords":["needle","sewing","spool","string","ひも","いと","いとまき","ぬいはり","さいほう"]},{"category":"people","char":"🧥","name":"coat","keywords":["jacket","アウター","オーバー","コート","ジャケット","ふく"]},{"category":"people","char":"🥼","name":"labcoat","keywords":["doctor","experiment","scientist","chemist","いしゃ","じっけん","はくい","かがくしゃ"]},{"category":"people","char":"👚","name":"womans_clothes","keywords":["fashion","shopping_bags","female","シャツ","ブラウス","ふじんふく","ふく"]},{"category":"people","char":"👕","name":"tshirt","keywords":["fashion","cloth","casual","shirt","tee","Tシャツ","シャツ","ふく"]},{"category":"people","char":"👖","name":"jeans","keywords":["fashion","shopping","ジーンズ","ズボン","デニム","パンツ","ふく"]},{"category":"people","char":"👔","name":"necktie","keywords":["shirt","suitup","formal","fashion","cloth","business","シャツ","ネクタイ","ワイシャツ","ふく"]},{"category":"people","char":"👗","name":"dress","keywords":["clothes","fashion","shopping","ドレス","ワンピース","ふく"]},{"category":"people","char":"👙","name":"bikini","keywords":["swimming","female","woman","girl","fashion","beach","summer","ビキニ","みずぎ"]},{"category":"people","char":"🩱","name":"one_piece_swimsuit","keywords":["swimming","female","woman","girl","fashion","beach","summer","スイムウェア","ワンピースのみずぎ","みずぎ"]},{"category":"people","char":"👘","name":"kimono","keywords":["dress","fashion","women","female","japanese","ふく","きもの"]},{"category":"people","char":"🥻","name":"sari","keywords":["dress","fashion","women","female","サリー","ふく","みんぞくいしょう"]},{"category":"people","char":"🩲","name":"briefs","keywords":["dress","fashion","スイムウェア","パンツ","ブリーフ","したぎ","みずぎ"]},{"category":"people","char":"🩳","name":"shorts","keywords":["dress","fashion","ショーツ","スイムウェア","パンツ","したぎ","みずぎ"]},{"category":"people","char":"💄","name":"lipstick","keywords":["female","girl","fashion","woman","リップ","リップスティック","けしょう","くちべに"]},{"category":"people","char":"💋","name":"kiss","keywords":["face","lips","love","like","affection","valentines","キス","キスマーク","ちゅっ","くちびる"]},{"category":"people","char":"👣","name":"footprints","keywords":["feet","tracking","walking","beach","あし","あしあと"]},{"category":"people","char":"🥿","name":"flat_shoe","keywords":["ballet","slip-on","slipper","スリッポン","バレエシューズ","フラットシューズ","ぺたんこくつ"]},{"category":"people","char":"👠","name":"high_heel","keywords":["fashion","shoes","female","pumps","stiletto","ハイヒール","ピンヒール","くつ"]},{"category":"people","char":"👡","name":"sandal","keywords":["shoes","fashion","flip flops","サンダル","くつ"]},{"category":"people","char":"👢","name":"boot","keywords":["shoes","fashion","ブーツ","ロングブーツ","くつ"]},{"category":"people","char":"👞","name":"mans_shoe","keywords":["fashion","male","ローファー","しんしくつ","かわぐつ","くつ"]},{"category":"people","char":"👟","name":"athletic_shoe","keywords":["shoes","sports","sneakers","スニーカー","くつ"]},{"category":"people","char":"🩴","name":"thong_sandal","keywords":["ゴムぞうり","サンダル","トングサンダル","ビーサン","ビーチサンダル","くつ"]},{"category":"people","char":"🩰","name":"ballet_shoes","keywords":["shoes","sports","シューズ","ダンス","トウシューズ","バレエ","くつ"]},{"category":"people","char":"🧦","name":"socks","keywords":["stockings","clothes","ソックス","くつした"]},{"category":"people","char":"🧤","name":"gloves","keywords":["hands","winter","clothes","てぶくろ"]},{"category":"people","char":"🧣","name":"scarf","keywords":["neck","winter","clothes","スカーフ","マフラー","えりまき","くびまき"]},{"category":"people","char":"👒","name":"womans_hat","keywords":["fashion","accessories","female","lady","spring","ふじんぼうし","ぼうし","むぎわら","むぎわらぼうし"]},{"category":"people","char":"🎩","name":"tophat","keywords":["magic","gentleman","classy","circus","シルクハット","ぼうし"]},{"category":"people","char":"🧢","name":"billed_hat","keywords":["cap","baseball","キャップ","ぼうし","やきゅうぼう"]},{"category":"people","char":"⛑","name":"rescue_worker_helmet","keywords":["construction","build","ヘルメット","きゅうきゅう","しろじゅうじ","しろじゅうじヘルメット"]},{"category":"people","char":"🪖","name":"military_helmet","keywords":["ヘルメット","ミリタリー","へいし","せんとう","ぐんようヘルメット","ぐんたい"]},{"category":"people","char":"🎓","name":"mortar_board","keywords":["school","college","degree","university","graduation","cap","hat","legal","learn","education","そつぎょう","ぼうし","かくぼう"]},{"category":"people","char":"👑","name":"crown","keywords":["king","kod","leader","royalty","lord","クラウン","かんむり","おうかん"]},{"category":"people","char":"🎒","name":"school_satchel","keywords":["student","education","bag","backpack","バックパック","ランドセル","リュックサック","がっこう","しょうがっこう","しょうがくせい"]},{"category":"people","char":"🧳","name":"luggage","keywords":["packing","travel","スーツケース","パッキング","りょこう","りょこうかばん"]},{"category":"people","char":"👝","name":"pouch","keywords":["bag","accessories","shopping","バッグ","ポーチ"]},{"category":"people","char":"👛","name":"purse","keywords":["fashion","accessories","money","sales","shopping","がまぐち","こぜにいれ","さいふ"]},{"category":"people","char":"👜","name":"handbag","keywords":["fashion","accessory","accessories","shopping","かばん","バッグ","ハンドバッグ"]},{"category":"people","char":"💼","name":"briefcase","keywords":["business","documents","work","law","legal","job","career","かばん","ブリーフケース"]},{"category":"people","char":"👓","name":"eyeglasses","keywords":["fashion","accessories","eyesight","nerdy","dork","geek","メガネ"]},{"category":"people","char":"🕶","name":"dark_sunglasses","keywords":["face","cool","accessories","サングラス","メガネ"]},{"category":"people","char":"🥽","name":"goggles","keywords":["eyes","protection","safety","ゴーグル","ほごメガネ","すいえい","ようせつ"]},{"category":"people","char":"💍","name":"ring","keywords":["wedding","propose","marriage","valentines","diamond","fashion","jewelry","gem","engagement","ジュエリー","ダイア","ダイヤ","リング","ゆびわ"]},{"category":"people","char":"🌂","name":"closed_umbrella","keywords":["weather","rain","drizzle","かさ","てんき","もちもの","とじたかさ","あめ"]},{"category":"animals_and_nature","char":"🐶","name":"dog","keywords":["animal","friend","nature","woof","puppy","pet","faithful","イヌ","イヌのかお","ペット","どうぶつ","いぬ","かお"]},{"category":"animals_and_nature","char":"🐱","name":"cat","keywords":["animal","meow","nature","pet","kitten","ネコ","ネコのかお","ペット","どうぶつ","ねこ","かお"]},{"category":"animals_and_nature","char":"🐈‍⬛","name":"black_cat","keywords":["animal","meow","nature","pet","kitten","ネコ","ふきつ","どうぶつ","ねこ","くろ","くろねこ"]},{"category":"animals_and_nature","char":"🐭","name":"mouse","keywords":["animal","nature","cheese_wedge","rodent","ねずみ","ネズミのかお","どうぶつ","かお"]},{"category":"animals_and_nature","char":"🐹","name":"hamster","keywords":["animal","nature","ネズミ","ハムスター","ハムスターのかお","ペット","どうぶつ","かお"]},{"category":"animals_and_nature","char":"🐰","name":"rabbit","keywords":["animal","nature","pet","spring","magic","bunny","うさぎ","ウサギ","ウサギのかお","ペット","どうぶつ","かお"]},{"category":"animals_and_nature","char":"🦊","name":"fox_face","keywords":["animal","nature","face","キツネ","キツネのかお","どうぶつ","きつね","かお"]},{"category":"animals_and_nature","char":"🐻","name":"bear","keywords":["animal","nature","wild","くま","クマ","クマのかお","どうぶつ","くま","かお"]},{"category":"animals_and_nature","char":"🐼","name":"panda_face","keywords":["animal","nature","panda","パンダ","パンダのかお","どうぶつ","かお"]},{"category":"animals_and_nature","char":"🐨","name":"koala","keywords":["animal","nature","コアラ","どうぶつ","かお"]},{"category":"animals_and_nature","char":"🐯","name":"tiger","keywords":["animal","cat","danger","wild","nature","roar","トラ","トラのかお","どうぶつ","とら","かお"]},{"category":"animals_and_nature","char":"🦁","name":"lion","keywords":["animal","nature","ライオン","ライオンのかお","どうぶつ","かお"]},{"category":"animals_and_nature","char":"🐮","name":"cow","keywords":["beef","ox","animal","nature","moo","milk","ウシ","どうぶつ","うし","うしのかお","かお"]},{"category":"animals_and_nature","char":"🐷","name":"pig","keywords":["animal","oink","nature","ブタ","ブタのかお","どうぶつ","ぶた","かお"]},{"category":"animals_and_nature","char":"🐽","name":"pig_nose","keywords":["animal","oink","ブタ","ブタはな","ぶた","かお","はな"]},{"category":"animals_and_nature","char":"🐸","name":"frog","keywords":["animal","nature","croak","toad","カエル","カエルのかお","どうぶつ","かお"]},{"category":"animals_and_nature","char":"🦑","name":"squid","keywords":["animal","nature","ocean","sea","イカ","シーフード","どうぶつ"]},{"category":"animals_and_nature","char":"🐙","name":"octopus","keywords":["animal","creature","ocean","sea","nature","beach","タコ","どうぶつ"]},{"category":"animals_and_nature","char":"🦐","name":"shrimp","keywords":["animal","ocean","nature","seafood","エビ","シーフード","シュリンプ","どうぶつ"]},{"category":"animals_and_nature","char":"🐵","name":"monkey_face","keywords":["animal","nature","circus","サル","サルのかお","どうぶつ","さる","かお"]},{"category":"animals_and_nature","char":"🦍","name":"gorilla","keywords":["animal","nature","circus","ゴリラ","どうぶつ","かお"]},{"category":"animals_and_nature","char":"🙈","name":"see_no_evil","keywords":["monkey","animal","nature","haha","さんえん","さる","みざる"]},{"category":"animals_and_nature","char":"🙉","name":"hear_no_evil","keywords":["animal","monkey","nature","さんえん","さる","きかざる"]},{"category":"animals_and_nature","char":"🙊","name":"speak_no_evil","keywords":["monkey","animal","nature","omg","さんえん","さる","いわざる"]},{"category":"animals_and_nature","char":"🐒","name":"monkey","keywords":["animal","nature","banana","circus","サル","どうぶつ","さる"]},{"category":"animals_and_nature","char":"🐔","name":"chicken","keywords":["animal","cluck","nature","bird","にわとり","どうぶつ","かお","とり"]},{"category":"animals_and_nature","char":"🐧","name":"penguin","keywords":["animal","nature","ペンギン","どうぶつ","とり"]},{"category":"animals_and_nature","char":"🐦","name":"bird","keywords":["animal","nature","fly","tweet","spring","どうぶつ","かお","とり"]},{"category":"animals_and_nature","char":"🐤","name":"baby_chick","keywords":["animal","chicken","bird","ひな","ひよこ","どうぶつ","よこをむいているひよこ","かお","とり"]},{"category":"animals_and_nature","char":"🐣","name":"hatching_chick","keywords":["animal","chicken","egg","born","baby","bird","ひな","ひよこ","どうぶつ","たまごからかえったひよこ","かお","とり"]},{"category":"animals_and_nature","char":"🐥","name":"hatched_chick","keywords":["animal","chicken","baby","bird","ひな","ひよこ","まえをむいているひよこ","どうぶつ","とり"]},{"category":"animals_and_nature","char":"🦆","name":"duck","keywords":["animal","nature","bird","mallard","カモ","どうぶつ","とり"]},{"category":"animals_and_nature","char":"🦅","name":"eagle","keywords":["animal","nature","bird","ワシ","どうぶつ","とり"]},{"category":"animals_and_nature","char":"🦉","name":"owl","keywords":["animal","nature","bird","hoot","フクロウ","どうぶつ","とり"]},{"category":"animals_and_nature","char":"🦇","name":"bat","keywords":["animal","nature","blind","vampire","こうもり","コウモリ","バンパイア","どうぶつ","きゅうけつき"]},{"category":"animals_and_nature","char":"🐺","name":"wolf","keywords":["animal","nature","wild","オオカミ","オオカミのかお","どうぶつ","おおかみ","かお"]},{"category":"animals_and_nature","char":"🐗","name":"boar","keywords":["animal","nature","イノシシ","どうぶつ","いのしし","かお"]},{"category":"animals_and_nature","char":"🐴","name":"horse","keywords":["animal","brown","nature","ウマ","どうぶつ","かお","うま","うまのかお"]},{"category":"animals_and_nature","char":"🦄","name":"unicorn","keywords":["animal","nature","mystical","ユニコーン","ユニコーンのかお","いっかくじゅう","どうぶつ","かお"]},{"category":"animals_and_nature","char":"🐝","name":"honeybee","keywords":["animal","insect","nature","bug","spring","honey","ハチ","ミツバチ","むし"]},{"category":"animals_and_nature","char":"🐛","name":"bug","keywords":["animal","insect","nature","worm","けむし","いもむし","むし"]},{"category":"animals_and_nature","char":"🦋","name":"butterfly","keywords":["animal","insect","nature","caterpillar","チョウ","むし"]},{"category":"animals_and_nature","char":"🐌","name":"snail","keywords":["slow","animal","shell","かたつむり","でんでんむし","むし"]},{"category":"animals_and_nature","char":"🐞","name":"lady_beetle","keywords":["animal","insect","nature","ladybug","テントウムシ","むし"]},{"category":"animals_and_nature","char":"🐜","name":"ant","keywords":["animal","insect","nature","bug","アリ","むし"]},{"category":"animals_and_nature","char":"🦗","name":"grasshopper","keywords":["animal","cricket","chirp","コオロギ","バッタ","むし"]},{"category":"animals_and_nature","char":"🕷","name":"spider","keywords":["animal","arachnid","クモ","スパイダー","むし"]},{"category":"animals_and_nature","char":"🪲","name":"beetle","keywords":["animal","カブトムシ","つの","こんちゅう","むし"]},{"category":"animals_and_nature","char":"🪳","name":"cockroach","keywords":["animal","ゴキブリ","がいちゅう","むし"]},{"category":"animals_and_nature","char":"🪰","name":"fly","keywords":["animal","ハエ","ふけつ","がいちゅう","びょうげんきん","むし"]},{"category":"animals_and_nature","char":"🪱","name":"worm","keywords":["animal","ミミズ","きせいちゅう","むし"]},{"category":"animals_and_nature","char":"🦂","name":"scorpion","keywords":["animal","arachnid","サソリ","せいざ","むし"]},{"category":"animals_and_nature","char":"🦀","name":"crab","keywords":["animal","crustacean","カニ","どうぶつ","せいざ"]},{"category":"animals_and_nature","char":"🐍","name":"snake","keywords":["animal","evil","nature","hiss","python","ヘビ","どうぶつ","せいざ","どく","はちゅうるい"]},{"category":"animals_and_nature","char":"🦎","name":"lizard","keywords":["animal","nature","reptile","トカゲ","どうぶつ","はちゅうるい"]},{"category":"animals_and_nature","char":"🦖","name":"t-rex","keywords":["animal","nature","dinosaur","tyrannosaurus","extinct","ティラノサウルス","きょうりゅう"]},{"category":"animals_and_nature","char":"🦕","name":"sauropod","keywords":["animal","nature","dinosaur","brachiosaurus","brontosaurus","diplodocus","extinct","ディプロドクス","ブラキオサウルス","きょうりゅう","りゅうあしるい","そうしょくきょうりゅう"]},{"category":"animals_and_nature","char":"🐢","name":"turtle","keywords":["animal","slow","nature","tortoise","カメ","どうぶつ","はちゅうるい"]},{"category":"animals_and_nature","char":"🐠","name":"tropical_fish","keywords":["animal","swim","ocean","beach","nemo","どうぶつ","ねったいぎょ","さかな"]},{"category":"animals_and_nature","char":"🐟","name":"fish","keywords":["animal","food","nature","どうぶつ","せいざ","さかな"]},{"category":"animals_and_nature","char":"🐡","name":"blowfish","keywords":["animal","nature","food","sea","ocean","フグ","どうぶつ","さかな"]},{"category":"animals_and_nature","char":"🐬","name":"dolphin","keywords":["animal","nature","fish","sea","ocean","flipper","fins","beach","イルカ","どうぶつ"]},{"category":"animals_and_nature","char":"🦈","name":"shark","keywords":["animal","nature","fish","sea","ocean","jaws","fins","beach","サメ","どうぶつ","さかな"]},{"category":"animals_and_nature","char":"🐳","name":"whale","keywords":["animal","nature","sea","ocean","クジラ","どうぶつ","しおふき","しおふきクジラ"]},{"category":"animals_and_nature","char":"🐋","name":"whale2","keywords":["animal","nature","sea","ocean","クジラ","どうぶつ"]},{"category":"animals_and_nature","char":"🐊","name":"crocodile","keywords":["animal","nature","reptile","lizard","alligator","ワニ","どうぶつ","はちゅうるい"]},{"category":"animals_and_nature","char":"🐆","name":"leopard","keywords":["animal","nature","ヒョウ","どうぶつ"]},{"category":"animals_and_nature","char":"🦓","name":"zebra","keywords":["animal","nature","stripes","safari","シマウマ","しましま","どうぶつ"]},{"category":"animals_and_nature","char":"🐅","name":"tiger2","keywords":["animal","nature","roar","トラ","どうぶつ","とら"]},{"category":"animals_and_nature","char":"🐃","name":"water_buffalo","keywords":["animal","nature","ox","cow","バッファロー","どうぶつ","すいぎゅう","うし"]},{"category":"animals_and_nature","char":"🐂","name":"ox","keywords":["animal","cow","beef","ウシ","どうぶつ","せいざ","うし","おすうし"]},{"category":"animals_and_nature","char":"🐄","name":"cow2","keywords":["beef","ox","animal","nature","moo","milk","ウシ","どうぶつ","うし","めすうし"]},{"category":"animals_and_nature","char":"🦌","name":"deer","keywords":["animal","nature","horns","venison","シカ","どうぶつ","かお","しか"]},{"category":"animals_and_nature","char":"🐪","name":"dromedary_camel","keywords":["animal","hot","desert","hump","ラクダ","どうぶつ"]},{"category":"animals_and_nature","char":"🐫","name":"camel","keywords":["animal","nature","hot","desert","hump","フタコブ","フタコブラクダ","ラクダ","どうぶつ"]},{"category":"animals_and_nature","char":"🦒","name":"giraffe","keywords":["animal","nature","spots","safari","キリン","どうぶつ"]},{"category":"animals_and_nature","char":"🐘","name":"elephant","keywords":["animal","nature","nose","th","circus","ゾウ","どうぶつ","ぞう"]},{"category":"animals_and_nature","char":"🦏","name":"rhinoceros","keywords":["animal","nature","horn","サイ","どうぶつ","かお"]},{"category":"animals_and_nature","char":"🐐","name":"goat","keywords":["animal","nature","ヤギ","どうぶつ","やぎ","せいざ"]},{"category":"animals_and_nature","char":"🐏","name":"ram","keywords":["animal","sheep","nature","ヒツジ","どうぶつ","せいざ","おすひつじ","ひつじ"]},{"category":"animals_and_nature","char":"🐑","name":"sheep","keywords":["animal","nature","wool","shipit","ヒツジ","どうぶつ","ひつじ"]},{"category":"animals_and_nature","char":"🐎","name":"racehorse","keywords":["animal","gamble","luck","ウマ","じょうば","どうぶつ","けいば","うま"]},{"category":"animals_and_nature","char":"🐖","name":"pig2","keywords":["animal","nature","ブタ","どうぶつ","ぶた"]},{"category":"animals_and_nature","char":"🐀","name":"rat","keywords":["animal","mouse","rodent","ねずみ","ネズミ","ラット","どうぶつ"]},{"category":"animals_and_nature","char":"🐁","name":"mouse2","keywords":["animal","nature","rodent","ねずみ","ネズミ","ハツカネズミ","マウス","どうぶつ"]},{"category":"animals_and_nature","char":"🐓","name":"rooster","keywords":["animal","nature","chicken","おんどり","どうぶつ","とり"]},{"category":"animals_and_nature","char":"🦃","name":"turkey","keywords":["animal","bird","ターキー","しちめんちょう","どうぶつ","とり"]},{"category":"animals_and_nature","char":"🕊","name":"dove","keywords":["animal","bird","ハト","どうぶつ","へいわ","とり"]},{"category":"animals_and_nature","char":"🐕","name":"dog2","keywords":["animal","nature","friend","doge","pet","faithful","イヌ","ペット","どうぶつ","いぬ"]},{"category":"animals_and_nature","char":"🐩","name":"poodle","keywords":["dog","animal","101","nature","pet","プードル","ペット","どうぶつ","いぬ"]},{"category":"animals_and_nature","char":"🐈","name":"cat2","keywords":["animal","meow","pet","cats","ネコ","ペット","どうぶつ","ねこ"]},{"category":"animals_and_nature","char":"🐇","name":"rabbit2","keywords":["animal","nature","pet","magic","spring","うさぎ","ウサギ","ペット","どうぶつ"]},{"category":"animals_and_nature","char":"🐿","name":"chipmunk","keywords":["animal","nature","rodent","squirrel","りす","リス","どうぶつ"]},{"category":"animals_and_nature","char":"🦔","name":"hedgehog","keywords":["animal","nature","spiny","とげ","はりねずみ","ハリネズミ","どうぶつ"]},{"category":"animals_and_nature","char":"🦝","name":"raccoon","keywords":["animal","nature","アライグマ","ずるかしこい","どうぶつ","こうきしん","せんさくすき"]},{"category":"animals_and_nature","char":"🦙","name":"llama","keywords":["animal","nature","alpaca","アルパカ","グアナコ","ビクーナ","ラマ","どうぶつ","ようもう"]},{"category":"animals_and_nature","char":"🦛","name":"hippopotamus","keywords":["animal","nature","カバ","どうぶつ"]},{"category":"animals_and_nature","char":"🦘","name":"kangaroo","keywords":["animal","nature","australia","joey","hop","marsupial","オーストラリア","カンガルー","ジャンプ","ジョーイ","ゆうぶくろるい"]},{"category":"animals_and_nature","char":"🦡","name":"badger","keywords":["animal","nature","honey","アナグマ","ミツアナグマ","どうぶつ"]},{"category":"animals_and_nature","char":"🦢","name":"swan","keywords":["animal","nature","bird","ひな","みにくいアヒルのこ","しらとり","とり"]},{"category":"animals_and_nature","char":"🦚","name":"peacock","keywords":["animal","nature","peahen","bird","クジャク","ピーコック","くじゃく","はで","とり"]},{"category":"animals_and_nature","char":"🦜","name":"parrot","keywords":["animal","nature","bird","pirate","talk","オウム","しゃべる","かいぞく","とり"]},{"category":"animals_and_nature","char":"🦞","name":"lobster","keywords":["animal","nature","bisque","claws","seafood","ザリガニ","シーフード","はさみ","ビスク","ロブスター"]},{"category":"animals_and_nature","char":"🦠","name":"microbe","keywords":["amoeba","bacteria","germs","アメーバ","ウイルス","バクテリア","びせいぶつ","さいきん"]},{"category":"animals_and_nature","char":"🦟","name":"mosquito","keywords":["animal","nature","insect","malaria","ウイルス","マラリア","ねつ","びょうき","むし","か"]},{"category":"animals_and_nature","char":"🦬","name":"bison","keywords":["animal","nature","バイソン","バッファロー","どうぶつ","うし","むれ","やぎゅう"]},{"category":"animals_and_nature","char":"🦣","name":"mammoth","keywords":["animal","nature","マンモス","どうぶつ","きょだい","きば","ぜつめつ"]},{"category":"animals_and_nature","char":"🦫","name":"beaver","keywords":["animal","nature","ダムづくり","ビーバー","どうぶつ"]},{"category":"animals_and_nature","char":"🐻‍❄️","name":"polar_bear","keywords":["animal","nature","クマ","シロクマ","ホッキョクグマ","どうぶつ","ほっきょく","しろ"]},{"category":"animals_and_nature","char":"🦤","name":"dodo","keywords":["animal","nature","ドードー","モーリシャスとう","ぜつめつ","とべないとり","とり"]},{"category":"animals_and_nature","char":"🪶","name":"feather","keywords":["animal","nature","はね","うもう","かるい","とぶ","とり"]},{"category":"animals_and_nature","char":"🦭","name":"seal","keywords":["animal","nature","アザラシ","アシカ","トド","どうぶつ","かいじゅう"]},{"category":"animals_and_nature","char":"🐾","name":"paw_prints","keywords":["animal","tracking","footprints","dog","cat","pet","feet","どうぶつ","いぬ","ねこ","にくだま","あしあと"]},{"category":"animals_and_nature","char":"🐉","name":"dragon","keywords":["animal","myth","nature","chinese","green","ドラゴン","どうぶつ","りゅう","りゅう"]},{"category":"animals_and_nature","char":"🐲","name":"dragon_face","keywords":["animal","myth","nature","chinese","green","ドラゴン","ドラゴンのかお","どうぶつ","りゅう","かお","りゅう"]},{"category":"animals_and_nature","char":"🦧","name":"orangutan","keywords":["animal","nature","オランウータン","サル","どうぶつ","もりのひと","さる"]},{"category":"animals_and_nature","char":"🦮","name":"guide_dog","keywords":["animal","nature","アクセシビリティ","いぬ","もうどうけん","しかく","ほじょけん","さわがい"]},{"category":"animals_and_nature","char":"🐕‍🦺","name":"service_dog","keywords":["animal","nature","アクセシビリティ","サービス","かいじょいぬ","いぬ","ほじょ"]},{"category":"animals_and_nature","char":"🦥","name":"sloth","keywords":["animal","nature","ナマケモノ","のんびり","ものぐさ","どうぶつ"]},{"category":"animals_and_nature","char":"🦦","name":"otter","keywords":["animal","nature","カワウソ","ラッコ","どうぶつ","あそびすき","さかなをたべる"]},{"category":"animals_and_nature","char":"🦨","name":"skunk","keywords":["animal","nature","スカンク","どうぶつ","あくしゅう"]},{"category":"animals_and_nature","char":"🦩","name":"flamingo","keywords":["animal","nature","ピンクいろ","フラミンゴ","どうぶつ","ねったい","とり"]},{"category":"animals_and_nature","char":"🌵","name":"cactus","keywords":["vegetable","plant","nature","サボテン","しょくぶつ"]},{"category":"animals_and_nature","char":"🎄","name":"christmas_tree","keywords":["festival","vacation","december","xmas","celebration","クリスマス","クリスマスイブ","クリスマスツリー","ツリー"]},{"category":"animals_and_nature","char":"🌲","name":"evergreen_tree","keywords":["plant","nature","じょうりょくじゅ","き"]},{"category":"animals_and_nature","char":"🌳","name":"deciduous_tree","keywords":["plant","nature","き","らくようじゅ"]},{"category":"animals_and_nature","char":"🌴","name":"palm_tree","keywords":["plant","vegetable","nature","summer","beach","mojito","tropical","ヤシ","ヤシのき","き"]},{"category":"animals_and_nature","char":"🌱","name":"seedling","keywords":["plant","nature","grass","lawn","spring","ふたば","しんめ","め"]},{"category":"animals_and_nature","char":"🌿","name":"herb","keywords":["vegetable","plant","medicine","weed","grass","lawn","ハーブ","くさ","は","やくそう"]},{"category":"animals_and_nature","char":"☘","name":"shamrock","keywords":["vegetable","plant","nature","irish","clover","クローバー","くさ","は"]},{"category":"animals_and_nature","char":"🍀","name":"four_leaf_clover","keywords":["vegetable","plant","nature","lucky","irish","4","クローバー","よっつはのクローバー","くさ","は"]},{"category":"animals_and_nature","char":"🎍","name":"bamboo","keywords":["plant","nature","vegetable","panda","pine_decoration","おいわい","わ","にっぽん","しょうがつ","たけ","かどまつ"]},{"category":"animals_and_nature","char":"🎋","name":"tanabata_tree","keywords":["plant","nature","branch","summer","たなばた","わ","にっぽん","たんざく"]},{"category":"animals_and_nature","char":"🍃","name":"leaves","keywords":["nature","plant","tree","vegetable","grass","lawn","spring","このは","おちば","かぜ","かぜにゆれるは"]},{"category":"animals_and_nature","char":"🍂","name":"fallen_leaf","keywords":["nature","plant","vegetable","leaves","このは","かれは","おちば"]},{"category":"animals_and_nature","char":"🍁","name":"maple_leaf","keywords":["nature","plant","vegetable","ca","fall","かえで","もみじ","こうよう"]},{"category":"animals_and_nature","char":"🌾","name":"ear_of_rice","keywords":["nature","plant","ススキ","いね","いなほ","くさ"]},{"category":"animals_and_nature","char":"🌺","name":"hibiscus","keywords":["plant","vegetable","flowers","beach","ハイビスカス","はな"]},{"category":"animals_and_nature","char":"🌻","name":"sunflower","keywords":["nature","plant","fall","ヒマワリ","はな"]},{"category":"animals_and_nature","char":"🌹","name":"rose","keywords":["flowers","valentines","love","spring","バラ","はな"]},{"category":"animals_and_nature","char":"🥀","name":"wilted_flower","keywords":["plant","nature","flower","しおれた","しおれたはな","はな"]},{"category":"animals_and_nature","char":"🌷","name":"tulip","keywords":["flowers","plant","nature","summer","spring","チューリップ","はな"]},{"category":"animals_and_nature","char":"🌼","name":"blossom","keywords":["nature","flowers","yellow","さいたはな","はな","かいか"]},{"category":"animals_and_nature","char":"🌸","name":"cherry_blossom","keywords":["nature","plant","spring","flower","さくら","はな"]},{"category":"animals_and_nature","char":"💐","name":"bouquet","keywords":["flowers","nature","spring","ブーケ","はな","はなたば"]},{"category":"animals_and_nature","char":"🍄","name":"mushroom","keywords":["plant","vegetable","きのこ","キノコ","マッシュルーム","どく","やさい"]},{"category":"animals_and_nature","char":"🪴","name":"potted_plant","keywords":["plant","ベランダ","うえきばち","しょくぶつ","みずやり","みどり","なえ","はちうえ"]},{"category":"animals_and_nature","char":"🌰","name":"chestnut","keywords":["food","squirrel","くり","やさい"]},{"category":"animals_and_nature","char":"🎃","name":"jack_o_lantern","keywords":["halloween","light","pumpkin","creepy","fall","かぼちゃ","ハロウィーン","ハロウィン","ハロウィンかぼちゃ"]},{"category":"animals_and_nature","char":"🐚","name":"shell","keywords":["nature","sea","beach","どうぶつ","まきがい","かい"]},{"category":"animals_and_nature","char":"🕸","name":"spider_web","keywords":["animal","insect","arachnid","silk","クモ","クモのす"]},{"category":"animals_and_nature","char":"🌎","name":"earth_americas","keywords":["globe","world","USA","international","アメリカ","アメリカたいりく","ちきゅう","ちきゅう(アメリカたいりく)"]},{"category":"animals_and_nature","char":"🌍","name":"earth_africa","keywords":["globe","world","international","アフリカ","ヨーロッパ","ちきゅう","ちきゅう(ヨーロッパとアフリカ)"]},{"category":"animals_and_nature","char":"🌏","name":"earth_asia","keywords":["globe","world","east","international","アジア","オーストラリア","ちきゅう","ちきゅう(アジアとオーストラリア)"]},{"category":"animals_and_nature","char":"🪐","name":"ringed_planet","keywords":["saturn","リング","どせい","てんたい","わくせい","ほし","たまきのあるわくせい"]},{"category":"animals_and_nature","char":"🌕","name":"full_moon","keywords":["nature","yellow","twilight","planet","space","night","evening","sleep","つき","まんげつ"]},{"category":"animals_and_nature","char":"🌖","name":"waning_gibbous_moon","keywords":["nature","twilight","planet","space","night","evening","sleep","waxing_gibbous_moon","ねまちのつき","いまちのつき","つき"]},{"category":"animals_and_nature","char":"🌗","name":"last_quarter_moon","keywords":["nature","twilight","planet","space","night","evening","sleep","かげん","かげんのつき","はんつき","つき"]},{"category":"animals_and_nature","char":"🌘","name":"waning_crescent_moon","keywords":["nature","twilight","planet","space","night","evening","sleep","みかづき","つき","ゆうめいげつ"]},{"category":"animals_and_nature","char":"🌑","name":"new_moon","keywords":["nature","twilight","planet","space","night","evening","sleep","しんげつ","つき"]},{"category":"animals_and_nature","char":"🌒","name":"waxing_crescent_moon","keywords":["nature","twilight","planet","space","night","evening","sleep","みかづき","はつつき","つき"]},{"category":"animals_and_nature","char":"🌓","name":"first_quarter_moon","keywords":["nature","twilight","planet","space","night","evening","sleep","じょうげん","じょうげんのつき","はんつき","つき"]},{"category":"animals_and_nature","char":"🌔","name":"waxing_gibbous_moon","keywords":["nature","night","sky","gray","twilight","planet","space","evening","sleep","じゅうさんやつき","じゅうにちよるのつき","つき"]},{"category":"animals_and_nature","char":"🌚","name":"new_moon_with_face","keywords":["nature","twilight","planet","space","night","evening","sleep","しんげつ","つき","かお","かおのあるしんげつ"]},{"category":"animals_and_nature","char":"🌝","name":"full_moon_with_face","keywords":["nature","twilight","planet","space","night","evening","sleep","つき","かお","かおのあるまんげつ"]},{"category":"animals_and_nature","char":"🌛","name":"first_quarter_moon_with_face","keywords":["nature","twilight","planet","space","night","evening","sleep","じょうげんのつき","つき","かお","かおのあるじょうげんのつき"]},{"category":"animals_and_nature","char":"🌜","name":"last_quarter_moon_with_face","keywords":["nature","twilight","planet","space","night","evening","sleep","かげんのつき","つき","かお","かおのあるかげんのつき"]},{"category":"animals_and_nature","char":"🌞","name":"sun_with_face","keywords":["nature","morning","sky","たいよう","かお","かおのあるたいよう"]},{"category":"animals_and_nature","char":"🌙","name":"crescent_moon","keywords":["night","sleep","sky","evening","magic","みかづき","つき"]},{"category":"animals_and_nature","char":"⭐","name":"star","keywords":["night","yellow","スター","ほし"]},{"category":"animals_and_nature","char":"🌟","name":"star2","keywords":["night","sparkle","awesome","good","magic","きらきら","きらきらほし","スター","ほし","かがやき"]},{"category":"animals_and_nature","char":"💫","name":"dizzy","keywords":["star","sparkle","shoot","magic","くらくら","ほし","めがまわる"]},{"category":"animals_and_nature","char":"✨","name":"sparkles","keywords":["stars","shine","shiny","cool","awesome","good","magic","きらきら","スター","ぴかぴか","ほし"]},{"category":"animals_and_nature","char":"☄","name":"comet","keywords":["space","すいせい","うちゅう","すいせい"]},{"category":"animals_and_nature","char":"☀️","name":"sunny","keywords":["weather","nature","brightness","summer","beach","spring","たいよう","はれ"]},{"category":"animals_and_nature","char":"🌤","name":"sun_behind_small_cloud","keywords":["weather","てんき","たいよう","はれ","はれときどきくもり","くも"]},{"category":"animals_and_nature","char":"⛅","name":"partly_sunny","keywords":["weather","nature","cloudy","morning","fall","spring","てんき","たいよう","くもり","くもりときどきはれ","くも"]},{"category":"animals_and_nature","char":"🌥","name":"sun_behind_large_cloud","keywords":["weather","てんき","たいよう","くもり","くもりいちじはれ","くも"]},{"category":"animals_and_nature","char":"🌦","name":"sun_behind_rain_cloud","keywords":["weather","てんき","てんきう","たいよう","くもり","あめじ々はれ","くも"]},{"category":"animals_and_nature","char":"☁️","name":"cloud","keywords":["weather","sky","てんき","くもり","くも"]},{"category":"animals_and_nature","char":"🌧","name":"cloud_with_rain","keywords":["weather","てんき","あめ","あまぐも","くも"]},{"category":"animals_and_nature","char":"⛈","name":"cloud_with_lightning_and_rain","keywords":["weather","lightning","てんき","いなづま","あめ","くも","かみなり","らいう"]},{"category":"animals_and_nature","char":"🌩","name":"cloud_with_lightning","keywords":["weather","thunder","てんき","いなづま","くも","かみなり","らいうん"]},{"category":"animals_and_nature","char":"⚡","name":"zap","keywords":["thunder","weather","lightning bolt","fast","きけん","いなづま","かみなり","でんき","こうでんあつ"]},{"category":"animals_and_nature","char":"🔥","name":"fire","keywords":["hot","cook","flame","ファイアー","ファイヤー","ひ","ほのお"]},{"category":"animals_and_nature","char":"💥","name":"boom","keywords":["bomb","explode","explosion","collision","blown","どんっ","しょうげき","しょうとつ"]},{"category":"animals_and_nature","char":"❄️","name":"snowflake","keywords":["winter","season","cold","weather","christmas","xmas","けっしょう","ゆき","ゆきのけっしょう"]},{"category":"animals_and_nature","char":"🌨","name":"cloud_with_snow","keywords":["weather","てんき","ゆき","ゆきぐも","くも"]},{"category":"animals_and_nature","char":"⛄","name":"snowman","keywords":["winter","season","cold","weather","christmas","xmas","frozen","without_snow","ゆき","ゆきだるま"]},{"category":"animals_and_nature","char":"☃","name":"snowman_with_snow","keywords":["winter","season","cold","weather","christmas","xmas","frozen","ゆき","ゆきだるま","ゆきだるまとゆき"]},{"category":"animals_and_nature","char":"🌬","name":"wind_face","keywords":["gust","air","かお","かおのあるかぜ","かぜ"]},{"category":"animals_and_nature","char":"💨","name":"dash","keywords":["wind","air","fast","shoo","fart","smoke","puff","ダッシュ","いそぐ","いそげ","はしる"]},{"category":"animals_and_nature","char":"🌪","name":"tornado","keywords":["weather","cyclone","twister","てんき","とっぷう","たつまき"]},{"category":"animals_and_nature","char":"🌫","name":"fog","keywords":["weather","てんき","きり"]},{"category":"animals_and_nature","char":"☂","name":"open_umbrella","keywords":["weather","spring","かさ","てんき","もちもの","あめ"]},{"category":"animals_and_nature","char":"☔","name":"umbrella","keywords":["rainy","weather","spring","かさ","かさとあめ","てんき","もちもの","あめ"]},{"category":"animals_and_nature","char":"💧","name":"droplet","keywords":["water","drip","faucet","spring","しずく","たらーっ","すいてき","あせ","なみだ"]},{"category":"animals_and_nature","char":"💦","name":"sweat_drops","keywords":["water","drip","oops","あせあせ","あせ"]},{"category":"animals_and_nature","char":"🌊","name":"ocean","keywords":["sea","water","wave","nature","tsunami","disaster","てんき","なみ","はろう","うみ"]},{"category":"animals_and_nature","char":"🪷","name":"lotus","keywords":["ハスのはな","ヒンドゥーきょう","ぶっきょう","せいじょう","はな"]},{"category":"animals_and_nature","char":"🪸","name":"coral","keywords":["サンゴ","うみ","さんごしょう"]},{"category":"animals_and_nature","char":"🪹","name":"empty_nest","keywords":["えいそう","す","すづくり","からっぽのす"]},{"category":"animals_and_nature","char":"🪺","name":"nest_with_eggs","keywords":["えいそう","す","すづくり","とりのたまごとす"]},{"category":"food_and_drink","char":"🍏","name":"green_apple","keywords":["fruit","nature","アップル","りんご","くだもの","あおリンゴ"]},{"category":"food_and_drink","char":"🍎","name":"apple","keywords":["fruit","mac","school","アップル","りんご","くだもの","あかリンゴ"]},{"category":"food_and_drink","char":"🍐","name":"pear","keywords":["fruit","nature","food","ナシ","くだもの","ようナシ","せいようナシ"]},{"category":"food_and_drink","char":"🍊","name":"tangerine","keywords":["food","fruit","nature","orange","オレンジ","みかん","くだもの"]},{"category":"food_and_drink","char":"🍋","name":"lemon","keywords":["fruit","nature","レモン","くだもの"]},{"category":"food_and_drink","char":"🍌","name":"banana","keywords":["fruit","food","monkey","バナナ","くだもの"]},{"category":"food_and_drink","char":"🍉","name":"watermelon","keywords":["fruit","food","picnic","summer","スイカ","くだもの","やさい"]},{"category":"food_and_drink","char":"🍇","name":"grapes","keywords":["fruit","food","wine","グレープ","ぶどう","くだもの"]},{"category":"food_and_drink","char":"🍓","name":"strawberry","keywords":["fruit","food","nature","いちご","ストロベリー","くだもの"]},{"category":"food_and_drink","char":"🍈","name":"melon","keywords":["fruit","nature","food","メロン","くだもの","やさい"]},{"category":"food_and_drink","char":"🍒","name":"cherries","keywords":["food","fruit","さくらんぼ","チェリー","くだもの"]},{"category":"food_and_drink","char":"🍑","name":"peach","keywords":["fruit","nature","food","ピーチ","くだもの","もも"]},{"category":"food_and_drink","char":"🍍","name":"pineapple","keywords":["fruit","nature","food","トロピカル","パイナップル","パイン","くだもの"]},{"category":"food_and_drink","char":"🥥","name":"coconut","keywords":["fruit","nature","food","palm","ココナツ","ココナッツ","ヤシのみ","くだもの"]},{"category":"food_and_drink","char":"🥝","name":"kiwi_fruit","keywords":["fruit","food","キウイ","キウイフルーツ","くだもの"]},{"category":"food_and_drink","char":"🥭","name":"mango","keywords":["fruit","food","tropical","トロピカル","マンゴー","くだもの"]},{"category":"food_and_drink","char":"🥑","name":"avocado","keywords":["fruit","food","アボカド","くだもの","やさい"]},{"category":"food_and_drink","char":"🥦","name":"broccoli","keywords":["fruit","food","vegetable","ブロッコリー","やさい"]},{"category":"food_and_drink","char":"🍅","name":"tomato","keywords":["fruit","vegetable","nature","food","トマト","くだもの","やさい"]},{"category":"food_and_drink","char":"🍆","name":"eggplant","keywords":["vegetable","nature","food","aubergine","ナス","やさい"]},{"category":"food_and_drink","char":"🥒","name":"cucumber","keywords":["fruit","food","pickle","キュウリ","ピクルス","つけぶつ","やさい"]},{"category":"food_and_drink","char":"🫐","name":"blueberries","keywords":["fruit","food","ビルベリー","ブルーベリー","ベリー","くだもの"]},{"category":"food_and_drink","char":"🫒","name":"olive","keywords":["fruit","food","オリーブ","み","かじつ"]},{"category":"food_and_drink","char":"🫑","name":"bell_pepper","keywords":["fruit","food","とうがらし","パプリカ","ピーマン","あかピーマン","やさい"]},{"category":"food_and_drink","char":"🥕","name":"carrot","keywords":["vegetable","food","orange","ニンジン","にんじん","やさい"]},{"category":"food_and_drink","char":"🌶","name":"hot_pepper","keywords":["food","spicy","chilli","chili","とうがらし","つらい","やさい","こうしんりょう"]},{"category":"food_and_drink","char":"🥔","name":"potato","keywords":["food","tuber","vegatable","starch","イモ","ジャガイモ","ポテト","やさい"]},{"category":"food_and_drink","char":"🌽","name":"corn","keywords":["food","vegetable","plant","コーン","とうもろこし","やさい"]},{"category":"food_and_drink","char":"🥬","name":"leafy_greens","keywords":["food","vegetable","plant","bok choy","cabbage","kale","lettuce","キャベツ","ケール","チンゲンさい","レタス","はやさい"]},{"category":"food_and_drink","char":"🍠","name":"sweet_potato","keywords":["food","nature","サツマイモ","やきいも","いも"]},{"category":"food_and_drink","char":"🥜","name":"peanuts","keywords":["food","nut","ナッツ","ピーナッツ","らっかせい","やさい"]},{"category":"food_and_drink","char":"🧄","name":"garlic","keywords":["food","におい","ニンニク","やくみ","やさい","かおり"]},{"category":"food_and_drink","char":"🧅","name":"onion","keywords":["food","タマネギ","ねぎ","たまねぎ","やくみ","やさい"]},{"category":"food_and_drink","char":"🍯","name":"honey_pot","keywords":["bees","sweet","kitchen","はちみつ","ハニー","あまい"]},{"category":"food_and_drink","char":"🥐","name":"croissant","keywords":["food","bread","french","クロワッサン","パン","フランス","ベーカリー"]},{"category":"food_and_drink","char":"🍞","name":"bread","keywords":["food","wheat","breakfast","toast","パン","ベーカリー","しょくパン"]},{"category":"food_and_drink","char":"🥖","name":"baguette_bread","keywords":["food","bread","french","バゲット","パン","フランスパン","ベーカリー"]},{"category":"food_and_drink","char":"🥯","name":"bagel","keywords":["food","bread","bakery","schmear","クリームチーズ","パンや","ベーカリー","ベーグル"]},{"category":"food_and_drink","char":"🥨","name":"pretzel","keywords":["food","bread","twisted","ねじり","パン","プレッツェル"]},{"category":"food_and_drink","char":"🧀","name":"cheese","keywords":["food","chadder","チーズ"]},{"category":"food_and_drink","char":"🥚","name":"egg","keywords":["food","chicken","breakfast","たまご"]},{"category":"food_and_drink","char":"🥓","name":"bacon","keywords":["food","breakfast","pork","pig","meat","ベーコン","にく"]},{"category":"food_and_drink","char":"🥩","name":"steak","keywords":["food","cow","meat","cut","chop","lambchop","porkchop","ステーキ","ステーキにく","ビーフ","ポーク","ラム","にく"]},{"category":"food_and_drink","char":"🥞","name":"pancakes","keywords":["food","breakfast","flapjacks","hotcakes","パンケーキ","ホットケーキ"]},{"category":"food_and_drink","char":"🍗","name":"poultry_leg","keywords":["food","meat","drumstick","bird","chicken","turkey","チキン","ももにく","にく","ほねつきにく","にわとりももにく"]},{"category":"food_and_drink","char":"🍖","name":"meat_on_bone","keywords":["good","food","drumstick","ももにく","にく","ほねつきにく"]},{"category":"food_and_drink","char":"🦴","name":"bone","keywords":["skeleton","ほね","こっかく"]},{"category":"food_and_drink","char":"🍤","name":"fried_shrimp","keywords":["food","animal","appetizer","summer","エビ","エビフライ","フライ","てんぷら"]},{"category":"food_and_drink","char":"🍳","name":"fried_egg","keywords":["food","breakfast","kitchen","egg","フライパン","たまご","りょうり","めだまやき"]},{"category":"food_and_drink","char":"🍔","name":"hamburger","keywords":["meat","fast food","beef","cheeseburger","mcdonalds","burger king","バーガー","ハンバーガー"]},{"category":"food_and_drink","char":"🍟","name":"fries","keywords":["chips","snack","fast food","フライドポテト","フレンチフライ","ポテト","ポテトフライ"]},{"category":"food_and_drink","char":"🥙","name":"stuffed_flatbread","keywords":["food","flatbread","stuffed","gyro","ケバブ","ケバブサンド","ラップサンド"]},{"category":"food_and_drink","char":"🌭","name":"hotdog","keywords":["food","frankfurter","ソーセージ","フランクフルト","ホットドッグ"]},{"category":"food_and_drink","char":"🍕","name":"pizza","keywords":["food","party","チーズ","ピザ","ピッツァ"]},{"category":"food_and_drink","char":"🥪","name":"sandwich","keywords":["food","lunch","bread","サンドイッチ","パン"]},{"category":"food_and_drink","char":"🥫","name":"canned_food","keywords":["food","soup","かん","かんづめ","ひじょうしょく"]},{"category":"food_and_drink","char":"🍝","name":"spaghetti","keywords":["food","italian","noodle","スパゲッティ","スパゲッティー","パスタ"]},{"category":"food_and_drink","char":"🌮","name":"taco","keywords":["food","mexican","タコス","メキシカン"]},{"category":"food_and_drink","char":"🌯","name":"burrito","keywords":["food","mexican","ソフトタコス","ブリトー","メキシカン"]},{"category":"food_and_drink","char":"🥗","name":"green_salad","keywords":["food","healthy","lettuce","グリーン","グリーンサラダ","サラダ"]},{"category":"food_and_drink","char":"🥘","name":"shallow_pan_of_food","keywords":["food","cooking","casserole","paella","シーフード","パエリア"]},{"category":"food_and_drink","char":"🍜","name":"ramen","keywords":["food","japanese","noodle","chopsticks","どんぶり","ラーメン","あつあつ"]},{"category":"food_and_drink","char":"🍲","name":"stew","keywords":["food","meat","soup","シチュー","なべ","にもの","にこみ"]},{"category":"food_and_drink","char":"🍥","name":"fish_cake","keywords":["food","japan","sea","beach","narutomaki","pink","swirl","kamaboko","surimi","ramen","なると","うずまき","ねりもの"]},{"category":"food_and_drink","char":"🥠","name":"fortune_cookie","keywords":["food","prophecy","おみくじ","フォーチュンクッキー"]},{"category":"food_and_drink","char":"🍣","name":"sushi","keywords":["food","fish","japanese","rice","おすし","すし"]},{"category":"food_and_drink","char":"🍱","name":"bento","keywords":["food","japanese","box","まくのうち","べんとう","えきべん"]},{"category":"food_and_drink","char":"🍛","name":"curry","keywords":["food","spicy","hot","indian","カレー","カレーライス","ライス"]},{"category":"food_and_drink","char":"🍙","name":"rice_ball","keywords":["food","japanese","おにぎり","おむすび","べい"]},{"category":"food_and_drink","char":"🍚","name":"rice","keywords":["food","china","asian","ごはん","ライス","べい"]},{"category":"food_and_drink","char":"🍘","name":"rice_cracker","keywords":["food","japanese","おかし","せんべい"]},{"category":"food_and_drink","char":"🍢","name":"oden","keywords":["food","japanese","おでん"]},{"category":"food_and_drink","char":"🍡","name":"dango","keywords":["food","dessert","sweet","japanese","barbecue","meat","おだんご","だんご","わがし"]},{"category":"food_and_drink","char":"🍧","name":"shaved_ice","keywords":["hot","dessert","summer","アイス","かきごおり","デザート","こおり"]},{"category":"food_and_drink","char":"🍨","name":"ice_cream","keywords":["food","hot","dessert","アイス","アイスクリーム","スイーツ","デザート"]},{"category":"food_and_drink","char":"🍦","name":"icecream","keywords":["food","hot","dessert","summer","アイス","スイーツ","ソフト","ソフトクリーム","デザート"]},{"category":"food_and_drink","char":"🥧","name":"pie","keywords":["food","dessert","pastry","おかし","タルト","パイ"]},{"category":"food_and_drink","char":"🍰","name":"cake","keywords":["food","dessert","おかし","ケーキ","ショートケーキ","スイーツ","デザート"]},{"category":"food_and_drink","char":"🧁","name":"cupcake","keywords":["food","dessert","bakery","sweet","おかし","カップケーキ","ケーキ","スイーツ","ベーカリー"]},{"category":"food_and_drink","char":"🥮","name":"moon_cake","keywords":["food","autumn","おいわい","ちゅうしゅうぶし","ちゅうかかし","げっぺい","あき"]},{"category":"food_and_drink","char":"🎂","name":"birthday","keywords":["food","dessert","cake","ケーキ","バースデー","バースデーケーキ","たんじょうび"]},{"category":"food_and_drink","char":"🍮","name":"custard","keywords":["dessert","food","おかし","スイーツ","デザート","プリン"]},{"category":"food_and_drink","char":"🍬","name":"candy","keywords":["snack","dessert","sweet","lolly","あめ","おかし","キャンディ"]},{"category":"food_and_drink","char":"🍭","name":"lollipop","keywords":["food","snack","candy","sweet","あめ","おかし","キャンディ","ぺろぺろキャンディ","ぼうつきキャンディ"]},{"category":"food_and_drink","char":"🍫","name":"chocolate_bar","keywords":["food","snack","dessert","sweet","おかし","スイーツ","チョコ","チョコレート","デザート"]},{"category":"food_and_drink","char":"🍿","name":"popcorn","keywords":["food","movie theater","films","snack","おかし","ポップコーン"]},{"category":"food_and_drink","char":"🥟","name":"dumpling","keywords":["food","empanada","pierogi","potsticker","ぎょうざ","てんしん","にくまん"]},{"category":"food_and_drink","char":"🍩","name":"doughnut","keywords":["food","dessert","snack","sweet","donut","おかし","スイーツ","デザート","ドーナツ"]},{"category":"food_and_drink","char":"🍪","name":"cookie","keywords":["food","snack","oreo","chocolate","sweet","dessert","おかし","クッキー","スイーツ","デザート"]},{"category":"food_and_drink","char":"🧇","name":"waffle","keywords":["food","ふんわり","ワッフル","やきかし"]},{"category":"food_and_drink","char":"🧆","name":"falafel","keywords":["food","コロッケ","ヒヨコマメ","ファラフェル","にくだんご"]},{"category":"food_and_drink","char":"🧈","name":"butter","keywords":["food","バター","にゅうせいひん"]},{"category":"food_and_drink","char":"🦪","name":"oyster","keywords":["food","オイスター","カキ","シーフード","かき","なまガキ","かい","ぎょかい"]},{"category":"food_and_drink","char":"🫓","name":"flatbread","keywords":["food","アレパ","ナン","パン","ピタパン","フラットブレッド","ラバシ"]},{"category":"food_and_drink","char":"🫔","name":"tamale","keywords":["food","タマル","タマレス","ちまき","メキシカン"]},{"category":"food_and_drink","char":"🫕","name":"fondue","keywords":["food","スイス","チーズ","チョコ","フォンデュ","とかす","なべ"]},{"category":"food_and_drink","char":"🥛","name":"milk_glass","keywords":["beverage","drink","cow","コップ","ミルク","ぎゅうにゅう","ぎゅうにゅういりのコップ"]},{"category":"food_and_drink","char":"🍺","name":"beer","keywords":["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze","ジョッキ","バー","ビール","ビールジョッキ","いざかや"]},{"category":"food_and_drink","char":"🍻","name":"beers","keywords":["relax","beverage","drink","drunk","party","pub","summer","alcohol","booze","ジョッキ","ビール","ビールでかんぱい","かんぱい","えんかい"]},{"category":"food_and_drink","char":"🥂","name":"clinking_glasses","keywords":["beverage","drink","party","alcohol","celebrate","cheers","wine","champagne","toast","グラスでかんぱい","シャンパン","シャンペン","スパークリングワイン","スプマンテ","かんぱい"]},{"category":"food_and_drink","char":"🍷","name":"wine_glass","keywords":["drink","beverage","drunk","alcohol","booze","グラス","バー","ワイン","ワイングラス"]},{"category":"food_and_drink","char":"🥃","name":"tumbler_glass","keywords":["drink","beverage","drunk","alcohol","liquor","booze","bourbon","scotch","whisky","glass","shot","ウィスキー","ウイスキー","グラス","タンブラー","タンブラーグラス"]},{"category":"food_and_drink","char":"🍸","name":"cocktail","keywords":["drink","drunk","alcohol","beverage","booze","mojito","カクテル","カクテルグラス","グラス","バー"]},{"category":"food_and_drink","char":"🍹","name":"tropical_drink","keywords":["beverage","cocktail","summer","beach","alcohol","booze","mojito","カクテル","トロピカル","トロピカルドリンク","バー"]},{"category":"food_and_drink","char":"🍾","name":"champagne","keywords":["drink","wine","bottle","celebration","シャンパン","シャンペン","スパークリングワイン","スプマンテ","ボトル","あわ"]},{"category":"food_and_drink","char":"🍶","name":"sake","keywords":["wine","drink","drunk","beverage","japanese","alcohol","booze","とっくり","にっぽんしゅ","さけ"]},{"category":"food_and_drink","char":"🍵","name":"tea","keywords":["drink","bowl","breakfast","green","british","おちゃ","ゆのみ","りょくちゃ","のみもの"]},{"category":"food_and_drink","char":"🥤","name":"cup_with_straw","keywords":["drink","soda","コップ","ジュース","ストローカップ"]},{"category":"food_and_drink","char":"☕","name":"coffee","keywords":["beverage","caffeine","latte","espresso","コーヒー","ホットドリンク","あたたかいのみもの","こうちゃ","のみもの"]},{"category":"food_and_drink","char":"🫖","name":"teapot","keywords":["おちゃ","ティーポット","きゅうす","ちゃき","のみもの"]},{"category":"food_and_drink","char":"🧋","name":"bubble_tea","keywords":["tapioca","おちゃ","タピオカ","タピオカドリンク","ドリンク","バブルティー","ミルクティー"]},{"category":"food_and_drink","char":"🍼","name":"baby_bottle","keywords":["food","container","milk","ほにゅうびん","ミルク","あかちゃん"]},{"category":"food_and_drink","char":"🧃","name":"beverage_box","keywords":["food","drink","ジュース","ドリンク","かみパックいんりょう","のみもの"]},{"category":"food_and_drink","char":"🧉","name":"mate","keywords":["food","drink","おちゃ","ドリンク","マテちゃ","のみもの"]},{"category":"food_and_drink","char":"🧊","name":"ice_cube","keywords":["food","つめたい","こおり","すみこおり"]},{"category":"food_and_drink","char":"🧂","name":"salt","keywords":["condiment","shaker","しお","ふりかけようき","やくみ","ちょうみりょう"]},{"category":"food_and_drink","char":"🥄","name":"spoon","keywords":["cutlery","kitchen","tableware","カトラリー","スプーン","しょっき"]},{"category":"food_and_drink","char":"🍴","name":"fork_and_knife","keywords":["cutlery","kitchen","カトラリー","ナイフ","ナイフとフォーク","フォーク","レストラン"]},{"category":"food_and_drink","char":"🍽","name":"plate_with_cutlery","keywords":["food","eat","meal","lunch","dinner","restaurant","おさら","ナイフ","ナイフとフォークとさら","フォーク","さら"]},{"category":"food_and_drink","char":"🥣","name":"bowl_with_spoon","keywords":["food","breakfast","cereal","oatmeal","porridge","おかゆ","シリアル","ボウルとスプーン","ちょうしょく"]},{"category":"food_and_drink","char":"🥡","name":"takeout_box","keywords":["food","leftovers","テイクアウトべんとう","ちゅうか","べんとう","もちかえり"]},{"category":"food_and_drink","char":"🥢","name":"chopsticks","keywords":["food","おはし","はし"]},{"category":"food_and_drink","char":"🫗","name":"pouring_liquid","keywords":["ガラス","コップからそそぐ","こぼす","えきたい","のみもの"]},{"category":"food_and_drink","char":"🫘","name":"beans","keywords":["レッドキドニー","あずき","まめ","きんときまめ"]},{"category":"food_and_drink","char":"🫙","name":"jar","keywords":["びん","ほぞん","ようき","びん","そら","ちょうみりょう"]},{"category":"activity","char":"⚽","name":"soccer","keywords":["sports","football","サッカー","サッカーボール","スポーツ","ボール"]},{"category":"activity","char":"🏀","name":"basketball","keywords":["sports","balls","NBA","スポーツ","バスケ","バスケットボール","ボール"]},{"category":"activity","char":"🏈","name":"football","keywords":["sports","balls","NFL","アメフト","アメリカンフットボール","スポーツ","ボール"]},{"category":"activity","char":"⚾","name":"baseball","keywords":["sports","balls","スポーツ","ボール","やきゅう"]},{"category":"activity","char":"🥎","name":"softball","keywords":["sports","balls","スポーツ","ソフトボール","ボール","へたなげ"]},{"category":"activity","char":"🎾","name":"tennis","keywords":["sports","balls","green","スポーツ","テニス","テニスボール","ボール","ラケット"]},{"category":"activity","char":"🏐","name":"volleyball","keywords":["sports","balls","スポーツ","バレー","バレーボール","ボール"]},{"category":"activity","char":"🏉","name":"rugby_football","keywords":["sports","team","スポーツ","ボール","ラグビー","ラグビーボール"]},{"category":"activity","char":"🥏","name":"flying_disc","keywords":["sports","frisbee","ultimate","アルティメット","フライングディスク","フリスビー"]},{"category":"activity","char":"🎱","name":"8ball","keywords":["pool","hobby","game","luck","magic","エイトボール","スポーツ","ビリヤード","ボール"]},{"category":"activity","char":"⛳","name":"golf","keywords":["sports","business","flag","hole","summer","ゴルフ","スポーツ","フラグ","フラッグ","ホール","はた"]},{"category":"activity","char":"🏌️‍♀️","name":"golfing_woman","keywords":["sports","business","woman","female","ゴルフ","ゴルファー","ゴルフをするひと","スポーツ"]},{"category":"activity","char":"🏌","name":"golfing_man","keywords":["sports","business","ゴルフ","ゴルファー","ゴルフをするひと","スポーツ"]},{"category":"activity","char":"🏓","name":"ping_pong","keywords":["sports","pingpong","スポーツ","ピンポン","ボール","ラケット","たっきゅう"]},{"category":"activity","char":"🏸","name":"badminton","keywords":["sports","シャトル","スポーツ","バドミントン","ラケット"]},{"category":"activity","char":"🥅","name":"goal_net","keywords":["sports","ゴール","ゴールネット","スポーツ","ネット"]},{"category":"activity","char":"🏒","name":"ice_hockey","keywords":["sports","アイスホッケー","スティック","スポーツ","パック","ホッケー"]},{"category":"activity","char":"🏑","name":"field_hockey","keywords":["sports","スティック","スポーツ","フィールドホッケー","ボール","ホッケー"]},{"category":"activity","char":"🥍","name":"lacrosse","keywords":["sports","ball","stick","ゴール","スティック","スポーツ","ボール","ラクロス"]},{"category":"activity","char":"🏏","name":"cricket","keywords":["sports","クリケット","スポーツ","バット","ボール"]},{"category":"activity","char":"🎿","name":"ski","keywords":["sports","winter","cold","snow","スキー","ストック","スポーツ","いた","ゆき"]},{"category":"activity","char":"⛷","name":"skier","keywords":["sports","winter","snow","スキー","スキーヤー","ストック","スポーツ"]},{"category":"activity","char":"🏂","name":"snowboarder","keywords":["sports","winter","スノーボーダー","スノーボード","スノボ","スポーツ"]},{"category":"activity","char":"🤺","name":"person_fencing","keywords":["sports","fencing","sword","スポーツ","フェンシング","フェンシングをするひと"]},{"category":"activity","char":"🤼‍♀️","name":"women_wrestling","keywords":["sports","wrestlers","スポーツ","レスリング","レスリングするおんな","レスリングをするおんな","おんな","じょせい"]},{"category":"activity","char":"🤼‍♂️","name":"men_wrestling","keywords":["sports","wrestlers","スポーツ","レスリング","レスリングするおとこ","レスリングをするおとこ","おとこ","だんせい"]},{"category":"activity","char":"🤸‍♀️","name":"woman_cartwheeling","keywords":["gymnastics","スポーツ","たいそう","がわてん","がわてんするおんな","おんな","じょせい"]},{"category":"activity","char":"🤸‍♂️","name":"man_cartwheeling","keywords":["gymnastics","スポーツ","たいそう","がわてん","がわてんするおとこ","おとこ","だんせい"]},{"category":"activity","char":"🤾‍♀️","name":"woman_playing_handball","keywords":["sports","スポーツ","ハンドボール","ハンドボールをするおんな","おんな","じょせい","きゅうぎ"]},{"category":"activity","char":"🤾‍♂️","name":"man_playing_handball","keywords":["sports","スポーツ","ハンドボール","ハンドボールをするおとこ","きゅうぎ","おとこ","だんせい"]},{"category":"activity","char":"⛸","name":"ice_skate","keywords":["sports","アイススケート","スケート","スポーツ","フィギュアスケート"]},{"category":"activity","char":"🥌","name":"curling_stone","keywords":["sports","カーリング","ストーン","スポーツ"]},{"category":"activity","char":"🛹","name":"skateboard","keywords":["board","スケートボード","スケボー","ボード"]},{"category":"activity","char":"🛷","name":"sled","keywords":["sleigh","luge","toboggan","スケルトン","スポーツ","そり","そりきょうぎ","ボブスレー","リュージュ"]},{"category":"activity","char":"🏹","name":"bow_and_arrow","keywords":["sports","しゃしゅ","ゆみや","せいざ","ぶき","や"]},{"category":"activity","char":"🎣","name":"fishing_pole_and_fish","keywords":["food","hobby","summer","スポーツ","つり","つりざお","さかな"]},{"category":"activity","char":"🥊","name":"boxing_glove","keywords":["sports","fighting","グローブ","スポーツ","ボクシング","ボクシンググローブ"]},{"category":"activity","char":"🥋","name":"martial_arts_uniform","keywords":["judo","karate","taekwondo","スポーツ","テコンドー","じゅうどう","ぶどう","からて","どうぎ"]},{"category":"activity","char":"🚣‍♀️","name":"rowing_woman","keywords":["sports","hobby","water","ship","woman","female","カヌー","カヤック","スポーツ","ボートをこぐおんな","じょせい","ふね"]},{"category":"activity","char":"🚣","name":"rowing_man","keywords":["sports","hobby","water","ship","カヌー","カヤック","スポーツ","ボートをこぐひと","のりもの","ふね"]},{"category":"activity","char":"🧗‍♀️","name":"climbing_woman","keywords":["sports","hobby","woman","female","rock","おんな","じょせい","やまをのぼるおんな","やまのぼり","とざん"]},{"category":"activity","char":"🧗‍♂️","name":"climbing_man","keywords":["sports","hobby","man","male","rock","やまをのぼるおとこ","やまのぼり","おとこ","だんせい","とざん"]},{"category":"activity","char":"🏊‍♀️","name":"swimming_woman","keywords":["sports","exercise","human","athlete","water","summer","woman","female","スイマー","スイミング","スイム","スポーツ","じょせい","すいえい","およぐおんな"]},{"category":"activity","char":"🏊","name":"swimming_man","keywords":["sports","exercise","human","athlete","water","summer","スイマー","スイミング","スイム","スポーツ","すいえい","およぐひと"]},{"category":"activity","char":"🤽‍♀️","name":"woman_playing_water_polo","keywords":["sports","pool","スポーツ","おんな","じょせい","すいきゅう","すいきゅうをするおんな","きゅうぎ"]},{"category":"activity","char":"🤽‍♂️","name":"man_playing_water_polo","keywords":["sports","pool","スポーツ","すいきゅう","すいきゅうをするおとこ","きゅうぎ","おとこ","だんせい"]},{"category":"activity","char":"🧘‍♀️","name":"woman_in_lotus_position","keywords":["woman","female","meditation","yoga","serenity","zen","mindfulness","ヨガ","ヨガのポーズをするおんな","おんな","じょせい","めいそう"]},{"category":"activity","char":"🧘‍♂️","name":"man_in_lotus_position","keywords":["man","male","meditation","yoga","serenity","zen","mindfulness","ヨガ","ヨガのポーズをするおとこ","おとこ","だんせい","めいそう"]},{"category":"activity","char":"🏄‍♀️","name":"surfing_woman","keywords":["sports","ocean","sea","summer","beach","woman","female","サーファー","サーフィンするおんな","スポーツ","おんな","じょせい","なみのり"]},{"category":"activity","char":"🏄","name":"surfing_man","keywords":["sports","ocean","sea","summer","beach","サーファー","サーフィン","サーフィンするひと","スポーツ","なみのり"]},{"category":"activity","char":"🛀","name":"bath","keywords":["clean","shower","bathroom","バスタブ","よくそう","ふろ","ふろにはいるひと"]},{"category":"activity","char":"⛹️‍♀️","name":"basketball_woman","keywords":["sports","human","woman","female","スポーツ","バスケ","バスケットボール","バスケットボールをするひと","きゅうぎ"]},{"category":"activity","char":"⛹","name":"basketball_man","keywords":["sports","human","スポーツ","バスケ","バスケットボール","バスケットボールをするひと","きゅうぎ"]},{"category":"activity","char":"🏋️‍♀️","name":"weight_lifting_woman","keywords":["sports","training","exercise","woman","female","ウェイトリフティング","スポーツ","じゅうりょうあげ","じゅうりょうあげをするひと"]},{"category":"activity","char":"🏋","name":"weight_lifting_man","keywords":["sports","training","exercise","ウェイトリフティング","スポーツ","じゅうりょうあげ","じゅうりょうあげをするひと"]},{"category":"activity","char":"🚴‍♀️","name":"biking_woman","keywords":["sports","bike","exercise","hipster","woman","female","サイクリング","スポーツ","ロードレース","じょせい","けいりん","じてんしゃにのるおんな"]},{"category":"activity","char":"🚴","name":"biking_man","keywords":["sports","bike","exercise","hipster","サイクリング","スポーツ","ロードバイク","ロードレース","けいりん","じてんしゃにのるひと"]},{"category":"activity","char":"🚵‍♀️","name":"mountain_biking_woman","keywords":["transportation","sports","human","race","bike","woman","female","サイクリング","スポーツ","マウンテンバイクにのるおんな","おんな","じょせい","じてんしゃ"]},{"category":"activity","char":"🚵","name":"mountain_biking_man","keywords":["transportation","sports","human","race","bike","サイクリング","スポーツ","マウンテンバイク","マウンテンバイクにのるひと","じてんしゃ"]},{"category":"activity","char":"🏇","name":"horse_racing","keywords":["animal","betting","competition","gambling","luck","ジョッキー","スポーツ","じょうば","けいば","きしゅ"]},{"category":"activity","char":"🤿","name":"diving_mask","keywords":["sports","シュノーケリング","スキューバ","スポーツ","ダイビング マスク","マスク"]},{"category":"activity","char":"🪀","name":"yo_yo","keywords":["sports","おもちゃ","ヨーヨー","かいてん","おもちゃ"]},{"category":"activity","char":"🪁","name":"kite","keywords":["sports","おもちゃ","カイト","たこ","たこあげ","おもちゃ"]},{"category":"activity","char":"🦺","name":"safety_vest","keywords":["sports","チョッキ","ベスト","はんしゃ","あんぜんベスト","こうじ","きんきゅう"]},{"category":"activity","char":"🪡","name":"sewing_needle","keywords":["ステッチ","ししゅう","ぬいはり","ぬう","さいほう","はり"]},{"category":"activity","char":"🪢","name":"knot","keywords":["ねじれ","ノット","ひも","ロープ","むすびめ","つな"]},{"category":"activity","char":"🕴","name":"business_suit_levitating","keywords":["suit","business","levitate","hover","jump","スーツ","ビジネスマン","ういてるビジネスマン","おとこ","だんせい","くうちゅうふゆう"]},{"category":"activity","char":"🏆","name":"trophy","keywords":["win","award","contest","place","ftw","ceremony","トロフィー","ゆうしょうカップ","ひょうしょう","しょうはい"]},{"category":"activity","char":"🎽","name":"running_shirt_with_sash","keywords":["play","pageant","スポーツ","たすき","ランニング","ランニングシャツ","ちょうきょりはし","えきでん"]},{"category":"activity","char":"🏅","name":"medal_sports","keywords":["award","winning","スポーツ","メダル","ひょうしょう"]},{"category":"activity","char":"🎖","name":"medal_military","keywords":["award","winning","army","おいわい","メダル","くんしょう","ひょうしょう"]},{"category":"activity","char":"🥇","name":"1st_place_medal","keywords":["award","winning","first","1い","ゴールド","メダル","きむ","きんメダル"]},{"category":"activity","char":"🥈","name":"2nd_place_medal","keywords":["award","second","2い","シルバー","メダル","ぎん","ぎんメダル"]},{"category":"activity","char":"🥉","name":"3rd_place_medal","keywords":["award","third","3い","ブロンズ","メダル","どう","どうメダル"]},{"category":"activity","char":"🎗","name":"reminder_ribbon","keywords":["sports","cause","support","awareness","おいわい","リボン","リマインダー","リマインダーリボン"]},{"category":"activity","char":"🏵","name":"rosette","keywords":["flower","decoration","military","しょくぶつ","はな","はなかざり"]},{"category":"activity","char":"🎫","name":"ticket","keywords":["event","concert","pass","チケット","きっぷ"]},{"category":"activity","char":"🎟","name":"tickets","keywords":["sports","concert","entrance","チケット","にゅうじょうパス","にゅうじょうけん","はんけん"]},{"category":"activity","char":"🎭","name":"performing_arts","keywords":["acting","theater","drama","かめん","げきじょう","えんげき","えんげい","ぶたいげいじゅつ","げいじゅつ"]},{"category":"activity","char":"🎨","name":"art","keywords":["design","paint","draw","colors","パレット","え","えのぐパレット","びじゅつかん"]},{"category":"activity","char":"🎪","name":"circus_tent","keywords":["festival","carnival","party","サーカス","テント"]},{"category":"activity","char":"🤹‍♀️","name":"woman_juggling","keywords":["juggle","balance","skill","multitask","ジャグラー","ジャグリング","ジャグリングをするおんな","だいどうげい","おんな","じょせい"]},{"category":"activity","char":"🤹‍♂️","name":"man_juggling","keywords":["juggle","balance","skill","multitask","ジャグラー","ジャグリング","ジャグリングをするおとこ","だいどうげい","おとこ","だんせい"]},{"category":"activity","char":"🎤","name":"microphone","keywords":["sound","music","PA","sing","talkshow","カラオケ","マイク","うた","おんがく"]},{"category":"activity","char":"🎧","name":"headphones","keywords":["music","score","gadgets","イヤホン","ヘッドフォン","ヘッドホン","おんがく"]},{"category":"activity","char":"🎼","name":"musical_score","keywords":["treble","clef","compose","スコア","トおんきごう","ごせんふ","がくふ","おんがく"]},{"category":"activity","char":"🎹","name":"musical_keyboard","keywords":["piano","instrument","compose","キーボード","ピアノ","がっき","けんばん","おんがく"]},{"category":"activity","char":"🥁","name":"drum","keywords":["music","instrument","drumsticks","snare","ドラム","たいこ","がっき","おんがく"]},{"category":"activity","char":"🎷","name":"saxophone","keywords":["music","instrument","jazz","blues","サキソフォン","サクソフォン","サックス","がっき","おんがく"]},{"category":"activity","char":"🎺","name":"trumpet","keywords":["music","brass","トランペット","ラッパ","がっき","おんがく"]},{"category":"activity","char":"🎸","name":"guitar","keywords":["music","instrument","ギター","がっき","おんがく"]},{"category":"activity","char":"🎻","name":"violin","keywords":["music","instrument","orchestra","symphony","ヴィオラ","バイオリン","がっき","おんがく"]},{"category":"activity","char":"🪕","name":"banjo","keywords":["music","instrument","バンジョー","がっき","おんがく"]},{"category":"activity","char":"🪗","name":"accordion","keywords":["music","instrument","アコーディオン","じゃばら","がっき","おんがく"]},{"category":"activity","char":"🪘","name":"long_drum","keywords":["music","instrument","コンガ","ビート","リズム","たいこ","がっき","おんがく"]},{"category":"activity","char":"🎬","name":"clapper","keywords":["movie","film","record","カチンコ","えいが"]},{"category":"activity","char":"🎮","name":"video_game","keywords":["play","console","PS4","controller","ゲーム","コントローラ","テレビゲーム","ビデオゲーム"]},{"category":"activity","char":"👾","name":"space_invader","keywords":["game","arcade","play","インベーダー","エイリアン","モンスター","うちゅうじん","いほしじん"]},{"category":"activity","char":"🎯","name":"dart","keywords":["game","play","bar","target","bullseye","スポーツ","ダーツ","ブルズアイ","あたり","てき"]},{"category":"activity","char":"🎲","name":"game_die","keywords":["dice","random","tabletop","play","luck","ゲーム","サイコロ","ダイス"]},{"category":"activity","char":"♟️","name":"chess_pawn","keywords":["expendable","チェス","チェスのこま","すてこま","こま"]},{"category":"activity","char":"🎰","name":"slot_machine","keywords":["bet","gamble","vegas","fruit machine","luck","casino","スリーセブン","スロット","スロットマシン"]},{"category":"activity","char":"🧩","name":"jigsaw","keywords":["interlocking","puzzle","piece","ジグソーパズル","パズル","ピース","くみあわせ","かぎ"]},{"category":"activity","char":"🎳","name":"bowling","keywords":["sports","fun","play","スポーツ","ボール","ボウリング","たま"]},{"category":"activity","char":"🪄","name":"magic_wand","keywords":["つえ","つえ","まほうのつえ","まじゅつ"]},{"category":"activity","char":"🪅","name":"pinata","keywords":["おいわい","パーティー","ピニャータ","にんぎょう","たんじょうび"]},{"category":"activity","char":"🪆","name":"nesting_dolls","keywords":["マトリョーシカ","ロシア","にんぎょう","いれこ"]},{"category":"activity","char":"🪬","name":"hamsa","keywords":["おまもり","ハムサ","ファーティマ","ミリアム","て","め","ごふう"]},{"category":"activity","char":"🪩","name":"mirror_ball","keywords":["キラキラ","ダンス","ディスコ","パーティー","ミラーボール"]},{"category":"travel_and_places","char":"🚗","name":"red_car","keywords":["red","transportation","vehicle","のりもの","じどうしゃ","くるま"]},{"category":"travel_and_places","char":"🚕","name":"taxi","keywords":["uber","vehicle","cars","transportation","タクシー","のりもの"]},{"category":"travel_and_places","char":"🚙","name":"blue_car","keywords":["transportation","vehicle","RVしゃ","SUVしゃ","アールブイしゃ","のりもの","じどうしゃ","くるま"]},{"category":"travel_and_places","char":"🚌","name":"bus","keywords":["car","vehicle","transportation","バス","のりもの"]},{"category":"travel_and_places","char":"🚎","name":"trolleybus","keywords":["bart","transportation","vehicle","トロリーバス","バス","のりもの"]},{"category":"travel_and_places","char":"🏎","name":"racing_car","keywords":["sports","race","fast","formula","f1","F1","スポーツ","モータースポーツ","レーシングカー","のりもの","くるま"]},{"category":"travel_and_places","char":"🚓","name":"police_car","keywords":["vehicle","cars","transportation","law","legal","enforcement","パトカー","のりもの","けいさつ"]},{"category":"travel_and_places","char":"🚑","name":"ambulance","keywords":["health","911","hospital","のりもの","きゅうきゅうしゃ"]},{"category":"travel_and_places","char":"🚒","name":"fire_engine","keywords":["transportation","cars","vehicle","のりもの","しょうぼうしゃ"]},{"category":"travel_and_places","char":"🚐","name":"minibus","keywords":["vehicle","car","transportation","バス","マイクロバス","のりもの"]},{"category":"travel_and_places","char":"🚚","name":"truck","keywords":["cars","transportation","トラック","のりもの","くるま","はいたつ"]},{"category":"travel_and_places","char":"🚛","name":"articulated_lorry","keywords":["vehicle","cars","transportation","express","トラック","トレーラー","のりもの","くるま"]},{"category":"travel_and_places","char":"🚜","name":"tractor","keywords":["vehicle","car","farming","agriculture","トラクター","のりもの","くるま"]},{"category":"travel_and_places","char":"🛴","name":"kick_scooter","keywords":["vehicle","kick","razor","キックスクーター","キックスケーター","キックボード"]},{"category":"travel_and_places","char":"🏍","name":"motorcycle","keywords":["race","sports","fast","オートバイ","オートレース","スポーツ","バイク","モータースポーツ","のりもの"]},{"category":"travel_and_places","char":"🚲","name":"bike","keywords":["sports","bicycle","exercise","hipster","のりもの","じてんしゃ"]},{"category":"travel_and_places","char":"🛵","name":"motor_scooter","keywords":["vehicle","vespa","sasha","スクーター","げんつき"]},{"category":"travel_and_places","char":"🦽","name":"manual_wheelchair","keywords":["vehicle","アクセシビリティ","しゅどうしきくるまいす","いす","くるまいす"]},{"category":"travel_and_places","char":"🦼","name":"motorized_wheelchair","keywords":["vehicle","アクセシビリティ","いす","くるまいす","でんどうくるまいす"]},{"category":"travel_and_places","char":"🛺","name":"auto_rickshaw","keywords":["vehicle","オートリクシャー","タクシー","トゥクトゥク","リクシャー","さんりんタクシー"]},{"category":"travel_and_places","char":"🪂","name":"parachute","keywords":["vehicle","スカイダイビング","パラグライダー","パラシュート"]},{"category":"travel_and_places","char":"🚨","name":"rotating_light","keywords":["police","ambulance","911","emergency","alert","error","pinged","law","legal","パトカー","パトランプ"]},{"category":"travel_and_places","char":"🚔","name":"oncoming_police_car","keywords":["vehicle","law","legal","enforcement","911","パトカー","パトカーしょうめん","のりもの","けいさつ"]},{"category":"travel_and_places","char":"🚍","name":"oncoming_bus","keywords":["vehicle","transportation","バス","バスしょうめん","のりもの"]},{"category":"travel_and_places","char":"🚘","name":"oncoming_automobile","keywords":["car","vehicle","transportation","のりもの","じどうしゃ","じどうしゃしょうめん","くるま"]},{"category":"travel_and_places","char":"🚖","name":"oncoming_taxi","keywords":["vehicle","cars","uber","タクシー","タクシーしょうめん","のりもの"]},{"category":"travel_and_places","char":"🚡","name":"aerial_tramway","keywords":["transportation","vehicle","ski","ゴンドラ","ロープウェイ","のりもの"]},{"category":"travel_and_places","char":"🚠","name":"mountain_cableway","keywords":["transportation","vehicle","ski","ケーブルカー","ゴンドラ","のりもの"]},{"category":"travel_and_places","char":"🚟","name":"suspension_railway","keywords":["vehicle","transportation","モノレール","のりもの","けんすいしきモノレール","でんしゃ"]},{"category":"travel_and_places","char":"🚃","name":"railway_car","keywords":["transportation","vehicle","train","のりもの","れっしゃ","でんしゃ"]},{"category":"travel_and_places","char":"🚋","name":"train","keywords":["transportation","vehicle","carriage","public","travel","のりもの","ろめんでんしゃ","でんしゃ"]},{"category":"travel_and_places","char":"🚝","name":"monorail","keywords":["transportation","vehicle","モノレール","のりもの","でんしゃ"]},{"category":"travel_and_places","char":"🚄","name":"bullettrain_side","keywords":["transportation","vehicle","のりもの","しんかんせん","でんしゃ"]},{"category":"travel_and_places","char":"🚅","name":"bullettrain_front","keywords":["transportation","vehicle","speed","fast","public","travel","0けい","0けいしんかんせん","まるいしんかんせん","のりもの","しんかんせん","でんしゃ"]},{"category":"travel_and_places","char":"🚈","name":"light_rail","keywords":["transportation","vehicle","ライトレール","のりもの","でんしゃ"]},{"category":"travel_and_places","char":"🚞","name":"mountain_railway","keywords":["transportation","vehicle","のりもの","とざんてつどう","とざんでんしゃ","でんしゃ"]},{"category":"travel_and_places","char":"🚂","name":"steam_locomotive","keywords":["transportation","vehicle","train","SL","のりもの","れっしゃ","じょうききかんしゃ"]},{"category":"travel_and_places","char":"🚆","name":"train2","keywords":["transportation","vehicle","のりもの","れっしゃ","でんしゃ","でんしゃしょうめん"]},{"category":"travel_and_places","char":"🚇","name":"metro","keywords":["transportation","blue-square","mrt","underground","tube","メトロ","のりもの","ちかてつ","でんしゃ"]},{"category":"travel_and_places","char":"🚊","name":"tram","keywords":["transportation","vehicle","のりもの","ろめんでんしゃ","ろめんでんしゃしょうめん","でんしゃ"]},{"category":"travel_and_places","char":"🚉","name":"station","keywords":["transportation","vehicle","public","のりもの","れっしゃ","でんしゃ","えき"]},{"category":"travel_and_places","char":"🛸","name":"flying_saucer","keywords":["transportation","vehicle","ufo","UFO","のりもの","うちゅう","そらとぶえんばん"]},{"category":"travel_and_places","char":"🚁","name":"helicopter","keywords":["transportation","vehicle","fly","ヘリ","ヘリコプター","のりもの"]},{"category":"travel_and_places","char":"🛩","name":"small_airplane","keywords":["flight","transportation","fly","vehicle","のりもの","こがたき","こがたひこうき","ひこうき"]},{"category":"travel_and_places","char":"✈️","name":"airplane","keywords":["vehicle","transportation","flight","fly","のりもの","ひこうき"]},{"category":"travel_and_places","char":"🛫","name":"flight_departure","keywords":["airport","flight","landing","のりもの","りりく","ひこうき","ひこうきりりく"]},{"category":"travel_and_places","char":"🛬","name":"flight_arrival","keywords":["airport","flight","boarding","のりもの","ちゃくりく","ひこうき","ひこうきちゃくりく"]},{"category":"travel_and_places","char":"⛵","name":"sailboat","keywords":["ship","summer","transportation","water","sailing","ヨット","のりもの","はんせん","ふね"]},{"category":"travel_and_places","char":"🛥","name":"motor_boat","keywords":["ship","ボート","モーターボート","のりもの","ふね"]},{"category":"travel_and_places","char":"🚤","name":"speedboat","keywords":["ship","transportation","vehicle","summer","スピードボート","ボート","のりもの","ふね"]},{"category":"travel_and_places","char":"⛴","name":"ferry","keywords":["boat","ship","yacht","フェリー","のりもの","ふね"]},{"category":"travel_and_places","char":"🛳","name":"passenger_ship","keywords":["yacht","cruise","ferry","のりもの","きゃくせん","りょかくせん","ふね"]},{"category":"travel_and_places","char":"🚀","name":"rocket","keywords":["launch","ship","staffmode","NASA","outer space","outer_space","fly","ロケット","のりもの","うちゅう"]},{"category":"travel_and_places","char":"🛰","name":"artificial_satellite","keywords":["communication","gps","orbit","spaceflight","NASA","ISS","じんこうえいせい","うちゅう"]},{"category":"travel_and_places","char":"🛻","name":"pickup_truck","keywords":["car","トラック","ピックアップトラック","のりもの","くるま","けいトラック","はいたつ"]},{"category":"travel_and_places","char":"🛼","name":"roller_skate","keywords":["スケート","スポーツ","ローラースケート"]},{"category":"travel_and_places","char":"💺","name":"seat","keywords":["sit","airplane","transport","bus","flight","fly","せき","ざせき","いす"]},{"category":"travel_and_places","char":"🛶","name":"canoe","keywords":["boat","paddle","water","ship","カヌー","カヤック","ボート","のりもの","ふね"]},{"category":"travel_and_places","char":"⚓","name":"anchor","keywords":["ship","ferry","sea","boat","いかり","いかり","ふね","いかり"]},{"category":"travel_and_places","char":"🚧","name":"construction","keywords":["wip","progress","caution","warning","サイン","こうじちゅう","つうこうどめ"]},{"category":"travel_and_places","char":"⛽","name":"fuelpump","keywords":["gas station","petroleum","ガソリン","ガソリンスタンド","ガソリンノズル"]},{"category":"travel_and_places","char":"🚏","name":"busstop","keywords":["transportation","wait","バスのりば","バスてい","のりもの"]},{"category":"travel_and_places","char":"🚦","name":"vertical_traffic_light","keywords":["transportation","driving","しんごう","しんごうき","しんごうたて"]},{"category":"travel_and_places","char":"🚥","name":"traffic_light","keywords":["transportation","signal","しんごう","しんごうよこ","しんごうき"]},{"category":"travel_and_places","char":"🏁","name":"checkered_flag","keywords":["contest","finishline","race","gokart","スポーツ","チェッカーフラッグ","フラグ","フラッグ","レース","はた"]},{"category":"travel_and_places","char":"🚢","name":"ship","keywords":["transportation","titanic","deploy","のりもの","ふね"]},{"category":"travel_and_places","char":"🎡","name":"ferris_wheel","keywords":["photo","carnival","londoneye","かんらんしゃ","ゆうえんち"]},{"category":"travel_and_places","char":"🎢","name":"roller_coaster","keywords":["carnival","playground","photo","fun","ジェットコースター","ゆうえんち"]},{"category":"travel_and_places","char":"🎠","name":"carousel_horse","keywords":["photo","carnival","メリーゴーランド","ゆうえんち"]},{"category":"travel_and_places","char":"🏗","name":"building_construction","keywords":["wip","working","progress","クレーン","こうじ","けんせつ","けんせつちゅう"]},{"category":"travel_and_places","char":"🌁","name":"foggy","keywords":["photo","mountain","とかい","きり","きりのとかい"]},{"category":"travel_and_places","char":"🏭","name":"factory","keywords":["building","industry","pollution","smoke","こうじょう","たてもの"]},{"category":"travel_and_places","char":"⛲","name":"fountain","keywords":["photo","summer","water","fresh","ふんすい"]},{"category":"travel_and_places","char":"🎑","name":"rice_scene","keywords":["photo","japan","asia","tsukimi","ススキ","じゅうごや","つき","つきみ"]},{"category":"travel_and_places","char":"⛰","name":"mountain","keywords":["photo","nature","environment","やま","さんがく"]},{"category":"travel_and_places","char":"🏔","name":"mountain_snow","keywords":["photo","nature","environment","winter","cold","かんむりゆき","やま","ゆき","ゆきやま"]},{"category":"travel_and_places","char":"🗻","name":"mount_fuji","keywords":["photo","mountain","nature","japanese","ふじさん","やま"]},{"category":"travel_and_places","char":"🌋","name":"volcano","keywords":["photo","nature","disaster","ふんか","やま","かざん"]},{"category":"travel_and_places","char":"🗾","name":"japan","keywords":["nation","country","japanese","asia","ちず","にっぽん","にっぽんちず"]},{"category":"travel_and_places","char":"🏕","name":"camping","keywords":["photo","outdoors","tent","キャンプ","テント","やま"]},{"category":"travel_and_places","char":"⛺","name":"tent","keywords":["photo","camping","outdoors","キャンプ","テント"]},{"category":"travel_and_places","char":"🏞","name":"national_park","keywords":["photo","environment","nature","こうえん","こくりつこうえん","しぜん"]},{"category":"travel_and_places","char":"🛣","name":"motorway","keywords":["road","cupertino","interstate","highway","どうろ","こうそく","こうそくどうろ"]},{"category":"travel_and_places","char":"🛤","name":"railway_track","keywords":["train","transportation","せんろ","てつどう"]},{"category":"travel_and_places","char":"🌅","name":"sunrise","keywords":["morning","view","vacation","photo","たいよう","ひので","あさ","あさひ"]},{"category":"travel_and_places","char":"🌄","name":"sunrise_over_mountains","keywords":["view","vacation","photo","ごらいこう","たいよう","やまからひので","ひので","あさ","あさひ"]},{"category":"travel_and_places","char":"🏜","name":"desert","keywords":["photo","warm","saharah","サボテン","サボテンのあるさばく","さばく"]},{"category":"travel_and_places","char":"🏖","name":"beach_umbrella","keywords":["weather","summer","sunny","sand","mojito","パラソル","ビーチ","ビーチパラソル","すなはま"]},{"category":"travel_and_places","char":"🏝","name":"desert_island","keywords":["photo","tropical","mojito","ヤシ","ヤシのきのあるしま","しま","むじんとう"]},{"category":"travel_and_places","char":"🌇","name":"city_sunrise","keywords":["photo","good morning","dawn","ゆうがた","ゆうひ","ゆうぐれ","たいよう"]},{"category":"travel_and_places","char":"🌆","name":"city_sunset","keywords":["photo","evening","sky","buildings","ゆうがた","ゆうひ","ゆうぐれ","ゆうぐれのとかい","とかい"]},{"category":"travel_and_places","char":"🏙","name":"cityscape","keywords":["photo","night life","urban","ビルぐん","とかい","こうそうビル"]},{"category":"travel_and_places","char":"🌃","name":"night_with_stars","keywords":["evening","city","downtown","よる","よるのとかい","やけい","ほし"]},{"category":"travel_and_places","char":"🌉","name":"bridge_at_night","keywords":["photo","sanfrancisco","よる","よるのはし","やけい","はし"]},{"category":"travel_and_places","char":"🌌","name":"milky_way","keywords":["photo","space","stars","よぞら","あまのがわ","ほしぞら","ぎんが"]},{"category":"travel_and_places","char":"🌠","name":"stars","keywords":["night","photo","スター","ほし","ながれぼし"]},{"category":"travel_and_places","char":"🎇","name":"sparkler","keywords":["stars","night","shine","おまつり","せんこうはなび","はなび"]},{"category":"travel_and_places","char":"🎆","name":"fireworks","keywords":["photo","festival","carnival","congratulations","おまつり","うちあげはなび","はなび","はなびたいかい"]},{"category":"travel_and_places","char":"🌈","name":"rainbow","keywords":["nature","happy","unicorn_face","photo","sky","spring","レインボー","にじ"]},{"category":"travel_and_places","char":"🏘","name":"houses","keywords":["buildings","photo","じゅうたくがい","いえ","まち","ふくすうのいえ"]},{"category":"travel_and_places","char":"🏰","name":"european_castle","keywords":["building","royalty","history","キャッスル","しろ","たてもの","せいようのしろ"]},{"category":"travel_and_places","char":"🏯","name":"japanese_castle","keywords":["photo","building","しろ","たてもの","にっぽん"]},{"category":"travel_and_places","char":"🗼","name":"tokyo_tower","keywords":["photo","japanese","タワー","とうきょう","とうきょうタワー"]},{"category":"travel_and_places","char":"","name":"shibuya_109","keywords":["photo","japanese"]},{"category":"travel_and_places","char":"🏟","name":"stadium","keywords":["photo","place","sports","concert","venue"]},{"category":"travel_and_places","char":"🗽","name":"statue_of_liberty","keywords":["american","newyork"]},{"category":"travel_and_places","char":"🏠","name":"house","keywords":["building","home"]},{"category":"travel_and_places","char":"🏡","name":"house_with_garden","keywords":["home","plant","nature"]},{"category":"travel_and_places","char":"🏚","name":"derelict_house","keywords":["abandon","evict","broken","building"]},{"category":"travel_and_places","char":"🏢","name":"office","keywords":["building","bureau","work"]},{"category":"travel_and_places","char":"🏬","name":"department_store","keywords":["building","shopping","mall"]},{"category":"travel_and_places","char":"🏣","name":"post_office","keywords":["building","envelope","communication"]},{"category":"travel_and_places","char":"🏤","name":"european_post_office","keywords":["building","email"]},{"category":"travel_and_places","char":"🏥","name":"hospital","keywords":["building","health","surgery","doctor"]},{"category":"travel_and_places","char":"🏦","name":"bank","keywords":["building","money","sales","cash","business","enterprise"]},{"category":"travel_and_places","char":"🏨","name":"hotel","keywords":["building","accomodation","checkin"]},{"category":"travel_and_places","char":"🏪","name":"convenience_store","keywords":["building","shopping","groceries"]},{"category":"travel_and_places","char":"🏫","name":"school","keywords":["building","student","education","learn","teach"]},{"category":"travel_and_places","char":"🏩","name":"love_hotel","keywords":["like","affection","dating"]},{"category":"travel_and_places","char":"💒","name":"wedding","keywords":["love","like","affection","couple","marriage","bride","groom"]},{"category":"travel_and_places","char":"🏛","name":"classical_building","keywords":["art","culture","history"]},{"category":"travel_and_places","char":"⛪","name":"church","keywords":["building","religion","christ"]},{"category":"travel_and_places","char":"🕌","name":"mosque","keywords":["islam","worship","minaret"]},{"category":"travel_and_places","char":"🕍","name":"synagogue","keywords":["judaism","worship","temple","jewish"]},{"category":"travel_and_places","char":"🕋","name":"kaaba","keywords":["mecca","mosque","islam"]},{"category":"travel_and_places","char":"⛩","name":"shinto_shrine","keywords":["temple","japan","kyoto"]},{"category":"travel_and_places","char":"🛕","name":"hindu_temple","keywords":["temple"]},{"category":"travel_and_places","char":"🪨","name":"rock","keywords":[]},{"category":"travel_and_places","char":"🪵","name":"wood","keywords":[]},{"category":"travel_and_places","char":"🛖","name":"hut","keywords":[]},{"category":"travel_and_places","char":"🛝","name":"playground_slide","keywords":[]},{"category":"travel_and_places","char":"🛞","name":"wheel","keywords":[]},{"category":"travel_and_places","char":"🛟","name":"ring_buoy","keywords":[]},{"category":"objects","char":"⌚","name":"watch","keywords":["time","accessories"]},{"category":"objects","char":"📱","name":"iphone","keywords":["technology","apple","gadgets","dial"]},{"category":"objects","char":"📲","name":"calling","keywords":["iphone","incoming"]},{"category":"objects","char":"💻","name":"computer","keywords":["technology","laptop","screen","display","monitor"]},{"category":"objects","char":"⌨","name":"keyboard","keywords":["technology","computer","type","input","text"]},{"category":"objects","char":"🖥","name":"desktop_computer","keywords":["technology","computing","screen"]},{"category":"objects","char":"🖨","name":"printer","keywords":["paper","ink"]},{"category":"objects","char":"🖱","name":"computer_mouse","keywords":["click"]},{"category":"objects","char":"🖲","name":"trackball","keywords":["technology","trackpad"]},{"category":"objects","char":"🕹","name":"joystick","keywords":["game","play"]},{"category":"objects","char":"🗜","name":"clamp","keywords":["tool"]},{"category":"objects","char":"💽","name":"minidisc","keywords":["technology","record","data","disk","90s"]},{"category":"objects","char":"💾","name":"floppy_disk","keywords":["oldschool","technology","save","90s","80s"]},{"category":"objects","char":"💿","name":"cd","keywords":["technology","dvd","disk","disc","90s"]},{"category":"objects","char":"📀","name":"dvd","keywords":["cd","disk","disc"]},{"category":"objects","char":"📼","name":"vhs","keywords":["record","video","oldschool","90s","80s"]},{"category":"objects","char":"📷","name":"camera","keywords":["gadgets","photography"]},{"category":"objects","char":"📸","name":"camera_flash","keywords":["photography","gadgets"]},{"category":"objects","char":"📹","name":"video_camera","keywords":["film","record"]},{"category":"objects","char":"🎥","name":"movie_camera","keywords":["film","record"]},{"category":"objects","char":"📽","name":"film_projector","keywords":["video","tape","record","movie"]},{"category":"objects","char":"🎞","name":"film_strip","keywords":["movie"]},{"category":"objects","char":"📞","name":"telephone_receiver","keywords":["technology","communication","dial"]},{"category":"objects","char":"☎️","name":"phone","keywords":["technology","communication","dial","telephone"]},{"category":"objects","char":"📟","name":"pager","keywords":["bbcall","oldschool","90s"]},{"category":"objects","char":"📠","name":"fax","keywords":["communication","technology"]},{"category":"objects","char":"📺","name":"tv","keywords":["technology","program","oldschool","show","television"]},{"category":"objects","char":"📻","name":"radio","keywords":["communication","music","podcast","program"]},{"category":"objects","char":"🎙","name":"studio_microphone","keywords":["sing","recording","artist","talkshow"]},{"category":"objects","char":"🎚","name":"level_slider","keywords":["scale"]},{"category":"objects","char":"🎛","name":"control_knobs","keywords":["dial"]},{"category":"objects","char":"🧭","name":"compass","keywords":["magnetic","navigation","orienteering"]},{"category":"objects","char":"⏱","name":"stopwatch","keywords":["time","deadline"]},{"category":"objects","char":"⏲","name":"timer_clock","keywords":["alarm"]},{"category":"objects","char":"⏰","name":"alarm_clock","keywords":["time","wake"]},{"category":"objects","char":"🕰","name":"mantelpiece_clock","keywords":["time"]},{"category":"objects","char":"⏳","name":"hourglass_flowing_sand","keywords":["oldschool","time","countdown"]},{"category":"objects","char":"⌛","name":"hourglass","keywords":["time","clock","oldschool","limit","exam","quiz","test"]},{"category":"objects","char":"📡","name":"satellite","keywords":["communication","future","radio","space"]},{"category":"objects","char":"🔋","name":"battery","keywords":["power","energy","sustain"]},{"category":"objects","char":"🪫","name":"battery","keywords":[]},{"category":"objects","char":"🔌","name":"electric_plug","keywords":["charger","power"]},{"category":"objects","char":"💡","name":"bulb","keywords":["light","electricity","idea"]},{"category":"objects","char":"🔦","name":"flashlight","keywords":["dark","camping","sight","night"]},{"category":"objects","char":"🕯","name":"candle","keywords":["fire","wax"]},{"category":"objects","char":"🧯","name":"fire_extinguisher","keywords":["quench"]},{"category":"objects","char":"🗑","name":"wastebasket","keywords":["bin","trash","rubbish","garbage","toss"]},{"category":"objects","char":"🛢","name":"oil_drum","keywords":["barrell"]},{"category":"objects","char":"💸","name":"money_with_wings","keywords":["dollar","bills","payment","sale"]},{"category":"objects","char":"💵","name":"dollar","keywords":["money","sales","bill","currency"]},{"category":"objects","char":"💴","name":"yen","keywords":["money","sales","japanese","dollar","currency"]},{"category":"objects","char":"💶","name":"euro","keywords":["money","sales","dollar","currency"]},{"category":"objects","char":"💷","name":"pound","keywords":["british","sterling","money","sales","bills","uk","england","currency"]},{"category":"objects","char":"💰","name":"moneybag","keywords":["dollar","payment","coins","sale"]},{"category":"objects","char":"🪙","name":"coin","keywords":["dollar","payment","coins","sale"]},{"category":"objects","char":"💳","name":"credit_card","keywords":["money","sales","dollar","bill","payment","shopping"]},{"category":"objects","char":"🪫","name":"identification_card","keywords":[]},{"category":"objects","char":"💎","name":"gem","keywords":["blue","ruby","diamond","jewelry"]},{"category":"objects","char":"⚖","name":"balance_scale","keywords":["law","fairness","weight"]},{"category":"objects","char":"🧰","name":"toolbox","keywords":["tools","diy","fix","maintainer","mechanic"]},{"category":"objects","char":"🔧","name":"wrench","keywords":["tools","diy","ikea","fix","maintainer"]},{"category":"objects","char":"🔨","name":"hammer","keywords":["tools","build","create"]},{"category":"objects","char":"⚒","name":"hammer_and_pick","keywords":["tools","build","create"]},{"category":"objects","char":"🛠","name":"hammer_and_wrench","keywords":["tools","build","create"]},{"category":"objects","char":"⛏","name":"pick","keywords":["tools","dig"]},{"category":"objects","char":"🪓","name":"axe","keywords":["tools"]},{"category":"objects","char":"🦯","name":"probing_cane","keywords":["tools"]},{"category":"objects","char":"🔩","name":"nut_and_bolt","keywords":["handy","tools","fix"]},{"category":"objects","char":"⚙","name":"gear","keywords":["cog"]},{"category":"objects","char":"🪃","name":"boomerang","keywords":["tool"]},{"category":"objects","char":"🪚","name":"carpentry_saw","keywords":["tool"]},{"category":"objects","char":"🪛","name":"screwdriver","keywords":["tool"]},{"category":"objects","char":"🪝","name":"hook","keywords":["tool"]},{"category":"objects","char":"🪜","name":"ladder","keywords":["tool"]},{"category":"objects","char":"🧱","name":"brick","keywords":["bricks"]},{"category":"objects","char":"⛓","name":"chains","keywords":["lock","arrest"]},{"category":"objects","char":"🧲","name":"magnet","keywords":["attraction","magnetic"]},{"category":"objects","char":"🔫","name":"gun","keywords":["violence","weapon","pistol","revolver"]},{"category":"objects","char":"💣","name":"bomb","keywords":["boom","explode","explosion","terrorism"]},{"category":"objects","char":"🧨","name":"firecracker","keywords":["dynamite","boom","explode","explosion","explosive"]},{"category":"objects","char":"🔪","name":"hocho","keywords":["knife","blade","cutlery","kitchen","weapon"]},{"category":"objects","char":"🗡","name":"dagger","keywords":["weapon"]},{"category":"objects","char":"⚔","name":"crossed_swords","keywords":["weapon"]},{"category":"objects","char":"🛡","name":"shield","keywords":["protection","security"]},{"category":"objects","char":"🚬","name":"smoking","keywords":["kills","tobacco","cigarette","joint","smoke"]},{"category":"objects","char":"☠","name":"skull_and_crossbones","keywords":["poison","danger","deadly","scary","death","pirate","evil"]},{"category":"objects","char":"⚰","name":"coffin","keywords":["vampire","dead","die","death","rip","graveyard","cemetery","casket","funeral","box"]},{"category":"objects","char":"⚱","name":"funeral_urn","keywords":["dead","die","death","rip","ashes"]},{"category":"objects","char":"🏺","name":"amphora","keywords":["vase","jar"]},{"category":"objects","char":"🔮","name":"crystal_ball","keywords":["disco","party","magic","circus","fortune_teller"]},{"category":"objects","char":"📿","name":"prayer_beads","keywords":["dhikr","religious"]},{"category":"objects","char":"🧿","name":"nazar_amulet","keywords":["bead","charm"]},{"category":"objects","char":"💈","name":"barber","keywords":["hair","salon","style"]},{"category":"objects","char":"⚗","name":"alembic","keywords":["distilling","science","experiment","chemistry"]},{"category":"objects","char":"🔭","name":"telescope","keywords":["stars","space","zoom","science","astronomy"]},{"category":"objects","char":"🔬","name":"microscope","keywords":["laboratory","experiment","zoomin","science","study"]},{"category":"objects","char":"🕳","name":"hole","keywords":["embarrassing"]},{"category":"objects","char":"💊","name":"pill","keywords":["health","medicine","doctor","pharmacy","drug"]},{"category":"objects","char":"💉","name":"syringe","keywords":["health","hospital","drugs","blood","medicine","needle","doctor","nurse"]},{"category":"objects","char":"🩸","name":"drop_of_blood","keywords":["health","hospital","medicine","needle","doctor","nurse"]},{"category":"objects","char":"🩹","name":"adhesive_bandage","keywords":["health","hospital","medicine","needle","doctor","nurse"]},{"category":"objects","char":"🩺","name":"stethoscope","keywords":["health","hospital","medicine","needle","doctor","nurse"]},{"category":"objects","char":"🪒","name":"razor","keywords":["health"]},{"category":"objects","char":"🩻","name":"xray","keywords":[]},{"category":"objects","char":"🩼","name":"crutch","keywords":[]},{"category":"objects","char":"🧬","name":"dna","keywords":["biologist","genetics","life"]},{"category":"objects","char":"🧫","name":"petri_dish","keywords":["bacteria","biology","culture","lab"]},{"category":"objects","char":"🧪","name":"test_tube","keywords":["chemistry","experiment","lab","science"]},{"category":"objects","char":"🌡","name":"thermometer","keywords":["weather","temperature","hot","cold"]},{"category":"objects","char":"🧹","name":"broom","keywords":["cleaning","sweeping","witch"]},{"category":"objects","char":"🧺","name":"basket","keywords":["laundry"]},{"category":"objects","char":"🧻","name":"toilet_paper","keywords":["roll"]},{"category":"objects","char":"🏷","name":"label","keywords":["sale","tag"]},{"category":"objects","char":"🔖","name":"bookmark","keywords":["favorite","label","save"]},{"category":"objects","char":"🚽","name":"toilet","keywords":["restroom","wc","washroom","bathroom","potty"]},{"category":"objects","char":"🚿","name":"shower","keywords":["clean","water","bathroom"]},{"category":"objects","char":"🛁","name":"bathtub","keywords":["clean","shower","bathroom"]},{"category":"objects","char":"🧼","name":"soap","keywords":["bar","bathing","cleaning","lather"]},{"category":"objects","char":"🧽","name":"sponge","keywords":["absorbing","cleaning","porous"]},{"category":"objects","char":"🧴","name":"lotion_bottle","keywords":["moisturizer","sunscreen"]},{"category":"objects","char":"🔑","name":"key","keywords":["lock","door","password"]},{"category":"objects","char":"🗝","name":"old_key","keywords":["lock","door","password"]},{"category":"objects","char":"🛋","name":"couch_and_lamp","keywords":["read","chill"]},{"category":"objects","char":"🪔","name":"diya_Lamp","keywords":["light","oil"]},{"category":"objects","char":"🛌","name":"sleeping_bed","keywords":["bed","rest"]},{"category":"objects","char":"🛏","name":"bed","keywords":["sleep","rest"]},{"category":"objects","char":"🚪","name":"door","keywords":["house","entry","exit"]},{"category":"objects","char":"🪑","name":"chair","keywords":["house","desk"]},{"category":"objects","char":"🛎","name":"bellhop_bell","keywords":["service"]},{"category":"objects","char":"🧸","name":"teddy_bear","keywords":["plush","stuffed"]},{"category":"objects","char":"🖼","name":"framed_picture","keywords":["photography"]},{"category":"objects","char":"🗺","name":"world_map","keywords":["location","direction"]},{"category":"objects","char":"🛗","name":"elevator","keywords":["household"]},{"category":"objects","char":"🪞","name":"mirror","keywords":["household"]},{"category":"objects","char":"🪟","name":"window","keywords":["household"]},{"category":"objects","char":"🪠","name":"plunger","keywords":["household"]},{"category":"objects","char":"🪤","name":"mouse_trap","keywords":["household"]},{"category":"objects","char":"🪣","name":"bucket","keywords":["household"]},{"category":"objects","char":"🪥","name":"toothbrush","keywords":["household"]},{"category":"objects","char":"🫧","name":"bubbles","keywords":[]},{"category":"objects","char":"⛱","name":"parasol_on_ground","keywords":["weather","summer"]},{"category":"objects","char":"🗿","name":"moyai","keywords":["rock","easter island","moai"]},{"category":"objects","char":"🛍","name":"shopping","keywords":["mall","buy","purchase"]},{"category":"objects","char":"🛒","name":"shopping_cart","keywords":["trolley"]},{"category":"objects","char":"🎈","name":"balloon","keywords":["party","celebration","birthday","circus"]},{"category":"objects","char":"🎏","name":"flags","keywords":["fish","japanese","koinobori","carp","banner"]},{"category":"objects","char":"🎀","name":"ribbon","keywords":["decoration","pink","girl","bowtie"]},{"category":"objects","char":"🎁","name":"gift","keywords":["present","birthday","christmas","xmas"]},{"category":"objects","char":"🎊","name":"confetti_ball","keywords":["festival","party","birthday","circus"]},{"category":"objects","char":"🎉","name":"tada","keywords":["party","congratulations","birthday","magic","circus","celebration"]},{"category":"objects","char":"🎎","name":"dolls","keywords":["japanese","toy","kimono"]},{"category":"objects","char":"🎐","name":"wind_chime","keywords":["nature","ding","spring","bell"]},{"category":"objects","char":"🎌","name":"crossed_flags","keywords":["japanese","nation","country","border"]},{"category":"objects","char":"🏮","name":"izakaya_lantern","keywords":["light","paper","halloween","spooky"]},{"category":"objects","char":"🧧","name":"red_envelope","keywords":["gift"]},{"category":"objects","char":"✉️","name":"email","keywords":["letter","postal","inbox","communication"]},{"category":"objects","char":"📩","name":"envelope_with_arrow","keywords":["email","communication"]},{"category":"objects","char":"📨","name":"incoming_envelope","keywords":["email","inbox"]},{"category":"objects","char":"📧","name":"e-mail","keywords":["communication","inbox"]},{"category":"objects","char":"💌","name":"love_letter","keywords":["email","like","affection","envelope","valentines"]},{"category":"objects","char":"📮","name":"postbox","keywords":["email","letter","envelope"]},{"category":"objects","char":"📪","name":"mailbox_closed","keywords":["email","communication","inbox"]},{"category":"objects","char":"📫","name":"mailbox","keywords":["email","inbox","communication"]},{"category":"objects","char":"📬","name":"mailbox_with_mail","keywords":["email","inbox","communication"]},{"category":"objects","char":"📭","name":"mailbox_with_no_mail","keywords":["email","inbox"]},{"category":"objects","char":"📦","name":"package","keywords":["mail","gift","cardboard","box","moving"]},{"category":"objects","char":"📯","name":"postal_horn","keywords":["instrument","music"]},{"category":"objects","char":"📥","name":"inbox_tray","keywords":["email","documents"]},{"category":"objects","char":"📤","name":"outbox_tray","keywords":["inbox","email"]},{"category":"objects","char":"📜","name":"scroll","keywords":["documents","ancient","history","paper"]},{"category":"objects","char":"📃","name":"page_with_curl","keywords":["documents","office","paper"]},{"category":"objects","char":"📑","name":"bookmark_tabs","keywords":["favorite","save","order","tidy"]},{"category":"objects","char":"🧾","name":"receipt","keywords":["accounting","expenses"]},{"category":"objects","char":"📊","name":"bar_chart","keywords":["graph","presentation","stats"]},{"category":"objects","char":"📈","name":"chart_with_upwards_trend","keywords":["graph","presentation","stats","recovery","business","economics","money","sales","good","success"]},{"category":"objects","char":"📉","name":"chart_with_downwards_trend","keywords":["graph","presentation","stats","recession","business","economics","money","sales","bad","failure"]},{"category":"objects","char":"📄","name":"page_facing_up","keywords":["documents","office","paper","information"]},{"category":"objects","char":"📅","name":"date","keywords":["calendar","schedule"]},{"category":"objects","char":"📆","name":"calendar","keywords":["schedule","date","planning"]},{"category":"objects","char":"🗓","name":"spiral_calendar","keywords":["date","schedule","planning"]},{"category":"objects","char":"📇","name":"card_index","keywords":["business","stationery"]},{"category":"objects","char":"🗃","name":"card_file_box","keywords":["business","stationery"]},{"category":"objects","char":"🗳","name":"ballot_box","keywords":["election","vote"]},{"category":"objects","char":"🗄","name":"file_cabinet","keywords":["filing","organizing"]},{"category":"objects","char":"📋","name":"clipboard","keywords":["stationery","documents"]},{"category":"objects","char":"🗒","name":"spiral_notepad","keywords":["memo","stationery"]},{"category":"objects","char":"📁","name":"file_folder","keywords":["documents","business","office"]},{"category":"objects","char":"📂","name":"open_file_folder","keywords":["documents","load"]},{"category":"objects","char":"🗂","name":"card_index_dividers","keywords":["organizing","business","stationery"]},{"category":"objects","char":"🗞","name":"newspaper_roll","keywords":["press","headline"]},{"category":"objects","char":"📰","name":"newspaper","keywords":["press","headline"]},{"category":"objects","char":"📓","name":"notebook","keywords":["stationery","record","notes","paper","study"]},{"category":"objects","char":"📕","name":"closed_book","keywords":["read","library","knowledge","textbook","learn"]},{"category":"objects","char":"📗","name":"green_book","keywords":["read","library","knowledge","study"]},{"category":"objects","char":"📘","name":"blue_book","keywords":["read","library","knowledge","learn","study"]},{"category":"objects","char":"📙","name":"orange_book","keywords":["read","library","knowledge","textbook","study"]},{"category":"objects","char":"📔","name":"notebook_with_decorative_cover","keywords":["classroom","notes","record","paper","study"]},{"category":"objects","char":"📒","name":"ledger","keywords":["notes","paper"]},{"category":"objects","char":"📚","name":"books","keywords":["literature","library","study"]},{"category":"objects","char":"📖","name":"open_book","keywords":["book","read","library","knowledge","literature","learn","study"]},{"category":"objects","char":"🧷","name":"safety_pin","keywords":["diaper"]},{"category":"objects","char":"🔗","name":"link","keywords":["rings","url"]},{"category":"objects","char":"📎","name":"paperclip","keywords":["documents","stationery"]},{"category":"objects","char":"🖇","name":"paperclips","keywords":["documents","stationery"]},{"category":"objects","char":"✂️","name":"scissors","keywords":["stationery","cut"]},{"category":"objects","char":"📐","name":"triangular_ruler","keywords":["stationery","math","architect","sketch"]},{"category":"objects","char":"📏","name":"straight_ruler","keywords":["stationery","calculate","length","math","school","drawing","architect","sketch"]},{"category":"objects","char":"🧮","name":"abacus","keywords":["calculation"]},{"category":"objects","char":"📌","name":"pushpin","keywords":["stationery","mark","here"]},{"category":"objects","char":"📍","name":"round_pushpin","keywords":["stationery","location","map","here"]},{"category":"objects","char":"🚩","name":"triangular_flag_on_post","keywords":["mark","milestone","place"]},{"category":"objects","char":"🏳","name":"white_flag","keywords":["losing","loser","lost","surrender","give up","fail"]},{"category":"objects","char":"🏴","name":"black_flag","keywords":["pirate"]},{"category":"objects","char":"🏳️‍🌈","name":"rainbow_flag","keywords":["flag","rainbow","pride","gay","lgbt","glbt","queer","homosexual","lesbian","bisexual","transgender"]},{"category":"objects","char":"🏳️‍⚧️","name":"transgender_flag","keywords":["flag","transgender"]},{"category":"objects","char":"🔐","name":"closed_lock_with_key","keywords":["security","privacy"]},{"category":"objects","char":"🔒","name":"lock","keywords":["security","password","padlock"]},{"category":"objects","char":"🔓","name":"unlock","keywords":["privacy","security"]},{"category":"objects","char":"🔏","name":"lock_with_ink_pen","keywords":["security","secret"]},{"category":"objects","char":"🖊","name":"pen","keywords":["stationery","writing","write"]},{"category":"objects","char":"🖋","name":"fountain_pen","keywords":["stationery","writing","write"]},{"category":"objects","char":"✒️","name":"black_nib","keywords":["pen","stationery","writing","write"]},{"category":"objects","char":"📝","name":"memo","keywords":["write","documents","stationery","pencil","paper","writing","legal","exam","quiz","test","study","compose"]},{"category":"objects","char":"✏️","name":"pencil2","keywords":["stationery","write","paper","writing","school","study"]},{"category":"objects","char":"🖍","name":"crayon","keywords":["drawing","creativity"]},{"category":"objects","char":"🖌","name":"paintbrush","keywords":["drawing","creativity","art"]},{"category":"objects","char":"🔍","name":"mag","keywords":["search","zoom","find","detective"]},{"category":"objects","char":"🔎","name":"mag_right","keywords":["search","zoom","find","detective"]},{"category":"objects","char":"🪦","name":"headstone","keywords":[]},{"category":"objects","char":"🪧","name":"placard","keywords":[]},{"category":"symbols","char":"💯","name":"100","keywords":["score","perfect","numbers","century","exam","quiz","test","pass","hundred"]},{"category":"symbols","char":"🔢","name":"1234","keywords":["numbers","blue-square"]},{"category":"symbols","char":"❤️","name":"heart","keywords":["love","like","affection","valentines"]},{"category":"symbols","char":"🧡","name":"orange_heart","keywords":["love","like","affection","valentines"]},{"category":"symbols","char":"💛","name":"yellow_heart","keywords":["love","like","affection","valentines"]},{"category":"symbols","char":"💚","name":"green_heart","keywords":["love","like","affection","valentines"]},{"category":"symbols","char":"💙","name":"blue_heart","keywords":["love","like","affection","valentines"]},{"category":"symbols","char":"💜","name":"purple_heart","keywords":["love","like","affection","valentines"]},{"category":"symbols","char":"🤎","name":"brown_heart","keywords":["love","like","affection","valentines"]},{"category":"symbols","char":"🖤","name":"black_heart","keywords":["love","like","affection","valentines"]},{"category":"symbols","char":"🤍","name":"white_heart","keywords":["love","like","affection","valentines"]},{"category":"symbols","char":"💔","name":"broken_heart","keywords":["sad","sorry","break","heart","heartbreak"]},{"category":"symbols","char":"❣","name":"heavy_heart_exclamation","keywords":["decoration","love"]},{"category":"symbols","char":"💕","name":"two_hearts","keywords":["love","like","affection","valentines","heart"]},{"category":"symbols","char":"💞","name":"revolving_hearts","keywords":["love","like","affection","valentines"]},{"category":"symbols","char":"💓","name":"heartbeat","keywords":["love","like","affection","valentines","pink","heart"]},{"category":"symbols","char":"💗","name":"heartpulse","keywords":["like","love","affection","valentines","pink"]},{"category":"symbols","char":"💖","name":"sparkling_heart","keywords":["love","like","affection","valentines"]},{"category":"symbols","char":"💘","name":"cupid","keywords":["love","like","heart","affection","valentines"]},{"category":"symbols","char":"💝","name":"gift_heart","keywords":["love","valentines"]},{"category":"symbols","char":"💟","name":"heart_decoration","keywords":["purple-square","love","like"]},{"category":"symbols","char":"❤️‍🔥","name":"heart_on_fire","keywords":[]},{"category":"symbols","char":"❤️‍🩹","name":"mending_heart","keywords":[]},{"category":"symbols","char":"☮","name":"peace_symbol","keywords":["hippie"]},{"category":"symbols","char":"✝","name":"latin_cross","keywords":["christianity"]},{"category":"symbols","char":"☪","name":"star_and_crescent","keywords":["islam"]},{"category":"symbols","char":"🕉","name":"om","keywords":["hinduism","buddhism","sikhism","jainism"]},{"category":"symbols","char":"☸","name":"wheel_of_dharma","keywords":["hinduism","buddhism","sikhism","jainism"]},{"category":"symbols","char":"✡","name":"star_of_david","keywords":["judaism"]},{"category":"symbols","char":"🔯","name":"six_pointed_star","keywords":["purple-square","religion","jewish","hexagram"]},{"category":"symbols","char":"🕎","name":"menorah","keywords":["hanukkah","candles","jewish"]},{"category":"symbols","char":"☯","name":"yin_yang","keywords":["balance"]},{"category":"symbols","char":"☦","name":"orthodox_cross","keywords":["suppedaneum","religion"]},{"category":"symbols","char":"🛐","name":"place_of_worship","keywords":["religion","church","temple","prayer"]},{"category":"symbols","char":"⛎","name":"ophiuchus","keywords":["sign","purple-square","constellation","astrology"]},{"category":"symbols","char":"♈","name":"aries","keywords":["sign","purple-square","zodiac","astrology"]},{"category":"symbols","char":"♉","name":"taurus","keywords":["purple-square","sign","zodiac","astrology"]},{"category":"symbols","char":"♊","name":"gemini","keywords":["sign","zodiac","purple-square","astrology"]},{"category":"symbols","char":"♋","name":"cancer","keywords":["sign","zodiac","purple-square","astrology"]},{"category":"symbols","char":"♌","name":"leo","keywords":["sign","purple-square","zodiac","astrology"]},{"category":"symbols","char":"♍","name":"virgo","keywords":["sign","zodiac","purple-square","astrology"]},{"category":"symbols","char":"♎","name":"libra","keywords":["sign","purple-square","zodiac","astrology"]},{"category":"symbols","char":"♏","name":"scorpius","keywords":["sign","zodiac","purple-square","astrology","scorpio"]},{"category":"symbols","char":"♐","name":"sagittarius","keywords":["sign","zodiac","purple-square","astrology"]},{"category":"symbols","char":"♑","name":"capricorn","keywords":["sign","zodiac","purple-square","astrology"]},{"category":"symbols","char":"♒","name":"aquarius","keywords":["sign","purple-square","zodiac","astrology"]},{"category":"symbols","char":"♓","name":"pisces","keywords":["purple-square","sign","zodiac","astrology"]},{"category":"symbols","char":"🆔","name":"id","keywords":["purple-square","words"]},{"category":"symbols","char":"⚛","name":"atom_symbol","keywords":["science","physics","chemistry"]},{"category":"symbols","char":"⚧️","name":"transgender_symbol","keywords":["purple-square","woman","female","toilet","loo","restroom","gender"]},{"category":"symbols","char":"🈳","name":"u7a7a","keywords":["kanji","japanese","chinese","empty","sky","blue-square","aki"]},{"category":"symbols","char":"🈹","name":"u5272","keywords":["cut","divide","chinese","kanji","pink-square","waribiki"]},{"category":"symbols","char":"☢","name":"radioactive","keywords":["nuclear","danger"]},{"category":"symbols","char":"☣","name":"biohazard","keywords":["danger"]},{"category":"symbols","char":"📴","name":"mobile_phone_off","keywords":["mute","orange-square","silence","quiet"]},{"category":"symbols","char":"📳","name":"vibration_mode","keywords":["orange-square","phone"]},{"category":"symbols","char":"🈶","name":"u6709","keywords":["orange-square","chinese","have","kanji","ari"]},{"category":"symbols","char":"🈚","name":"u7121","keywords":["nothing","chinese","kanji","japanese","orange-square","nashi"]},{"category":"symbols","char":"🈸","name":"u7533","keywords":["chinese","japanese","kanji","orange-square","moushikomi"]},{"category":"symbols","char":"🈺","name":"u55b6","keywords":["japanese","opening hours","orange-square","eigyo"]},{"category":"symbols","char":"🈷️","name":"u6708","keywords":["chinese","month","moon","japanese","orange-square","kanji","tsuki","tsukigime","getsugaku"]},{"category":"symbols","char":"✴️","name":"eight_pointed_black_star","keywords":["orange-square","shape","polygon"]},{"category":"symbols","char":"🆚","name":"vs","keywords":["words","orange-square"]},{"category":"symbols","char":"🉑","name":"accept","keywords":["ok","good","chinese","kanji","agree","yes","orange-circle"]},{"category":"symbols","char":"💮","name":"white_flower","keywords":["japanese","spring"]},{"category":"symbols","char":"🉐","name":"ideograph_advantage","keywords":["chinese","kanji","obtain","get","circle"]},{"category":"symbols","char":"㊙️","name":"secret","keywords":["privacy","chinese","sshh","kanji","red-circle"]},{"category":"symbols","char":"㊗️","name":"congratulations","keywords":["chinese","kanji","japanese","red-circle"]},{"category":"symbols","char":"🈴","name":"u5408","keywords":["japanese","chinese","join","kanji","red-square","goukaku","pass"]},{"category":"symbols","char":"🈵","name":"u6e80","keywords":["full","chinese","japanese","red-square","kanji","man"]},{"category":"symbols","char":"🈲","name":"u7981","keywords":["kanji","japanese","chinese","forbidden","limit","restricted","red-square","kinshi"]},{"category":"symbols","char":"🅰️","name":"a","keywords":["red-square","alphabet","letter"]},{"category":"symbols","char":"🅱️","name":"b","keywords":["red-square","alphabet","letter"]},{"category":"symbols","char":"🆎","name":"ab","keywords":["red-square","alphabet"]},{"category":"symbols","char":"🆑","name":"cl","keywords":["alphabet","words","red-square"]},{"category":"symbols","char":"🅾️","name":"o2","keywords":["alphabet","red-square","letter"]},{"category":"symbols","char":"🆘","name":"sos","keywords":["help","red-square","words","emergency","911"]},{"category":"symbols","char":"⛔","name":"no_entry","keywords":["limit","security","privacy","bad","denied","stop","circle"]},{"category":"symbols","char":"📛","name":"name_badge","keywords":["fire","forbid"]},{"category":"symbols","char":"🚫","name":"no_entry_sign","keywords":["forbid","stop","limit","denied","disallow","circle"]},{"category":"symbols","char":"❌","name":"x","keywords":["no","delete","remove","cancel","red"]},{"category":"symbols","char":"⭕","name":"o","keywords":["circle","round"]},{"category":"symbols","char":"🛑","name":"stop_sign","keywords":["stop"]},{"category":"symbols","char":"💢","name":"anger","keywords":["angry","mad"]},{"category":"symbols","char":"♨️","name":"hotsprings","keywords":["bath","warm","relax"]},{"category":"symbols","char":"🚷","name":"no_pedestrians","keywords":["rules","crossing","walking","circle"]},{"category":"symbols","char":"🚯","name":"do_not_litter","keywords":["trash","bin","garbage","circle"]},{"category":"symbols","char":"🚳","name":"no_bicycles","keywords":["cyclist","prohibited","circle"]},{"category":"symbols","char":"🚱","name":"non-potable_water","keywords":["drink","faucet","tap","circle"]},{"category":"symbols","char":"🔞","name":"underage","keywords":["18","drink","pub","night","minor","circle"]},{"category":"symbols","char":"📵","name":"no_mobile_phones","keywords":["iphone","mute","circle"]},{"category":"symbols","char":"❗","name":"exclamation","keywords":["heavy_exclamation_mark","danger","surprise","punctuation","wow","warning"]},{"category":"symbols","char":"❕","name":"grey_exclamation","keywords":["surprise","punctuation","gray","wow","warning"]},{"category":"symbols","char":"❓","name":"question","keywords":["doubt","confused"]},{"category":"symbols","char":"❔","name":"grey_question","keywords":["doubts","gray","huh","confused"]},{"category":"symbols","char":"‼️","name":"bangbang","keywords":["exclamation","surprise"]},{"category":"symbols","char":"⁉️","name":"interrobang","keywords":["wat","punctuation","surprise"]},{"category":"symbols","char":"🔅","name":"low_brightness","keywords":["sun","afternoon","warm","summer"]},{"category":"symbols","char":"🔆","name":"high_brightness","keywords":["sun","light"]},{"category":"symbols","char":"🔱","name":"trident","keywords":["weapon","spear"]},{"category":"symbols","char":"⚜","name":"fleur_de_lis","keywords":["decorative","scout"]},{"category":"symbols","char":"〽️","name":"part_alternation_mark","keywords":["graph","presentation","stats","business","economics","bad"]},{"category":"symbols","char":"⚠️","name":"warning","keywords":["exclamation","wip","alert","error","problem","issue"]},{"category":"symbols","char":"🚸","name":"children_crossing","keywords":["school","warning","danger","sign","driving","yellow-diamond"]},{"category":"symbols","char":"🔰","name":"beginner","keywords":["badge","shield"]},{"category":"symbols","char":"♻️","name":"recycle","keywords":["arrow","environment","garbage","trash"]},{"category":"symbols","char":"🈯","name":"u6307","keywords":["chinese","point","green-square","kanji","reserved","shiteiseki"]},{"category":"symbols","char":"💹","name":"chart","keywords":["green-square","graph","presentation","stats"]},{"category":"symbols","char":"❇️","name":"sparkle","keywords":["stars","green-square","awesome","good","fireworks"]},{"category":"symbols","char":"✳️","name":"eight_spoked_asterisk","keywords":["star","sparkle","green-square"]},{"category":"symbols","char":"❎","name":"negative_squared_cross_mark","keywords":["x","green-square","no","deny"]},{"category":"symbols","char":"✅","name":"white_check_mark","keywords":["green-square","ok","agree","vote","election","answer","tick"]},{"category":"symbols","char":"💠","name":"diamond_shape_with_a_dot_inside","keywords":["jewel","blue","gem","crystal","fancy"]},{"category":"symbols","char":"🌀","name":"cyclone","keywords":["weather","swirl","blue","cloud","vortex","spiral","whirlpool","spin","tornado","hurricane","typhoon"]},{"category":"symbols","char":"➿","name":"loop","keywords":["tape","cassette"]},{"category":"symbols","char":"🌐","name":"globe_with_meridians","keywords":["earth","international","world","internet","interweb","i18n"]},{"category":"symbols","char":"Ⓜ️","name":"m","keywords":["alphabet","blue-circle","letter"]},{"category":"symbols","char":"🏧","name":"atm","keywords":["money","sales","cash","blue-square","payment","bank"]},{"category":"symbols","char":"🈂️","name":"sa","keywords":["japanese","blue-square","katakana"]},{"category":"symbols","char":"🛂","name":"passport_control","keywords":["custom","blue-square"]},{"category":"symbols","char":"🛃","name":"customs","keywords":["passport","border","blue-square"]},{"category":"symbols","char":"🛄","name":"baggage_claim","keywords":["blue-square","airport","transport"]},{"category":"symbols","char":"🛅","name":"left_luggage","keywords":["blue-square","travel"]},{"category":"symbols","char":"♿","name":"wheelchair","keywords":["blue-square","disabled","a11y","accessibility"]},{"category":"symbols","char":"🚭","name":"no_smoking","keywords":["cigarette","blue-square","smell","smoke"]},{"category":"symbols","char":"🚾","name":"wc","keywords":["toilet","restroom","blue-square"]},{"category":"symbols","char":"🅿️","name":"parking","keywords":["cars","blue-square","alphabet","letter"]},{"category":"symbols","char":"🚰","name":"potable_water","keywords":["blue-square","liquid","restroom","cleaning","faucet"]},{"category":"symbols","char":"🚹","name":"mens","keywords":["toilet","restroom","wc","blue-square","gender","male"]},{"category":"symbols","char":"🚺","name":"womens","keywords":["purple-square","woman","female","toilet","loo","restroom","gender"]},{"category":"symbols","char":"🚼","name":"baby_symbol","keywords":["orange-square","child"]},{"category":"symbols","char":"🚻","name":"restroom","keywords":["blue-square","toilet","refresh","wc","gender"]},{"category":"symbols","char":"🚮","name":"put_litter_in_its_place","keywords":["blue-square","sign","human","info"]},{"category":"symbols","char":"🎦","name":"cinema","keywords":["blue-square","record","film","movie","curtain","stage","theater"]},{"category":"symbols","char":"📶","name":"signal_strength","keywords":["blue-square","reception","phone","internet","connection","wifi","bluetooth","bars"]},{"category":"symbols","char":"🈁","name":"koko","keywords":["blue-square","here","katakana","japanese","destination"]},{"category":"symbols","char":"🆖","name":"ng","keywords":["blue-square","words","shape","icon"]},{"category":"symbols","char":"🆗","name":"ok","keywords":["good","agree","yes","blue-square"]},{"category":"symbols","char":"🆙","name":"up","keywords":["blue-square","above","high"]},{"category":"symbols","char":"🆒","name":"cool","keywords":["words","blue-square"]},{"category":"symbols","char":"🆕","name":"new","keywords":["blue-square","words","start"]},{"category":"symbols","char":"🆓","name":"free","keywords":["blue-square","words"]},{"category":"symbols","char":"0️⃣","name":"zero","keywords":["0","numbers","blue-square","null"]},{"category":"symbols","char":"1️⃣","name":"one","keywords":["blue-square","numbers","1"]},{"category":"symbols","char":"2️⃣","name":"two","keywords":["numbers","2","prime","blue-square"]},{"category":"symbols","char":"3️⃣","name":"three","keywords":["3","numbers","prime","blue-square"]},{"category":"symbols","char":"4️⃣","name":"four","keywords":["4","numbers","blue-square"]},{"category":"symbols","char":"5️⃣","name":"five","keywords":["5","numbers","blue-square","prime"]},{"category":"symbols","char":"6️⃣","name":"six","keywords":["6","numbers","blue-square"]},{"category":"symbols","char":"7️⃣","name":"seven","keywords":["7","numbers","blue-square","prime"]},{"category":"symbols","char":"8️⃣","name":"eight","keywords":["8","blue-square","numbers"]},{"category":"symbols","char":"9️⃣","name":"nine","keywords":["blue-square","numbers","9"]},{"category":"symbols","char":"🔟","name":"keycap_ten","keywords":["numbers","10","blue-square"]},{"category":"symbols","char":"*⃣","name":"asterisk","keywords":["star","keycap"]},{"category":"symbols","char":"⏏️","name":"eject_button","keywords":["blue-square"]},{"category":"symbols","char":"▶️","name":"arrow_forward","keywords":["blue-square","right","direction","play"]},{"category":"symbols","char":"⏸","name":"pause_button","keywords":["pause","blue-square"]},{"category":"symbols","char":"⏭","name":"next_track_button","keywords":["forward","next","blue-square"]},{"category":"symbols","char":"⏹","name":"stop_button","keywords":["blue-square"]},{"category":"symbols","char":"⏺","name":"record_button","keywords":["blue-square"]},{"category":"symbols","char":"⏯","name":"play_or_pause_button","keywords":["blue-square","play","pause"]},{"category":"symbols","char":"⏮","name":"previous_track_button","keywords":["backward"]},{"category":"symbols","char":"⏩","name":"fast_forward","keywords":["blue-square","play","speed","continue"]},{"category":"symbols","char":"⏪","name":"rewind","keywords":["play","blue-square"]},{"category":"symbols","char":"🔀","name":"twisted_rightwards_arrows","keywords":["blue-square","shuffle","music","random"]},{"category":"symbols","char":"🔁","name":"repeat","keywords":["loop","record"]},{"category":"symbols","char":"🔂","name":"repeat_one","keywords":["blue-square","loop"]},{"category":"symbols","char":"◀️","name":"arrow_backward","keywords":["blue-square","left","direction"]},{"category":"symbols","char":"🔼","name":"arrow_up_small","keywords":["blue-square","triangle","direction","point","forward","top"]},{"category":"symbols","char":"🔽","name":"arrow_down_small","keywords":["blue-square","direction","bottom"]},{"category":"symbols","char":"⏫","name":"arrow_double_up","keywords":["blue-square","direction","top"]},{"category":"symbols","char":"⏬","name":"arrow_double_down","keywords":["blue-square","direction","bottom"]},{"category":"symbols","char":"➡️","name":"arrow_right","keywords":["blue-square","next"]},{"category":"symbols","char":"⬅️","name":"arrow_left","keywords":["blue-square","previous","back"]},{"category":"symbols","char":"⬆️","name":"arrow_up","keywords":["blue-square","continue","top","direction"]},{"category":"symbols","char":"⬇️","name":"arrow_down","keywords":["blue-square","direction","bottom"]},{"category":"symbols","char":"↗️","name":"arrow_upper_right","keywords":["blue-square","point","direction","diagonal","northeast"]},{"category":"symbols","char":"↘️","name":"arrow_lower_right","keywords":["blue-square","direction","diagonal","southeast"]},{"category":"symbols","char":"↙️","name":"arrow_lower_left","keywords":["blue-square","direction","diagonal","southwest"]},{"category":"symbols","char":"↖️","name":"arrow_upper_left","keywords":["blue-square","point","direction","diagonal","northwest"]},{"category":"symbols","char":"↕️","name":"arrow_up_down","keywords":["blue-square","direction","way","vertical"]},{"category":"symbols","char":"↔️","name":"left_right_arrow","keywords":["shape","direction","horizontal","sideways"]},{"category":"symbols","char":"🔄","name":"arrows_counterclockwise","keywords":["blue-square","sync","cycle"]},{"category":"symbols","char":"↪️","name":"arrow_right_hook","keywords":["blue-square","return","rotate","direction"]},{"category":"symbols","char":"↩️","name":"leftwards_arrow_with_hook","keywords":["back","return","blue-square","undo","enter"]},{"category":"symbols","char":"⤴️","name":"arrow_heading_up","keywords":["blue-square","direction","top"]},{"category":"symbols","char":"⤵️","name":"arrow_heading_down","keywords":["blue-square","direction","bottom"]},{"category":"symbols","char":"#️⃣","name":"hash","keywords":["symbol","blue-square","twitter"]},{"category":"symbols","char":"ℹ️","name":"information_source","keywords":["blue-square","alphabet","letter"]},{"category":"symbols","char":"🔤","name":"abc","keywords":["blue-square","alphabet"]},{"category":"symbols","char":"🔡","name":"abcd","keywords":["blue-square","alphabet"]},{"category":"symbols","char":"🔠","name":"capital_abcd","keywords":["alphabet","words","blue-square"]},{"category":"symbols","char":"🔣","name":"symbols","keywords":["blue-square","music","note","ampersand","percent","glyphs","characters"]},{"category":"symbols","char":"🎵","name":"musical_note","keywords":["score","tone","sound"]},{"category":"symbols","char":"🎶","name":"notes","keywords":["music","score"]},{"category":"symbols","char":"〰️","name":"wavy_dash","keywords":["draw","line","moustache","mustache","squiggle","scribble"]},{"category":"symbols","char":"➰","name":"curly_loop","keywords":["scribble","draw","shape","squiggle"]},{"category":"symbols","char":"✔️","name":"heavy_check_mark","keywords":["ok","nike","answer","yes","tick"]},{"category":"symbols","char":"🔃","name":"arrows_clockwise","keywords":["sync","cycle","round","repeat"]},{"category":"symbols","char":"➕","name":"heavy_plus_sign","keywords":["math","calculation","addition","more","increase"]},{"category":"symbols","char":"➖","name":"heavy_minus_sign","keywords":["math","calculation","subtract","less"]},{"category":"symbols","char":"➗","name":"heavy_division_sign","keywords":["divide","math","calculation"]},{"category":"symbols","char":"✖️","name":"heavy_multiplication_x","keywords":["math","calculation"]},{"category":"symbols","char":"🟰","name":"heavy_equals_sign","keywords":[]},{"category":"symbols","char":"♾","name":"infinity","keywords":["forever"]},{"category":"symbols","char":"💲","name":"heavy_dollar_sign","keywords":["money","sales","payment","currency","buck"]},{"category":"symbols","char":"💱","name":"currency_exchange","keywords":["money","sales","dollar","travel"]},{"category":"symbols","char":"©️","name":"copyright","keywords":["ip","license","circle","law","legal"]},{"category":"symbols","char":"®️","name":"registered","keywords":["alphabet","circle"]},{"category":"symbols","char":"™️","name":"tm","keywords":["trademark","brand","law","legal"]},{"category":"symbols","char":"🔚","name":"end","keywords":["words","arrow"]},{"category":"symbols","char":"🔙","name":"back","keywords":["arrow","words","return"]},{"category":"symbols","char":"🔛","name":"on","keywords":["arrow","words"]},{"category":"symbols","char":"🔝","name":"top","keywords":["words","blue-square"]},{"category":"symbols","char":"🔜","name":"soon","keywords":["arrow","words"]},{"category":"symbols","char":"☑️","name":"ballot_box_with_check","keywords":["ok","agree","confirm","black-square","vote","election","yes","tick"]},{"category":"symbols","char":"🔘","name":"radio_button","keywords":["input","old","music","circle"]},{"category":"symbols","char":"⚫","name":"black_circle","keywords":["shape","button","round"]},{"category":"symbols","char":"⚪","name":"white_circle","keywords":["shape","round"]},{"category":"symbols","char":"🔴","name":"red_circle","keywords":["shape","error","danger"]},{"category":"symbols","char":"🟠","name":"orange_circle","keywords":["shape"]},{"category":"symbols","char":"🟡","name":"yellow_circle","keywords":["shape"]},{"category":"symbols","char":"🟢","name":"green_circle","keywords":["shape"]},{"category":"symbols","char":"🔵","name":"large_blue_circle","keywords":["shape","icon","button"]},{"category":"symbols","char":"🟣","name":"purple_circle","keywords":["shape"]},{"category":"symbols","char":"🟤","name":"brown_circle","keywords":["shape"]},{"category":"symbols","char":"🔸","name":"small_orange_diamond","keywords":["shape","jewel","gem"]},{"category":"symbols","char":"🔹","name":"small_blue_diamond","keywords":["shape","jewel","gem"]},{"category":"symbols","char":"🔶","name":"large_orange_diamond","keywords":["shape","jewel","gem"]},{"category":"symbols","char":"🔷","name":"large_blue_diamond","keywords":["shape","jewel","gem"]},{"category":"symbols","char":"🔺","name":"small_red_triangle","keywords":["shape","direction","up","top"]},{"category":"symbols","char":"▪️","name":"black_small_square","keywords":["shape","icon"]},{"category":"symbols","char":"▫️","name":"white_small_square","keywords":["shape","icon"]},{"category":"symbols","char":"⬛","name":"black_large_square","keywords":["shape","icon","button"]},{"category":"symbols","char":"⬜","name":"white_large_square","keywords":["shape","icon","stone","button"]},{"category":"symbols","char":"🟥","name":"red_square","keywords":["shape"]},{"category":"symbols","char":"🟧","name":"orange_square","keywords":["shape"]},{"category":"symbols","char":"🟨","name":"yellow_square","keywords":["shape"]},{"category":"symbols","char":"🟩","name":"green_square","keywords":["shape"]},{"category":"symbols","char":"🟦","name":"blue_square","keywords":["shape"]},{"category":"symbols","char":"🟪","name":"purple_square","keywords":["shape"]},{"category":"symbols","char":"🟫","name":"brown_square","keywords":["shape"]},{"category":"symbols","char":"🔻","name":"small_red_triangle_down","keywords":["shape","direction","bottom"]},{"category":"symbols","char":"◼️","name":"black_medium_square","keywords":["shape","button","icon"]},{"category":"symbols","char":"◻️","name":"white_medium_square","keywords":["shape","stone","icon"]},{"category":"symbols","char":"◾","name":"black_medium_small_square","keywords":["icon","shape","button"]},{"category":"symbols","char":"◽","name":"white_medium_small_square","keywords":["shape","stone","icon","button"]},{"category":"symbols","char":"🔲","name":"black_square_button","keywords":["shape","input","frame"]},{"category":"symbols","char":"🔳","name":"white_square_button","keywords":["shape","input"]},{"category":"symbols","char":"🔈","name":"speaker","keywords":["sound","volume","silence","broadcast"]},{"category":"symbols","char":"🔉","name":"sound","keywords":["volume","speaker","broadcast"]},{"category":"symbols","char":"🔊","name":"loud_sound","keywords":["volume","noise","noisy","speaker","broadcast"]},{"category":"symbols","char":"🔇","name":"mute","keywords":["sound","volume","silence","quiet"]},{"category":"symbols","char":"📣","name":"mega","keywords":["sound","speaker","volume"]},{"category":"symbols","char":"📢","name":"loudspeaker","keywords":["volume","sound"]},{"category":"symbols","char":"🔔","name":"bell","keywords":["sound","notification","christmas","xmas","chime"]},{"category":"symbols","char":"🔕","name":"no_bell","keywords":["sound","volume","mute","quiet","silent"]},{"category":"symbols","char":"🃏","name":"black_joker","keywords":["poker","cards","game","play","magic"]},{"category":"symbols","char":"🀄","name":"mahjong","keywords":["game","play","chinese","kanji"]},{"category":"symbols","char":"♠️","name":"spades","keywords":["poker","cards","suits","magic"]},{"category":"symbols","char":"♣️","name":"clubs","keywords":["poker","cards","magic","suits"]},{"category":"symbols","char":"♥️","name":"hearts","keywords":["poker","cards","magic","suits"]},{"category":"symbols","char":"♦️","name":"diamonds","keywords":["poker","cards","magic","suits"]},{"category":"symbols","char":"🎴","name":"flower_playing_cards","keywords":["game","sunset","red"]},{"category":"symbols","char":"💭","name":"thought_balloon","keywords":["bubble","cloud","speech","thinking","dream"]},{"category":"symbols","char":"🗯","name":"right_anger_bubble","keywords":["caption","speech","thinking","mad"]},{"category":"symbols","char":"💬","name":"speech_balloon","keywords":["bubble","words","message","talk","chatting"]},{"category":"symbols","char":"🗨","name":"left_speech_bubble","keywords":["words","message","talk","chatting"]},{"category":"symbols","char":"🕐","name":"clock1","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕑","name":"clock2","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕒","name":"clock3","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕓","name":"clock4","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕔","name":"clock5","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕕","name":"clock6","keywords":["time","late","early","schedule","dawn","dusk"]},{"category":"symbols","char":"🕖","name":"clock7","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕗","name":"clock8","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕘","name":"clock9","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕙","name":"clock10","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕚","name":"clock11","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕛","name":"clock12","keywords":["time","noon","midnight","midday","late","early","schedule"]},{"category":"symbols","char":"🕜","name":"clock130","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕝","name":"clock230","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕞","name":"clock330","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕟","name":"clock430","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕠","name":"clock530","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕡","name":"clock630","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕢","name":"clock730","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕣","name":"clock830","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕤","name":"clock930","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕥","name":"clock1030","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕦","name":"clock1130","keywords":["time","late","early","schedule"]},{"category":"symbols","char":"🕧","name":"clock1230","keywords":["time","late","early","schedule"]},{"category":"flags","char":"🇦🇫","name":"afghanistan","keywords":["af","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇽","name":"aland_islands","keywords":["Åland","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇱","name":"albania","keywords":["al","flag","nation","country","banner"]},{"category":"flags","char":"🇩🇿","name":"algeria","keywords":["dz","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇸","name":"american_samoa","keywords":["american","ws","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇩","name":"andorra","keywords":["ad","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇴","name":"angola","keywords":["ao","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇮","name":"anguilla","keywords":["ai","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇶","name":"antarctica","keywords":["aq","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇬","name":"antigua_barbuda","keywords":["antigua","barbuda","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇷","name":"argentina","keywords":["ar","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇲","name":"armenia","keywords":["am","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇼","name":"aruba","keywords":["aw","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇨","name":"ascension_island","keywords":["flag","nation","country","banner"]},{"category":"flags","char":"🇦🇺","name":"australia","keywords":["au","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇹","name":"austria","keywords":["at","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇿","name":"azerbaijan","keywords":["az","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇸","name":"bahamas","keywords":["bs","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇭","name":"bahrain","keywords":["bh","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇩","name":"bangladesh","keywords":["bd","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇧","name":"barbados","keywords":["bb","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇾","name":"belarus","keywords":["by","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇪","name":"belgium","keywords":["be","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇿","name":"belize","keywords":["bz","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇯","name":"benin","keywords":["bj","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇲","name":"bermuda","keywords":["bm","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇹","name":"bhutan","keywords":["bt","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇴","name":"bolivia","keywords":["bo","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇶","name":"caribbean_netherlands","keywords":["bonaire","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇦","name":"bosnia_herzegovina","keywords":["bosnia","herzegovina","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇼","name":"botswana","keywords":["bw","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇷","name":"brazil","keywords":["br","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇴","name":"british_indian_ocean_territory","keywords":["british","indian","ocean","territory","flag","nation","country","banner"]},{"category":"flags","char":"🇻🇬","name":"british_virgin_islands","keywords":["british","virgin","islands","bvi","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇳","name":"brunei","keywords":["bn","darussalam","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇬","name":"bulgaria","keywords":["bg","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇫","name":"burkina_faso","keywords":["burkina","faso","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇮","name":"burundi","keywords":["bi","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇻","name":"cape_verde","keywords":["cabo","verde","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇭","name":"cambodia","keywords":["kh","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇲","name":"cameroon","keywords":["cm","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇦","name":"canada","keywords":["ca","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇨","name":"canary_islands","keywords":["canary","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇾","name":"cayman_islands","keywords":["cayman","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇫","name":"central_african_republic","keywords":["central","african","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇩","name":"chad","keywords":["td","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇱","name":"chile","keywords":["flag","nation","country","banner"]},{"category":"flags","char":"🇨🇳","name":"cn","keywords":["china","chinese","prc","flag","country","nation","banner"]},{"category":"flags","char":"🇨🇽","name":"christmas_island","keywords":["christmas","island","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇨","name":"cocos_islands","keywords":["cocos","keeling","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇴","name":"colombia","keywords":["co","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇲","name":"comoros","keywords":["km","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇬","name":"congo_brazzaville","keywords":["congo","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇩","name":"congo_kinshasa","keywords":["congo","democratic","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇰","name":"cook_islands","keywords":["cook","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇷","name":"costa_rica","keywords":["costa","rica","flag","nation","country","banner"]},{"category":"flags","char":"🇭🇷","name":"croatia","keywords":["hr","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇺","name":"cuba","keywords":["cu","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇼","name":"curacao","keywords":["curaçao","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇾","name":"cyprus","keywords":["cy","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇿","name":"czech_republic","keywords":["cz","flag","nation","country","banner"]},{"category":"flags","char":"🇩🇰","name":"denmark","keywords":["dk","flag","nation","country","banner"]},{"category":"flags","char":"🇩🇯","name":"djibouti","keywords":["dj","flag","nation","country","banner"]},{"category":"flags","char":"🇩🇲","name":"dominica","keywords":["dm","flag","nation","country","banner"]},{"category":"flags","char":"🇩🇴","name":"dominican_republic","keywords":["dominican","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇪🇨","name":"ecuador","keywords":["ec","flag","nation","country","banner"]},{"category":"flags","char":"🇪🇬","name":"egypt","keywords":["eg","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇻","name":"el_salvador","keywords":["el","salvador","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇶","name":"equatorial_guinea","keywords":["equatorial","gn","flag","nation","country","banner"]},{"category":"flags","char":"🇪🇷","name":"eritrea","keywords":["er","flag","nation","country","banner"]},{"category":"flags","char":"🇪🇪","name":"estonia","keywords":["ee","flag","nation","country","banner"]},{"category":"flags","char":"🇪🇹","name":"ethiopia","keywords":["et","flag","nation","country","banner"]},{"category":"flags","char":"🇪🇺","name":"eu","keywords":["european","union","flag","banner"]},{"category":"flags","char":"🇫🇰","name":"falkland_islands","keywords":["falkland","islands","malvinas","flag","nation","country","banner"]},{"category":"flags","char":"🇫🇴","name":"faroe_islands","keywords":["faroe","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇫🇯","name":"fiji","keywords":["fj","flag","nation","country","banner"]},{"category":"flags","char":"🇫🇮","name":"finland","keywords":["fi","flag","nation","country","banner"]},{"category":"flags","char":"🇫🇷","name":"fr","keywords":["banner","flag","nation","france","french","country"]},{"category":"flags","char":"🇬🇫","name":"french_guiana","keywords":["french","guiana","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇫","name":"french_polynesia","keywords":["french","polynesia","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇫","name":"french_southern_territories","keywords":["french","southern","territories","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇦","name":"gabon","keywords":["ga","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇲","name":"gambia","keywords":["gm","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇪","name":"georgia","keywords":["ge","flag","nation","country","banner"]},{"category":"flags","char":"🇩🇪","name":"de","keywords":["german","nation","flag","country","banner"]},{"category":"flags","char":"🇬🇭","name":"ghana","keywords":["gh","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇮","name":"gibraltar","keywords":["gi","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇷","name":"greece","keywords":["gr","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇱","name":"greenland","keywords":["gl","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇩","name":"grenada","keywords":["gd","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇵","name":"guadeloupe","keywords":["gp","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇺","name":"guam","keywords":["gu","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇹","name":"guatemala","keywords":["gt","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇬","name":"guernsey","keywords":["gg","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇳","name":"guinea","keywords":["gn","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇼","name":"guinea_bissau","keywords":["gw","bissau","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇾","name":"guyana","keywords":["gy","flag","nation","country","banner"]},{"category":"flags","char":"🇭🇹","name":"haiti","keywords":["ht","flag","nation","country","banner"]},{"category":"flags","char":"🇭🇳","name":"honduras","keywords":["hn","flag","nation","country","banner"]},{"category":"flags","char":"🇭🇰","name":"hong_kong","keywords":["hong","kong","flag","nation","country","banner"]},{"category":"flags","char":"🇭🇺","name":"hungary","keywords":["hu","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇸","name":"iceland","keywords":["is","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇳","name":"india","keywords":["in","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇩","name":"indonesia","keywords":["flag","nation","country","banner"]},{"category":"flags","char":"🇮🇷","name":"iran","keywords":["iran, ","islamic","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇶","name":"iraq","keywords":["iq","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇪","name":"ireland","keywords":["ie","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇲","name":"isle_of_man","keywords":["isle","man","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇱","name":"israel","keywords":["il","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇹","name":"it","keywords":["italy","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇮","name":"cote_divoire","keywords":["ivory","coast","flag","nation","country","banner"]},{"category":"flags","char":"🇯🇲","name":"jamaica","keywords":["jm","flag","nation","country","banner"]},{"category":"flags","char":"🇯🇵","name":"jp","keywords":["japanese","nation","flag","country","banner"]},{"category":"flags","char":"🇯🇪","name":"jersey","keywords":["je","flag","nation","country","banner"]},{"category":"flags","char":"🇯🇴","name":"jordan","keywords":["jo","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇿","name":"kazakhstan","keywords":["kz","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇪","name":"kenya","keywords":["ke","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇮","name":"kiribati","keywords":["ki","flag","nation","country","banner"]},{"category":"flags","char":"🇽🇰","name":"kosovo","keywords":["xk","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇼","name":"kuwait","keywords":["kw","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇬","name":"kyrgyzstan","keywords":["kg","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇦","name":"laos","keywords":["lao","democratic","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇻","name":"latvia","keywords":["lv","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇧","name":"lebanon","keywords":["lb","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇸","name":"lesotho","keywords":["ls","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇷","name":"liberia","keywords":["lr","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇾","name":"libya","keywords":["ly","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇮","name":"liechtenstein","keywords":["li","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇹","name":"lithuania","keywords":["lt","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇺","name":"luxembourg","keywords":["lu","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇴","name":"macau","keywords":["macao","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇰","name":"macedonia","keywords":["macedonia, ","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇬","name":"madagascar","keywords":["mg","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇼","name":"malawi","keywords":["mw","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇾","name":"malaysia","keywords":["my","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇻","name":"maldives","keywords":["mv","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇱","name":"mali","keywords":["ml","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇹","name":"malta","keywords":["mt","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇭","name":"marshall_islands","keywords":["marshall","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇶","name":"martinique","keywords":["mq","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇷","name":"mauritania","keywords":["mr","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇺","name":"mauritius","keywords":["mu","flag","nation","country","banner"]},{"category":"flags","char":"🇾🇹","name":"mayotte","keywords":["yt","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇽","name":"mexico","keywords":["mx","flag","nation","country","banner"]},{"category":"flags","char":"🇫🇲","name":"micronesia","keywords":["micronesia, ","federated","states","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇩","name":"moldova","keywords":["moldova, ","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇨","name":"monaco","keywords":["mc","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇳","name":"mongolia","keywords":["mn","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇪","name":"montenegro","keywords":["me","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇸","name":"montserrat","keywords":["ms","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇦","name":"morocco","keywords":["ma","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇿","name":"mozambique","keywords":["mz","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇲","name":"myanmar","keywords":["mm","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇦","name":"namibia","keywords":["na","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇷","name":"nauru","keywords":["nr","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇵","name":"nepal","keywords":["np","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇱","name":"netherlands","keywords":["nl","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇨","name":"new_caledonia","keywords":["new","caledonia","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇿","name":"new_zealand","keywords":["new","zealand","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇮","name":"nicaragua","keywords":["ni","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇪","name":"niger","keywords":["ne","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇬","name":"nigeria","keywords":["flag","nation","country","banner"]},{"category":"flags","char":"🇳🇺","name":"niue","keywords":["nu","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇫","name":"norfolk_island","keywords":["norfolk","island","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇵","name":"northern_mariana_islands","keywords":["northern","mariana","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇵","name":"north_korea","keywords":["north","korea","nation","flag","country","banner"]},{"category":"flags","char":"🇳🇴","name":"norway","keywords":["no","flag","nation","country","banner"]},{"category":"flags","char":"🇴🇲","name":"oman","keywords":["om_symbol","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇰","name":"pakistan","keywords":["pk","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇼","name":"palau","keywords":["pw","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇸","name":"palestinian_territories","keywords":["palestine","palestinian","territories","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇦","name":"panama","keywords":["pa","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇬","name":"papua_new_guinea","keywords":["papua","new","guinea","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇾","name":"paraguay","keywords":["py","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇪","name":"peru","keywords":["pe","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇭","name":"philippines","keywords":["ph","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇳","name":"pitcairn_islands","keywords":["pitcairn","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇱","name":"poland","keywords":["pl","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇹","name":"portugal","keywords":["pt","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇷","name":"puerto_rico","keywords":["puerto","rico","flag","nation","country","banner"]},{"category":"flags","char":"🇶🇦","name":"qatar","keywords":["qa","flag","nation","country","banner"]},{"category":"flags","char":"🇷🇪","name":"reunion","keywords":["réunion","flag","nation","country","banner"]},{"category":"flags","char":"🇷🇴","name":"romania","keywords":["ro","flag","nation","country","banner"]},{"category":"flags","char":"🇷🇺","name":"ru","keywords":["russian","federation","flag","nation","country","banner"]},{"category":"flags","char":"🇷🇼","name":"rwanda","keywords":["rw","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇱","name":"st_barthelemy","keywords":["saint","barthélemy","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇭","name":"st_helena","keywords":["saint","helena","ascension","tristan","cunha","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇳","name":"st_kitts_nevis","keywords":["saint","kitts","nevis","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇨","name":"st_lucia","keywords":["saint","lucia","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇲","name":"st_pierre_miquelon","keywords":["saint","pierre","miquelon","flag","nation","country","banner"]},{"category":"flags","char":"🇻🇨","name":"st_vincent_grenadines","keywords":["saint","vincent","grenadines","flag","nation","country","banner"]},{"category":"flags","char":"🇼🇸","name":"samoa","keywords":["ws","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇲","name":"san_marino","keywords":["san","marino","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇹","name":"sao_tome_principe","keywords":["sao","tome","principe","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇦","name":"saudi_arabia","keywords":["flag","nation","country","banner"]},{"category":"flags","char":"🇸🇳","name":"senegal","keywords":["sn","flag","nation","country","banner"]},{"category":"flags","char":"🇷🇸","name":"serbia","keywords":["rs","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇨","name":"seychelles","keywords":["sc","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇱","name":"sierra_leone","keywords":["sierra","leone","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇬","name":"singapore","keywords":["sg","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇽","name":"sint_maarten","keywords":["sint","maarten","dutch","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇰","name":"slovakia","keywords":["sk","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇮","name":"slovenia","keywords":["si","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇧","name":"solomon_islands","keywords":["solomon","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇴","name":"somalia","keywords":["so","flag","nation","country","banner"]},{"category":"flags","char":"🇿🇦","name":"south_africa","keywords":["south","africa","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇸","name":"south_georgia_south_sandwich_islands","keywords":["south","georgia","sandwich","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇷","name":"kr","keywords":["south","korea","nation","flag","country","banner"]},{"category":"flags","char":"🇸🇸","name":"south_sudan","keywords":["south","sd","flag","nation","country","banner"]},{"category":"flags","char":"🇪🇸","name":"es","keywords":["spain","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇰","name":"sri_lanka","keywords":["sri","lanka","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇩","name":"sudan","keywords":["sd","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇷","name":"suriname","keywords":["sr","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇿","name":"swaziland","keywords":["sz","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇪","name":"sweden","keywords":["se","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇭","name":"switzerland","keywords":["ch","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇾","name":"syria","keywords":["syrian","arab","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇼","name":"taiwan","keywords":["tw","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇯","name":"tajikistan","keywords":["tj","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇿","name":"tanzania","keywords":["tanzania, ","united","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇭","name":"thailand","keywords":["th","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇱","name":"timor_leste","keywords":["timor","leste","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇬","name":"togo","keywords":["tg","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇰","name":"tokelau","keywords":["tk","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇴","name":"tonga","keywords":["to","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇹","name":"trinidad_tobago","keywords":["trinidad","tobago","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇦","name":"tristan_da_cunha","keywords":["flag","nation","country","banner"]},{"category":"flags","char":"🇹🇳","name":"tunisia","keywords":["tn","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇷","name":"tr","keywords":["turkey","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇲","name":"turkmenistan","keywords":["flag","nation","country","banner"]},{"category":"flags","char":"🇹🇨","name":"turks_caicos_islands","keywords":["turks","caicos","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇻","name":"tuvalu","keywords":["flag","nation","country","banner"]},{"category":"flags","char":"🇺🇬","name":"uganda","keywords":["ug","flag","nation","country","banner"]},{"category":"flags","char":"🇺🇦","name":"ukraine","keywords":["ua","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇪","name":"united_arab_emirates","keywords":["united","arab","emirates","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇧","name":"uk","keywords":["united","kingdom","great","britain","northern","ireland","flag","nation","country","banner","british","UK","english","england","union jack"]},{"category":"flags","char":"🏴󠁧󠁢󠁥󠁮󠁧󠁿","name":"england","keywords":["flag","english"]},{"category":"flags","char":"🏴󠁧󠁢󠁳󠁣󠁴󠁿","name":"scotland","keywords":["flag","scottish"]},{"category":"flags","char":"🏴󠁧󠁢󠁷󠁬󠁳󠁿","name":"wales","keywords":["flag","welsh"]},{"category":"flags","char":"🇺🇸","name":"us","keywords":["united","states","america","flag","nation","country","banner"]},{"category":"flags","char":"🇻🇮","name":"us_virgin_islands","keywords":["virgin","islands","us","flag","nation","country","banner"]},{"category":"flags","char":"🇺🇾","name":"uruguay","keywords":["uy","flag","nation","country","banner"]},{"category":"flags","char":"🇺🇿","name":"uzbekistan","keywords":["uz","flag","nation","country","banner"]},{"category":"flags","char":"🇻🇺","name":"vanuatu","keywords":["vu","flag","nation","country","banner"]},{"category":"flags","char":"🇻🇦","name":"vatican_city","keywords":["vatican","city","flag","nation","country","banner"]},{"category":"flags","char":"🇻🇪","name":"venezuela","keywords":["ve","bolivarian","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇻🇳","name":"vietnam","keywords":["viet","nam","flag","nation","country","banner"]},{"category":"flags","char":"🇼🇫","name":"wallis_futuna","keywords":["wallis","futuna","flag","nation","country","banner"]},{"category":"flags","char":"🇪🇭","name":"western_sahara","keywords":["western","sahara","flag","nation","country","banner"]},{"category":"flags","char":"🇾🇪","name":"yemen","keywords":["ye","flag","nation","country","banner"]},{"category":"flags","char":"🇿🇲","name":"zambia","keywords":["zm","flag","nation","country","banner"]},{"category":"flags","char":"🇿🇼","name":"zimbabwe","keywords":["zw","flag","nation","country","banner"]},{"category":"flags","char":"🇺🇳","name":"united_nations","keywords":["un","flag","banner"]},{"category":"flags","char":"🏴‍☠️","name":"pirate_flag","keywords":["skull","crossbones","flag","banner"]}] \ No newline at end of file +[{"category":"face","char":"😀","name":"grinning","keywords":["にやにやしたかお","かお","にやにや","しあわせ","にやにやした顔","顔","にやにや","幸せ","しあわせ","face","smile","happy","joy",": D","grin"]},{"category":"face","char":"😬","name":"grimacing","keywords":["しかめっつら","かお","しかめっ面","顔","しかめっつら","face","grimace","teeth"]},{"category":"face","char":"😁","name":"grin","keywords":["にやにやしたかお","め","かお","にやにや","えがお","にやにやした顔","目","顔","にやにや","笑顔","face","happy","smile","joy","kawaii"]},{"category":"face","char":"😂","name":"joy","keywords":["うれしなき","かお","うれしい","わらう","なく","なみだ","嬉し泣き","顔","嬉しい","うれしい","笑う","泣く","涙","face","cry","tears","weep","happy","happytears","haha"]},{"category":"face","char":"🤣","name":"rofl","keywords":["だいばくしょう","かお","ゆか","わらい","おおわらい","ばくしょう","ぐるぐる","大爆笑","顔","床","笑い","大笑い","爆笑","ぐるぐる","face","rolling","floor","laughing","lol","haha"]},{"category":"face","char":"🥳","name":"partying","keywords":["ぱーてぃーふぇいす","かお","しゅくてん","ぼうし","つの","ぱーてぃー","パーティーフェイス","顔","祝典","帽子","角","パーティー","face","celebration","woohoo"]},{"category":"face","char":"😃","name":"smiley","keywords":["くちをあけたえがお","かお","くち","あける","えがお","しあわせ","口を開けた笑顔","顔","口","開ける","笑顔","幸せ","しあわせ","face","happy","joy","haha",": D",": )","smile","funny"]},{"category":"face","char":"😄","name":"smile","keywords":["くちをあけてめがわらっているえがお","め","かお","くち","あける","えがお","しあわせ","口を開けて目が笑っている笑顔","目","顔","口","開ける","笑顔","幸せ","しあわせ","face","happy","joy","funny","haha","laugh","like",": D",": )"]},{"category":"face","char":"😅","name":"sweat_smile","keywords":["くちをあけてひやあせをかいたえがお","ぞっとする","かお","くちをあける","えがお","ひやあせ","口を開けて冷や汗をかいた笑顔","ぞっとする","顔","口を開ける","笑顔","冷や汗","face","hot","happy","laugh","sweat","smile","relief"]},{"category":"face","char":"🥲","name":"smiling_face_with_tear","keywords":["なみだのでているえがお","なく","しあわせ","かんしゃする","ほこりにおもう","あんしんする","わらう","涙の出ている笑顔","泣く","幸せ","感謝する","誇りに思う","安心する","笑う","face"]},{"category":"face","char":"😆","name":"laughing","keywords":["くちをあけてわらっているかお","かお","わらい","くち","あける","まんぞく","えがお","口を開けて笑っている顔","顔","笑い","口","開ける","満足","笑顔","happy","joy","lol","satisfied","haha","face","glad","XD","laugh"]},{"category":"face","char":"😇","name":"innocent","keywords":["てんしのえがお","てんし","かお","おとぎばなし","ふぁんたじー","てんしのわ","むじゃき","えがお","天使の笑顔","天使","顔","おとぎ話","ファンタジー","天使の輪","無邪気","笑顔","face","angel","heaven","halo"]},{"category":"face","char":"😉","name":"wink","keywords":["ういんくしたかお","かお","ういんく","ウインクした顔","顔","ウインク","face","happy","mischievous","secret",";)","smile","eye"]},{"category":"face","char":"😊","name":"blush","keywords":["めがわらっているえがお","せきめん","め","かお","えがお","目が笑っている笑顔","赤面","目","顔","笑顔","face","smile","happy","flushed","crush","embarrassed","shy","joy"]},{"category":"face","char":"🙂","name":"slightly_smiling_face","keywords":["ほほえみ","かお","えがお","しあわせ","微笑み","顔","笑顔","幸せ","しあわせ","face","smile"]},{"category":"face","char":"🙃","name":"upside_down_face","keywords":["さかさのかお","かお","さかさ","逆さの顔","顔","逆さ","さかさ","face","flipped","silly","smile"]},{"category":"face","char":"☺️","name":"relaxed","keywords":["えがお","かお","りんかく","りらっくす","笑顔","顔","輪郭","リラックス","face","blush","massage","happiness"]},{"category":"face","char":"😋","name":"yum","keywords":["たべものをあじわうかお","おいしい","かお","あじわう","ふーむ","うまい","食べ物を味わう顔","美味しい","おいしい","顔","味わう","ふーむ","うまい","happy","joy","tongue","smile","face","silly","yummy","nom","delicious","savouring"]},{"category":"face","char":"😌","name":"relieved","keywords":["ほっとしたかお","かお","あんしん","ほっとする","ほっとした顔","顔","安心","ほっとする","face","relaxed","phew","massage","happiness"]},{"category":"face","char":"😍","name":"heart_eyes","keywords":["めがはーとのえがお","め","かお","はーと","あい","えがお","目がハートの笑顔","目","顔","ハート","愛","笑顔","face","love","like","affection","valentines","infatuation","crush","heart"]},{"category":"face","char":"🥰","name":"smiling_face_with_three_hearts","keywords":["えがおとはーと","かお","けいあい","べたぼれ","あい","笑顔とハート","顔","敬愛","べたぼれ","愛","face","love","like","affection","valentines","infatuation","crush","hearts","adore"]},{"category":"face","char":"😘","name":"kissing_heart","keywords":["なげきっす","かお","はーと","きす","投げキッス","顔","ハート","キス","face","love","like","affection","valentines","infatuation","kiss"]},{"category":"face","char":"😗","name":"kissing","keywords":["きすをするかお","かお","きす","キスをする顔","顔","キス","love","like","face","3","valentines","infatuation","kiss"]},{"category":"face","char":"😙","name":"kissing_smiling_eyes","keywords":["えがおできす","め","かお","きす","えがお","笑顔でキス","目","顔","キス","笑顔","face","affection","valentines","infatuation","kiss"]},{"category":"face","char":"😚","name":"kissing_closed_eyes","keywords":["めをとじてきすをするかお","とじた","め","かお","きす","目を閉じてキスをする顔","閉じた","目","顔","キス","face","love","like","affection","valentines","infatuation","kiss"]},{"category":"face","char":"😜","name":"stuck_out_tongue_winking_eye","keywords":["したをだしてういんくしているかお","め","かお","じょうだん","した","ういんく","舌を出してウインクしている顔","目","顔","冗談","舌","ウインク","face","prank","childish","playful","mischievous","smile","wink","tongue"]},{"category":"face","char":"🤪","name":"zany","keywords":["おどけたかお","め","にやにや","へん","こうふん","わいるど","おどけた顔","目","にやにや","変","興奮","ワイルド","face","goofy","crazy"]},{"category":"face","char":"🤨","name":"raised_eyebrow","keywords":["まゆがあがっているかお","ふしん","うたがいぶかい","ひなん","ぎねん","ややおどろき","かいぎてき","眉が上がっている顔","不信","疑い深い","非難","疑念","やや驚き","懐疑的","face","distrust","scepticism","disapproval","disbelief","surprise"]},{"category":"face","char":"🧐","name":"monocle","keywords":["かためがねをかけたかお","たいくつ","ゆうふく","ゆたか","片メガネをかけた顔","退屈","裕福","豊か","face","stuffy","wealthy"]},{"category":"face","char":"😝","name":"stuck_out_tongue_closed_eyes","keywords":["したをだしてめをほそめているかお","め","かお","こわい","あじ","した","舌を出して目を細めている顔","目","顔","怖い","恐い","こわい","味","舌","face","prank","playful","mischievous","smile","tongue"]},{"category":"face","char":"😛","name":"stuck_out_tongue","keywords":["したをだしているかお","かお","した","舌を出している顔","顔","舌","face","prank","childish","playful","mischievous","smile","tongue"]},{"category":"face","char":"🤑","name":"money_mouth_face","keywords":["ごうよくなかお","かお","おかね","くち","強欲な顔","顔","お金","口","face","rich","dollar","money"]},{"category":"face","char":"🤓","name":"nerd_face","keywords":["おたく","かお","へんなひと","オタク","顔","変な人","face","nerdy","geek","dork"]},{"category":"face","char":"🥸","name":"disguised_face","keywords":["かそうしたかお","かそう","めがね","とくめいのひと","はな","仮装した顔","仮装","メガネ","匿名の人","鼻","face","nose","glasses","incognito"]},{"category":"face","char":"😎","name":"sunglasses","keywords":["さんぐらすをかけたかお","あかるい","かっこいい","め","あいうぇあ","かお","めがね","えがお","たいよう","さんぐらす","てんき","サングラスをかけた顔","明るい","かっこいい","目","アイウエア","顔","眼鏡","メガネ","笑顔","太陽","サングラス","天気","face","cool","smile","summer","beach","sunglass"]},{"category":"face","char":"🤩","name":"star_struck","keywords":["すたーにむちゅう","め","かお","にやにや","ほし","むそうてき","スターに夢中","目","顔","にやにや","星","夢想的","face","smile","starry","eyes","grinning"]},{"category":"face","char":"🤡","name":"clown_face","keywords":["ぴえろのかお","ぴえろ","かお","ピエロの顔","ピエロ","顔","face"]},{"category":"face","char":"🤠","name":"cowboy_hat_face","keywords":["かうぼーいはっとのかお","かうぼーい","かうがーる","かお","ぼうし","カウボーイハットの顔","カウボーイ","カウガール","顔","帽子","face","cowgirl","hat"]},{"category":"face","char":"🤗","name":"hugs","keywords":["りょうてをひろげたえがお","かお","はぐ","だきしめる","両手を広げた笑顔","顔","ハグ","抱きしめる","face","smile","hug"]},{"category":"face","char":"😏","name":"smirk","keywords":["にやにやしたかお","かお","にやにや","にやにやした顔","顔","にやにや","face","smile","mean","prank","smug","sarcasm"]},{"category":"face","char":"😶","name":"no_mouth","keywords":["くちのないかお","かお","くち","しずかに","ちんもく","口のない顔","顔","口","静かに","沈黙","face","hellokitty"]},{"category":"face","char":"😐","name":"neutral_face","keywords":["ふつうのかお","むひょうじょう","かお","へいせい","普通の顔","無表情","顔","平静","indifference","meh",": |","neutral"]},{"category":"face","char":"😑","name":"expressionless","keywords":["むひょうじょう","かお","ぽーかーふぇいす","むかんじょう","無表情","顔","ポーカーフェイス","無感情","face","indifferent","-_-","meh","deadpan"]},{"category":"face","char":"😒","name":"unamused","keywords":["おもしろくなさそうなかお","かお","つまらない","ふこう","面白くなさそうな顔","顔","つまらない","不幸","indifference","bored","straight face","serious","sarcasm","unimpressed","skeptical","dubious","side_eye"]},{"category":"face","char":"🙄","name":"roll_eyes","keywords":["ぐるぐるめのかお","め","かお","ぐるぐる","ぐるぐる目の顔","目","顔","ぐるぐる","face","eyeroll","frustrated"]},{"category":"face","char":"🤔","name":"thinking","keywords":["かんがえているかお","かお","かんがえちゅう","考えている顔","顔","考え中","face","hmmm","think","consider"]},{"category":"face","char":"🤥","name":"lying_face","keywords":["うそつきがお","かお","うそ","ぴのきお","嘘つき顔","顔","嘘","うそ","ピノキオ","face","lie","pinocchio"]},{"category":"face","char":"🤭","name":"hand_over_mouth","keywords":["くちをてでおおったかお","め","えがお","おおう","くち","て","口を手で覆った顔","目","笑顔","覆う","口","手","face","whoops","shock","surprise"]},{"category":"face","char":"🤫","name":"shushing","keywords":["しっといっているかお","しーっ","しずか","だまる","シッと言っている顔","シーッ","静か","黙る","face","quiet","shhh"]},{"category":"face","char":"🤬","name":"symbols_over_mouth","keywords":["くちがきごうでおおわれたかお","のろい","ののしり","口が記号で覆われた顔","呪い","ののしり","罵り","face","swearing","cursing","cussing","profanity","expletive"]},{"category":"face","char":"🤯","name":"exploding_head","keywords":["ばくはつしたあたま","かお","しょっく","ばくはつ","きょうき","びっくり","爆発した頭","顔","ショック","爆発","狂気","びっくり","face","shocked","mind","blown"]},{"category":"face","char":"😳","name":"flushed","keywords":["あかくなったかお","ぼーっとした","ぼうっとした","かお","せきめん","赤くなった顔","ぼーっとした","ぼうっとした","顔","赤面","face","blush","shy","flattered"]},{"category":"face","char":"😞","name":"disappointed","keywords":["がっかりしたかお","がっかり","かお","がっかりした顔","がっかり","顔","face","sad","upset","depressed",": ("]},{"category":"face","char":"😟","name":"worried","keywords":["ふあんなかお","かお","しんぱい","ふあん","不安な顔","顔","心配","不安","face","concern","nervous",": ("]},{"category":"face","char":"😠","name":"angry","keywords":["おこったかお","いかり","おこった","かお","げきど","怒った顔","怒り","怒った","顔","激怒","mad","face","annoyed","frustrated"]},{"category":"face","char":"😡","name":"rage","keywords":["ふくれがお","いかり","おこった","かお","げきど","ふくれっつら","ふんど","あか","ふくれ顔","怒り","怒った","顔","激怒","ふくれっ面","ふくれっつら","憤怒","赤","angry","mad","hate","despise"]},{"category":"face","char":"😔","name":"pensive","keywords":["かなしげなかお","がっかり","かお","かなしい","悲しげな顔","がっかり","顔","悲しい","face","sad","depressed","upset"]},{"category":"face","char":"😕","name":"confused","keywords":["こまったかお","こまった","かお","困った顔","困った","こまった","顔","face","indifference","huh","weird","hmmm",": /"]},{"category":"face","char":"🙁","name":"slightly_frowning_face","keywords":["ごきげんななめ","かお","しかめっつら","かなしい","ふこう","ご機嫌斜め","顔","しかめっ面","しかめっつら","悲しい","不幸","face","frowning","disappointed","sad","upset"]},{"category":"face","char":"☹","name":"frowning_face","keywords":["しかめっつら","かお","かなしい","ふこう","しかめっつら","顔","しかめっ面","悲しい","不幸","face","sad","upset","frown"]},{"category":"face","char":"😣","name":"persevere","keywords":["がまんしているかお","かお","がんばる","我慢している顔","顔","がんばる","頑張る","face","sick","no","upset","oops"]},{"category":"face","char":"😖","name":"confounded","keywords":["うろたえたかお","とまどい","うろたえ","かお","うろたえた顔","戸惑い","とまどい","うろたえ","顔","face","confused","sick","unwell","oops",": S"]},{"category":"face","char":"😫","name":"tired_face","keywords":["つかれたかお","かお","つかれた","疲れた顔","顔","疲れた","つかれた","sick","whine","upset","frustrated"]},{"category":"face","char":"😩","name":"weary","keywords":["うんざりしているかお","かお","つかれた","うんざり","うんざりしている顔","顔","疲れた","つかれた","うんざり","face","tired","sleepy","sad","frustrated","upset"]},{"category":"face","char":"🥺","name":"pleading","keywords":["うったえかけるかお","かお","ものごい","じひ","こいぬのめ","訴えかける顔","顔","物乞い","慈悲","子犬の目","face","begging","mercy"]},{"category":"face","char":"😤","name":"triumph","keywords":["かちほこったかお","かお","しょうり","かつ","勝ち誇った顔","顔","勝利","勝つ","face","gas","phew","proud","pride"]},{"category":"face","char":"😮","name":"open_mouth","keywords":["くちをあけたえがお","かお","くち","あける","どうじょう","口を開けた笑顔","顔","口","開ける","同情","face","surprise","impressed","wow","whoa",": O"]},{"category":"face","char":"😱","name":"scream","keywords":["ぜっきょうしたかお","かお","きょうふ","こわい","むんく","おびえ","ぜっきょう","絶叫した顔","顔","恐怖","怖い","恐い","こわい","ムンク","怯え","絶叫","face","munch","scared","omg"]},{"category":"face","char":"😨","name":"fearful","keywords":["ぞっとしているかお","かお","きょうふ","こわい","おびえ","ゾッとしている顔","顔","恐怖","恐い","怖い","こわい","怯え","face","scared","terrified","nervous","oops","huh"]},{"category":"face","char":"😰","name":"cold_sweat","keywords":["くちをあけてひやあせをかいたかお","あおざめる","ぞっとする","かお","くち","あける","いそぐ","ひやあせ","口を開けて冷や汗をかいた顔","青ざめる","ぞっとする","顔","口","開ける","急ぐ","冷や汗","face","nervous","sweat"]},{"category":"face","char":"😯","name":"hushed","keywords":["おちついたかお","かお","だまる","ぼうぜん","おどろき","落ち着いた顔","顔","黙る","呆然","驚き","face","woo","shh"]},{"category":"face","char":"😦","name":"frowning","keywords":["しんぱいそうなかおのえもじ","かお","しかめっつら","くち","あける","心配そうな顔の絵文字","顔","しかめっ面","しかめっつら","口","開ける","face","aw","what"]},{"category":"face","char":"😧","name":"anguished","keywords":["くのうにみちたかお","くのう","かお","苦悩に満ちた顔","苦悩","顔","face","stunned","nervous"]},{"category":"face","char":"😢","name":"cry","keywords":["なきがお","なく","かお","かなしい","なみだ","泣き顔","泣く","顔","悲しい","涙","face","tears","sad","depressed","upset",": '("]},{"category":"face","char":"😥","name":"disappointed_relieved","keywords":["がっかりしたがあんしんしたかお","がっかり","かお","あんしん","ほっとする","やれやれ","がっかりしたが安心した顔","がっかり","顔","安心","ほっとする","やれやれ","face","phew","sweat","nervous"]},{"category":"face","char":"🤤","name":"drooling_face","keywords":["よだれをたらしたかお","よだれ","かお","よだれを垂らした顔","よだれ","顔","face"]},{"category":"face","char":"😪","name":"sleepy","keywords":["ねむいかお","かお","ねる","すいみん","眠い顔","顔","寝る","睡眠","face","tired","rest","nap"]},{"category":"face","char":"😓","name":"sweat","keywords":["ひやあせをかいているかお","ぞっとする","かお","ひやあせ","冷や汗をかいている顔","ぞっとする","顔","冷や汗","face","hot","sad","tired","exercise"]},{"category":"face","char":"🥵","name":"hot","keywords":["ほてったかお","かお","ねつっぽい","ねっしゃびょう","ほてった","あからがお","あせをかいた","ほてった顔","顔","熱っぽい","熱射病","ほてった","赤ら顔","汗をかいた","face","feverish","heat","red","sweating"]},{"category":"face","char":"🥶","name":"cold","keywords":["あおざめたかお","かお","ぞっとする","こごえる","とうしょう","つらら","青ざめた顔","顔","ぞっとする","凍える","凍傷","つらら","face","blue","freezing","frozen","frostbite","icicles"]},{"category":"face","char":"😭","name":"sob","keywords":["ごうきゅう","なく","かお","かなしい","なみだ","号泣","泣く","顔","悲しい","涙","face","cry","tears","sad","upset","depressed"]},{"category":"face","char":"😵","name":"dizzy_face","keywords":["めがばつになったかお","めまい","かお","ばつ","め","目がバツになった顔","めまい","顔","バツ","目","spent","unconscious","xox","dizzy"]},{"category":"face","char":"😲","name":"astonished","keywords":["おどろいたかお","おどろき","びっくり","かお","しょっく","きょうがく","驚いた顔","驚き","びっくり","顔","ショック","驚愕","face","xox","surprised","poisoned"]},{"category":"face","char":"🤐","name":"zipper_mouth_face","keywords":["おくちちゃっく","かお","くち","ちゃっく","お口チャック","顔","口","チャック","face","sealed","zipper","secret"]},{"category":"face","char":"🤢","name":"nauseated_face","keywords":["はきそうなかお","かお","はきけ","おうと","吐きそうな顔","顔","吐き気","嘔吐","face","vomit","gross","green","sick","throw up","ill"]},{"category":"face","char":"🤧","name":"sneezing_face","keywords":["くしゃみをするかお","かお","くしゃみ","はくしょん","くしゃみをする顔","顔","くしゃみ","ハクション","face","gesundheit","sneeze","sick","allergy"]},{"category":"face","char":"🤮","name":"vomiting","keywords":["はきそうなかお","びょうき","おうと","かぜ","はく","吐きそうな顔","病気","嘔吐","風邪","かぜ","吐く","face","sick"]},{"category":"face","char":"😷","name":"mask","keywords":["ますくをしたかお","かぜ","いしゃ","かお","ますく","くすり","びょうき","マスクをした顔","風邪","かぜ","医者","顔","マスク","薬","病気","face","sick","ill","disease"]},{"category":"face","char":"🤒","name":"face_with_thermometer","keywords":["おんどけいをくわえたかお","かお","びょうき","かぜ","たいおんけい","温度計をくわえた顔","顔","病気","風邪","かぜ","体温計","sick","temperature","thermometer","cold","fever"]},{"category":"face","char":"🤕","name":"face_with_head_bandage","keywords":["けが","ほうたい","かお","きず","怪我","包帯","顔","傷","キズ","けが","injured","clumsy","bandage","hurt"]},{"category":"face","char":"🥴","name":"woozy","keywords":["ぼんやしりたかお","かお","めまい","めいてい","ほろよい","まっすぐでないめ","はじょうのくち","ぼんやしりた顔","顔","目まい","酩酊","ほろ酔い","まっすぐでない目","波状の口","face","dizzy","intoxicated","tipsy","wavy"]},{"category":"face","char":"🥱","name":"yawning","keywords":["あくびしているかお","あきた","つかれた","あくび","あくびしている顔","飽きた","疲れた","あくび","face","tired","yawning"]},{"category":"face","char":"😴","name":"sleeping","keywords":["ねがお","かお","ねる","すいみん","すやすや","寝顔","顔","寝る","睡眠","スヤスヤ","face","tired","sleepy","night","zzz"]},{"category":"face","char":"💤","name":"zzz","keywords":["すいみん","まんが","ねる","すやすや","睡眠","マンガ","漫画","寝る","スヤスヤ","sleepy","tired","dream"]},{"category":"face","char":"😶‍🌫️","name":"face_in_clouds","keywords":["くもでおおわれたかお","かお","おっちょこちょい","ひげんじつてき","ゆめ","もや","くもでおおわれたあたま","雲で覆われた顔","顔","おっちょこちょい","非現実的","夢","もや","雲で覆われた頭"]},{"category":"face","char":"😮‍💨","name":"face_exhaling","keywords":["ためいきのでているかお","かお","ためいき","いきぎれ","うめき","あんしん","ささやき","くちぶえ","ため息の出ている顔","顔","ため息","息切れ","うめき","安心","ささやき","口笛"]},{"category":"face","char":"😵‍💫","name":"face_with_spiral_eyes","keywords":["めがぐるぐるしているかお","めまい","かお","め","うっとり","ぐるぐる","とらぶる","おー","目がぐるぐるしている顔","めまい","顔","目","うっとり","ぐるぐる","トラブル","おー"]},{"category":"face","char":"🫠","name":"melting_face","keywords":["ほろりとしたかお","きえる","ようかいする","えきたい","とける","ほろりとした顔","消える","溶解する","液体","溶ける","disappear","dissolve","liquid","melt","toketa"]},{"category":"face","char":"🫢","name":"face_with_open_eyes_and_hand_over_mouth","keywords":["めをひらいてくちをてでおおったかお","きょうたん","いけい","ふしん","ろうばい","こわい","おどろき","目を開いて口を手で覆った顔","驚嘆","畏敬","不信","狼狽","怖い","驚き","amazement","awe","disbelief","embarrass","scared","surprise","ohoho"]},{"category":"face","char":"🫣","name":"face_with_peeking_eye","keywords":["のぞきみしているかお","みりょう","のぞきみ","ぎょうし","ちらみ","のぞき見している顔","魅了","のぞき見","凝視","チラ見","captivated","peep","stare","chunibyo"]},{"category":"face","char":"🫡","name":"saluting_face","keywords":["けいれいしているかお","ok","けいれい","せいてん","ぶたい","はい","敬礼している顔","ok","敬礼","晴天","部隊","はい","ok","salute","sunny","troops","yes","raja"]},{"category":"face","char":"🫥","name":"dotted_line_face","keywords":["てんせんのかお","おちこんだ","きえる","かくれる","ないこうてき","めにみえない","点線の顔","落ち込んだ","消える","隠れる","内向的","目に見えない","depressed","disappear","hide","introvert","invisible","tensen"]},{"category":"face","char":"🫤","name":"face_with_diagonal_mouth","keywords":["くちがななめになったかお","がっかり","むかんしん","うたがいぶかい","ふあん","口が斜めになった顔","がっかり","無関心","疑い深い","不安","disappointed","meh","skeptical","unsure"]},{"category":"face","char":"🥹","name":"face_holding_back_tears","keywords":["なみだをこらえているかお","おこる","なく","ほこりにおもう","さからう","かなしむ","涙をこらえている顔","怒る","泣く","誇りに思う","逆らう","悲しむ","angry","cry","proud","resist","sad"]},{"category":"face","char":"🫨","name":"shaking_face","keywords":["ふるえるかお","じしん","かお","ふるえ","しょうげき","しんどう","震える顔","地震","顔","震え","衝撃","振動","earthquake","face","shaking","shock","vibrate"]},{"category":"face","char":"💩","name":"poop","keywords":["うんち","まんが","ふん","かお","もんすたー","うんち","マンガ","漫画","フン","顔","モンスター","hankey","shitface","fail","turd","shit"]},{"category":"face","char":"😈","name":"smiling_imp","keywords":["つのつきえがお","かお","おとぎばなし","ふぁんたじー","つの","えがお","角つき笑顔","顔","おとぎ話","ファンタジー","角","笑顔","devil","horns"]},{"category":"face","char":"👿","name":"imp","keywords":["しょうあくま","おに","あくま","かお","おとぎばなし","ふぁんたじー","小悪魔","鬼","悪魔","顔","おとぎ話","ファンタジー","devil","angry","horns"]},{"category":"face","char":"👹","name":"japanese_ogre","keywords":["おに","ようかい","かお","むかしばなし","ふぁんたじー","にっぽん","もんすたー","鬼","妖怪","顔","昔話","ファンタジー","日本","モンスター","monster","red","mask","halloween","scary","creepy","devil","demon","japanese","ogre"]},{"category":"face","char":"👺","name":"japanese_goblin","keywords":["てんぐ","ようかい","かお","むかしばなし","ふぁんたじー","にっぽん","もんすたー","天狗","妖怪","顔","昔話","ファンタジー","日本","モンスター","red","evil","mask","monster","scary","creepy","japanese","goblin"]},{"category":"face","char":"💀","name":"skull","keywords":["どくろ","からだ","し","かお","おとぎばなし","もんすたー","がいこつ","はろうぃーん","ドクロ","体","死","顔","おとぎ話","モンスター","骸骨","ハロウィーン","dead","skeleton","creepy","death"]},{"category":"face","char":"👻","name":"ghost","keywords":["おばけ","ようかい","かお","おとぎばなし","ふぁんたじー","ゆうれい","もんすたー","はろうぃーん","お化け","妖怪","顔","おとぎ話","ファンタジー","幽霊","モンスター","ハロウィーン","halloween","spooky","scary"]},{"category":"face","char":"👽","name":"alien","keywords":["うちゅうじん","かいじゅう","いせいじん","かお","おとぎばなし","ふぁんたじー","もんすたー","うちゅう","UFO","宇宙人","怪獣","異星人","顔","おとぎ話","ファンタジー","モンスター","宇宙","UFO","UFO","paul","weird","outer_space"]},{"category":"face","char":"🤖","name":"robot","keywords":["ろぼっとのかお","かお","もんすたー","ろぼっと","ロボットの顔","顔","モンスター","ロボット","computer","machine","bot"]},{"category":"face","char":"😺","name":"smiley_cat","keywords":["くちをあけてわらうねこ","ねこ","かお","くち","あける","えがお","口を開けて笑う猫","猫","ネコ","顔","口","開ける","笑顔","animal","cats","happy","smile"]},{"category":"face","char":"😸","name":"smile_cat","keywords":["にやにやわらうねこ","ねこ","め","かお","にやにや","えがお","ニヤニヤ笑う猫","猫","ネコ","目","顔","ニヤニヤ","笑顔","animal","cats","smile"]},{"category":"face","char":"😹","name":"joy_cat","keywords":["うれしなきしたねこのかお","ねこ","かお","うれしい","なみだ","嬉し泣きしたネコの顔","猫","ネコ","顔","嬉しい","うれしい","涙","animal","cats","haha","happy","tears"]},{"category":"face","char":"😻","name":"heart_eyes_cat","keywords":["はーとのめをしたねこのえがお","ねこ","め","かお","はーと","あい","えがお","ハートの目をした猫の笑顔","猫","ネコ","目","顔","ハート","愛","笑顔","animal","love","like","affection","cats","valentines","heart"]},{"category":"face","char":"😼","name":"smirk_cat","keywords":["にやりとわらうねこのかお","ねこ","かお","ひにく","えがお","にやり","ニヤリと笑う猫の顔","猫","ネコ","顔","皮肉","笑顔","ニヤリ","animal","cats","smirk"]},{"category":"face","char":"😽","name":"kissing_cat","keywords":["めをとじてきすをするねこ","ねこ","め","かお","きす","目を閉じてキスをする猫","猫","ネコ","目","顔","キス","animal","cats","kiss"]},{"category":"face","char":"🙀","name":"scream_cat","keywords":["つかれたねこのかお","ねこ","かお","びっくり","おどろく","うんざり","疲れたネコの顔","猫","ネコ","顔","びっくり","驚く","うんざり","animal","cats","munch","scared","scream"]},{"category":"face","char":"😿","name":"crying_cat_face","keywords":["ないたねこのかお","ねこ","なく","かお","かなしい","なみだ","泣いたネコの顔","猫","ネコ","泣く","顔","悲しい","涙","animal","tears","weep","sad","cats","upset","cry"]},{"category":"face","char":"😾","name":"pouting_cat","keywords":["おこったねこのかお","ねこ","かお","おこる","ふくれっつら","怒ったネコの顔","猫","ネコ","顔","怒る","ふくれっ面","ふくれっつら","animal","cats"]},{"category":"people","char":"🤲","name":"palms_up","keywords":["うえにむけたりょうてのひら","からだ","いのり","かっぷのようにまるめたて","上に向けた両手のひら","体","祈り","カップのように丸めた手","hands","gesture","cupped","prayer"]},{"category":"people","char":"🙌","name":"raised_hands","keywords":["りょうてをあげる","からだ","おいわい","じぇすちゃー","て","ばんざい","あげる","両手を上げる","体","お祝い","ジェスチャー","手","バンザイ","万歳","挙げる","gesture","hooray","yea","celebration","hands"]},{"category":"people","char":"👏","name":"clap","keywords":["はくしゅ","からだ","てをたたく","て","拍手","体","手を叩く","手","hands","praise","applause","congrats","yay"]},{"category":"people","char":"👋","name":"wave","keywords":["ばいばい","からだ","て","ふる","やっほー","こんにちは","バイバイ","体","手","振る","やっほー","ヤッホー","こんにちは","hands","gesture","goodbye","solong","farewell","hello","hi","palm"]},{"category":"people","char":"🤙","name":"call_me_hand","keywords":["でんわのかたちのて","からだ","でんわ","て","電話の形の手","体","電話","手","hands","gesture"]},{"category":"people","char":"👍","name":"+1","keywords":["いいね","からだ","うえ","て","ゆび","さむずあっぷ","+1","イイね","体","上","手","指","サムズアップ","+1","thumbsup","yes","awesome","good","agree","accept","cool","hand","like"]},{"category":"people","char":"👎","name":"-1","keywords":["だめ","からだ","した","て","ゆび","さむずだうん","-1","ダメ","体","下","手","指","サムズダウン","-1","thumbsdown","no","dislike","hand"]},{"category":"people","char":"👊","name":"facepunch","keywords":["にぎりこぶし","からだ","にぎる","こぶし","ぐー","て","ぱんち","せっきん","握りこぶし","体","握る","拳","こぶし","グー","手","パンチ","接近","angry","violence","fist","hit","attack","hand"]},{"category":"people","char":"✊","name":"fist","keywords":["こぶし","からだ","にぎる","ぐー","て","ぱんち","こぶし","体","握る","拳","グー","手","パンチ","fingers","hand","grasp"]},{"category":"people","char":"🤛","name":"fist_left","keywords":["ひだりむきのこぶし","からだ","こぶし","ひだりむき","左向きのこぶし","体","拳","左向き","hand","fistbump"]},{"category":"people","char":"🤜","name":"fist_right","keywords":["みぎむきのこぶし","からだ","こぶし","みぎむき","右向きのこぶし","体","拳","右向き","hand","fistbump"]},{"category":"people","char":"🫷","name":"leftwards_pushing_hand","keywords":["ひだりをおしているて","じたい","はいたっち","ひだりほうこう","おしつける","ことわる","ていし","まつ","左を押している手","辞退","ハイタッチ","左方向","押し付ける","断る","停止","待つ","hand","high_five","leftward","push","refuse","stop","wait"]},{"category":"people","char":"🫸","name":"rightwards_pushing_hand","keywords":["みぎをおしているて","じたい","はいたっち","おしつける","ことわる","みぎほうこう","ていし","まつ","右を押している手","辞退","ハイタッチ","押し付ける","断る","右方向","停止","待つ","hand","high_five","push","refuse","rightward","stop","wait"]},{"category":"people","char":"✌","name":"v","keywords":["Vさいん","からだ","て","V","ぶい","かつ","しょうり","ぴーす","Vサイン","体","手","V","ブイ","勝つ","勝利","ピース","fingers","ohyeah","hand","peace","victory","two"]},{"category":"people","char":"👌","name":"ok_hand","keywords":["OKさいん","からだ","て","OK","OKサイン","体","手","OK","fingers","limbs","perfect","ok","okay"]},{"category":"people","char":"✋","name":"raised_hand","keywords":["きょしゅ","からだ","て","挙手","体","手","fingers","stop","highfive","palm","ban"]},{"category":"people","char":"🤚","name":"raised_back_of_hand","keywords":["てのこう","からだ","あげる","手の甲","体","挙げる","fingers","raised","backhand"]},{"category":"people","char":"👐","name":"open_hands","keywords":["ひらいたて","からだ","て","ひろげる","開いた手","体","手","広げる","fingers","butterfly","hands","open"]},{"category":"people","char":"💪","name":"muscle","keywords":["まげたじょうわんにとうきん","ちからこぶ","からだ","まんが","うんどう","きんにく","ちから","まっする","まっちょ","曲げた上腕二頭筋","力こぶ","体","マンガ","漫画","運動","筋肉","力","マッスル","マッチョ","arm","flex","hand","summer","strong","biceps"]},{"category":"people","char":"🦾","name":"mechanical_arm","keywords":["めかにかるあーむ","あくせしびりてぃ","ぎしゅ","じんこうそうぐ","からだ","メカニカルアーム","アクセシビリティ","義手","人口装具","体","flex","hand","strong","biceps"]},{"category":"people","char":"🙏","name":"pray","keywords":["にぎったて","たのむ","からだ","おじぎ","てをあわせる","じぇすちゃー","て","おねがい","いのる","ありがとう","かんしゃ","握った手","頼む","体","お辞儀","手を合わせる","ジェスチャー","手","お願い","祈る","ありがとう","感謝","please","hope","wish","namaste","highfive"]},{"category":"people","char":"🦶","name":"foot","keywords":["あし","からだ","きっく","ふみつける","足","体","キック","踏みつける","kick","stomp"]},{"category":"people","char":"🦵","name":"leg","keywords":["あし","からだ","きっく","てあし","脚","体","キック","手足","kick","limb"]},{"category":"people","char":"🦿","name":"mechanical_leg","keywords":["きかいのあし","あくせしびりてぃ","ぎそく","じんこうそうぐ","からだ","機械の脚","アクセシビリティ","義足","人口装具","体","kick","limb"]},{"category":"people","char":"🤝","name":"handshake","keywords":["あくしゅ","ごうい","て","しゅをむすぶ","かいぎ","握手","合意","手","手を結ぶ","会議","agreement","shake"]},{"category":"people","char":"☝","name":"point_up","keywords":["ゆびさし","からだ","ゆび","て","ひとさしゆび","ゆびさす","うえ","指差し","体","指","手","人差し指","指さす","上","hand","fingers","direction","up"]},{"category":"people","char":"👆","name":"point_up_2","keywords":["ゆびさし","てのこう","からだ","ゆび","て","ひとさしゆび","ゆびさす","うえ","指差し","手の甲","体","指","手","人差し指","指さす","上","fingers","hand","direction","up"]},{"category":"people","char":"👇","name":"point_down","keywords":["ゆびさし","てのこう","からだ","した","ゆび","て","ひとさしゆび","ゆびさす","指差し","手の甲","体","下","指","手","人差し指","指さす","fingers","hand","direction","down"]},{"category":"people","char":"👈","name":"point_left","keywords":["ひだりゆびさし","てのこう","からだ","ゆび","て","ひとさしゆび","ゆびさす","左指差し","手の甲","体","指","手","人差し指","指さす","direction","fingers","hand","left"]},{"category":"people","char":"👉","name":"point_right","keywords":["ゆびさし","てのこう","からだ","ゆび","て","ひとさしゆび","ゆびさす","指差し","手の甲","体","指","手","人差し指","指さす","fingers","hand","direction","right"]},{"category":"people","char":"🖕","name":"fu","keywords":["なかゆびをたてたて","からだ","ゆび","て","なかゆび","中指を立てた手","体","指","手","中指","hand","fingers","rude","middle","flipping"]},{"category":"people","char":"🖐","name":"raised_hand_with_fingers_splayed","keywords":["ひろげたてのひら","からだ","ゆび","て","ひろげる","広げた手のひら","体","指","手","広げる","hand","fingers","palm"]},{"category":"people","char":"🤟","name":"love_you","keywords":["あいしてるのじぇすちゃー","からだ","あいしてる","すき","て","愛してるのジェスチャー","体","愛してる","好き","手","hand","fingers","gesture"]},{"category":"people","char":"🤘","name":"metal","keywords":["こるな","からだ","ゆび","て","つの","さいこう","コルナ","体","指","手","角","最高","hand","fingers","evil_eye","sign_of_horns","rock_on"]},{"category":"people","char":"🤞","name":"crossed_fingers","keywords":["こうささせたゆび","からだ","こうさ","ゆび","て","こううん","交差させた指","体","交差","指","手","幸運","good","lucky"]},{"category":"people","char":"🖖","name":"vulcan_salute","keywords":["ちょうじゅとはんえいを","からだ","ゆび","て","すぽっく","ばるかん","長寿と繁栄を","体","指","手","スポック","バルカン","hand","fingers","spock","star trek"]},{"category":"people","char":"✍","name":"writing_hand","keywords":["かいているて","からだ","て","かく","書いている手","体","手","書く","lower_left_ballpoint_pen","stationery","write","compose"]},{"category":"people","char":"🫰","name":"hand_with_index_finger_and_thumb_crossed","keywords":["ひとさしゆびとおやゆびをこうさしたて","たかい","はーと","あい","おかね","すなっぷ","人差し指と親指を交差した手","高い","ハート","愛","お金","スナップ"]},{"category":"people","char":"🫱","name":"rightwards_hand","keywords":["みぎて","て","みぎ","右手","手","右","みぎ"]},{"category":"people","char":"🫲","name":"leftwards_hand","keywords":["ひだりて","て","ひだり","左手","手","左","ひだり"]},{"category":"people","char":"🫳","name":"palm_down_hand","keywords":["てのひらをしたにしたて","しりぞける","おとす","しっし","手のひらを下にした手","退ける","落とす","シッシ"]},{"category":"people","char":"🫴","name":"palm_up_hand","keywords":["てのひらをうえにしたて","てまねき","ほかく","くる","もうしで","手のひらを上にした手","手招き","捕獲","来る","申し出"]},{"category":"people","char":"🫵","name":"index_pointing_at_the_viewer","keywords":["みているひとをさしているひとさしゆび","さす","あなた","ゆび","見ている人を指している人差し指","指す","あなた","指"]},{"category":"people","char":"🫶","name":"heart_hands","keywords":["はーとぽーず","あい","ハートポーズ","愛","moemoekyun"]},{"category":"people","char":"🤏","name":"pinching_hand","keywords":["つまんでいるて","からだ","て","ちいさい","こがた","ちっちゃい","つまんでいる手","体","手","小さい","小型","ちっちゃい","hand","fingers"]},{"category":"people","char":"🤌","name":"pinched_fingers","keywords":["つまんでいるゆび","ゆび","てぶり","じんもん","つまむ","ひにく","つまんでいる指","指","手ぶり","尋問","つまむ","皮肉","hand","fingers"]},{"category":"people","char":"🤳","name":"selfie","keywords":["じどり","かめら","けいたい","うで","自撮り","カメラ","携帯","腕","camera","phone"]},{"category":"people","char":"💅","name":"nail_care","keywords":["まにきゅあ","からだ","けあ","けしょうひん","こすめ","つめ","ねいる","マニキュア","体","ケア","化粧品","コスメ","爪","ネイル","beauty","manicure","finger","fashion","nail"]},{"category":"people","char":"👄","name":"lips","keywords":["くち","からだ","くちびる","口","体","唇","クチビル","mouth","kiss"]},{"category":"people","char":"🫦","name":"biting_lip","keywords":["かんでいるくちびる","しんぱい","こわい","うわき","しんけいしつ","ふゆかい","ふあん","かんでいる唇","心配","怖い","浮気","神経質","不愉快","不安"]},{"category":"people","char":"🦷","name":"tooth","keywords":["は","からだ","はいしゃ","歯","体","歯医者","teeth","dentist"]},{"category":"people","char":"👅","name":"tongue","keywords":["した","からだ","舌","体","mouth","playful"]},{"category":"people","char":"👂","name":"ear","keywords":["みみ","からだ","はな","耳","体","鼻","face","hear","sound","listen"]},{"category":"people","char":"🦻","name":"ear_with_hearing_aid","keywords":["ほちょうきをつけているみみ","あくせしびりてぃ","ほちょうき","きく","からだ","みみ","補聴器を付けている耳","アクセシビリティ","補聴器","聞く","体","耳","face","hear","sound","listen"]},{"category":"people","char":"👃","name":"nose","keywords":["はな","からだ","鼻","体","smell","sniff"]},{"category":"people","char":"👁","name":"eye","keywords":["め","からだ","目","体","face","look","see","watch","stare"]},{"category":"people","char":"👀","name":"eyes","keywords":["め","からだ","かお","目","体","顔","look","watch","stalk","peek","see"]},{"category":"people","char":"🧠","name":"brain","keywords":["のう","からだ","ぞうき","ちてき","かしこい","脳","体","臓器","知的","賢い","smart","intelligent"]},{"category":"people","char":"🫀","name":"anatomical_heart","keywords":["かいぼうがくてきなしんぞう","かいぼうがく","しんぞうがく","しんぞう","ぞうき","みゃく","解剖学的な心臓","解剖学","心臓学","心臓","臓器","脈"]},{"category":"people","char":"🫁","name":"lungs","keywords":["はい","いき","こき","きゅうにゅう","ぞうき","こきゅう","肺","息","呼気","吸入","臓器","呼吸"]},{"category":"people","char":"👤","name":"bust_in_silhouette","keywords":["じょうはんしんのしるえっと","じょうはんしん","しるえっと","上半身のシルエット","上半身","シルエット","user","person","human"]},{"category":"people","char":"👥","name":"busts_in_silhouette","keywords":["じょうはんしんのしるえっと","じょうはんしん","しるえっと","上半身のシルエット","上半身","シルエット","user","person","human","group","team"]},{"category":"people","char":"🗣","name":"speaking_head","keywords":["しゃべるあたまのしるえっと","かお","あたま","しるえっと","しゃべる","はなす","喋る頭のシルエット","顔","頭","シルエット","しゃべる","話す","user","person","human","sing","say","talk"]},{"category":"people","char":"👶","name":"baby","keywords":["あかちゃん","赤ちゃん","child","boy","girl","toddler"]},{"category":"people","char":"🧒","name":"child","keywords":["こども","ひと","しょうねん","しょうじょ","子供","人","少年","少女","gender-neutral","young"]},{"category":"people","char":"👦","name":"boy","keywords":["おとこのこ","しょうねん","こども","男の子","少年","子供","man","male","guy","teenager"]},{"category":"people","char":"👧","name":"girl","keywords":["おんなのこ","しょうじょ","しょじょ","おとめざ","せいざ","こども","女の子","少女","処女","おとめ座","星座","子供","female","woman","teenager"]},{"category":"people","char":"🧑","name":"adult","keywords":["せいじんむけ","ひと","おとな","だんせい","じょせい","おんな","おとこ","成人向け","人","大人","男性","女性","女","男","おとこ","おんな","gender-neutral","person"]},{"category":"people","char":"👨","name":"man","keywords":["だんせい","くちひげ","おとこ","男性","口ひげ","男","おとこ","mustache","father","dad","guy","classy","sir","moustache"]},{"category":"people","char":"👩","name":"woman","keywords":["じょせい","おんな","女性","女","おんな","female","girls","lady"]},{"category":"people","char":"🧑‍🦱","name":"curly_hair","keywords":["ひと","まきげ","かみ","人,巻き毛","巻き毛","髪","curly","afro","braids","ringlets"]},{"category":"people","char":"👩‍🦱","name":"curly_hair_woman","keywords":["じょせい","まきげ","かみ","おんな","女性,巻き毛","巻き毛","髪","女性","女","おんな","woman","female","girl","curly","afro","braids","ringlets"]},{"category":"people","char":"👨‍🦱","name":"curly_hair_man","keywords":["だんせい","まきげ","かみ","おとこ","男性,巻き毛","巻き毛","髪","男性","男","おとこ","man","male","boy","guy","curly","afro","braids","ringlets"]},{"category":"people","char":"🧑‍🦰","name":"red_hair","keywords":["ひと","あかげ","あか","かみ","人,赤毛","赤","髪","redhead"]},{"category":"people","char":"👩‍🦰","name":"red_hair_woman","keywords":["じょせい","あかげ","あか","かみ","おんな","女性,赤毛","赤","髪","女性","女","おんな","woman","female","girl","ginger","redhead"]},{"category":"people","char":"👨‍🦰","name":"red_hair_man","keywords":["だんせい","あかげ","あか","かみ","おとこ","男性,赤毛","赤","髪","男性","男","おとこ","man","male","boy","guy","ginger","redhead"]},{"category":"people","char":"👱‍♀️","name":"blonde_woman","keywords":["じょせい","きんぱつ","ぶろんど","かみ","おんな","女性,金髪","ブロンド","髪","女","おんな","woman","female","girl","blonde","person"]},{"category":"people","char":"👱","name":"blonde_man","keywords":["ひと","きんぱつ","ぶろんど","かみ","人,金髪","金髪","ブロンド","髪","man","male","boy","blonde","guy","person"]},{"category":"people","char":"🧑‍🦳","name":"white_hair","keywords":["ひと","はくはつ","しろ","かみ","人,白髪","白","髪","gray","old","white"]},{"category":"people","char":"👩‍🦳","name":"white_hair_woman","keywords":["じょせい","はくはつ","しろ","かみ","おんな","女性,白髪","白","髪","女性","女","おんな","woman","female","girl","gray","old","white"]},{"category":"people","char":"👨‍🦳","name":"white_hair_man","keywords":["だんせい","はくはつ","しろ","かみ","おとこ","男性,白髪","白","髪","男性","男","おとこ","man","male","boy","guy","gray","old","white"]},{"category":"people","char":"🧑‍🦲","name":"bald","keywords":["ひと","はげ","人,禿","禿","bald","chemotherapy","hairless","shaven"]},{"category":"people","char":"👩‍🦲","name":"bald_woman","keywords":["じょせい","はげ","おんな","女性,禿","禿","女性","女","おんな","woman","female","girl","bald","chemotherapy","hairless","shaven"]},{"category":"people","char":"👨‍🦲","name":"bald_man","keywords":["だんせい","はげ","おとこ","男性,禿","禿","男性","男","おとこ","man","male","boy","guy","bald","chemotherapy","hairless","shaven"]},{"category":"people","char":"🧔","name":"bearded_person","keywords":["あごひげのあるひと","あごひげ","ひげをはやした","あごひげのある人","あごひげ","ひげを生やした","person","bewhiskered"]},{"category":"people","char":"🧓","name":"older_adult","keywords":["こうれいしゃ","ひと","だんせい","じょせい","おんな","おとこ","高齢者","人","男性","女性","女","男","おとこ","おんな","human","elder","senior","gender-neutral"]},{"category":"people","char":"👴","name":"older_man","keywords":["おじいさん","おじいちゃん","ろうじん","おとこ","だんせい","おじいさん","おじいちゃん","老人","男","おとこ","男性","human","male","men","old","elder","senior"]},{"category":"people","char":"👵","name":"older_woman","keywords":["おばあさん","おばあちゃん","ろうじん","じょせい","おんな","おばあさん","おばあちゃん","老人","女性","女","おんな","human","female","women","lady","old","elder","senior"]},{"category":"people","char":"👲","name":"man_with_gua_pi_mao","keywords":["すかるきゃっぷをかぶっているひと","ちゅうごくぼう","ぼうし","スカルキャップをかぶっている人","中国帽","帽子","male","boy","chinese"]},{"category":"people","char":"🧕","name":"woman_with_headscarf","keywords":["へっどすかーふをかぶったじょせい","へっどすかーふ","ひじゃぶ","まんてぃら","てぃちぇる","ばんだな","あたまのすかーふ","じょせい","おんな","ヘッドスカーフをかぶった女性","ヘッドスカーフ","ヒジャブ","マンティラ","ティチェル","バンダナ","頭のスカーフ","女性","女","おんな","female","hijab","mantilla","tichel"]},{"category":"people","char":"👳‍♀️","name":"woman_with_turban","keywords":["たーばんをまいているじょせい","たーばん","じょせい","おんな","ターバンを巻いている女性","ターバン","女性","女","おんな","female","indian","hinduism","arabs","woman"]},{"category":"people","char":"👳","name":"man_with_turban","keywords":["たーばんをまいているひと","たーばん","ターバンを巻いている人","ターバン","male","indian","hinduism","arabs"]},{"category":"people","char":"👮‍♀️","name":"policewoman","keywords":["じょせいけいさつかん","けいさつかん","けいかん","けいさつ","じょせい","おんな","女性警察官","警察官","警官","警察","女性","女","おんな","woman","police","law","legal","enforcement","arrest","911","female"]},{"category":"people","char":"👮","name":"policeman","keywords":["けいさつかん","けいかん","けいさつ","警察官","警官","警察","man","police","law","legal","enforcement","arrest","911"]},{"category":"people","char":"👷‍♀️","name":"construction_worker_woman","keywords":["じょせいのけんせつさぎょういん","こうじ","けんせつ","さぎょういん","じょせい","おんな","女性の建設作業員","工事","建設","作業員","女性","女","おんな","female","human","wip","build","construction","worker","labor","woman"]},{"category":"people","char":"👷","name":"construction_worker_man","keywords":["けんせつさぎょういん","こうじ","けんせつ","さぎょういん","建設作業員","工事","建設","作業員","male","human","wip","guy","build","construction","worker","labor"]},{"category":"people","char":"💂‍♀️","name":"guardswoman","keywords":["じょせいけいびいん","けいびいん","けいび","じょせい","おんな","女性警備員","警備員","警備","女性","女","おんな","uk","gb","british","female","royal","woman"]},{"category":"people","char":"💂","name":"guardsman","keywords":["けいびいん","けいび","警備員","警備","uk","gb","british","male","guy","royal"]},{"category":"people","char":"🕵️‍♀️","name":"female_detective","keywords":["じょせいのたんてい","たんてい","けいじ","すぱい","じょせい","おんな","女性の探偵","探偵","刑事","スパイ","女性","女","おんな","human","spy","detective","female","woman"]},{"category":"people","char":"🕵","name":"male_detective","keywords":["たんてい","けいじ","すぱい","探偵","刑事","スパイ","human","spy","detective"]},{"category":"people","char":"🧑‍⚕️","name":"health_worker","keywords":["いりょうかんけいしゃ","いし","ないかい","いがくはかせ","かんごし","しかい","いりょうせんもんか","りょうほうし","医療関係者","医師","内科医","医学博士","看護師","歯科医","医療専門家","療法士","doctor","nurse","therapist","healthcare","human"]},{"category":"people","char":"👩‍⚕️","name":"woman_health_worker","keywords":["じょせいいりょうかんけいしゃ","いし","ないかい","いがくはかせ","かんごし","しかい","いりょうせんもんか","りょうほうし","じょせい","おんな","女性医療関係者","医師","内科医","医学博士","看護師","歯科医","医療専門家","療法士","女性","女","おんな","doctor","nurse","therapist","healthcare","woman","human"]},{"category":"people","char":"👨‍⚕️","name":"man_health_worker","keywords":["だんせいいりょうかんけいしゃ","いし","ないかい","いがくはかせ","かんごし","しかい","いりょうせんもんか","りょうほうし","おとこ","だんせい","男性医療関係者","医師","内科医","医学博士","看護師","歯科医","医療専門家","療法士","男","おとこ","男性","doctor","nurse","therapist","healthcare","man","human"]},{"category":"people","char":"🧑‍🌾","name":"farmer","keywords":["のうぎょうじゅうじしゃ","のうじょうろうどうしゃ","ぼくじょうぬし","にわし","のうか","農業従事者","農場労働者","牧場主","庭師","農家","rancher","gardener","human"]},{"category":"people","char":"👩‍🌾","name":"woman_farmer","keywords":["じょせいののうぎょうじゅうじしゃ","のうじょうろうどうしゃ","ぼくじょうぬし","にわし","のうか","じょせい","おんな","女性の農業従事者","農場労働者","牧場主","庭師","農家","女性","女","おんな","rancher","gardener","woman","human"]},{"category":"people","char":"👨‍🌾","name":"man_farmer","keywords":["だんせいののうぎょうじゅうじしゃ","のうじょうろうどうしゃ","ぼくじょうぬし","にわし","のうか","おとこ","だんせい","男性の農業従事者","農場労働者","牧場主","庭師","農家","男","おとこ","男性","rancher","gardener","man","human"]},{"category":"people","char":"🧑‍🍳","name":"cook","keywords":["りょうりにん","しょくひん","さーびす","しぇふ","こっく","りょうり","料理人","食品","サービス","シェフ","コック","料理","chef","human"]},{"category":"people","char":"👩‍🍳","name":"woman_cook","keywords":["じょせいのりょうりにん","しょくひん","さーびす","しぇふ","こっく","りょうりにん","りょうり","じょせい","おんな","女性の料理人","食品","サービス","シェフ","コック","料理人","料理","女性","女","おんな","chef","woman","human"]},{"category":"people","char":"👨‍🍳","name":"man_cook","keywords":["だんせいのりょうりじん","しょくひん","さーびす","しぇふ","こっく","りょうりにん","りょうり","おとこ","だんせい","男性の料理人","食品","サービス","シェフ","コック","料理人","料理","男","おとこ","男性","chef","man","human"]},{"category":"people","char":"🧑‍🎓","name":"student","keywords":["せいと","がくせい","そつぎょうせい","きょういく","がっこう","生徒","学生","卒業生","教育","学校","graduate","human"]},{"category":"people","char":"👩‍🎓","name":"woman_student","keywords":["じょしせいと","がくせい","そつぎょうせい","きょういく","がっこう","じょせい","おんな","女子生徒","学生","卒業生","教育","学校","女性","女","おんな","graduate","woman","human"]},{"category":"people","char":"👨‍🎓","name":"man_student","keywords":["だんしせいと","がくせい","そつぎょうせい","きょういく","がっこう","おとこ","だんせい","男子生徒","学生","卒業生","教育","学校","男","おとこ","男性","graduate","man","human"]},{"category":"people","char":"🧑‍🎤","name":"singer","keywords":["かしゅ","おんがく","みゅーじしゃん","ろっく","ろっかー","ろっくすたー","げいのうじん","歌手","音楽","ミュージシャン","ロック","ロッカー","ロックスター","芸能人","rockstar","entertainer","human"]},{"category":"people","char":"👩‍🎤","name":"woman_singer","keywords":["だんせいしんがー","おんがく","みゅーじしゃん","ろっく","ろっかー","ろっくすたー","げいのうじん","じょせい","おんな","男性シンガー","音楽","ミュージシャン","ロック","ロッカー","ロックスター","芸能人","女性","女","おんな","rockstar","entertainer","woman","human"]},{"category":"people","char":"👨‍🎤","name":"man_singer","keywords":["だんせいしんがー","おんがく","みゅーじしゃん","ろっく","ろっかー","ろっくすたー","げいのうじん","おとこ","だんせい","男性シンガー","音楽","ミュージシャン","ロック","ロッカー","ロックスター","芸能人","男","おとこ","男性","rockstar","entertainer","man","human"]},{"category":"people","char":"🧑‍🏫","name":"teacher","keywords":["きょうし","きょういく","せんせい","きょうじゅ","こうし","教師","教育","先生","教授","講師","instructor","professor","human"]},{"category":"people","char":"👩‍🏫","name":"woman_teacher","keywords":["じょせいのきょうし","きょういく","せんせい","きょうじゅ","きょうし","こうし","じょせい","おんな","女性の教師","教育","先生","教授","教師","講師","女性","女","おんな","instructor","professor","woman","human"]},{"category":"people","char":"👨‍🏫","name":"man_teacher","keywords":["だんせいのきょうし","きょういく","せんせい","きょうじゅ","きょうし","こうし","おとこ","だんせい","男性の教師","教育","先生","教授","教師","講師","男","おとこ","男性","instructor","professor","man","human"]},{"category":"people","char":"🧑‍🏭","name":"factory_worker","keywords":["こうじょうさぎょういん","こうじょう","こうぎょう","ようせつ","工場作業員","工場","工業","溶接","assembly","industrial","human"]},{"category":"people","char":"👩‍🏭","name":"woman_factory_worker","keywords":["だんせいのこうじょうさぎょういん","こうじょう","こうぎょう","さぎょういん","じょせい","おんな","男性の工場作業員","工場","工業","作業員","女性","女","おんな","assembly","industrial","woman","human"]},{"category":"people","char":"👨‍🏭","name":"man_factory_worker","keywords":["だんせいのこうじょうさぎょういん","こうじょう","こうぎょう","さぎょういん","おとこ","だんせい","男性の工場作業員","工場","工業","作業員","男","おとこ","男性","assembly","industrial","man","human"]},{"category":"people","char":"🧑‍💻","name":"technologist","keywords":["ぎじゅつしゃ","てくのろじー","そふとうぇあ","えんじにあ","ぷろぐらまー","らっぷとっぷ","のーとぱそこん","技術者","テクノロジー","ソフトウェア","エンジニア","プログラマー","ラップトップ","ノートパソコン","coder","developer","engineer","programmer","software","human","laptop","computer"]},{"category":"people","char":"👩‍💻","name":"woman_technologist","keywords":["じょせいぎじゅつしゃ","てくのろじー","そふとうぇあ","えんじにあ","ぷろぐらまー","らっぷとっぷ","のーとぱそこん","じょせい","おんな","女性技術者","テクノロジー","ソフトウェア","エンジニア","プログラマー","ラップトップ","ノートパソコン","女性","女","おんな","coder","developer","engineer","programmer","software","woman","human","laptop","computer"]},{"category":"people","char":"👨‍💻","name":"man_technologist","keywords":["だんせいぎじゅつしゃ","てくのろじー","そふとうぇあ","えんじにあ","ぷろぐらまー","らっぷとっぷ","のーとぱそこん","おとこ","だんせい","男性技術者","テクノロジー","ソフトウェア","エンジニア","プログラマー","ラップトップ","ノートパソコン","男","おとこ","男性","coder","developer","engineer","programmer","software","man","human","laptop","computer"]},{"category":"people","char":"🧑‍💼","name":"office_worker","keywords":["かいしゃいん","おふぃす","かいけいし","ぎんこうか","かんりしょく","こもん","じむいん","あなりすと","会社員","オフィス","会計士","銀行家","管理職","顧問","事務員","アナリスト","business","manager","human"]},{"category":"people","char":"👩‍💼","name":"woman_office_worker","keywords":["だんせいかいしゃいん","おふぃす","かいけいし","ぎんこうか","かんりしょく","こもん","じむいん","あなりすと","じょせい","おんな","男性会社員","オフィス","会計士","銀行家","管理職","顧問","事務員","アナリスト","女性","女","おんな","business","manager","woman","human"]},{"category":"people","char":"👨‍💼","name":"man_office_worker","keywords":["だんせいかいしゃいん","おふぃす","かいけいし","ぎんこうか","かんりしょく","こもん","じむいん","あなりすと","おとこ","だんせい","男性会社員","オフィス","会計士","銀行家","管理職","顧問","事務員","アナリスト","男","おとこ","男性","business","manager","man","human"]},{"category":"people","char":"🧑‍🔧","name":"mechanic","keywords":["せいびし","しょくにん","はいかんこう","でんきぎし","しゅうりじん","整備士","職人","配管工","電気技師","修理人","plumber","human","wrench"]},{"category":"people","char":"👩‍🔧","name":"woman_mechanic","keywords":["じょせいせいびし","しょくにん","はいかんこう","でんきぎし","しゅうりにん","じょせい","おんな","女性整備士","職人","配管工","電気技師","修理人","女性","女","おんな","plumber","woman","human","wrench"]},{"category":"people","char":"👨‍🔧","name":"man_mechanic","keywords":["だんせいせいびし","しょくにん","はいかんこう","でんきぎし","しゅうりじん","おとこ","だんせい","男性整備士","職人","配管工","電気技師","修理人","男","おとこ","男性","plumber","man","human","wrench"]},{"category":"people","char":"🧑‍🔬","name":"scientist","keywords":["かがくしゃ","ぎじゅつしゃ","すうがくしゃ","ぶつりがくしゃ","せいぶつがくしゃ","けんさぎし","科学者","化学者","技術者","数学者","物理学者","生物学者","検査技師","biologist","chemist","engineer","physicist","human"]},{"category":"people","char":"👩‍🔬","name":"woman_scientist","keywords":["じょせいかがくしゃ","かがくしゃ","ぎじゅつしゃ","すうがくしゃ","ぶつりがくしゃ","せいぶつがくしゃ","けんさぎし","じょせい","おんな","女性科学者","科学者","化学者","技術者","数学者","物理学者","生物学者","検査技師","女性","女","おんな","biologist","chemist","engineer","physicist","woman","human"]},{"category":"people","char":"👨‍🔬","name":"man_scientist","keywords":["だんせいかがくしゃ","かがくしゃ","ぎじゅつしゃ","すうがくしゃ","ぶつりがくしゃ","せいぶつがくしゃ","けんさぎし","おとこ","だんせい","男性科学者","科学者","化学者","技術者","数学者","物理学者","生物学者","検査技師","男","おとこ","男性","biologist","chemist","engineer","physicist","man","human"]},{"category":"people","char":"🧑‍🎨","name":"artist","keywords":["あーてぃすと","げいじゅつ","あーと","げいじゅつか","かいが","がか","アーティスト","芸術","アート","芸術家","絵画","画家","painter","human"]},{"category":"people","char":"👩‍🎨","name":"woman_artist","keywords":["じょせいあーてぃすと","げいじゅつ","あーと","げいじゅつか","あーてぃすと","かいが","がか","じょせい","おんな","女性アーティスト","芸術","アート","芸術家","アーティスト","絵画","画家","女性","女","おんな","painter","woman","human"]},{"category":"people","char":"👨‍🎨","name":"man_artist","keywords":["だんせいあーてぃすと","げいじゅつ","あーと","げいじゅつか","あーてぃすと","かいが","がか","おとこ","だんせい","男性アーティスト","芸術","アート","芸術家","アーティスト","絵画","画家","男","おとこ","男性","painter","man","human"]},{"category":"people","char":"🧑‍🚒","name":"firefighter","keywords":["しょうぼうし","かじ","消防士","火事","fireman","human"]},{"category":"people","char":"👩‍🚒","name":"woman_firefighter","keywords":["じょせいしょうぼうし","ひ","かじ","しょうぼう","しょうぼうし","じょせい","おんな","女性消防士","火","火事","消防","消防士","女性","女","おんな","fireman","woman","human"]},{"category":"people","char":"👨‍🚒","name":"man_firefighter","keywords":["だんせいしょうぼうし","ひ","かじ","しょうぼう","しょうぼうし","おとこ","だんせい","男性消防士","火","火事","消防","消防士","男","おとこ","男性","fireman","man","human"]},{"category":"people","char":"🧑‍✈️","name":"pilot","keywords":["ぱいろっと","ひこうき","そうじゅうし","こうくう","パイロット","飛行機","操縦士","航空","aviator","plane","human"]},{"category":"people","char":"👩‍✈️","name":"woman_pilot","keywords":["じょせいぱいろっと","ぱいろっと","ひこうき","そうじゅうし","こうくう","じょせい","おんな","女性パイロット","パイロット","飛行機","操縦士","航空","女性","女","おんな","aviator","plane","woman","human"]},{"category":"people","char":"👨‍✈️","name":"man_pilot","keywords":["だんせいぱいろっと","ぱいろっと","ひこうき","そうじゅうし","こうくう","おとこ","だんせい","男性パイロット","パイロット","飛行機","操縦士","航空","男","おとこ","男性","aviator","plane","man","human"]},{"category":"people","char":"🧑‍🚀","name":"astronaut","keywords":["うちゅうひこうし","うちゅう","ほし","つき","わくせい","宇宙飛行士","宇宙","星","月","惑星","space","rocket","human"]},{"category":"people","char":"👩‍🚀","name":"woman_astronaut","keywords":["じょせいうちゅうひこうし","うちゅう","ほし","つき","わくせい","じょせい","おんな","女性宇宙飛行士","宇宙","星","月","惑星","女性","女","おんな","space","rocket","woman","human"]},{"category":"people","char":"👨‍🚀","name":"man_astronaut","keywords":["だんせいうちゅうひこうし","うちゅう","ほし","つき","わくせい","おとこ","だんせい","男性宇宙飛行士","宇宙","星","月","惑星","男","おとこ","男性","space","rocket","man","human"]},{"category":"people","char":"🧑‍⚖️","name":"judge","keywords":["さいばんかん","ほうてい","さいばんしょ","ほうりつ","裁判官","法廷","裁判所","法律","justice","court","human"]},{"category":"people","char":"👩‍⚖️","name":"woman_judge","keywords":["じょせいさいばんかん","さいばんかん","ほうてい","さいばんしょ","ほうりつ","じょせい","おんな","女性裁判官","裁判官","法廷","裁判所","法律","女性","女","おんな","justice","court","woman","human"]},{"category":"people","char":"👨‍⚖️","name":"man_judge","keywords":["だんせいさいばんかん","さいばんかん","ほうてい","さいばんしょ","ほうりつ","おとこ","だんせい","男性裁判官","裁判官","法廷","裁判所","法律","男","おとこ","男性","justice","court","man","human"]},{"category":"people","char":"🦸‍♀️","name":"woman_superhero","keywords":["じょせいのすーぱーひーろー","くうそう","ぜん","ひろいん","ちょうたいこく","じょせい","おんな","女性のスーパーヒーロー","空想","善","ヒロイン","超大国","女性","女","おんな","woman","female","good","heroine","superpowers"]},{"category":"people","char":"🦸‍♂️","name":"man_superhero","keywords":["だんせいのすーぱーひーろー","くうそう","ぜん","ひーろー","ちょうたいこく","だんせい","おとこ","男性のスーパーヒーロー","空想","善","ヒーロー","超大国","男性","男","おとこ","man","male","good","hero","superpowers"]},{"category":"people","char":"🦹‍♀️","name":"woman_supervillain","keywords":["じょせいのあくとう","くうそう","あく","はんざい","あくじ","ちょうたいこく","あくやく","じょせい","おんな","女性の悪党","空想","悪","犯罪","悪事","超大国","悪役","女性","女","おんな","woman","female","evil","bad","criminal","heroine","superpowers"]},{"category":"people","char":"🦹‍♂️","name":"man_supervillain","keywords":["だんせいのあくとう","くうそう","あく","はんざい","あくじ","ちょうたいこく","あくやく","だんせい","おとこ","男性の悪党","空想","悪","犯罪","悪事","超大国","悪役","男性","男","おとこ","man","male","evil","bad","criminal","hero","superpowers"]},{"category":"people","char":"🤶","name":"mrs_claus","keywords":["みせす・くろーす","いべんと","おいわい","くりすます","はは","さんた","くろーす","じょせい","おんな","ミセス・クロース","イベント","お祝い","クリスマス","母","サンタ","クロース","女性","女","おんな","woman","female","xmas","mother christmas"]},{"category":"people","char":"🧑‍🎄","name":"mx_claus","keywords":["みくすくろーす","あくてぃびてぃ","おいわい","くりすます","さんた","くろーす","ミクスクロース","アクティビティ","お祝い","クリスマス","サンタ","クロース","xmas","christmas"]},{"category":"people","char":"🎅","name":"santa","keywords":["さんたくろーす","いべんと","おいわい","くりすます","ちち","さんた","くろーす","おとこ","だんせい","サンタクロース","イベント","お祝い","クリスマス","父","サンタ","クロース","男","おとこ","男性","festival","man","male","xmas","father christmas"]},{"category":"people","char":"🥷","name":"ninja","keywords":["にんじゃ","せんし","かくされた","すてるす","忍者","戦士","隠された","ステルス"]},{"category":"people","char":"🧙‍♀️","name":"sorceress","keywords":["じょせいのまほうつかい","くうそう","まじょ","おんなのまほうつかい","じょせい","おんな","女性の魔法使い","空想","魔女","女の魔法使い","女性","女","おんな","woman","female","mage","witch"]},{"category":"people","char":"🧙‍♂️","name":"wizard","keywords":["だんせいのまほうつかい","くうそう","まじゅつし","おとこのまほうつかい","だんせい","おとこ","男性の魔法使い","空想","魔術師","男の魔法使い","男性","男","おとこ","man","male","mage","sorcerer"]},{"category":"people","char":"🧝‍♀️","name":"woman_elf","keywords":["じょせいのこども","くうそう","こども","さきのとがったみみ","じょせい","おんな","女性の小人","空想","小人","先のとがった耳","女性","女","おんな","woman","female"]},{"category":"people","char":"🧝‍♂️","name":"man_elf","keywords":["だんせいのこども","くうそう","こども","さきのとがったみみ","だんせい","おとこ","男性の小人","空想","小人","先のとがった耳","男性","男","おとこ","man","male"]},{"category":"people","char":"🧛‍♀️","name":"woman_vampire","keywords":["じょせいのきゅうけつき","くうそう","あんでっど","じょせい","おんな","女性の吸血鬼","空想","アンデッド","女性","女","おんな","woman","female"]},{"category":"people","char":"🧛‍♂️","name":"man_vampire","keywords":["だんせいのきゅうけつき","くうそう","どらきゅら","あんでっど","だんせい","おとこ","男性の吸血鬼","空想","ドラキュラ","アンデッド","男性","男","おとこ","man","male","dracula"]},{"category":"people","char":"🧟‍♀️","name":"woman_zombie","keywords":["じょせいのぞんび","くうそう","あんでっど","じょせい","おんな","女性のゾンビ","空想","アンデッド","女性","女","おんな","woman","female","undead","walking dead"]},{"category":"people","char":"🧟‍♂️","name":"man_zombie","keywords":["だんせいのぞんび","くうそう","あんでっど","だんせい","おとこ","男性のゾンビ","空想","アンデッド","男性","男","おとこ","man","male","dracula","undead","walking dead"]},{"category":"people","char":"🧞‍♀️","name":"woman_genie","keywords":["じょせいのせいれい","くうそう","せいれい","じょせい","おんな","女性の精霊","空想","精霊","女性","女","おんな","woman","female"]},{"category":"people","char":"🧞‍♂️","name":"man_genie","keywords":["だんせいのせいれい","くうそう","せいれい","だんせい","おとこ","男性の精霊","空想","精霊","男性","男","おとこ","man","male"]},{"category":"people","char":"🧜‍♀️","name":"mermaid","keywords":["じょせいのにんぎょ","くうそう","じょせい","おんな","女性の人魚","空想","女性","女","おんな","woman","female","merwoman","ariel"]},{"category":"people","char":"🧜‍♂️","name":"merman","keywords":["だんせいのにんぎょ","くうそう","にんぎょ","だんせい","おとこ","男性の人魚","空想","人魚","男性","男","おとこ","man","male","triton"]},{"category":"people","char":"🧚‍♀️","name":"woman_fairy","keywords":["じょせいのようせい","くうそう","てぃたーにあ","うぃんぐす","じょせい","おんな","女性の妖精","空想","ティターニア","ウィングス","女性","女","おんな","woman","female"]},{"category":"people","char":"🧚‍♂️","name":"man_fairy","keywords":["だんせいのようせい","くうそう","おべろん","しょうようせい","だんせい","おとこ","男性の妖精","空想","オベロン","小妖精","男性","男","おとこ","man","male"]},{"category":"people","char":"👼","name":"angel","keywords":["てんしのあかちゃん","てんし","あかちゃん","かお","おとぎばなし","ふぁんたじー","天使の赤ちゃん","天使","赤ちゃん","顔","おとぎ話","ファンタジー","heaven","wings","halo"]},{"category":"people","char":"🧌","name":"troll","keywords":["つり","おとぎばなし","ふぁんたじ","もんすたー","釣り","おとぎ話","ファンタジ","モンスター"]},{"category":"people","char":"🤰","name":"pregnant_woman","keywords":["にんぷ","にんしん","あかちゃん","じょせい","おんな","はら","ふくれた","ふっくらした","妊婦","妊娠","赤ちゃん","女性","女","おんな","腹","ふくれた","ふっくらした","baby"]},{"category":"people","char":"🫃","name":"pregnant_man","keywords":["にんしんしているだんせい","はら","ふくれた","ふっくらした","にんしん","あかちゃん","だんせい","おとこ","妊娠している男性","腹","ふくれた","ふっくらした","妊娠","赤ちゃん","男性","男","おとこ"]},{"category":"people","char":"🫄","name":"pregnant_person","keywords":["にんしんしたひと","はら","ふくれた","ふっくらした","にんしん","あかちゃん","妊娠した人","腹","ふくれた","ふっくらした","妊娠","赤ちゃん"]},{"category":"people","char":"🫅","name":"person_with_crown","keywords":["おうかんをかぶったひと","おとぎばなし","ふぁんたじー","こくおう","きぞく","おう","おうぞく","王冠をかぶった人","おとぎ話","ファンタジー","国王","貴族","王","王族"]},{"category":"people","char":"🤱","name":"breastfeeding","keywords":["ぼにゅう","むね","あかちゃん","あかんぼう","にゅうじ","ようじ","はは","こども","ほいく","みるく","じょせい","おんな","母乳","胸","赤ちゃん","赤ん坊","乳児","幼児","母","子供","保育","ミルク","女性","女","おんな","nursing","baby"]},{"category":"people","char":"👩‍🍼","name":"woman_feeding_baby","keywords":["あかちゃんにごはんをあげるじょせい","あかちゃん","にゅうじ","こども","じゅにゅう","みるく","ぼとる","じょせい","おんな","赤ちゃんにご飯をあげる女性","赤ちゃん","乳児","子供","授乳","ミルク","ボトル","女性","女","おんな"]},{"category":"people","char":"👨‍🍼","name":"man_feeding_baby","keywords":["あかちゃんにごはんをあげるだんせい","あかちゃん","にゅうじ","こども","じゅにゅう","みるく","ぼとる","だんせい","おとこ","赤ちゃんにご飯をあげる男性","赤ちゃん","乳児","子供","授乳","ミルク","ボトル","男性","男","おとこ"]},{"category":"people","char":"🧑‍🍼","name":"person_feeding_baby","keywords":["あかちゃんにごはんをあげるひと","あかちゃん","にゅうじ","こども","じゅにゅう","みるく","ぼとる","赤ちゃんにご飯をあげる人","赤ちゃん","乳児","子供","授乳","ミルク","ボトル"]},{"category":"people","char":"👸","name":"princess","keywords":["おひめさま","おとぎばなし","ふぁんたじー","じょおう","じょせい","おんな","お姫さま","おとぎ話","ファンタジー","女王","女性","女","おんな","girl","woman","female","blond","crown","royal","queen"]},{"category":"people","char":"🤴","name":"prince","keywords":["おうじさま","おとぎばなし","ふぁんたじー","おう","おとこ","だんせい","王子様","おとぎ話","ファンタジー","王","男","おとこ","男性","boy","man","male","crown","royal","king"]},{"category":"people","char":"👰","name":"person_with_veil","keywords":["べーるをつけたじょせい","はなよめ","べーる","けっこんしき","じょせい","おんな","ベールを付けた女性","花嫁","ベール","結婚式","女性","女","おんな","couple","marriage","wedding","woman","bride"]},{"category":"people","char":"👰","name":"bride_with_veil","keywords":["べーるをつけたじょせい","はなよめ","べーる","けっこんしき","じょせい","おんな","ベールを付けた女性","花嫁","ベール","結婚式","女性","女","おんな","couple","marriage","wedding","woman","bride"]},{"category":"people","char":"🤵","name":"person_in_tuxedo","keywords":["たきしーどをきるひと","はなむこ","たきしーど","うぇでぃんぐ","タキシードを着る人","花婿","タキシード","ウェディング","couple","marriage","wedding","groom"]},{"category":"people","char":"🤵","name":"man_in_tuxedo","keywords":["たきしーどをきるひと","はなむこ","たきしーど","うぇでぃんぐ","タキシードを着る人","花婿","タキシード","ウェディング","couple","marriage","wedding","groom"]},{"category":"people","char":"🏃‍♀️","name":"running_woman","keywords":["はしるじょせい","まらそん","らんなー","らんにんぐ","じょせい","おんな","走る女性","マラソン","ランナー","ランニング","女性","女","おんな","woman","walking","exercise","race","running","female"]},{"category":"people","char":"🏃","name":"running_man","keywords":["はしるひと","まらそん","らんなー","らんにんぐ","走る人","マラソン","ランナー","ランニング","man","walking","exercise","race","running"]},{"category":"people","char":"🚶‍♀️","name":"walking_woman","keywords":["あるくじょせい","はいきんぐ","ほこうしゃ","あるく","うぉーきんぐ","じょせい","おんな","歩く女性","ハイキング","歩行者","歩く","ウォーキング","女性","女","おんな","human","feet","steps","woman","female"]},{"category":"people","char":"🚶","name":"walking_man","keywords":["あるくひと","はいきんぐ","ほこうしゃ","あるく","うぉーきんぐ","歩く人","ハイキング","歩行者","歩く","ウォーキング","human","feet","steps"]},{"category":"people","char":"💃","name":"dancer","keywords":["じょせいだんさー","だんす","おどる","だんさー","じょせい","おんな","女性ダンサー","ダンス","踊る","ダンサー","女性","女","おんな","female","girl","woman","fun"]},{"category":"people","char":"🕺","name":"man_dancing","keywords":["だんせいだんさー","だんす","おどる","だんさー","おとこ","だんせい","男性ダンサー","ダンス","踊る","ダンサー","男","おとこ","男性","male","boy","fun","dancer"]},{"category":"people","char":"👯","name":"dancing_women","keywords":["うさぎみみのひと","うさぎみみ","だんさー","うさぎ耳の人","うさぎ耳","ダンサー","female","bunny","women","girls"]},{"category":"people","char":"👯‍♂️","name":"dancing_men","keywords":["うさぎみみのだんせい","うさぎみみ","だんさー","おとこ","だんせい","うさぎ耳の男性","うさぎ耳","ダンサー","男","おとこ","男性","male","bunny","men","boys"]},{"category":"people","char":"👫","name":"couple","keywords":["しゅをつないだだんじょ","かっぷる","て","つなぐ","おとこ","おんな","だんじょ","手をつないだ男女","カップル","手","つなぐ","男","女","男女","おとこ","おんな","pair","people","human","love","date","dating","like","affection","valentines","marriage"]},{"category":"people","char":"🧑‍🤝‍🧑","name":"people_holding_hands","keywords":["しゅをつないだひとたち","かっぷる","て","にぎる","手をつないだ人たち","カップル","手","握る","pair","couple","love","like","bromance","friendship","people","human"]},{"category":"people","char":"👬","name":"two_men_holding_hands","keywords":["しゅをつないだだんせい","かっぷる","て","つなぐ","だんせい","おとこ","ぷらいど","lgbt","げい","手をつないだ男性","カップル","手","つなぐ","男性","男","おとこ","プライド","lgbt","ゲイ","pair","couple","love","like","bromance","friendship","people","man","human"]},{"category":"people","char":"👭","name":"two_women_holding_hands","keywords":["しゅをつないだじょせい","かっぷる","て","つなぐ","じょせい","おんな","ぷらいど","lgbt","れずびあん","手をつないだ女性","カップル","手","つなぐ","女性","女","おんな","プライド","lgbt","レズビアン","pair","couple","love","like","bromance","friendship","people","female","human"]},{"category":"people","char":"🫂","name":"people_hugging","keywords":["はぐしているひとたち","さようなら","こんにちは","はぐ","ありがとう","ハグしている人たち","さようなら","こんにちは","ハグ","ありがとう"]},{"category":"people","char":"🙇‍♀️","name":"bowing_woman","keywords":["ふかくおじぎするじょせい","しゃざい","おじぎ","じぇすちゃー","ごめんなさい","じょせい","おんな","深くお辞儀する女性","謝罪","お辞儀","ジェスチャー","ごめんなさい","女性","女","おんな","woman","female","girl"]},{"category":"people","char":"🙇","name":"bowing_man","keywords":["ふかくおじぎしたひと","しゃざい","おじぎ","じぇすちゃー","ごめんなさい","深くお辞儀した人","謝罪","お辞儀","ジェスチャー","ごめんなさい","man","male","boy"]},{"category":"people","char":"🤦‍♂️","name":"man_facepalming","keywords":["がおをおさえるだんせい","ふしん","ふんがい","かお","てのひら","おとこ","だんせい","顔を押さえる男性","不信","憤慨","顔","手のひら","男","おとこ","男性","man","male","boy","disbelief"]},{"category":"people","char":"🤦‍♀️","name":"woman_facepalming","keywords":["かおをおさえるじょせい","ふしん","ふんがい","かお","てのひら","じょせい","おんな","顔を押さえる女性","不信","憤慨","顔","手のひら","女性","女","おんな","woman","female","girl","disbelief"]},{"category":"people","char":"🤷","name":"woman_shrugging","keywords":["かたをすくめるひと","うたがい","むち","むかんしん","かたをすくめる","肩をすくめる人","疑い","無知","無関心","肩をすくめる","woman","female","girl","confused","indifferent","doubt"]},{"category":"people","char":"🤷‍♂️","name":"man_shrugging","keywords":["かたをすくめるだんせい","うたがい","むち","むかんしん","かたをすくめる","おとこ","だんせい","肩をすくめる男性","疑い","無知","無関心","肩をすくめる","男","おとこ","男性","man","male","boy","confused","indifferent","doubt"]},{"category":"people","char":"💁","name":"tipping_hand_woman","keywords":["あんないするひと","て","たすけ","じょうほう","ずうずうしい","じょせい","おんな","案内する人","手","助け","情報","ずうずうしい","女性","女","おんな","female","girl","woman","human","information"]},{"category":"people","char":"💁‍♂️","name":"tipping_hand_man","keywords":["あんないするだんせい","て","たすけ","じょうほう","ずうずうしい","おとこ","だんせい","案内する男性","手","助け","情報","ずうずうしい","男","おとこ","男性","male","boy","man","human","information"]},{"category":"people","char":"🙅","name":"no_good_woman","keywords":["NGさいんのひと","きんじる","じぇすちゃー","て","だめ","きんし","NGサインの人","禁じる","ジェスチャー","手","だめ","ダメ","禁止","female","girl","woman","nope"]},{"category":"people","char":"🙅‍♂️","name":"no_good_man","keywords":["NGさいんのだんせい","きんじる","じぇすちゃー","て","だめ","きんし","おとこ","だんせい","NGサインの男性","禁じる","ジェスチャー","手","だめ","ダメ","禁止","男","おとこ","男性","male","boy","man","nope"]},{"category":"people","char":"🙆","name":"ok_woman","keywords":["OKさいんのひと","じぇすちゃー","て","OK","OKサインの人","ジェスチャー","手","OK","women","girl","female","pink","human","woman"]},{"category":"people","char":"🙆‍♂️","name":"ok_man","keywords":["OKさいんのだんせい","じぇすちゃー","て","ok","おとこ","だんせい","OKサインの男性","ジェスチャー","手","ok","男","おとこ","男性","men","boy","male","blue","human","man"]},{"category":"people","char":"🙋","name":"raising_hand_woman","keywords":["かたてをあげてよろこぶひと","じぇすちゃー","て","しあわせ","あげる","片手を上げて喜ぶ人","ジェスチャー","手","幸せ","しあわせ","挙げる","female","girl","woman"]},{"category":"people","char":"🙋‍♂️","name":"raising_hand_man","keywords":["かたてをあげてよろこぶだんせい","じぇすちゃー","て","しあわせ","あげる","おとこ","だんせい","片手を上げて喜ぶ男性","ジェスチャー","手","幸せ","しあわせ","挙げる","男","おとこ","男性","male","boy","man"]},{"category":"people","char":"🙎","name":"pouting_woman","keywords":["おこったかおのひと","じぇすちゃー","ふくれっつら","怒った顔の人","ジェスチャー","ふくれっ面","ふくれっつら","female","girl","woman"]},{"category":"people","char":"🙎‍♂️","name":"pouting_man","keywords":["ふくれっつらのだんせい","じぇすちゃー","ふくれっつら","おとこ","だんせい","ふくれっ面の男性","ジェスチャー","ふくれっ面","ふくれっつら","男","おとこ","男性","male","boy","man"]},{"category":"people","char":"🙍","name":"frowning_woman","keywords":["ふまんなかおのひと","しかめめん","じぇすちゃー","かなしい","不満な顔の人","しかめ面","ジェスチャー","悲しい","female","girl","woman","sad","depressed","discouraged","unhappy"]},{"category":"people","char":"🙍‍♂️","name":"frowning_man","keywords":["がおをしかめただんせい","しかめめん","じぇすちゃー","かなしい","だんせい","おとこ","顔をしかめた男性","しかめ面","ジェスチャー","悲しい","男性","男","おとこ","male","boy","man","sad","depressed","discouraged","unhappy"]},{"category":"people","char":"💇","name":"haircut_woman","keywords":["かみをきられているひと","りはつし","びようし","びよう","さんぱつ","へあかっと","びよういん","髪を切られている人","理髪師","美容師","美容","散髪","ヘアカット","美容院","female","girl","woman"]},{"category":"people","char":"💇‍♂️","name":"haircut_man","keywords":["かみをきられているだんせい","りはつし","びようし","びよう","さんぱつ","へあかっと","びよういん","おとこ","だんせい","髪を切られている男性","理髪師","美容師","美容","散髪","ヘアカット","美容院","男","おとこ","男性","male","boy","man"]},{"category":"people","char":"💆","name":"massage_woman","keywords":["ふぇいすまっさーじをうけるひと","まっさーじ","さろん","フェイスマッサージを受ける人","マッサージ","サロン","female","girl","woman","head"]},{"category":"people","char":"💆‍♂️","name":"massage_man","keywords":["ふぇいすまっさーじをうけるだんせい","まっさーじ","さろん","おとこ","だんせい","フェイスマッサージを受ける男性","マッサージ","サロン","男","おとこ","男性","male","boy","man","head"]},{"category":"people","char":"🧖‍♀️","name":"woman_in_steamy_room","keywords":["すちーむるーむにいるじょせい","さうな","すちーむるーむ","はまむ","すちーむばす","じょせい","おんな","スチームルームにいる女性","サウナ","スチームルーム","ハマム","スチームバス","女性","女","おんな","female","woman","spa","steamroom","sauna"]},{"category":"people","char":"🧖‍♂️","name":"man_in_steamy_room","keywords":["すちーむるーむにいるだんせい","さうな","すちーむるーむ","はまむ","すちーむばす","だんせい","おとこ","スチームルームにいる男性","サウナ","スチームルーム","ハマム","スチームバス","男性","男","おとこ","male","man","spa","steamroom","sauna"]},{"category":"people","char":"🧏‍♀️","name":"woman_deaf","keywords":["みみがふじゆうなじょせい","あくせしびりてぃ","みみがふじゆう","じょせい","おんな","耳が不自由な女性","アクセシビリティ","耳が不自由","女性","女","おんな","woman","female"]},{"category":"people","char":"🧏‍♂️","name":"man_deaf","keywords":["みみがふじゆうなだんせい","あくせしびりてぃ","みみがふじゆう","だんせい","おとこ","耳が不自由な男性","アクセシビリティ","耳が不自由","男性","男","おとこ","man","male"]},{"category":"people","char":"🧍‍♀️","name":"woman_standing","keywords":["たっているじょせい","たつ","すたんでぃんぐ","じょせい","おんな","立っている女性","立つ","スタンディング","女性","女","おんな","woman","female"]},{"category":"people","char":"🧍‍♂️","name":"man_standing","keywords":["たっているだんせい","たつ","すたんでぃんぐ","だんせい","おとこ","立っている男性","立つ","スタンディング","男性","男","おとこ","man","male"]},{"category":"people","char":"🧎‍♀️","name":"woman_kneeling","keywords":["ひざたちしているじょせい","ひざ","ひざたち","じょせい","おんな","膝立ちしている女性","膝","膝立ち","女性","女","おんな","woman","female"]},{"category":"people","char":"🧎‍♂️","name":"man_kneeling","keywords":["ひざたちしているだんせい","ひざ","ひざたち","だんせい","おとこ","膝立ちしている男性","膝","膝立ち","男性","男","おとこ","man","male"]},{"category":"people","char":"🧑‍🦯","name":"person_with_probing_cane","keywords":["しろつえをもったひと","あくせしびりてぃ","めがふじゆう","白杖を持った人","アクセシビリティ","目が不自由","accessibility","blind"]},{"category":"people","char":"👩‍🦯","name":"woman_with_probing_cane","keywords":["しろつえをもったじょせい","あくせしびりてぃ","めがふじゆう","じょせい","おんな","白杖を持った女性","アクセシビリティ","目が不自由","女性","女","おんな","woman","female","accessibility","blind"]},{"category":"people","char":"👨‍🦯","name":"man_with_probing_cane","keywords":["しろつえをもっただんせい","あくせしびりてぃ","めがふじゆう","だんせい","おとこ","白杖を持った男性","アクセシビリティ","目が不自由","男性","男","おとこ","man","male","accessibility","blind"]},{"category":"people","char":"🧑‍🦼","name":"person_in_motorized_wheelchair","keywords":["でんどうくるまいすにすわっているひと","あくせしびりてぃ","くるまいす","電動車いすに座っている人","アクセシビリティ","車いす","accessibility"]},{"category":"people","char":"👩‍🦼","name":"woman_in_motorized_wheelchair","keywords":["でんどうくるまいすにすわっているじょせい","あくせしびりてぃ","くるまいす","じょせい","おんな","電動車いすに座っている女性","アクセシビリティ","車いす","女性","女","おんな","woman","female","accessibility"]},{"category":"people","char":"👨‍🦼","name":"man_in_motorized_wheelchair","keywords":["でんどうくるまいすにすわっているだんせい","あくせしびりてぃ","くるまいす","だんせい","おとこ","電動車いすに座っている男性","アクセシビリティ","車いす","男性","男","おとこ","man","male","accessibility"]},{"category":"people","char":"🧑‍🦽","name":"person_in_manual_wheelchair","keywords":["しゅどうくるまいすにすわっているひと","あくせしびりてぃ","くるまいす","手動車いすに座っている人","アクセシビリティ","車いす","accessibility"]},{"category":"people","char":"👩‍🦽","name":"woman_in_manual_wheelchair","keywords":["しゅどうくるまいすにすわっているじょせい","あくせしびりてぃ","くるまいす","じょせい","おんな","手動車いすに座っている女性","アクセシビリティ","車いす","女性","女","おんな","woman","female","accessibility"]},{"category":"people","char":"👨‍🦽","name":"man_in_manual_wheelchair","keywords":["しゅどうくるまいすにすわっているだんせい","あくせしびりてぃ","くるまいす","だんせい","おとこ","手動車いすに座っている男性","アクセシビリティ","車いす","男性","男","おとこ","man","male","accessibility"]},{"category":"people","char":"💑","name":"couple_with_heart_woman_man","keywords":["はーとのかっぷる","かっぷる","はーと","あい","れんあい","おとこ","おんな","だんじょ","ハートのカップル","カップル","ハート","愛","恋愛","男","女","男女","おとこ","おんな","pair","love","like","affection","human","dating","valentines","marriage"]},{"category":"people","char":"👩‍❤️‍👩","name":"couple_with_heart_woman_woman","keywords":["はーとのかっぷる (じょせい、じょせい)","かっぷる","はーと","あい","れんあい","じょせい","おんな","ぷらいど","lgbt","れずびあん","ハートのカップル (女性、女性)","カップル","ハート","愛","恋愛","女性","女","おんな","プライド","lgbt","レズビアン","pair","love","like","affection","human","dating","valentines","marriage"]},{"category":"people","char":"👨‍❤️‍👨","name":"couple_with_heart_man_man","keywords":["はーとのかっぷる (だんせい、だんせい)","かっぷる","はーと","あい","れんあい","だんせい","おとこ","ぷらいど","lgbt","げい","ハートのカップル (男性、男性)","カップル","ハート","愛","恋愛","男性","男","おとこ","プライド","lgbt","ゲイ","pair","love","like","affection","human","dating","valentines","marriage"]},{"category":"people","char":"💏","name":"couplekiss_man_woman","keywords":["きす","かっぷる","はーと","あい","れんあい","おとこ","おんな","だんじょ","キス","カップル","キス","ハート","愛","恋愛","男","女","男女","おとこ","おんな","pair","valentines","love","like","dating","marriage"]},{"category":"people","char":"👩‍❤️‍💋‍👩","name":"couplekiss_woman_woman","keywords":["きす (じょせい、じょせい)","かっぷる","きす","はーと","あい","れんあい","じょせい","おんな","ぷらいど","lgbt","げい","キス (女性、女性)","カップル","キス","ハート","愛","恋愛","女性","女","おんな","プライド","lgbt","ゲイ","pair","valentines","love","like","dating","marriage"]},{"category":"people","char":"👨‍❤️‍💋‍👨","name":"couplekiss_man_man","keywords":["きす (だんせい、だんせい)","かっぷる","きす","はーと","あい","れんあい","だんせい","おとこ","ぷらいど","lgbt","げい","キス (男性、男性)","カップル","キス","ハート","愛","恋愛","男性","男","おとこ","プライド","lgbt","ゲイ","pair","valentines","love","like","dating","marriage"]},{"category":"people","char":"👪","name":"family_man_woman_boy","keywords":["かぞく","ちちおや","ははおや","おとこ","おんな","だんじょ","おとこのこ","こども","家族","父親","母親","男","女","男女","おとこ","おんな","男の子","こども","home","parents","child","mom","dad","father","mother","people","human"]},{"category":"people","char":"👨‍👩‍👧","name":"family_man_woman_girl","keywords":["かぞく (だんせい、じょせい、おんなのこ)","ちちおや","ははおや","おとこ","おんな","だんじょ","おんなのこ","こども","家族 (男性、女性、女の子)","父親","母親","男","女","男女","おとこ","おんな","女の子","こども","home","parents","people","human","child"]},{"category":"people","char":"👨‍👩‍👧‍👦","name":"family_man_woman_girl_boy","keywords":["かぞく (だんせい、じょせい、おんなのこ、おとこのこ)","ちちおや","ははおや","おとこ","おんな","だんじょ","おとこのこ","おんなのこ","こども","家族 (男性、女性、女の子、男の子)","父親","母親","男","女","男女","おとこ","おんな","男の子","女の子","こども","home","parents","people","human","children"]},{"category":"people","char":"👨‍👩‍👦‍👦","name":"family_man_woman_boy_boy","keywords":["かぞく (だんせい、じょせい、おとこのこ、おとこのこ)","ちちおや","ははおや","おとこ","おんな","だんじょ","おとこのこ","こども","家族 (男性、女性、男の子、男の子)","父親","母親","男","女","男女","おとこ","おんな","男の子","こども","home","parents","people","human","children"]},{"category":"people","char":"👨‍👩‍👧‍👧","name":"family_man_woman_girl_girl","keywords":["かぞく (だんせい、じょせい、おんなのこ、おんなのこ)","ちちおや","ははおや","おとこ","おんな","だんじょ","おんなのこ","こども","家族 (男性、女性、女の子、女の子)","父親","母親","男","女","男女","おとこ","おんな","女の子","こども","home","parents","people","human","children"]},{"category":"people","char":"👩‍👩‍👦","name":"family_woman_woman_boy","keywords":["かぞく (じょせい、じょせい、おとこのこ)","かぞく","ははおや","じょせい","おんな","おとこのこ","こども","ぷらいど","lgbt","れずびあん","家族 (女性、女性、男の子)","家族","母親","女性","女","おんな","男の子","子供","プライド","lgbt","レズビアン","home","parents","people","human","children"]},{"category":"people","char":"👩‍👩‍👧","name":"family_woman_woman_girl","keywords":["かぞく (じょせい、じょせい、おんなのこ)","かぞく","ははおや","じょせい","おんな","おんなのこ","こども","ぷらいど","lgbt","れずびあん","家族 (女性、女性、女の子)","家族","母親","女性","女","おんな","女の子","子供","プライド","lgbt","レズビアン","home","parents","people","human","children"]},{"category":"people","char":"👩‍👩‍👧‍👦","name":"family_woman_woman_girl_boy","keywords":["かぞく (じょせい、じょせい、おんなのこ、おとこのこ)","かぞく","ははおや","じょせい","おんな","おとこのこ","おんなのこ","こども","ぷらいど","lgbt","れずびあん","家族 (女性、女性、女の子、男の子)","家族","母親","女性","女","おんな","男の子","女の子","子供","プライド","lgbt","レズビアン","home","parents","people","human","children"]},{"category":"people","char":"👩‍👩‍👦‍👦","name":"family_woman_woman_boy_boy","keywords":["かぞく (じょせい、じょせい、おとこのこ、おとこのこ)","かぞく","ははおや","じょせい","おんな","おとこのこ","こども","ぷらいど","lgbt","れずびあん","家族 (女性、女性、男の子、男の子)","家族","母親","女性","女","おんな","男の子","子供","プライド","lgbt","レズビアン","home","parents","people","human","children"]},{"category":"people","char":"👩‍👩‍👧‍👧","name":"family_woman_woman_girl_girl","keywords":["かぞく (じょせい、じょせい、おんなのこ、おんなのこ)","かぞく","ははおや","じょせい","おんな","おんなのこ","こども","ぷらいど","lgbt","れずびあん","家族 (女性、女性、女の子、女の子)","家族","母親","女性","女","おんな","女の子","子供","プライド","lgbt","レズビアン","home","parents","people","human","children"]},{"category":"people","char":"👨‍👨‍👦","name":"family_man_man_boy","keywords":["かぞく (だんせい、だんせい、おとこのこ)","かぞく","ちちおや","だんせい","おとこ","おとこのこ","こども","ぷらいど","lgbt","げい","家族 (男性、男性、男の子)","家族","父親","男性","男","おとこ","男の子","子供","プライド","lgbt","ゲイ","home","parents","people","human","children"]},{"category":"people","char":"👨‍👨‍👧","name":"family_man_man_girl","keywords":["かぞく (だんせい、だんせい、おんなのこ)","かぞく","ちちおや","だんせい","おとこ","おんなのこ","こども","ぷらいど","lgbt","げい","家族 (男性、男性、女の子)","家族","父親","男性","男","おとこ","女の子","子供","プライド","lgbt","ゲイ","home","parents","people","human","children"]},{"category":"people","char":"👨‍👨‍👧‍👦","name":"family_man_man_girl_boy","keywords":["かぞく (だんせい、だんせい、おんなのこ、おとこのこ)","かぞく","ちちおや","だんせい","おとこ","おとこのこ","おんなのこ","こども","ぷらいど","lgbt","げい","家族 (男性、男性、女の子、男の子)","家族","父親","男性","男","おとこ","男の子","女の子","子供","プライド","lgbt","ゲイ","home","parents","people","human","children"]},{"category":"people","char":"👨‍👨‍👦‍👦","name":"family_man_man_boy_boy","keywords":["かぞく (だんせい、だんせい、おとこのこ、おとこのこ)","かぞく","ちちおや","だんせい","おとこ","おとこのこ","こども","ぷらいど","lgbt","げい","家族 (男性、男性、男の子、男の子)","家族","父親","男性","男","おとこ","男の子","子供","プライド","lgbt","ゲイ","home","parents","people","human","children"]},{"category":"people","char":"👨‍👨‍👧‍👧","name":"family_man_man_girl_girl","keywords":["かぞく (だんせい、だんせい、おんなのこ、おんなのこ)","かぞく","ちちおや","だんせい","おとこ","おんなのこ","こども","ぷらいど","lgbt","げい","家族 (男性、男性、女の子、女の子)","家族","父親","男性","男","おとこ","女の子","子供","プライド","lgbt","ゲイ","home","parents","people","human","children"]},{"category":"people","char":"👩‍👦","name":"family_woman_boy","keywords":["かぞく(じょせい、おとこのこ)","かぞく","ははおや","じょせい","おんな","おとこのこ","こども","家族(女性、男の子)","家族","母親","女性","女","おんな","男の子","こども","home","parent","people","human","child"]},{"category":"people","char":"👩‍👧","name":"family_woman_girl","keywords":["かぞく(じょせい、おんなのこ)","かぞく","ははおや","じょせい","おんな","おんなのこ","こども","家族(女性、女の子)","家族","母親","女性","女","おんな","女の子","こども","home","parent","people","human","child"]},{"category":"people","char":"👩‍👧‍👦","name":"family_woman_girl_boy","keywords":["かぞく(じょせい、おんなのこ、おとこのこ)","かぞく","ははおや","じょせい","おんな","だんせい","おんなのこ","おとこのこ","こども","家族(女性、女の子、男の子)","家族","母親","女性","女","男性","女の子","男の子","こども","home","parent","people","human","children"]},{"category":"people","char":"👩‍👦‍👦","name":"family_woman_boy_boy","keywords":["かぞく(じょせい、おとこのこ、おとこのこ)","かぞく","ははおや","じょせい","おんな","おとこのこ","こども","家族(女性、男の子、男の子)","家族","母親","女性","女","おんな","男の子","こども","home","parent","people","human","children"]},{"category":"people","char":"👩‍👧‍👧","name":"family_woman_girl_girl","keywords":["かぞく(じょせい、おんなのこ、おんなのこ)","かぞく","ははおや","じょせい","おんな","おんなのこ","こども","家族(女性、女の子、女の子)","家族","母親","女性","女","おんな","女の子","こども","home","parent","people","human","children"]},{"category":"people","char":"👨‍👦","name":"family_man_boy","keywords":["かぞく(だんせい、おとこのこ)","ちちおや","おとこ","だんせい","おとこのこ","こども","家族(男性、男の子)","父親","男","おとこ","男性","男の子","こども","home","parent","people","human","child"]},{"category":"people","char":"👨‍👧","name":"family_man_girl","keywords":["かぞく(だんせい、おんなのこ)","ちちおや","おとこ","だんじょ","おんなのこ","こども","家族(男性、女の子)","父親","男","男女","女の子","こども","home","parent","people","human","child"]},{"category":"people","char":"👨‍👧‍👦","name":"family_man_girl_boy","keywords":["かぞく(だんせい、おんなのこ、おとこのこ)","ちちおや","おとこ","だんせい","おとこのこ","おんなのこ","こども","家族(男性、女の子、男の子)","父親","男","おとこ","男性","男の子","女の子","こども","home","parent","people","human","children"]},{"category":"people","char":"👨‍👦‍👦","name":"family_man_boy_boy","keywords":["かぞく(だんせい、おとこのこ、おとこのこ)","ちちおや","おとこ","だんせい","おとこのこ","こども","家族(男性、男の子、男の子)","父親","男","おとこ","男性","男の子","こども","home","parent","people","human","children"]},{"category":"people","char":"👨‍👧‍👧","name":"family_man_girl_girl","keywords":["かぞく(だんせい、おんなのこ、おんなのこ)","ちちおや","おとこ","だんじょ","おんなのこ","こども","家族(男性、女の子、女の子)","父親","男","男女","女の子","こども","home","parent","people","human","children"]},{"category":"people","char":"🧶","name":"yarn","keywords":["いと","ぼーる","かぎばりあみ","にっと","しゅこうげい","糸","ボール","かぎ針編み","ニット","手工芸","ball","crochet","knit"]},{"category":"people","char":"🧵","name":"thread","keywords":["すれっど","ぬいあみ","さいほう","いとまき","いと","しゅこうげい","スレッド","縫い編み","裁縫","糸巻","糸","手工芸","needle","sewing","spool","string"]},{"category":"people","char":"🧥","name":"coat","keywords":["こーと","ふく","じゃけっと","コート","服","ジャケット","jacket"]},{"category":"people","char":"🥼","name":"labcoat","keywords":["はくい","ふく","いしゃ","じっけん","かがくしゃ","白衣","服","医者","実験","科学者","doctor","experiment","scientist","chemist"]},{"category":"people","char":"👚","name":"womans_clothes","keywords":["れでぃーすうぇあ","ふく","じょせい","おんな","レディースウェア","服","女性","おんな","fashion","shopping_bags","female"]},{"category":"people","char":"👕","name":"tshirt","keywords":["てぃーしゃつ","ふく","しゃつ","Tシャツ","服","シャツ","fashion","cloth","casual","shirt","tee"]},{"category":"people","char":"👖","name":"jeans","keywords":["じーんず","ふく","ぱんつ","ずぼん","ジーンズ","服","パンツ","ズボン","fashion","shopping"]},{"category":"people","char":"👔","name":"necktie","keywords":["ねくたい","ふく","ネクタイ","服","shirt","suitup","formal","fashion","cloth","business"]},{"category":"people","char":"👗","name":"dress","keywords":["どれす","ふく","ドレス","服","clothes","fashion","shopping"]},{"category":"people","char":"👙","name":"bikini","keywords":["びきに","ふく","すいえい","ビキニ","服","水泳","swimming","female","woman","girl","fashion","beach","summer"]},{"category":"people","char":"🩱","name":"one_piece_swimsuit","keywords":["わんぴーす","ふく","みずぎ","すいみんぐうぇあ","すいえい","ワンピース","服","水着","スイミングウェア","水泳","swimming","female","woman","girl","fashion","beach","summer"]},{"category":"people","char":"👘","name":"kimono","keywords":["きもの","ふく","わふく","着物","服","和服","dress","fashion","women","female","japanese"]},{"category":"people","char":"🥻","name":"sari","keywords":["さりー","ふく","どれす","サリー","服","ドレス","dress","fashion","women","female"]},{"category":"people","char":"🩲","name":"briefs","keywords":["ぶりーふ","ふく","みずぎ","すいみんぐうぇあ","すいえい","したぎ","ブリーフ","服","水着","スイミングウェア","水泳","下着","dress","fashion"]},{"category":"people","char":"🩳","name":"shorts","keywords":["しょーつ","ふく","みずぎ","すいみんぐうぇあ","すいえい","したぎ","ショーツ","服","水着","スイミングウェア","水泳","下着","dress","fashion"]},{"category":"people","char":"💄","name":"lipstick","keywords":["くちべに","けしょうひん","こすめ","けしょう","めいく","口紅","化粧品","コスメ","化粧","メイク","female","girl","fashion","woman"]},{"category":"people","char":"💋","name":"kiss","keywords":["きすまーく","はーと","きす","くちびる","まーく","れんあい","ろまんす","キスマーク","ハート","キス","唇","クチビル","マーク","恋愛","ロマンス","face","lips","love","like","affection","valentines"]},{"category":"people","char":"👣","name":"footprints","keywords":["あしあと","からだ","ふく","足あと","体","服","足跡","あしあと","feet","tracking","walking","beach"]},{"category":"people","char":"🥿","name":"flat_shoe","keywords":["れでぃーすふらっとしゅーず","ふく","ばれえふらっと","すりっぽん","すりっぱ","レディースフラットシューズ","服","バレエフラット","スリッポン","スリッパ","ballet","slip-on","slipper"]},{"category":"people","char":"👠","name":"high_heel","keywords":["はいひーる","ふく","ひーる","くつ","じょせい","おんな","ハイヒール","服","ヒール","靴","女性","おんな","fashion","shoes","female","pumps","stiletto"]},{"category":"people","char":"👡","name":"sandal","keywords":["れでぃーすさんだる","ふく","さんだる","くつ","じょせい","おんな","レディースサンダル","服","サンダル","靴","女性","おんな","shoes","fashion","flip flops"]},{"category":"people","char":"👢","name":"boot","keywords":["れでぃーすぶーつ","ぶーつ","ふく","くつ","じょせい","おんな","レディースブーツ","ブーツ","服","靴","女性","おんな","shoes","fashion"]},{"category":"people","char":"👞","name":"mans_shoe","keywords":["めんずしゅーず","ふく","だんせい","おとこ","くつ","メンズシューズ","服","男性","おとこ","靴","fashion","male"]},{"category":"people","char":"👟","name":"athletic_shoe","keywords":["うんどうくつ","うんどう","ふく","しゅーず","すにーかー","運動靴","運動","服","シューズ","スニーカー","shoes","sports","sneakers"]},{"category":"people","char":"🩴","name":"thong_sandal","keywords":["ごむせいさんだる","びーち","さんだる","ぞうり","ゴム製サンダル","ビーチ","サンダル","草履"]},{"category":"people","char":"🩰","name":"ballet_shoes","keywords":["ばれえしゅーず","ふく","しゅーず","ばれえ","だんす","バレエシューズ","服","シューズ","バレエ","ダンス","shoes","sports"]},{"category":"people","char":"🧦","name":"socks","keywords":["くつした","ふく","そっくす","いちくみ","靴下","服","ソックス","一組","stockings","clothes"]},{"category":"people","char":"🧤","name":"gloves","keywords":["てぶくろ","ふく","て","手袋","服","手","hands","winter","clothes"]},{"category":"people","char":"🧣","name":"scarf","keywords":["すかーふ","ふく","くび","スカーフ","服","首","neck","winter","clothes"]},{"category":"people","char":"👒","name":"womans_hat","keywords":["れでぃーすはっと","ふく","ぼうし","じょせい","おんな","レディースハット","服","帽子","女性","おんな","fashion","accessories","female","lady","spring"]},{"category":"people","char":"🎩","name":"tophat","keywords":["しるくはっと","あくてぃびてぃ","ふく","えんたーていんめんと","ごらく","ぼうし","とっぷす","シルクハット","アクティビティ","服","エンターテインメント","娯楽","帽子","トップス","magic","gentleman","classy","circus"]},{"category":"people","char":"🧢","name":"billed_hat","keywords":["きゃっぷ","ふく","やきゅう","はっと","ぼうし","キャップ","服","野球","ハット","帽子","cap","baseball"]},{"category":"people","char":"⛑","name":"rescue_worker_helmet","keywords":["しろじゅうじのへるめっと","きゅうじょ","じゅうじ","かお","ぼうし","へるめっと","白十字のヘルメット","救助","十字","顔","帽子","ヘルメット","construction","build"]},{"category":"people","char":"🪖","name":"military_helmet","keywords":["ぐんたいのへるめっと","ぐん","へるめっと","ぐんたい","ぐんじん","へいし","軍隊のヘルメット","軍","ヘルメット","軍隊","軍人","兵士"]},{"category":"people","char":"🎓","name":"mortar_board","keywords":["そつぎょうしきのかくぼう","あくてぃびてぃ","ぼうし","おいわい","ふく","そつぎょう","はっと","卒業式の角帽","アクティビティ","帽子","お祝い","服","卒業","ハット","school","college","degree","university","graduation","cap","hat","legal","learn","education"]},{"category":"people","char":"👑","name":"crown","keywords":["かんむり","ふく","おうかん","おう","じょおう","冠","服","王冠","王","女王","king","kod","leader","royalty","lord"]},{"category":"people","char":"🎒","name":"school_satchel","keywords":["らんどせる","あくてぃびてぃ","かばん","ばっぐ","がくせいかばん","がっこう","ランドセル","アクティビティ","鞄","バッグ","学生鞄","学校","student","education","bag","backpack"]},{"category":"people","char":"🧳","name":"luggage","keywords":["てにもつ","ぱっきんぐ","りょこう","すーつけーす","手荷物","パッキング","旅行","スーツケース","packing","travel"]},{"category":"people","char":"👝","name":"pouch","keywords":["ぽーち","かばん","ばっぐ","ふく","ポーチ","鞄","バッグ","服","bag","accessories","shopping"]},{"category":"people","char":"👛","name":"purse","keywords":["さいふ","ふく","こいん","財布","服","コイン","fashion","accessories","money","sales","shopping"]},{"category":"people","char":"👜","name":"handbag","keywords":["はんどばっぐ","かばん","ばっぐ","ふく","ハンドバッグ","鞄","バッグ","服","fashion","accessory","accessories","shopping"]},{"category":"people","char":"💼","name":"briefcase","keywords":["ぶりーふけーす","ブリーフケース","business","documents","work","law","legal","job","career"]},{"category":"people","char":"👓","name":"eyeglasses","keywords":["めがね","ふく","め","あいうぇあ","眼鏡","服","目","メガネ","アイウェア","fashion","accessories","eyesight","nerdy","dork","geek"]},{"category":"people","char":"🕶","name":"dark_sunglasses","keywords":["さんぐらす","くらい","め","めがね","サングラス","暗い","目","眼鏡","メガネ","face","cool","accessories"]},{"category":"people","char":"🥽","name":"goggles","keywords":["ごーぐる","ふく","めのほご","すいえい","ようせつ","ゴーグル","服","目の保護","水泳","溶接","eyes","protection","safety"]},{"category":"people","char":"💍","name":"ring","keywords":["ゆびわ","だいやもんど","れんあい","ろまんす","指輪","ダイヤモンド","恋愛","ロマンス","wedding","propose","marriage","valentines","diamond","fashion","jewelry","gem","engagement"]},{"category":"people","char":"🌂","name":"closed_umbrella","keywords":["とじたかさ","ふく","あめ","かさ","てんき","閉じた傘","服","雨","傘","天気","weather","rain","drizzle"]},{"category":"animals_and_nature","char":"🐶","name":"dog","keywords":["いぬのかお","けん","いぬ","かお","ぺっと","イヌの顔","犬","イヌ","顔","ペット","animal","friend","nature","woof","puppy","pet","faithful"]},{"category":"animals_and_nature","char":"🐱","name":"cat","keywords":["ねこのかお","ねこ","かお","ぺっと","ネコの顔","猫","ネコ","顔","ペット","animal","meow","nature","pet","kitten"]},{"category":"animals_and_nature","char":"🐈‍⬛","name":"black_cat","keywords":["くろねこ","くろ","ねこ","ぺっと","はろうぃーん","黒猫","黒","猫","ペット","ハロウィーン","animal","meow","nature","pet","kitten"]},{"category":"animals_and_nature","char":"🐭","name":"mouse","keywords":["ねずみのかお","かお","ねずみ","ネズミの顔","顔","ネズミ","animal","nature","cheese_wedge","rodent"]},{"category":"animals_and_nature","char":"🐹","name":"hamster","keywords":["はむすたーのかお","かお","はむすたー","ぺっと","ハムスターの顔","顔","ハムスター","ペット","animal","nature"]},{"category":"animals_and_nature","char":"🐰","name":"rabbit","keywords":["うさぎのかお","ばにー","かお","ぺっと","うさぎ","ウサギの顔","バニー","顔","ペット","ウサギ","animal","nature","pet","spring","magic","bunny"]},{"category":"animals_and_nature","char":"🦊","name":"fox_face","keywords":["きつねのかお","かお","きつね","キツネの顔","顔","キツネ","animal","nature","face"]},{"category":"animals_and_nature","char":"🐻","name":"bear","keywords":["くまのかお","くま","かお","クマの顔","熊","クマ","顔","animal","nature","wild"]},{"category":"animals_and_nature","char":"🐼","name":"panda_face","keywords":["ぱんだのかお","かお","ぱんだ","くま","パンダの顔","顔","パンダ","熊","animal","nature","panda"]},{"category":"animals_and_nature","char":"🐨","name":"koala","keywords":["こあら","くま","ゆうぶくろるい","おーすとらりあ","コアラ","熊","有袋類","オーストラリア","animal","nature"]},{"category":"animals_and_nature","char":"🐯","name":"tiger","keywords":["とらのかお","かお","とら","トラの顔","顔","虎","トラ","animal","cat","danger","wild","nature","roar"]},{"category":"animals_and_nature","char":"🦁","name":"lion","keywords":["らいおんのかお","かお","ししざ","らいおん","せいざ","ライオンの顔","顔","しし座","ライオン","星座","animal","nature"]},{"category":"animals_and_nature","char":"🐮","name":"cow","keywords":["うしのかお","うし","かお","ウシの顔","牛","ウシ","顔","beef","ox","animal","nature","moo","milk"]},{"category":"animals_and_nature","char":"🐷","name":"pig","keywords":["ぶたのかお","かお","ぶた","ブタの顔","顔","豚","ブタ","animal","oink","nature"]},{"category":"animals_and_nature","char":"🐽","name":"pig_nose","keywords":["ぶたのはな","かお","はな","ぶた","ブタの鼻","顔","鼻","豚","ブタ","animal","oink"]},{"category":"animals_and_nature","char":"🐸","name":"frog","keywords":["かえるのかお","かお","かえる","カエルの顔","顔","蛙","カエル","animal","nature","croak","toad"]},{"category":"animals_and_nature","char":"🦑","name":"squid","keywords":["いか","なんたいどうぶつ","イカ","軟体動物","烏賊","animal","nature","ocean","sea"]},{"category":"animals_and_nature","char":"🐙","name":"octopus","keywords":["たこ","タコ","蛸","animal","creature","ocean","sea","nature","beach"]},{"category":"animals_and_nature","char":"🪼","name":"jellyfish","keywords":["くらげ","くすり","むせきついどうぶつ","ぜりー","うみ","いたい","しもう","クラゲ","焼く","無脊椎動物","ゼリー","海","痛い","刺毛","animal","creature","ocean","sea","nature","beach"]},{"category":"animals_and_nature","char":"🦐","name":"shrimp","keywords":["えび","かい","ちいさい","エビ","貝","小さい","animal","ocean","nature","seafood"]},{"category":"animals_and_nature","char":"🐵","name":"monkey_face","keywords":["さるのかお","かお","さる","サルの顔","顔","猿","サル","animal","nature","circus"]},{"category":"animals_and_nature","char":"🦍","name":"gorilla","keywords":["ごりら","ゴリラ","animal","nature","circus"]},{"category":"animals_and_nature","char":"🙈","name":"see_no_evil","keywords":["みざる","わるい","かお","きんじる","じぇすちゃー","さる","だめ","きんし","みる","見ざる","悪い","顔","禁じる","ジェスチャー","猿","サル","だめ","ダメ","禁止","見る","monkey","animal","nature","haha"]},{"category":"animals_and_nature","char":"🙉","name":"hear_no_evil","keywords":["きかざる","わるい","かお","きんじる","じぇすちゃー","きく","さる","ない","なし","きんし","聞かざる","悪い","顔","禁じる","ジェスチャー","聞く","サル","ない","なし","禁止","animal","monkey","nature"]},{"category":"animals_and_nature","char":"🙊","name":"speak_no_evil","keywords":["いわざる","わるい","かお","きんじる","じぇすちゃー","さる","ない","なし","きんし","はなす","言わざる","悪い","顔","禁じる","ジェスチャー","猿","サル","ない","なし","禁止","話す","monkey","animal","nature","omg"]},{"category":"animals_and_nature","char":"🐒","name":"monkey","keywords":["さる","サル","猿","animal","nature","banana","circus"]},{"category":"animals_and_nature","char":"🐔","name":"chicken","keywords":["にわとり","ニワトリ","animal","cluck","nature","bird"]},{"category":"animals_and_nature","char":"🐧","name":"penguin","keywords":["ぺんぎん","ペンギン","animal","nature"]},{"category":"animals_and_nature","char":"🐦","name":"bird","keywords":["とり","鳥","animal","nature","fly","tweet","spring"]},{"category":"animals_and_nature","char":"🐤","name":"baby_chick","keywords":["ひよこ","あかちゃん","ヒヨコ","赤ちゃん","ひよこ","animal","chicken","bird"]},{"category":"animals_and_nature","char":"🐣","name":"hatching_chick","keywords":["ひよこ","あかちゃん","ふか","ひよこ","赤ちゃん","孵化","animal","chicken","egg","born","baby","bird"]},{"category":"animals_and_nature","char":"🐥","name":"hatched_chick","keywords":["しょうめんをむいたひよこ","あかちゃん","ひよこ","正面を向いたヒヨコ","赤ちゃん","ひよこ","animal","chicken","baby","bird"]},{"category":"animals_and_nature","char":"🪿","name":"goose","keywords":["がちょう","とり","かきん","けいてきのおと","ガチョウ","鳥","家禽","警笛の音","animal","nature","bird","fowl","goose","honk","silly"]},{"category":"animals_and_nature","char":"🦆","name":"duck","keywords":["あひる","とり","アヒル","鳥","animal","nature","bird","mallard"]},{"category":"animals_and_nature","char":"🐦‍⬛","name":"black_bird","keywords":["くろいとり","とり","くろ","からす","わたりがらす","みやまがらす","黒い鳥","鳥","黒","カラス","ワタリガラス","ミヤマガラス","animal","nature","bird","black","crow","raven","rook"]},{"category":"animals_and_nature","char":"🦅","name":"eagle","keywords":["わし","とり","ワシ","鳥","animal","nature","bird"]},{"category":"animals_and_nature","char":"🦉","name":"owl","keywords":["ふくろう","とり","かしこい","フクロウ","鳥","賢い","animal","nature","bird","hoot"]},{"category":"animals_and_nature","char":"🦇","name":"bat","keywords":["こうもり","きゅうけつき","コウモリ","吸血鬼","animal","nature","blind","vampire"]},{"category":"animals_and_nature","char":"🐺","name":"wolf","keywords":["おおかみのかお","かお","おおかみ","オオカミの顔","顔","オオカミ","animal","nature","wild"]},{"category":"animals_and_nature","char":"🐗","name":"boar","keywords":["いのしし","ぶた","イノシシ","豚","animal","nature"]},{"category":"animals_and_nature","char":"🐴","name":"horse","keywords":["うまのかお","かお","うま","ウマの顔","顔","馬","animal","brown","nature"]},{"category":"animals_and_nature","char":"🦄","name":"unicorn","keywords":["ゆにこーんのかお","かお","ゆにこーん","ユニコーンの顔","顔","ユニコーン","animal","nature","mystical"]},{"category":"animals_and_nature","char":"🫎","name":"moose","keywords":["へらじか","どうぶつ","えだつの","えるく","ほにゅうるい","ヘラジカ","動物","枝角","エルク","哺乳類","animal","nature","antlers","elk","mammal"]},{"category":"animals_and_nature","char":"🐝","name":"honeybee","keywords":["みつばち","はち","こんちゅう","ミツバチ","ハチ","昆虫","animal","insect","nature","bug","spring","honey"]},{"category":"animals_and_nature","char":"🐛","name":"bug","keywords":["むし","こんちゅう","虫","昆虫","animal","insect","nature","worm"]},{"category":"animals_and_nature","char":"🦋","name":"butterfly","keywords":["ちょう","こんちゅう","うつくしい","チョウ","蝶","昆虫","美しい","animal","insect","nature","caterpillar"]},{"category":"animals_and_nature","char":"🐌","name":"snail","keywords":["かたつむり","カタツムリ","slow","animal","shell"]},{"category":"animals_and_nature","char":"🐞","name":"lady_beetle","keywords":["てんとうむし","かぶとむし","こんちゅう","てんとうちゅう","テントウムシ","カブトムシ","昆虫","てんとう虫","animal","insect","nature","ladybug"]},{"category":"animals_and_nature","char":"🐜","name":"ant","keywords":["あり","こんちゅう","アリ","蟻","昆虫","animal","insect","nature","bug"]},{"category":"animals_and_nature","char":"🦗","name":"grasshopper","keywords":["くりけっと","こおろぎ","ばっため","こんちゅう","クリケット","コオロギ","バッタ目","昆虫","animal","cricket","chirp"]},{"category":"animals_and_nature","char":"🕷","name":"spider","keywords":["くも","こんちゅう","クモ","昆虫","蜘蛛","animal","arachnid"]},{"category":"animals_and_nature","char":"🪲","name":"beetle","keywords":["かぶとむし","むし","こんちゅう","甲虫","虫","昆虫","animal"]},{"category":"animals_and_nature","char":"🪳","name":"cockroach","keywords":["ごきぶり","こんちゅう","がいちゅう","ゴキブリ","昆虫","害虫","animal"]},{"category":"animals_and_nature","char":"🪰","name":"fly","keywords":["はえ","がいちゅう","こんちゅう","うじむし","ハエ","害虫","昆虫","蛆虫","animal"]},{"category":"animals_and_nature","char":"🪱","name":"worm","keywords":["ぜんちゅう","たまきがたどうぶつ","みみず","きせいちゅう","蠕虫","環形動物","ミミズ","寄生虫","animal"]},{"category":"animals_and_nature","char":"🦂","name":"scorpion","keywords":["さそり","さそりざ","せいざ","サソリ","さそり座","さそり","星座","animal","arachnid"]},{"category":"animals_and_nature","char":"🦀","name":"crab","keywords":["かに","かにざ","せいざ","カニ","かに座","蟹","星座","animal","crustacean"]},{"category":"animals_and_nature","char":"🐍","name":"snake","keywords":["へび","うんぱんにん","へびつかいざ","せいざ","ヘビ","運搬人","へびつかい座","蛇","星座","animal","evil","nature","hiss","python"]},{"category":"animals_and_nature","char":"🦎","name":"lizard","keywords":["とかげ","はちゅうるい","トカゲ","爬虫類","animal","nature","reptile"]},{"category":"animals_and_nature","char":"🦖","name":"t-rex","keywords":["てぃらのさうるす","Tれっくす","きょうりゅう","ティラノサウルス","Tレックス","恐竜","animal","nature","dinosaur","tyrannosaurus","extinct"]},{"category":"animals_and_nature","char":"🦕","name":"sauropod","keywords":["りゅうあしるい","ぶらきおさうるす","ぶろんとさうるす","でぃぷろどくす","きょうりゅう","竜脚類","ブラキオサウルス","ブロントサウルス","ディプロドクス","恐竜","animal","nature","dinosaur","brachiosaurus","brontosaurus","diplodocus","extinct"]},{"category":"animals_and_nature","char":"🐢","name":"turtle","keywords":["かめ","カメ","animal","slow","nature","tortoise"]},{"category":"animals_and_nature","char":"🐠","name":"tropical_fish","keywords":["ねったいぎょ","さかな","ねったい","熱帯魚","魚","熱帯","animal","swim","ocean","beach","nemo"]},{"category":"animals_and_nature","char":"🐟","name":"fish","keywords":["さかな","うおざ","せいざ","魚","うお座","星座","animal","food","nature"]},{"category":"animals_and_nature","char":"🐡","name":"blowfish","keywords":["ふぐ","さかな","フグ","魚","animal","nature","food","sea","ocean"]},{"category":"animals_and_nature","char":"🐬","name":"dolphin","keywords":["いるか","ひれ","イルカ","ひれ","animal","nature","fish","sea","ocean","flipper","fins","beach"]},{"category":"animals_and_nature","char":"🦈","name":"shark","keywords":["さめ","さかな","サメ","魚","animal","nature","fish","sea","ocean","jaws","fins","beach"]},{"category":"animals_and_nature","char":"🐳","name":"whale","keywords":["しおふきくじら","かお","しおふき","くじら","潮吹きクジラ","顔","潮吹き","クジラ","animal","nature","sea","ocean"]},{"category":"animals_and_nature","char":"🐋","name":"whale2","keywords":["くじら","クジラ","animal","nature","sea","ocean"]},{"category":"animals_and_nature","char":"🐊","name":"crocodile","keywords":["わに","ワニ","animal","nature","reptile","lizard","alligator"]},{"category":"animals_and_nature","char":"🐆","name":"leopard","keywords":["ひょう","ヒョウ","animal","nature"]},{"category":"animals_and_nature","char":"🦓","name":"zebra","keywords":["しまうま","かお","シマウマ","顔","animal","nature","stripes","safari"]},{"category":"animals_and_nature","char":"🐅","name":"tiger2","keywords":["とら","トラ","虎","animal","nature","roar"]},{"category":"animals_and_nature","char":"🐃","name":"water_buffalo","keywords":["すいぎゅう","みず","スイギュウ","水牛","水","animal","nature","ox","cow"]},{"category":"animals_and_nature","char":"🐂","name":"ox","keywords":["ゆううし","おすうし","おうしざ","せいざ","雄牛","牡牛","おうし座","星座","animal","cow","beef"]},{"category":"animals_and_nature","char":"🐄","name":"cow2","keywords":["うし","ウシ","牛","beef","ox","animal","nature","moo","milk"]},{"category":"animals_and_nature","char":"🦌","name":"deer","keywords":["しか","シカ","animal","nature","horns","venison"]},{"category":"animals_and_nature","char":"🐪","name":"dromedary_camel","keywords":["ひとこぶらくだ","らくだ","こぶ","ヒトコブラクダ","ラクダ","こぶ","animal","hot","desert","hump"]},{"category":"animals_and_nature","char":"🐫","name":"camel","keywords":["ふたこぶらくだ","ふたこぶ","らくだ","こぶ","フタコブラクダ","フタコブ","ラクダ","こぶ","animal","nature","hot","desert","hump"]},{"category":"animals_and_nature","char":"🦒","name":"giraffe","keywords":["きりん","かお","キリン","顔","animal","nature","spots","safari"]},{"category":"animals_and_nature","char":"🐘","name":"elephant","keywords":["ぞう","ゾウ","象","animal","nature","nose","th","circus"]},{"category":"animals_and_nature","char":"🦏","name":"rhinoceros","keywords":["さい","サイ","animal","nature","horn"]},{"category":"animals_and_nature","char":"🐐","name":"goat","keywords":["やぎ","やぎざ","せいざ","ヤギ","やぎ座","星座","animal","nature"]},{"category":"animals_and_nature","char":"🐏","name":"ram","keywords":["こひつじ","おひつじざ","ひつじ","せいざ","仔羊","おひつじ座","ヒツジ","星座","animal","sheep","nature"]},{"category":"animals_and_nature","char":"🐑","name":"sheep","keywords":["ひつじ","めすひつじ","ヒツジ","雌羊","animal","nature","wool","shipit"]},{"category":"animals_and_nature","char":"🫏","name":"donkey","keywords":["ろば","どうぶつ","ぶーろ","ほにゅうるい","らば","ロバ","動物","ブーロ","哺乳類","ラバ","animal","ass","burro","mammal","mule","stubborn"]},{"category":"animals_and_nature","char":"🐎","name":"racehorse","keywords":["うま","けいば","れーす","馬","競馬","レース","animal","gamble","luck"]},{"category":"animals_and_nature","char":"🐖","name":"pig2","keywords":["ぶた","めすぶた","ブタ","雌豚","animal","nature"]},{"category":"animals_and_nature","char":"🐀","name":"rat","keywords":["ねずみ","ネズミ","animal","mouse","rodent"]},{"category":"animals_and_nature","char":"🐁","name":"mouse2","keywords":["ねずみ","ネズミ","animal","nature","rodent"]},{"category":"animals_and_nature","char":"🐓","name":"rooster","keywords":["おんどり","おんどり","animal","nature","chicken"]},{"category":"animals_and_nature","char":"🦃","name":"turkey","keywords":["しちめんちょう(とり)","しちめんちょう","とり","七面鳥(鳥)","七面鳥","鳥","animal","bird"]},{"category":"animals_and_nature","char":"🕊","name":"dove","keywords":["へいわのはと","とり","はと","ひこう","へいわ","平和の鳩","鳥","鳩","飛行","平和","animal","bird"]},{"category":"animals_and_nature","char":"🐕","name":"dog2","keywords":["いぬ","けん","ぺっと","イヌ","犬","ペット","animal","nature","friend","doge","pet","faithful"]},{"category":"animals_and_nature","char":"🐩","name":"poodle","keywords":["ぷーどる","いぬ","けん","プードル","イヌ","犬","dog","animal","101","nature","pet"]},{"category":"animals_and_nature","char":"🐈","name":"cat2","keywords":["ねこ","ぺっと","ネコ","猫","ペット","animal","meow","pet","cats"]},{"category":"animals_and_nature","char":"🐇","name":"rabbit2","keywords":["うさぎ","ばにー","ぺっと","ウサギ","バニー","ペット","animal","nature","pet","magic","spring"]},{"category":"animals_and_nature","char":"🐿","name":"chipmunk","keywords":["しまりす","シマリス","animal","nature","rodent","squirrel"]},{"category":"animals_and_nature","char":"🦔","name":"hedgehog","keywords":["はりねずみ","かお","ハリネズミ","顔","animal","nature","spiny"]},{"category":"animals_and_nature","char":"🦝","name":"raccoon","keywords":["あらいぐま","かお","こうきしんがつよい","ずるかしこい","アライグマ","顔","好奇心が強い","ずる賢い","animal","nature"]},{"category":"animals_and_nature","char":"🦙","name":"llama","keywords":["らま","あるぱか","ぐあなこ","びくーにゃ","うーる","ラマ","アルパカ","グアナコ","ビクーニャ","ウール","animal","nature","alpaca"]},{"category":"animals_and_nature","char":"🦛","name":"hippopotamus","keywords":["かば","カバ","animal","nature"]},{"category":"animals_and_nature","char":"🦘","name":"kangaroo","keywords":["かんがるー","おーすとらりあ","じゃんぷ","ゆうぶくろるい","カンガルー","オーストラリア","ジャンプ","有袋類","animal","nature","australia","joey","hop","marsupial"]},{"category":"animals_and_nature","char":"🦡","name":"badger","keywords":["あなぐま","らーてる","ねだる","アナグマ","ラーテル","ねだる","animal","nature","honey"]},{"category":"animals_and_nature","char":"🦢","name":"swan","keywords":["はくちょう","とり","はくちょうのお","みにくいあひるのこ","白鳥","鳥","白鳥の雄","醜いアヒルの子","animal","nature","bird"]},{"category":"animals_and_nature","char":"🦚","name":"peacock","keywords":["おすのくじゃく","とり","めすのくじゃく","オスのクジャク","鳥","メスのクジャク","animal","nature","peahen","bird"]},{"category":"animals_and_nature","char":"🦜","name":"parrot","keywords":["おうむ","とり","かいぞく","オウム","鳥","海賊","animal","nature","bird","pirate","talk"]},{"category":"animals_and_nature","char":"🦞","name":"lobster","keywords":["ろぶすたー","びすく","つめ","しーふーど","ロブスター","ビスク","爪","シーフード","animal","nature","bisque","claws","seafood"]},{"category":"animals_and_nature","char":"🦠","name":"microbe","keywords":["びせいぶつ","あめーば","ばくてりあ","ういるす","微生物","アメーバ","バクテリア","ウイルス","amoeba","bacteria","germs"]},{"category":"animals_and_nature","char":"🦟","name":"mosquito","keywords":["か","びょうき","ねつ","こんちゅう","まらりあ","ういるす","蚊","病気","熱","昆虫","マラリア","ウイルス","animal","nature","insect","malaria"]},{"category":"animals_and_nature","char":"🦬","name":"bison","keywords":["ばいそん","ばっふぁろー","むれ","ヴぃせんと","バイソン","バッファロー","群れ","ヴィセント","animal","nature"]},{"category":"animals_and_nature","char":"🦣","name":"mammoth","keywords":["まんもす","ぜつめつ","おおがた","きば","けにおおわれた","マンモス","絶滅","大型","牙","毛に覆われた","animal","nature"]},{"category":"animals_and_nature","char":"🦫","name":"beaver","keywords":["びーばー","だむ","ビーバー","ダム","animal","nature"]},{"category":"animals_and_nature","char":"🐻‍❄️","name":"polar_bear","keywords":["しろくま","かお","ほっきょく","くま","しろ","シロクマ","顔","北極","熊","白","animal","nature"]},{"category":"animals_and_nature","char":"🦤","name":"dodo","keywords":["どーどー","とり","ぜつめつ","ドードー","鳥","絶滅","animal","nature"]},{"category":"animals_and_nature","char":"🪶","name":"feather","keywords":["うもう","とり","かるい","はね","羽毛","鳥","軽い","羽","animal","nature"]},{"category":"animals_and_nature","char":"🪽","name":"wing","keywords":["はね","てんし","こうくう","とり","ひこう","しんわ","羽","天使","航空","鳥","飛行","神話","angelic","aviation","bird","flying","mythology"]},{"category":"animals_and_nature","char":"🦭","name":"seal","keywords":["あざらし","あしか","アザラシ","アシカ","animal","nature"]},{"category":"animals_and_nature","char":"🐾","name":"paw_prints","keywords":["どうぶつのあしあと","あし","あと","動物の足あと","足","跡","animal","tracking","footprints","dog","cat","pet","feet"]},{"category":"animals_and_nature","char":"🐉","name":"dragon","keywords":["どらごん","おとぎばなし","ドラゴン","おとぎ話","animal","myth","nature","chinese","green"]},{"category":"animals_and_nature","char":"🐲","name":"dragon_face","keywords":["どらごんのかお","どらごん","かお","おとぎばなし","ドラゴンの顔","ドラゴン","顔","おとぎ話","animal","myth","nature","chinese","green"]},{"category":"animals_and_nature","char":"🦧","name":"orangutan","keywords":["おらんうーたん","るいじんえん","オランウータン","類人猿","animal","nature"]},{"category":"animals_and_nature","char":"🦮","name":"guide_dog","keywords":["もうどうけん","あくせしびりてぃ","めがふじゆう","けん","がいど","盲導犬","アクセシビリティ","目が不自由","犬","ガイド","animal","nature"]},{"category":"animals_and_nature","char":"🐕‍🦺","name":"service_dog","keywords":["かいじょいぬ","あくせしびりてぃ","しえん","けん","さーびす","介助犬","アクセシビリティ","支援","犬","サービス","animal","nature"]},{"category":"animals_and_nature","char":"🦥","name":"sloth","keywords":["たいだ","なまける","おそい","怠惰","なまける","遅い","animal","nature"]},{"category":"animals_and_nature","char":"🦦","name":"otter","keywords":["かわうそ","づり","ふざける","カワウソ","釣り","ふざける","animal","nature"]},{"category":"animals_and_nature","char":"🦨","name":"skunk","keywords":["すかんく","あくしゅう","におう","スカンク","悪臭","臭う","animal","nature"]},{"category":"animals_and_nature","char":"🦩","name":"flamingo","keywords":["ふらみんご","ねったい","あざやか","フラミンゴ","熱帯","鮮やか","animal","nature"]},{"category":"animals_and_nature","char":"🌵","name":"cactus","keywords":["さぼてん","しょくぶつ","サボテン","植物","vegetable","plant","nature"]},{"category":"animals_and_nature","char":"🎄","name":"christmas_tree","keywords":["くりすますつりー","あくてぃびてぃ","おいわい","くりすます","えんたーていめんと","つりー","クリスマスツリー","アクティビティ","お祝い","クリスマス","エンターテイメント","ツリー","festival","vacation","december","xmas","celebration"]},{"category":"animals_and_nature","char":"🌲","name":"evergreen_tree","keywords":["じょうりょくじゅ","じょうりょく","しょくぶつ","はた","常緑樹","常緑","植物","木","plant","nature"]},{"category":"animals_and_nature","char":"🌳","name":"deciduous_tree","keywords":["らくようじゅ","らくようせい","しょくぶつ","らくよう","はた","落葉樹","落葉性","植物","落葉","木","plant","nature"]},{"category":"animals_and_nature","char":"🌴","name":"palm_tree","keywords":["やしのき","やし","しょくぶつ","はた","ヤシの木","ヤシ","植物","木","plant","vegetable","nature","summer","beach","mojito","tropical"]},{"category":"animals_and_nature","char":"🌱","name":"seedling","keywords":["なえぎ","しょくぶつ","わかい","苗木","植物","若い","plant","nature","grass","lawn","spring"]},{"category":"animals_and_nature","char":"🌿","name":"herb","keywords":["はーぶ","は","しょくぶつ","ハーブ","葉","植物","vegetable","plant","medicine","weed","grass","lawn"]},{"category":"animals_and_nature","char":"☘","name":"shamrock","keywords":["くろーばー","しょくぶつ","クローバー","植物","vegetable","plant","nature","irish","clover"]},{"category":"animals_and_nature","char":"🍀","name":"four_leaf_clover","keywords":["よっつはのくろーばー","4","くろーばー","よん","は","しょくぶつ","四つ葉のクローバー","4","クローバー","四","葉","植物","vegetable","plant","nature","lucky","irish"]},{"category":"animals_and_nature","char":"🎍","name":"bamboo","keywords":["かどまつ","あくてぃびてぃ","たけ","おいわい","にっぽん","まつ","しょくぶつ","門松","アクティビティ","竹","お祝い","日本","松","植物","plant","nature","vegetable","panda","pine_decoration"]},{"category":"animals_and_nature","char":"🎋","name":"tanabata_tree","keywords":["ななゆう","あくてぃびてぃ","はた","おいわい","えんたーていめんと","にっぽん","七夕","アクティビティ","旗","お祝い","エンターテイメント","日本","木","plant","nature","branch","summer"]},{"category":"animals_and_nature","char":"🍃","name":"leaves","keywords":["かぜになびくは","ふく","はためく","は","しょくぶつ","ふう","風になびく葉","吹く","はためく","葉","植物","風","nature","plant","tree","vegetable","grass","lawn","spring"]},{"category":"animals_and_nature","char":"🍂","name":"fallen_leaf","keywords":["おちば","らっか","は","しょくぶつ","落ち葉","落下","葉","植物","nature","plant","vegetable","leaves"]},{"category":"animals_and_nature","char":"🍁","name":"maple_leaf","keywords":["かえでのは","らっか","は","かえで","しょくぶつ","カエデの葉","落下","葉","カエデ","植物","nature","plant","vegetable","ca","fall"]},{"category":"animals_and_nature","char":"🌾","name":"ear_of_rice","keywords":["いなほ","いねたば","ほ","しょくぶつ","こめ","稲穂","稲束","穂","植物","米","nature","plant"]},{"category":"animals_and_nature","char":"🌺","name":"hibiscus","keywords":["はいびすかす","はな","しょくぶつ","ハイビスカス","花","植物","plant","vegetable","flowers","beach"]},{"category":"animals_and_nature","char":"🌻","name":"sunflower","keywords":["ひまわり","はな","しょくぶつ","たいよう","ヒマワリ","花","植物","太陽","ひまわり","nature","plant","fall"]},{"category":"animals_and_nature","char":"🌹","name":"rose","keywords":["ばら","はな","しょくぶつ","バラ","花","植物","flowers","valentines","love","spring"]},{"category":"animals_and_nature","char":"🥀","name":"wilted_flower","keywords":["しおれたはな","はな","しおれた","しおれた花","花","しおれた","plant","nature","flower"]},{"category":"animals_and_nature","char":"🪻","name":"hyacinth","keywords":["ひあしんす","ぶるーぼんねっと","はな","らべんだー","るぴなす","のうるーず","むらさき","きんぎょそう","ヒアシンス","ブルーボンネット","花","ラベンダー","ルピナス","ノウルーズ","紫","キンギョソウ","plant","nature","flower","bluebonnet","lavender","lupine","snapdragon"]},{"category":"animals_and_nature","char":"🌷","name":"tulip","keywords":["ちゅーりっぷ","はな","しょくぶつ","チューリップ","花","植物","flowers","plant","nature","summer","spring"]},{"category":"animals_and_nature","char":"🌼","name":"blossom","keywords":["はな","しょくぶつ","花","植物","nature","flowers","yellow"]},{"category":"animals_and_nature","char":"🌸","name":"cherry_blossom","keywords":["さくら","はな","しょくぶつ","桜","花","植物","nature","plant","spring","flower"]},{"category":"animals_and_nature","char":"💐","name":"bouquet","keywords":["はなたば","はな","しょくぶつ","ろまんす","花束","花","植物","ロマンス","flowers","nature","spring"]},{"category":"animals_and_nature","char":"🍄","name":"mushroom","keywords":["きのこ","しょくぶつ","キノコ","植物","plant","vegetable"]},{"category":"animals_and_nature","char":"🪴","name":"potted_plant","keywords":["はちうえ","しょくぶつ","かんようしょくぶつ","鉢植え","植物","観葉植物","plant"]},{"category":"animals_and_nature","char":"🌰","name":"chestnut","keywords":["くり","しょくぶつ","栗","植物","food","squirrel"]},{"category":"animals_and_nature","char":"🎃","name":"jack_o_lantern","keywords":["じゃっく・お・らんたん","いべんと","おいわい","えんため","はろうぃん","じゃっくおらんたん","らんたん","かぼちゃ","ジャック・オ・ランタン","イベント","お祝い","エンタメ","ハロウィン","ジャックオランタン","ランタン","かぼちゃ","halloween","light","pumpkin","creepy","fall"]},{"category":"animals_and_nature","char":"🐚","name":"shell","keywords":["まきがい","かい","巻き貝","貝","nature","sea","beach"]},{"category":"animals_and_nature","char":"🕸","name":"spider_web","keywords":["くものす","くも","す","クモの巣","クモ","巣","animal","insect","arachnid","silk"]},{"category":"animals_and_nature","char":"🌎","name":"earth_americas","keywords":["あめりかたいりく","あめりか","ちきゅう","せかい","アメリカ大陸","アメリカ","地球","世界","globe","world","USA","international"]},{"category":"animals_and_nature","char":"🌍","name":"earth_africa","keywords":["よーろっぱとあふりかちいき","あふりか","ちきゅう","よーろっぱ","せかい","ヨーロッパとアフリカ地域","アフリカ","地球","ヨーロッパ","世界","globe","world","international"]},{"category":"animals_and_nature","char":"🌏","name":"earth_asia","keywords":["あじあとおーすとらりあ","あじあ","おーすとらりあ","ちきゅう","せかい","アジアとオーストラリア","アジア","オーストラリア","地球","世界","globe","world","east","international"]},{"category":"animals_and_nature","char":"🪐","name":"ringed_planet","keywords":["たまきのあるわくせい","うちゅう","わくせい","どせい","環のある惑星","宇宙","惑星","土星","saturn"]},{"category":"animals_and_nature","char":"🌕","name":"full_moon","keywords":["まんげつ","つき","うちゅう","てんき","満月","月","宇宙","天気","nature","yellow","twilight","planet","space","night","evening","sleep"]},{"category":"animals_and_nature","char":"🌖","name":"waning_gibbous_moon","keywords":["ねまちのつき","じゅうさんや","つき","うちゅう","かけ","てんき","寝待月","十三夜","月","宇宙","欠け","天気","nature","twilight","planet","space","night","evening","sleep","waxing_gibbous_moon"]},{"category":"animals_and_nature","char":"🌗","name":"last_quarter_moon","keywords":["かげんのつき","つき","げん","うちゅう","てんき","下弦の月","月","弦","宇宙","天気","nature","twilight","planet","space","night","evening","sleep"]},{"category":"animals_and_nature","char":"🌘","name":"waning_crescent_moon","keywords":["かけていくみかづき","さんじつげつ","つき","うちゅう","かけ","てんき","欠けていく三日月","三日月","月","宇宙","欠け","天気","nature","twilight","planet","space","night","evening","sleep"]},{"category":"animals_and_nature","char":"🌑","name":"new_moon","keywords":["しんげつ","かい","つき","うちゅう","てんき","新月","晦","月","宇宙","天気","nature","twilight","planet","space","night","evening","sleep"]},{"category":"animals_and_nature","char":"🌒","name":"waxing_crescent_moon","keywords":["みちていくみかづき","さんじつげつ","つき","うちゅう","じょうげん","てんき","満ちていく三日月","三日月","月","宇宙","上弦","天気","nature","twilight","planet","space","night","evening","sleep"]},{"category":"animals_and_nature","char":"🌓","name":"first_quarter_moon","keywords":["じょうげんのつき","つき","げん","うちゅう","てんき","上弦の月","月","弦","宇宙","天気","nature","twilight","planet","space","night","evening","sleep"]},{"category":"animals_and_nature","char":"🌔","name":"waxing_gibbous_moon","keywords":["じゅうさんやつき","じゅうさんや","つき","うちゅう","じょうげん","てんき","十三夜月","十三夜","月","宇宙","上弦","天気","nature","night","sky","gray","twilight","planet","space","evening","sleep"]},{"category":"animals_and_nature","char":"🌚","name":"new_moon_with_face","keywords":["かおつきしんげつ","かお","つき","うちゅう","てんき","顔つき新月","顔","月","宇宙","天気","nature","twilight","planet","space","night","evening","sleep"]},{"category":"animals_and_nature","char":"🌝","name":"full_moon_with_face","keywords":["かおつきまんげつ","あかるい","かお","みちた","つき","うちゅう","てんき","顔つき満月","明るい","顔","満ちた","月","宇宙","天気","nature","twilight","planet","space","night","evening","sleep"]},{"category":"animals_and_nature","char":"🌛","name":"first_quarter_moon_with_face","keywords":["かおつきじょうげんのつき","かお","つき","げん","うちゅう","てんき","顔つき上弦の月","顔","月","弦","宇宙","天気","nature","twilight","planet","space","night","evening","sleep"]},{"category":"animals_and_nature","char":"🌜","name":"last_quarter_moon_with_face","keywords":["がおがあるかげんのつき","かお","つき","げん","うちゅう","てんき","顔がある下弦の月","顔","月","弦","宇宙","天気","nature","twilight","planet","space","night","evening","sleep"]},{"category":"animals_and_nature","char":"🌞","name":"sun_with_face","keywords":["かおつきたいよう","あかるい","かお","うちゅう","たいよう","てんき","顔つき太陽","明るい","顔","宇宙","太陽","天気","nature","morning","sky"]},{"category":"animals_and_nature","char":"🌙","name":"crescent_moon","keywords":["さんじつげつ","つき","うちゅう","てんき","三日月","月","宇宙","天気","night","sleep","sky","evening","magic"]},{"category":"animals_and_nature","char":"⭐","name":"star","keywords":["ちゅうくらいのほし","ほし","中くらいの星","星","night","yellow"]},{"category":"animals_and_nature","char":"🌟","name":"star2","keywords":["ひかるほし","きらめき","あかいひかり","かがやく","かがやき","ほし","光る星","きらめき","赤い光","輝く","輝き","星","night","sparkle","awesome","good","magic"]},{"category":"animals_and_nature","char":"💫","name":"dizzy","keywords":["くらくら","まんが","めまい","ほし","くらくら","漫画","めまい","星","star","sparkle","shoot","magic"]},{"category":"animals_and_nature","char":"✨","name":"sparkles","keywords":["きらきら","えんたーていめんと","かがやき","ほし","キラキラ","エンターテイメント","輝き","星","stars","shine","shiny","cool","awesome","good","magic"]},{"category":"animals_and_nature","char":"☄","name":"comet","keywords":["すいせい","うちゅう","彗星","宇宙","space"]},{"category":"animals_and_nature","char":"☀️","name":"sunny","keywords":["たいようのひかり","あかるい","こうせん","うちゅう","たいよう","せいてん","てんき","太陽の光","明るい","光線","宇宙","太陽","晴天","天気","weather","nature","brightness","summer","beach","spring"]},{"category":"animals_and_nature","char":"🌤","name":"sun_behind_small_cloud","keywords":["たいようとちいさなくも","くも","たいよう","てんき","太陽と小さな雲","雲","太陽","天気","weather"]},{"category":"animals_and_nature","char":"⛅","name":"partly_sunny","keywords":["はれときどきくもり","くも","たいよう","てんき","晴れ時々曇り","雲","太陽","天気","weather","nature","cloudy","morning","fall","spring"]},{"category":"animals_and_nature","char":"🌥","name":"sun_behind_large_cloud","keywords":["はれのちくもり","くも","たいよう","てんき","晴れのち曇り","雲","太陽","天気","weather"]},{"category":"animals_and_nature","char":"🌦","name":"sun_behind_rain_cloud","keywords":["はれのちくもりときどきあめ","くも","あめ","たいよう","てんき","晴れのち曇り時々雨","雲","雨","太陽","天気","weather"]},{"category":"animals_and_nature","char":"☁️","name":"cloud","keywords":["くも","てんき","雲","天気","weather","sky"]},{"category":"animals_and_nature","char":"🌧","name":"cloud_with_rain","keywords":["あまぐも","くも","あめ","てんき","雨雲","雲","雨","天気","weather"]},{"category":"animals_and_nature","char":"⛈","name":"cloud_with_lightning_and_rain","keywords":["らいう","くも","あめ","かみなり","てんき","雷雨","雲","雨","雷","天気","weather","lightning"]},{"category":"animals_and_nature","char":"🌩","name":"cloud_with_lightning","keywords":["らいうん","くも","かみなり","てんき","雷雲","雲","雷","天気","weather","thunder"]},{"category":"animals_and_nature","char":"⚡","name":"zap","keywords":["だかでんあつきごう","きけん","でんき","かみなり","でんあつ","びりびり","高電圧記号","危険","電気","雷","電圧","ビリビリ","thunder","weather","lightning bolt","fast"]},{"category":"animals_and_nature","char":"🔥","name":"fire","keywords":["えん","ひ","どうぐ","炎","火","道具","hot","cook","flame"]},{"category":"animals_and_nature","char":"💥","name":"boom","keywords":["しょうとつまーく","どかーん","しょうとつ","まんが","衝突マーク","どかーん","衝突","漫画","bomb","explode","explosion","collision","blown"]},{"category":"animals_and_nature","char":"❄️","name":"snowflake","keywords":["せつのけっしょう","つめたい","ゆき","てんき","雪の結晶","冷たい","雪","天気","winter","season","cold","weather","christmas","xmas"]},{"category":"animals_and_nature","char":"🌨","name":"cloud_with_snow","keywords":["ゆきぐも","くも","れい","ゆき","てんき","雪雲","雲","冷","雪","天気","weather"]},{"category":"animals_and_nature","char":"⛄","name":"snowman","keywords":["ゆきだるま","れい","ゆき","てんき","雪だるま","冷","雪","天気","winter","season","cold","weather","christmas","xmas","frozen","without_snow"]},{"category":"animals_and_nature","char":"☃","name":"snowman_with_snow","keywords":["ゆきだるま","れい","ゆき","てんき","雪だるま","冷","雪","天気","winter","season","cold","weather","christmas","xmas","frozen"]},{"category":"animals_and_nature","char":"🌬","name":"wind_face","keywords":["かぜがふいている","かぜがふく","くも","かお","てんき","ふう","風が吹いている","風が吹く","雲","顔","天気","風","gust","air"]},{"category":"animals_and_nature","char":"💨","name":"dash","keywords":["だっしゅ","まんが","はしる","ダッシュ","漫画","走る","wind","air","fast","shoo","fart","smoke","puff"]},{"category":"animals_and_nature","char":"🌪","name":"tornado","keywords":["たつまきぐも","くも","たつまき","てんき","せんぷう","竜巻雲","雲","竜巻","天気","旋風","weather","cyclone","twister"]},{"category":"animals_and_nature","char":"🌫","name":"fog","keywords":["きり","くも","てんき","霧","雲","天気","weather"]},{"category":"animals_and_nature","char":"☂","name":"open_umbrella","keywords":["かさ","ふく","あめ","てんき","傘","服","雨","天気","weather","spring"]},{"category":"animals_and_nature","char":"☔","name":"umbrella","keywords":["うとかさ","いるい","しずく","あめ","かさ","てんき","雨と傘","衣類","しずく","雨","傘","天気","rainy","weather","spring"]},{"category":"animals_and_nature","char":"💧","name":"droplet","keywords":["しずく","ぞっとする","まんが","したたり","あせ","てんき","雫","ぞっとする","漫画","したたり","汗","天気","water","drip","faucet","spring"]},{"category":"animals_and_nature","char":"💦","name":"sweat_drops","keywords":["あせまーく","まんが","ぬれている","あせ","汗マーク","漫画","濡れている","汗","water","drip","oops"]},{"category":"animals_and_nature","char":"🌊","name":"ocean","keywords":["なみ","うみ","みず","てんき","波","海","水","天気","sea","water","wave","nature","tsunami","disaster"]},{"category":"animals_and_nature","char":"🪷","name":"lotus","keywords":["はす","ぶっきょう","はな","ひんどぅーきょう","いんど","せいじょう","べとなむ","ハス","仏教","花","ヒンドゥー教","インド","清浄","ベトナム"]},{"category":"animals_and_nature","char":"🪸","name":"coral","keywords":["さんご","たいよう","しょう","サンゴ","大洋","礁"]},{"category":"animals_and_nature","char":"🪹","name":"empty_nest","keywords":["そらのす","すづくり","とりのす","空の巣","巣作り","鳥の巣"]},{"category":"animals_and_nature","char":"🪺","name":"nest_with_eggs","keywords":["たまごのあるす","すづくり","とりのす","たまご","卵のある巣","巣作り","鳥の巣","卵"]},{"category":"food_and_drink","char":"🍏","name":"green_apple","keywords":["あおりんご","りんご","ふるーつ","くだもの","みどり","しょくぶつ","青りんご","リンゴ","フルーツ","果物","緑","植物","fruit","nature"]},{"category":"food_and_drink","char":"🍎","name":"apple","keywords":["あかいりんご","りんご","ふるーつ","くだもの","しょくぶつ","あか","赤いリンゴ","リンゴ","フルーツ","果物","植物","赤","fruit","mac","school"]},{"category":"food_and_drink","char":"🍐","name":"pear","keywords":["なし","ふるーつ","くだもの","しょくぶつ","梨","フルーツ","果物","植物","fruit","nature","food"]},{"category":"food_and_drink","char":"🍊","name":"tangerine","keywords":["みかん","ふるーつ","くだもの","おれんじ","しょくぶつ","あかだいだいいろ","みかん","フルーツ","果物","オレンジ","植物","赤橙色","food","fruit","nature","orange"]},{"category":"food_and_drink","char":"🍋","name":"lemon","keywords":["れもん","かんきつるい","ふるーつ","くだもの","しょくぶつ","レモン","柑橘類","フルーツ","果物","植物","fruit","nature"]},{"category":"food_and_drink","char":"🍌","name":"banana","keywords":["ばなな","ふるーつ","くだもの","しょくぶつ","バナナ","フルーツ","果物","植物","fruit","food","monkey"]},{"category":"food_and_drink","char":"🍉","name":"watermelon","keywords":["すいか","ふるーつ","くだもの","しょくぶつ","スイカ","フルーツ","果物","植物","fruit","food","picnic","summer"]},{"category":"food_and_drink","char":"🍇","name":"grapes","keywords":["ぶどう","ふるーつ","くだもの","しょくぶつ","ブドウ","フルーツ","果物","植物","fruit","food","wine"]},{"category":"food_and_drink","char":"🍓","name":"strawberry","keywords":["いちご","べりー","ふるーつ","くだもの","しょくぶつ","イチゴ","ベリー","フルーツ","果物","植物","fruit","food","nature"]},{"category":"food_and_drink","char":"🍈","name":"melon","keywords":["めろん","ふるーつ","くだもの","しょくぶつ","メロン","フルーツ","果物","植物","fruit","nature","food"]},{"category":"food_and_drink","char":"🍒","name":"cherries","keywords":["さくらんぼ","ふるーつ","くだもの","しょくぶつ","さくらんぼ","フルーツ","果物","植物","food","fruit"]},{"category":"food_and_drink","char":"🍑","name":"peach","keywords":["もも","ふるーつ","くだもの","しょくぶつ","桃","フルーツ","果物","植物","fruit","nature","food"]},{"category":"food_and_drink","char":"🍍","name":"pineapple","keywords":["ぱいなっぷる","ふるーつ","くだもの","しょくぶつ","パイナップル","フルーツ","果物","植物","fruit","nature","food"]},{"category":"food_and_drink","char":"🥥","name":"coconut","keywords":["ここなっつ","ふるーつ","ココナッツ","フルーツ","fruit","nature","food","palm"]},{"category":"food_and_drink","char":"🥝","name":"kiwi_fruit","keywords":["きういふるーつ","ふるーつ","くだもの","きうい","キウイフルーツ","フルーツ","果物","キウイ","fruit","food"]},{"category":"food_and_drink","char":"🥭","name":"mango","keywords":["まんごー","ねったい","ふるーつ","マンゴー","熱帯","フルーツ","fruit","food","tropical"]},{"category":"food_and_drink","char":"🥑","name":"avocado","keywords":["あぼかど","ふるーつ","くだもの","アボカド","フルーツ","果物","fruit","food"]},{"category":"food_and_drink","char":"🫛","name":"pea_pod","keywords":["えんどうまめのさや","まめ","えだまめ","まめか","えんどうまめ","さや","やさい","エンドウ豆のさや","豆","枝豆","マメ科","エンドウ豆","さや","野菜","beans","edamame","legume","pea","pod","vegetable","food"]},{"category":"food_and_drink","char":"🥦","name":"broccoli","keywords":["ぶろっこりー","やさい","ブロッコリー","野菜","fruit","food","vegetable"]},{"category":"food_and_drink","char":"🍅","name":"tomato","keywords":["とまと","しょくぶつ","やさい","トマト","植物","野菜","fruit","vegetable","nature","food"]},{"category":"food_and_drink","char":"🍆","name":"eggplant","keywords":["なす","なすび","しょくぶつ","やさい","ナス","茄子","植物","野菜","vegetable","nature","food","aubergine"]},{"category":"food_and_drink","char":"🥒","name":"cucumber","keywords":["きゅうり","ぴくるす","やさい","キュウリ","ピクルス","野菜","fruit","food","pickle"]},{"category":"food_and_drink","char":"🫐","name":"blueberries","keywords":["ぶるーべりー","べりー","びるべりー","あお","ふるーつ","ブルーベリー","ベリー","ビルベリー","青","フルーツ","fruit","food"]},{"category":"food_and_drink","char":"🫒","name":"olive","keywords":["おりーぶ","ふるーつ","オリーブ","フルーツ","fruit","food"]},{"category":"food_and_drink","char":"🫑","name":"bell_pepper","keywords":["ぴーまん","とうがらし","こしょう","しょくぶつ","やさい","ピーマン","唐辛子","コショウ","植物","野菜","fruit","food"]},{"category":"food_and_drink","char":"🥕","name":"carrot","keywords":["にんじん","やさい","ニンジン","野菜","vegetable","food","orange"]},{"category":"food_and_drink","char":"🌶","name":"hot_pepper","keywords":["とうがらし","からい","こしょう","しょくぶつ","トウガラシ","辛い","コショウ","植物","food","spicy","chilli","chili"]},{"category":"food_and_drink","char":"🥔","name":"potato","keywords":["じゃがいも","やさい","ジャガイモ","野菜","food","tuber","vegatable","starch"]},{"category":"food_and_drink","char":"🌽","name":"corn","keywords":["とうもろこし","こーん","しょくぶつ","トウモロコシ","コーン","植物","food","vegetable","plant"]},{"category":"food_and_drink","char":"🥬","name":"leafy_greens","keywords":["はっぱのみどり","ちんげんさい","きゃべつ","けーる","れたす","葉っぱの緑","チンゲン菜","キャベツ","ケール","レタス","food","vegetable","plant","bok choy","cabbage","kale","lettuce"]},{"category":"food_and_drink","char":"🍠","name":"sweet_potato","keywords":["やきいも","じゃがいも","やき","すいーつ","焼き芋","ジャガイモ","焼き","スイーツ","food","nature"]},{"category":"food_and_drink","char":"🫚","name":"ginger_root","keywords":["しょうが","びーる","ね","すぱいす","ショウガ","ビール","根","スパイス","food","nature","beer","root","spice"]},{"category":"food_and_drink","char":"🥜","name":"peanuts","keywords":["ぴーなっつ","なっつ","やさい","ピーナッツ","ナッツ","野菜","food","nut"]},{"category":"food_and_drink","char":"🧄","name":"garlic","keywords":["にんにく","やさい","しょくぶつ","こうみりょう","にんにく","野菜","植物","香味料","food"]},{"category":"food_and_drink","char":"🧅","name":"onion","keywords":["たまねぎ","やさい","しょくぶつ","こうみりょう","玉ねぎ","野菜","植物","香味料","food"]},{"category":"food_and_drink","char":"🍯","name":"honey_pot","keywords":["はにーぽっと","はちみつ","ぽっと","すいーつ","ハニーポット","はちみつ","ポット","スイーツ","bees","sweet","kitchen"]},{"category":"food_and_drink","char":"🥐","name":"croissant","keywords":["くろわっさん","ぱん","さんじつげつ","ろーる","ふれんち","クロワッサン","パン","三日月","ロール","フレンチ","food","bread","french"]},{"category":"food_and_drink","char":"🍞","name":"bread","keywords":["ぱん","ろーふ","パン","ローフ","food","wheat","breakfast","toast"]},{"category":"food_and_drink","char":"🥖","name":"baguette_bread","keywords":["ふらんすぱん","ぱん","ふれんち","フランスパン","パン","フレンチ","food","bread","french"]},{"category":"food_and_drink","char":"🥯","name":"bagel","keywords":["べーぐる","ぱん","くりーむちーず","ひとぬり","ベーグル","パン","クリームチーズ","ひと塗り","food","bread","bakery","schmear"]},{"category":"food_and_drink","char":"🥨","name":"pretzel","keywords":["ぷれっつぇる","そふとぷれっつぇる","ぷれっつぇるついすと","ぱん","プレッツェル","ソフトプレッツェル","プレッツェルツイスト","パン","food","bread","twisted"]},{"category":"food_and_drink","char":"🧀","name":"cheese","keywords":["ちーず","チーズ","food","chadder"]},{"category":"food_and_drink","char":"🥚","name":"egg","keywords":["たまご","卵","food","chicken","breakfast"]},{"category":"food_and_drink","char":"🥓","name":"bacon","keywords":["べーこん","にく","ベーコン","肉","food","breakfast","pork","pig","meat"]},{"category":"food_and_drink","char":"🥩","name":"steak","keywords":["いちきれのにく","にく","きりみ","らむちょっぷ","ぶた","すてーき","一切れの肉","肉","切り身","ラムチョップ","豚","ステーキ","food","cow","meat","cut","chop","lambchop","porkchop"]},{"category":"food_and_drink","char":"🥞","name":"pancakes","keywords":["ぱんけーき","くれーぷ","ほっとけーき","パンケーキ","クレープ","ホットケーキ","food","breakfast","flapjacks","hotcakes"]},{"category":"food_and_drink","char":"🍗","name":"poultry_leg","keywords":["たーきー","ほね","にわとり","あし","かきん","ターキー","骨","ニワトリ","脚","家禽","food","meat","drumstick","bird","chicken","turkey"]},{"category":"food_and_drink","char":"🍖","name":"meat_on_bone","keywords":["ほねつきにく","ほね","にく","骨付き肉","骨","肉","good","food","drumstick"]},{"category":"food_and_drink","char":"🦴","name":"bone","keywords":["ほね","からだ","こっかく","骨","体","骨格","skeleton"]},{"category":"food_and_drink","char":"🍤","name":"fried_shrimp","keywords":["えびふらい","ふらい","えび","こえび","てんぷら","エビフライ","フライ","エビ","小エビ","てんぷら","food","animal","appetizer","summer"]},{"category":"food_and_drink","char":"🍳","name":"fried_egg","keywords":["りょうり","たまご","ふらいぱん","なべ","料理","卵","フライパン","鍋","food","breakfast","kitchen","egg"]},{"category":"food_and_drink","char":"🍔","name":"hamburger","keywords":["はんばーがー","ばーがー","ハンバーガー","バーガー","meat","fast food","beef","cheeseburger","mcdonalds","burger king"]},{"category":"food_and_drink","char":"🍟","name":"fries","keywords":["ふらいどぽてと","ふらいど","ぽてと","フライドポテト","フライド","ポテト","chips","snack","fast food"]},{"category":"food_and_drink","char":"🥙","name":"stuffed_flatbread","keywords":["ふらっとぶれっどさんど","ふぁらふぇる","ふらっとぶれっど","じゃいろ","けばぶ","つめもの","フラットブレッドサンド","ファラフェル","フラットブレッド","ジャイロ","ケバブ","詰め物","food","flatbread","stuffed","gyro"]},{"category":"food_and_drink","char":"🌭","name":"hotdog","keywords":["ほっとどっぐ","ふらんくふるとそーせーじ","ほっとどっぐそーせーじ","そーせーじ","うぃんなー","れっどほっと","ホットドッグ","フランクフルトソーセージ","ホットドッグソーセージ","ソーセージ","ウィンナー","レッドホット","food","frankfurter"]},{"category":"food_and_drink","char":"🍕","name":"pizza","keywords":["ぴざ","ちーず","1まい","ピザ","チーズ","1枚","food","party"]},{"category":"food_and_drink","char":"🥪","name":"sandwich","keywords":["さんどうぃっち","ぱん","やさい","ちーず","にく","でり","サンドウィッチ","パン","野菜","チーズ","肉","デリ","food","lunch","bread"]},{"category":"food_and_drink","char":"🥫","name":"canned_food","keywords":["かんづめ","ほぞんようしょくひん","缶詰","かんづめ","保存用食品","food","soup"]},{"category":"food_and_drink","char":"🍝","name":"spaghetti","keywords":["すぱげってぃ","ぱすた","スパゲッティ","パスタ","food","italian","noodle"]},{"category":"food_and_drink","char":"🌮","name":"taco","keywords":["たこす","めきしこ","タコス","メキシコ","food","mexican"]},{"category":"food_and_drink","char":"🌯","name":"burrito","keywords":["ぶりとー","めきしこ","ブリトー","メキシコ","food","mexican"]},{"category":"food_and_drink","char":"🥗","name":"green_salad","keywords":["ぐりーんさらだ","みどり","さらだ","グリーンサラダ","緑","サラダ","food","healthy","lettuce"]},{"category":"food_and_drink","char":"🥘","name":"shallow_pan_of_food","keywords":["ぱえりあ","きゃせろーる","なべ","あさい","パエリア","キャセロール","鍋","浅い","food","cooking","casserole","paella"]},{"category":"food_and_drink","char":"🍜","name":"ramen","keywords":["どんぶり","めん","らーめん","むしかねつ","すーぷ","どんぶり","麺","ラーメン","蒸し加熱","スープ","food","japanese","noodle","chopsticks"]},{"category":"food_and_drink","char":"🍲","name":"stew","keywords":["なべ","しちゅー","なべ","鍋","シチュー","food","meat","soup"]},{"category":"food_and_drink","char":"🍥","name":"fish_cake","keywords":["なると","こけいのたべもの","さかな","ねりもの","なると","固形の食べ物","魚","練り物","food","japan","sea","beach","narutomaki","pink","swirl","kamaboko","surimi","ramen"]},{"category":"food_and_drink","char":"🥠","name":"fortune_cookie","keywords":["おみくじいりくっきー","ふぉーちゅんくっきー","おみくじ入りクッキー","フォーチュンクッキー","food","prophecy"]},{"category":"food_and_drink","char":"🍣","name":"sushi","keywords":["すし","寿司","food","fish","japanese","rice"]},{"category":"food_and_drink","char":"🍱","name":"bento","keywords":["べんとうばこ","べんとう","はこ","弁当箱","弁当","箱","food","japanese","box"]},{"category":"food_and_drink","char":"🍛","name":"curry","keywords":["かれーらいす","かれー","ごはん","カレーライス","カレー","ご飯","food","spicy","hot","indian"]},{"category":"food_and_drink","char":"🍙","name":"rice_ball","keywords":["おにぎり","にっぽん","こめ","おにぎり","日本","米","food","japanese"]},{"category":"food_and_drink","char":"🍚","name":"rice","keywords":["ごはん","りょうり","こめ","ごはん","料理","米","food","china","asian"]},{"category":"food_and_drink","char":"🍘","name":"rice_cracker","keywords":["せんべい","こめ","せんべい","米","food","japanese"]},{"category":"food_and_drink","char":"🍢","name":"oden","keywords":["おでん","しーふーど","くし","すてぃっく","おでん","シーフード","串","スティック","food","japanese"]},{"category":"food_and_drink","char":"🍡","name":"dango","keywords":["だんご","でざーと","にっぽん","くし","すてぃっく","すいーつ","団子","デザート","日本","串","スティック","スイーツ","food","dessert","sweet","japanese","barbecue","meat"]},{"category":"food_and_drink","char":"🍧","name":"shaved_ice","keywords":["かきごおり","でざーと","こおり","すいーつ","かき氷","デザート","氷","スイーツ","hot","dessert","summer"]},{"category":"food_and_drink","char":"🍨","name":"ice_cream","keywords":["あいすくりーむ","くりーむ","でざーと","こおり","すいーつ","アイスクリーム","クリーム","デザート","氷","スイーツ","food","hot","dessert"]},{"category":"food_and_drink","char":"🍦","name":"icecream","keywords":["そふとくりーむ","くりーむ","でざーと","こおり","あいすくりーむ","そふと","すいーつ","ソフトクリーム","クリーム","デザート","氷","アイスクリーム","ソフト","スイーツ","food","hot","dessert","summer"]},{"category":"food_and_drink","char":"🥧","name":"pie","keywords":["ぱい","でざーと","すいーつ","パイ","デザート","スイーツ","food","dessert","pastry"]},{"category":"food_and_drink","char":"🍰","name":"cake","keywords":["しょーとけーき","けーき","でざーと","ぺいすとりー","すらいす","すいーつ","ショートケーキ","ケーキ","デザート","ペイストリー","スライス","スイーツ","food","dessert"]},{"category":"food_and_drink","char":"🧁","name":"cupcake","keywords":["かっぷけーき","べーかりー","すいーつ","でざーと","ぺいすとりー","カップケーキ","ベーカリー","スイーツ","デザート","ペイストリー","food","dessert","bakery","sweet"]},{"category":"food_and_drink","char":"🥮","name":"moon_cake","keywords":["げっぺい","あき","まつり","月餅","秋","祭","food","autumn"]},{"category":"food_and_drink","char":"🎂","name":"birthday","keywords":["ばーすでーけーき","たんじょうび","けーき","おいわい","でざーと","ぺいすとりー","すいーつ","バースデーケーキ","誕生日","ケーキ","お祝い","デザート","ペイストリー","スイーツ","food","dessert","cake"]},{"category":"food_and_drink","char":"🍮","name":"custard","keywords":["かすたーど","でざーと","ぷりん","すいーつ","カスタード","デザート","プリン","スイーツ","dessert","food"]},{"category":"food_and_drink","char":"🍬","name":"candy","keywords":["あめ","でざーと","すいーつ","アメ","デザート","スイーツ","snack","dessert","sweet","lolly"]},{"category":"food_and_drink","char":"🍭","name":"lollipop","keywords":["ぺろぺろきゃんでぃー","きゃんでぃ","でざーと","ろりぽっぷきゃんでぃ","すいーつ","ペロペロキャンディー","キャンディ","デザート","ロリポップキャンディ","スイーツ","food","snack","candy","sweet"]},{"category":"food_and_drink","char":"🍫","name":"chocolate_bar","keywords":["ちょこれーと","ばー","でざーと","すいーつ","チョコレート","バー","デザート","スイーツ","food","snack","dessert","sweet"]},{"category":"food_and_drink","char":"🍿","name":"popcorn","keywords":["ぽっぷこーん","ポップコーン","food","movie theater","films","snack"]},{"category":"food_and_drink","char":"🥟","name":"dumpling","keywords":["ぎょうざ","餃子","ギョウザ","food","empanada","pierogi","potsticker"]},{"category":"food_and_drink","char":"🍩","name":"doughnut","keywords":["どーなつ","でざーと","すいーつ","ドーナツ","デザート","スイーツ","food","dessert","snack","sweet","donut"]},{"category":"food_and_drink","char":"🍪","name":"cookie","keywords":["くっきー","でざーと","あまい","クッキー","デザート","甘い","food","snack","oreo","chocolate","sweet","dessert"]},{"category":"food_and_drink","char":"🧇","name":"waffle","keywords":["わっふる","ほっとけーき","ワッフル","ホットケーキ","food"]},{"category":"food_and_drink","char":"🧆","name":"falafel","keywords":["ふぁらふぇる","ひよこまめ","ファラフェル","ひよこ豆","food"]},{"category":"food_and_drink","char":"🧈","name":"butter","keywords":["ばたー","にゅうせいひん","バター","乳製品","food"]},{"category":"food_and_drink","char":"🦪","name":"oyster","keywords":["かき","しんじゅ","だいびんぐ","カキ","真珠","ダイビング","food"]},{"category":"food_and_drink","char":"🫓","name":"flatbread","keywords":["ふらっとぶれっど","あれぱ","らヴぁしゅ","なん","ぴた","フラットブレッド","アレパ","ラヴァシュ","ナン","ピタ","food"]},{"category":"food_and_drink","char":"🫔","name":"tamale","keywords":["たまーれ","たまーり","めきしかん","つつまれた","タマーレ","タマーリ","メキシカン","包まれた","food"]},{"category":"food_and_drink","char":"🫕","name":"fondue","keywords":["ふぉんでゅ","ちーず","ちょこれーと","ふぉでゅ","とけた","ぽっと","すいす","フォンデュ","チーズ","チョコレート","フォデュ","溶けた","ポット","スイス","food"]},{"category":"food_and_drink","char":"🥛","name":"milk_glass","keywords":["こっぷにはいったぎゅうにゅう","どりんく","ぐらす","みるく","コップに入った牛乳","ドリンク","グラス","ミルク","beverage","drink","cow"]},{"category":"food_and_drink","char":"🍺","name":"beer","keywords":["びーる","ばー","のむ","まぐかっぷ","ビール","バー","飲む","マグカップ","relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"]},{"category":"food_and_drink","char":"🍻","name":"beers","keywords":["かんぱい","ばー","びーる","かちん","のみもの","まぐかっぷ","乾杯","バー","ビール","カチン","飲み物","マグカップ","relax","beverage","drink","drunk","party","pub","summer","alcohol","booze"]},{"category":"food_and_drink","char":"🥂","name":"clinking_glasses","keywords":["ぐらすでかんぱい","いわう","かちん","のみもの","ぐらす","グラスで乾杯","祝う","カチン","飲み物","グラス","beverage","drink","party","alcohol","celebrate","cheers","wine","champagne","toast"]},{"category":"food_and_drink","char":"🍷","name":"wine_glass","keywords":["わいんぐらす","ばー","いんりょう","のみもの","ぐらす","わいん","ワイングラス","バー","飲料","飲み物","グラス","ワイン","drink","beverage","drunk","alcohol","booze"]},{"category":"food_and_drink","char":"🥃","name":"tumbler_glass","keywords":["たんぶらー","ぐらす","て","しょっと","ういすきー","うぃすきー","ばーぼん","タンブラー","グラス","酒","ショット","ウイスキー","ウィスキー","バーボン","drink","beverage","drunk","alcohol","liquor","booze","bourbon","scotch","whisky","glass","shot"]},{"category":"food_and_drink","char":"🍸","name":"cocktail","keywords":["かくてるぐらす","ばー","かくてる","のみもの","ぐらす","カクテルグラス","バー","カクテル","飲み物","グラス","drink","drunk","alcohol","beverage","booze","mojito"]},{"category":"food_and_drink","char":"🍹","name":"tropical_drink","keywords":["とろぴかるどりんく","ばー","のみもの","とろぴかる","トロピカルドリンク","バー","飲み物","トロピカル","beverage","cocktail","summer","beach","alcohol","booze","mojito"]},{"category":"food_and_drink","char":"🍾","name":"champagne","keywords":["びんととびだすせん","ばー","ぼとる","しゃんぱん","しゃんぺん","しゃんぱーにゅ","こるく","のみもの","とびだす","ぱーてぃー","瓶と飛び出す栓","バー","ボトル","シャンパン","シャンペン","シャンパーニュ","コルク","飲み物","飛び出す","パーティー","drink","wine","bottle","celebration"]},{"category":"food_and_drink","char":"🍶","name":"sake","keywords":["とっくりとおちょこ","ばー","いんりょう","ぼとる","かっぷ","のみもの","て","とっくりとおちょこ","バー","飲料","ボトル","カップ","飲み物","酒","wine","drink","drunk","beverage","japanese","alcohol","booze"]},{"category":"food_and_drink","char":"🍵","name":"tea","keywords":["ゆのみ","いんりょう","かっぷ","のみもの","おちゃ","湯のみ","飲料","カップ","飲み物","お茶","湯飲み","drink","bowl","breakfast","green","british"]},{"category":"food_and_drink","char":"🥤","name":"cup_with_straw","keywords":["かっぷとすとろー","じゅーす","そーだ","もると","そふとどりんく","みず","しょっき","カップとストロー","ジュース","ソーダ","モルト","ソフトドリンク","水","食器","drink","soda"]},{"category":"food_and_drink","char":"☕","name":"coffee","keywords":["ほっとどりんく","いんりょう","こーひー","のみもの","あたたかい","じょうき","おちゃ","ホットドリンク","飲料","コーヒー","飲み物","温かい","蒸気","お茶","beverage","caffeine","latte","espresso"]},{"category":"food_and_drink","char":"🫖","name":"teapot","keywords":["てぃーぽっと","どりんく","ぽっと","てぃー","けとる","ティーポット","ドリンク","ポット","ティー","ケトル"]},{"category":"food_and_drink","char":"🧋","name":"bubble_tea","keywords":["たぴおかてぃー","ばぶる","みるく","ぱーる","てぃー","ぼば","たぴおか","もみ","タピオカティー","バブル","ミルク","パール","ティー","ボバ","タピオカ","モミ","tapioca"]},{"category":"food_and_drink","char":"🍼","name":"baby_bottle","keywords":["ほにゅうびん","あかちゃん","ぼとる","どりんく","みるく","哺乳瓶","赤ちゃん","ボトル","ドリンク","ミルク","food","container","milk"]},{"category":"food_and_drink","char":"🧃","name":"beverage_box","keywords":["いんりょうぼっくす","じゅーす","いんりょう","ぼっくす","どりんく","すとろー","飲料ボックス","ジュース","飲料","ボックス","ドリンク","ストロー","food","drink"]},{"category":"food_and_drink","char":"🧉","name":"mate","keywords":["まて","どりんく","ぼんびりや","いえるば","マテ","ドリンク","ボンビリヤ","イエルバ","food","drink"]},{"category":"food_and_drink","char":"🧊","name":"ice_cube","keywords":["かくこおり","こおり","りっぽうたい","つめたい","ひょうざん","角氷","氷","立方体","冷たい","氷山","food"]},{"category":"food_and_drink","char":"🧂","name":"salt","keywords":["しお","こうしんりょう","しぇーかー","塩","香辛料","シェーカー","condiment","shaker"]},{"category":"food_and_drink","char":"🥄","name":"spoon","keywords":["すぷーん","しょっき","スプーン","食器","cutlery","kitchen","tableware"]},{"category":"food_and_drink","char":"🍴","name":"fork_and_knife","keywords":["ふぉーくとないふ","ちょうり","ふぉーく","ないふ","しょっき","フォークとナイフ","調理","フォーク","ナイフ","食器","cutlery","kitchen"]},{"category":"food_and_drink","char":"🍽","name":"plate_with_cutlery","keywords":["ふぉーくとないふとぷれーと","ちょうり","ふぉーく","ないふ","ぷれーと","しょっき","フォークとナイフとプレート","調理","フォーク","ナイフ","プレート","食器","food","eat","meal","lunch","dinner","restaurant"]},{"category":"food_and_drink","char":"🥣","name":"bowl_with_spoon","keywords":["ぼうるとすぷーん","ちょうしょく","しりある","おかゆ","おーとみーる","ぽりっじ","しょっき","ボウルとスプーン","朝食","シリアル","お粥","オートミール","ポリッジ","食器","food","breakfast","cereal","oatmeal","porridge"]},{"category":"food_and_drink","char":"🥡","name":"takeout_box","keywords":["ていくあうとぼっくす","ていくあうと","ようき","おもちかえり","テイクアウトボックス","テイクアウト","容器","お持ち帰り","food","leftovers"]},{"category":"food_and_drink","char":"🥢","name":"chopsticks","keywords":["はし","箸","はし","food"]},{"category":"food_and_drink","char":"🫗","name":"pouring_liquid","keywords":["ながれこむえきたい","のみもの","そら","ぐらす","こぼれる","流れ込む液体","飲み物","空","グラス","こぼれる"]},{"category":"food_and_drink","char":"🫘","name":"beans","keywords":["まめ","たべもの","じんぞう","豆","食べ物","腎臓","マメ"]},{"category":"food_and_drink","char":"🫙","name":"jar","keywords":["びん","こうしんりょう","ようき","そら","そーす","ちょぞう","瓶","香辛料","容器","空","ソース","貯蔵"]},{"category":"activity","char":"⚽","name":"soccer","keywords":["さっかーぼーる","ぼーる","さっかー","サッカーボール","ボール","サッカー","sports","football"]},{"category":"activity","char":"🏀","name":"basketball","keywords":["ばすけっとぼーる","ぼーる","ばすけっとりんぐ","バスケットボール","ボール","バスケットリング","sports","balls","NBA"]},{"category":"activity","char":"🏈","name":"football","keywords":["あめりかんふっとぼーる","あめりかん","ぼーる","ふっとぼーる","アメリカンフットボール","アメリカン","ボール","フットボール","sports","balls","NFL"]},{"category":"activity","char":"⚾","name":"baseball","keywords":["やきゅう","ぼーる","野球","ボール","sports","balls"]},{"category":"activity","char":"🥎","name":"softball","keywords":["そふとぼーる","ぼーる","しあい","すぽーつ","ソフトボール","ボール","試合","スポーツ","sports","balls"]},{"category":"activity","char":"🎾","name":"tennis","keywords":["てにすぼーる","ぼーる","らけっと","てにす","テニスボール","ボール","ラケット","テニス","sports","balls","green"]},{"category":"activity","char":"🏐","name":"volleyball","keywords":["ばれーぼーる","ぼーる","しあい","バレーボール","ボール","試合","sports","balls"]},{"category":"activity","char":"🏉","name":"rugby_football","keywords":["らぐびー","ぼーる","ふっとぼーる","ラグビー","ボール","フットボール","sports","team"]},{"category":"activity","char":"🥏","name":"flying_disc","keywords":["そらとぶえんばん","でぃすく","あるてぃめっと","ごるふ","しあい","すぽーつ","ふりすびー","空飛ぶ円盤","ディスク","アルティメット","ゴルフ","試合","スポーツ","フリスビー","sports","frisbee","ultimate"]},{"category":"activity","char":"🎱","name":"8ball","keywords":["びりやーど","8","えいとぼーる","ぼーる","えいと","げーむ","ビリヤード","8","エイトボール","ボール","エイト","ゲーム","pool","hobby","game","luck","magic"]},{"category":"activity","char":"⛳","name":"golf","keywords":["ごるふのかっぷ","ぴんふらっぐ","ごるふ","ほーる","ゴルフのカップ","ピンフラッグ","ゴルフ","ホール","sports","business","flag","hole","summer"]},{"category":"activity","char":"🏌️‍♀️","name":"golfing_woman","keywords":["ごるふをするじょせい","ぼーる","ごるふ","ごるふぁー","ごるふする","じょせい","おんな","ゴルフをする女性","ボール","ゴルフ","ゴルファー","ゴルフする","女性","女","おんな","sports","business","woman","female"]},{"category":"activity","char":"🏌","name":"golfing_man","keywords":["ごるふをするひと","ぼーる","ごるふ","ごるふぁー","ごるふする","ゴルフをする人","ボール","ゴルフ","ゴルファー","ゴルフする","sports","business"]},{"category":"activity","char":"🏓","name":"ping_pong","keywords":["たっきゅうのらけっととぼーる","ぼーる","ばっと","しあい","ぱどる","たっきゅう","卓球のラケットとボール","ボール","バット","試合","パドル","卓球","sports","pingpong"]},{"category":"activity","char":"🏸","name":"badminton","keywords":["ばどみんとんのらけっととしゃとる","ばどみんとん","ばーでぃー","しあい","らけっと","しゃとる","バドミントンのラケットとシャトル","バドミントン","バーディー","試合","ラケット","シャトル","sports"]},{"category":"activity","char":"🥅","name":"goal_net","keywords":["ごーるねっと","ごーる","ねっと","ゴールネット","ゴール","ネット","sports"]},{"category":"activity","char":"🏒","name":"ice_hockey","keywords":["あいすほっけーのすてぃっくとぱっく","しあい","ほっけー","こおり","ぱっく","すてぃっく","アイスホッケーのスティックとパック","試合","ホッケー","氷","パック","スティック","sports"]},{"category":"activity","char":"🏑","name":"field_hockey","keywords":["ふぃーるどほっけーのすてぃっくとぼーる","ぼーる","ふぃーるど","しあい","ほっけー","すてぃっく","フィールドホッケーのスティックとボール","ボール","フィールド","試合","ホッケー","スティック","sports"]},{"category":"activity","char":"🥍","name":"lacrosse","keywords":["らくろす","ぼーる","すてぃっく","しあい","すぽーつ","ラクロス","ボール","スティック","試合","スポーツ","sports","ball","stick"]},{"category":"activity","char":"🏏","name":"cricket","keywords":["くりけっとのばっととぼーる","ぼーる","ふぃーるど","くりけっと","しあい","クリケットのバットとボール","ボール","フィールド","クリケット","試合","sports"]},{"category":"activity","char":"🎿","name":"ski","keywords":["すきーとすきーぶーつ","すきー","ゆき","スキーとスキーブーツ","スキー","雪","sports","winter","cold","snow"]},{"category":"activity","char":"⛷","name":"skier","keywords":["すきー","ゆき","スキー","雪","sports","winter","snow"]},{"category":"activity","char":"🏂","name":"snowboarder","keywords":["すのーぼーだー","すきー","ゆき","すのーぼーど","スノーボーダー","スキー","雪","スノーボード","sports","winter"]},{"category":"activity","char":"🤺","name":"person_fencing","keywords":["ふぇんしんぐをするひと","けんし","けんじゅつ","けん","フェンシングをする人","剣士","剣術","剣","sports","fencing","sword"]},{"category":"activity","char":"🤼‍♀️","name":"women_wrestling","keywords":["れすりんぐをするじょせい","れすりんぐ","れすりんぐせんしゅ","じょせい","おんな","レスリングをする女性","レスリング","レスリング選手","女性","女","おんな","sports","wrestlers"]},{"category":"activity","char":"🤼‍♂️","name":"men_wrestling","keywords":["れすりんぐをするだんせい","れすりんぐ","れすりんぐせんしゅ","おとこ","だんせい","レスリングをする男性","レスリング","レスリング選手","男","おとこ","男性","sports","wrestlers"]},{"category":"activity","char":"🤸‍♀️","name":"woman_cartwheeling","keywords":["そくてんをするじょせい","そくほうてんかい","たいそう","じょせい","おんな","側転をする女性","側方転回","体操","女性","女","おんな","gymnastics"]},{"category":"activity","char":"🤸‍♂️","name":"man_cartwheeling","keywords":["そくてんをするだんせい","そくほうてんかい","たいそう","おとこ","だんせい","側転をする男性","側方転回","体操","男","おとこ","男性","gymnastics"]},{"category":"activity","char":"🤾‍♀️","name":"woman_playing_handball","keywords":["はんどぼーるをするじょせい","ぼーる","はんどぼーる","じょせい","おんな","ハンドボールをする女性","ボール","ハンドボール","女性","女","おんな","sports"]},{"category":"activity","char":"🤾‍♂️","name":"man_playing_handball","keywords":["はんどぼーるをするだんせい","ぼーる","はんどぼーる","おとこ","だんせい","ハンドボールをする男性","ボール","ハンドボール","男","おとこ","男性","sports"]},{"category":"activity","char":"⛸","name":"ice_skate","keywords":["あいすすけーと","こおり","アイススケート","氷","sports"]},{"category":"activity","char":"🥌","name":"curling_stone","keywords":["かーりんぐすとーん","かーりんぐ","すとーん","カーリングストーン","カーリング","ストーン","sports"]},{"category":"activity","char":"🛹","name":"skateboard","keywords":["すけぼー","すけーと","ぼーど","スケボー","スケート","ボード","board"]},{"category":"activity","char":"🛷","name":"sled","keywords":["そり","るーじゅ","とぼがん","そり","ソリ","ルージュ","トボガン","sleigh","luge","toboggan"]},{"category":"activity","char":"🏹","name":"bow_and_arrow","keywords":["ゆみや","しゃしゅ","や","ゆみ","しゃしゅざ","どうぐ","せいざ","弓矢","射手","矢","弓","射手座","道具","星座","sports"]},{"category":"activity","char":"🎣","name":"fishing_pole_and_fish","keywords":["つりざおとさかな","えんたーていめんと","さかな","ぼう","釣竿と魚","エンターテイメント","魚","棒","food","hobby","summer"]},{"category":"activity","char":"🥊","name":"boxing_glove","keywords":["ぼくしんぐぐろーぶ","ぼくしんぐ","ぐろーぶ","ボクシンググローブ","ボクシング","グローブ","sports","fighting"]},{"category":"activity","char":"🥋","name":"martial_arts_uniform","keywords":["どうぎ","じゅうどう","からて","ぶどう","てこんどー","ゆにふぉーむ","道着","柔道","空手","武道","テコンドー","ユニフォーム","judo","karate","taekwondo"]},{"category":"activity","char":"🚣‍♀️","name":"rowing_woman","keywords":["ぼーとをこぐじょせい","ぼーと","こぎぶね","のりもの","そうてい","じょせい","おんな","ボートを漕ぐ女性","ボート","漕ぎ船","乗り物","漕艇","女性","女","おんな","sports","hobby","water","ship","woman","female"]},{"category":"activity","char":"🚣","name":"rowing_man","keywords":["ぼーとをこぐひと","ぼーと","こぎぶね","のりもの","そうてい","ボートをこぐ人","ボート","漕ぎ船","乗り物","漕艇","sports","hobby","water","ship"]},{"category":"activity","char":"🧗‍♀️","name":"climbing_woman","keywords":["くらいみんぐしているじょせい","くらいみんぐ","ろっく","じょせい","おんな","クライミングしている女性","クライミング","ロック","女性","女","おんな","sports","hobby","woman","female","rock"]},{"category":"activity","char":"🧗‍♂️","name":"climbing_man","keywords":["くらいみんぐしているだんせい","くらいみんぐ","ろっく","だんせい","おとこ","クライミングしている男性","クライミング","ロック","男性","男","おとこ","sports","hobby","man","male","rock"]},{"category":"activity","char":"🏊‍♀️","name":"swimming_woman","keywords":["およぐじょせい","およぐ","すいえい","じょせい","おんな","泳ぐ女性","泳ぐ","水泳","女性","女","おんな","sports","exercise","human","athlete","water","summer","woman","female"]},{"category":"activity","char":"🏊","name":"swimming_man","keywords":["すいえいをするひと","およぐ","すいえい","水泳をする人","泳ぐ","水泳","sports","exercise","human","athlete","water","summer"]},{"category":"activity","char":"🤽‍♀️","name":"woman_playing_water_polo","keywords":["すいきゅうをするじょせい","ぽろ","みず","すいきゅう","じょせい","おんな","水球をする女性","ポロ","水","水球","女性","女","おんな","sports","pool"]},{"category":"activity","char":"🤽‍♂️","name":"man_playing_water_polo","keywords":["すいきゅうをするだんせい","ぽろ","みず","すいきゅう","おとこ","だんせい","水球をする男性","ポロ","水","水球","男","おとこ","男性","sports","pool"]},{"category":"activity","char":"🧘‍♀️","name":"woman_in_lotus_position","keywords":["れんげざのじょせい","めいそう","よが","せいおん","じょせい","おんな","蓮華座の女性","瞑想","ヨガ","静穏","女性","女","おんな","woman","female","meditation","yoga","serenity","zen","mindfulness"]},{"category":"activity","char":"🧘‍♂️","name":"man_in_lotus_position","keywords":["れんげざのだんせい","めいそう","よが","せいおん","だんせい","おとこ","蓮華座の男性","瞑想","ヨガ","静穏","男性","男","おとこ","man","male","meditation","yoga","serenity","zen","mindfulness"]},{"category":"activity","char":"🏄‍♀️","name":"surfing_woman","keywords":["さーふぃんをするじょせい","さーふぁー","さーふぃん","なみのり","じょせい","おんな","サーフィンをする女性","サーファー","サーフィン","波乗り","女性","女","おんな","sports","ocean","sea","summer","beach","woman","female"]},{"category":"activity","char":"🏄","name":"surfing_man","keywords":["さーふぃんをするひと","さーふぁー","さーふぃん","なみのり","サーフィンをする人","サーファー","サーフィン","波乗り","sports","ocean","sea","summer","beach"]},{"category":"activity","char":"🛀","name":"bath","keywords":["ふろ","よくそう","風呂","浴槽","clean","shower","bathroom"]},{"category":"activity","char":"⛹️‍♀️","name":"basketball_woman","keywords":["ぼーるをばうんどさせるじょせい","ぼーる","じょせい","おんな","ボールをバウンドさせる女性","ボール","女性","女","おんな","sports","human","woman","female"]},{"category":"activity","char":"⛹","name":"basketball_man","keywords":["ぼーるをばうんどさせるひと","ぼーる","ボールをバウンドさせる人","ボール","sports","human"]},{"category":"activity","char":"🏋️‍♀️","name":"weight_lifting_woman","keywords":["うえいとをもちあげるじょせい","あげ","じゅうりょう","じょせい","おんな","ウエイトを持ち上げる女性","挙げ","重量","女性","女","おんな","sports","training","exercise","woman","female"]},{"category":"activity","char":"🏋","name":"weight_lifting_man","keywords":["うえいとをもちあげるひと","あげ","じゅうりょう","ウエイトを持ち上げる人","挙げ","重量","sports","training","exercise"]},{"category":"activity","char":"🚴‍♀️","name":"biking_woman","keywords":["じてんしゃにのるじょせい","じてんしゃ","じてんしゃのり","じてんしゃにのるひと","さいくりすと","じょせい","おんな","自転車に乗る女性","自転車","自転車乗り","自転車に乗る人","サイクリスト","女性","女","おんな","sports","bike","exercise","hipster","woman","female"]},{"category":"activity","char":"🚴","name":"biking_man","keywords":["じてんしゃにのるひと","じてんしゃ","じてんしゃのり","さいくりすと","自転車に乗る人","自転車","自転車乗り","サイクリスト","sports","bike","exercise","hipster"]},{"category":"activity","char":"🚵‍♀️","name":"mountain_biking_woman","keywords":["まうんてんばいくにのるじょせい","まうんてんばいくらいだー","くろすばいく","じてんしゃ","じてんしゃのり","じてんしゃにのるひと","さいくりすと","やま","じょせい","おんな","マウンテンバイクに乗る女性","マウンテンバイクライダー","クロスバイク","自転車","自転車乗り","自転車に乗る人","サイクリスト","山","女性","女","おんな","transportation","sports","human","race","bike","woman","female"]},{"category":"activity","char":"🚵","name":"mountain_biking_man","keywords":["まうんてんばいくにのるひと","まうんてんばいくらいだー","くろすばいく","じてんしゃ","じてんしゃのり","じてんしゃにのるひと","やま","マウンテンバイクに乗る人","マウンテンバイクライダー","クロスバイク","自転車","自転車乗り","自転車に乗る人","山","transportation","sports","human","race","bike"]},{"category":"activity","char":"🏇","name":"horse_racing","keywords":["けいば","うま","きしゅ","きょうそうば","競馬","馬","騎手","競走馬","animal","betting","competition","gambling","luck"]},{"category":"activity","char":"🤿","name":"diving_mask","keywords":["だいびんぐますく","だいびんぐ","すきゅーば","しゅのーける","ダイビングマスク","ダイビング","スキューバ","シュノーケル","sports"]},{"category":"activity","char":"🪀","name":"yo_yo","keywords":["よーよー","おもちゃ","じょうげ","ヨーヨー","おもちゃ","上下","sports"]},{"category":"activity","char":"🪁","name":"kite","keywords":["たこ","おもちゃ","とぶ","まう","凧","おもちゃ","飛ぶ","舞う","sports"]},{"category":"activity","char":"🦺","name":"safety_vest","keywords":["あんぜんべすと","きんきゅう","あんぜん","べすと","安全ベスト","緊急","安全","ベスト","sports"]},{"category":"activity","char":"🪡","name":"sewing_needle","keywords":["ぬいはり","ししゅう","さいほう","ぬいめ","ほうごう","したて","縫い針","刺しゅう","裁縫","縫い目","縫合","仕立て"]},{"category":"activity","char":"🪢","name":"knot","keywords":["むすびめ","ろーぷ","からんだ","ひも","よりいと","ねじれ","結び目","ロープ","絡んだ","ひも","より糸","ねじれ"]},{"category":"activity","char":"🕴","name":"business_suit_levitating","keywords":["ちゅうにういたすーつのひと","びじねす","すーつ","宙に浮いたスーツの人","ビジネス","スーツ","suit","business","levitate","hover","jump"]},{"category":"activity","char":"🏆","name":"trophy","keywords":["とろふぃー","しょう","トロフィー","賞","win","award","contest","place","ftw","ceremony"]},{"category":"activity","char":"🎽","name":"running_shirt_with_sash","keywords":["らんにんぐしゃつとたすき","らんにんぐ","たすき","しゃつ","ランニングシャツと襷","ランニング","襷","シャツ","play","pageant"]},{"category":"activity","char":"🏅","name":"medal_sports","keywords":["すぽーつのめだる","めだる","スポーツのメダル","メダル","award","winning"]},{"category":"activity","char":"🎖","name":"medal_military","keywords":["くんしょう","おいわい","めだる","ぐんじ","勲章","お祝い","メダル","軍事","award","winning","army"]},{"category":"activity","char":"🥇","name":"1st_place_medal","keywords":["きんめだる","1い","きん","めだる","1","だい1い","金メダル","1位","金","メダル","1","第1位","award","winning","first"]},{"category":"activity","char":"🥈","name":"2nd_place_medal","keywords":["ぎんめだる","めだる","2い","ぎん","2","だい2い","銀メダル","メダル","2位","銀","2","第2位","award","second"]},{"category":"activity","char":"🥉","name":"3rd_place_medal","keywords":["どうめだる","どう","めだる","3い","3","だい3い","銅メダル","銅","メダル","3位","3","第3位","award","third"]},{"category":"activity","char":"🎗","name":"reminder_ribbon","keywords":["りまいんだーりぼん","おいわい","りまいんだー","りぼん","リマインダーリボン","お祝い","リマインダー","リボン","sports","cause","support","awareness"]},{"category":"activity","char":"🏵","name":"rosette","keywords":["ばらかざり","しょくぶつ","バラ飾り","植物","flower","decoration","military"]},{"category":"activity","char":"🎫","name":"ticket","keywords":["きっぷ","あくてぃびてぃ","にゅうじょうりょう","えんたーていめんと","ちけっと","きっぷ","アクティビティ","入場料","エンターテイメント","チケット","event","concert","pass"]},{"category":"activity","char":"🎟","name":"tickets","keywords":["にゅうじょうけん","にゅうじょうりょう","えんたーていめんと","ちけっと","入場券","入場料","エンターテイメント","チケット","sports","concert","entrance"]},{"category":"activity","char":"🎭","name":"performing_arts","keywords":["ぶたいげいじゅつ","あくてぃびてぃ","げいじゅつ","えんたーていめんと","かめん","ぶたい","しあたー","舞台芸術","アクティビティ","芸術","エンターテイメント","仮面","舞台","シアター","acting","theater","drama"]},{"category":"activity","char":"🎨","name":"art","keywords":["えのぐぱれっと","あくてぃびてぃ","あーと","えんたーていめんと","びじゅつかん","かいが","ぱれっと","絵の具パレット","アクティビティ","アート","エンターテイメント","美術館","絵画","パレット","design","paint","draw","colors"]},{"category":"activity","char":"🎪","name":"circus_tent","keywords":["さーかすごや","あくてぃびてぃ","さーかす","えんたーていめんと","てんと","サーカス小屋","アクティビティ","サーカス","エンターテイメント","テント","festival","carnival","party"]},{"category":"activity","char":"🤹‍♀️","name":"woman_juggling","keywords":["じゃぐりんぐをするじょせい","てんびん","じゃぐりんぐ","じょせい","おんな","ジャグリングをする女性","天秤","ジャグリング","女性","女","おんな","juggle","balance","skill","multitask"]},{"category":"activity","char":"🤹‍♂️","name":"man_juggling","keywords":["じゃぐりんぐをするだんせい","てんびん","じゃぐりんぐ","だんせい","おとこ","ジャグリングをする男性","天秤","ジャグリング","男性","男","おとこ","juggle","balance","skill","multitask"]},{"category":"activity","char":"🎤","name":"microphone","keywords":["まいく","あくてぃびてぃ","えんたーていめんと","からおけ","まいくろふぉん","マイク","アクティビティ","エンターテイメント","カラオケ","マイクロフォン","sound","music","PA","sing","talkshow"]},{"category":"activity","char":"🎧","name":"headphones","keywords":["へっどほん","あくてぃびてぃ","いやほん","えんたーていめんと","へっどふぉん","ヘッドホン","アクティビティ","イヤホン","エンターテイメント","ヘッドフォン","music","score","gadgets"]},{"category":"activity","char":"🎼","name":"musical_score","keywords":["がくふ","あくてぃびてぃ","えんたーていめんと","おんがく","楽譜","アクティビティ","エンターテイメント","音楽","treble","clef","compose"]},{"category":"activity","char":"🎹","name":"musical_keyboard","keywords":["けんばん","あくてぃびてぃ","えんたーていめんと","がっき","きーぼーど","おんがく","ぴあの","鍵盤","アクティビティ","エンターテイメント","楽器","キーボード","音楽","ピアノ","piano","instrument","compose"]},{"category":"activity","char":"🪇","name":"maracas","keywords":["まらかす","いわう","がっき","おんがく","そうおん","だがっき","がたがた","りずむ","しぇいく","マラカス","祝う","楽器","音楽","騒音","打楽器","ガタガタ","リズム","シェイク","instrument","music","percussion","rattle","shake"]},{"category":"activity","char":"🥁","name":"drum","keywords":["どらむ","どらむすてぃっく","おんがく","ドラム","ドラムスティック","音楽","music","instrument","drumsticks","snare"]},{"category":"activity","char":"🎷","name":"saxophone","keywords":["さっくす","あくてぃびてぃ","えんたーていめんと","がっき","おんがく","さくそふぉーん","サックス","アクティビティ","エンターテイメント","楽器","音楽","サクソフォーン","music","instrument","jazz","blues"]},{"category":"activity","char":"🎺","name":"trumpet","keywords":["とらんぺっと","あくてぃびてぃ","えんたーていめんと","がっき","おんがく","トランペット","アクティビティ","エンターテイメント","楽器","音楽","music","brass"]},{"category":"activity","char":"🪈","name":"flute","keywords":["ふるーと","たけ","よこぶえそうしゃ","ふるーとそうしゃ","おんがく","ぱいぷ","りこーだー","ふく","もっかんがっき","フルート","竹","横笛奏者","フルート奏者","音楽","パイプ","リコーダー","吹く","木管楽器","music","fife","pipe","recorder","woodwind"]},{"category":"activity","char":"🎸","name":"guitar","keywords":["ぎたー","あくてぃびてぃ","えんたーていめんと","がっき","おんがく","ギター","アクティビティ","エンターテイメント","楽器","音楽","music","instrument"]},{"category":"activity","char":"🎻","name":"violin","keywords":["ばいおりん","あくてぃびてぃ","えんたーていめんと","がっき","おんがく","バイオリン","アクティビティ","エンターテイメント","楽器","音楽","music","instrument","orchestra","symphony"]},{"category":"activity","char":"🪕","name":"banjo","keywords":["ばんじょー","あくてぃびてぃ","えんたーていめんと","がっき","おんがく","バンジョー","アクティビティ","エンターテイメント","楽器","音楽","music","instrument"]},{"category":"activity","char":"🪗","name":"accordion","keywords":["あこーでぃおん","こんさーてぃーな","すくいーずぼっくす","アコーディオン","コンサーティーナ","スクイーズボックス","music","instrument"]},{"category":"activity","char":"🪘","name":"long_drum","keywords":["ながいどらむ","びーと","こんが","どらむ","りずむ","じゃんべ","長いドラム","ビート","コンガ","ドラム","リズム","ジャンベ","music","instrument"]},{"category":"activity","char":"🎬","name":"clapper","keywords":["かちんこ","あくてぃびてぃ","えんたーていめんと","えいが","カチンコ","アクティビティ","エンターテイメント","映画","movie","film","record"]},{"category":"activity","char":"🎮","name":"video_game","keywords":["てれびげーむ","こんとろーらー","えんたーていめんと","げーむ","びでおげーむ","テレビゲーム","コントローラー","エンターテイメント","ゲーム","ビデオゲーム","play","console","PS4","controller"]},{"category":"activity","char":"👾","name":"space_invader","keywords":["えいりあん","うちゅうじん","かいじゅう","いせいじん","かお","おとぎばなし","ふぁんたじー","もんすたー","うちゅう","UFO","エイリアン","宇宙人","怪獣","異星人","顔","おとぎ話","ファンタジー","モンスター","宇宙","UFO","game","arcade","play"]},{"category":"activity","char":"🎯","name":"dart","keywords":["てきちゅう","あくてぃびてぃ","ぶる","ぶるずあい","だーつ","えんたーていめんと","め","しあい","ひっと","ひょうてき","的中","アクティビティ","ブル","ブルズアイ","ダーツ","エンターテイメント","目","試合","ヒット","標的","game","play","bar","target","bullseye"]},{"category":"activity","char":"🎲","name":"game_die","keywords":["さいころ","さい","えんたーていめんと","げーむ","サイコロ","さい","エンターテイメント","ゲーム","dice","random","tabletop","play","luck"]},{"category":"activity","char":"♟️","name":"chess_pawn","keywords":["ちぇすのぽーん","ちぇす","こま","げーむ","すてこま","チェスのポーン","チェス","駒","ゲーム","捨て駒","expendable"]},{"category":"activity","char":"🎰","name":"slot_machine","keywords":["すろっとましん","あくてぃびてぃ","げーむ","すろっと","スロットマシン","アクティビティ","ゲーム","スロット","bet","gamble","vegas","fruit machine","luck","casino"]},{"category":"activity","char":"🧩","name":"jigsaw","keywords":["ぱずるのぴーす","てがかり","かみあう","ぴーす","ぱずる","じぐそー","パズルのピース","手がかり","噛み合う","ピース","パズル","ジグソー","interlocking","puzzle","piece"]},{"category":"activity","char":"🎳","name":"bowling","keywords":["ぼうりんぐ","ぼーる","しあい","ボウリング","ボール","試合","sports","fun","play"]},{"category":"activity","char":"🪄","name":"magic_wand","keywords":["まほうのつえ","まほう","ぼう","まじょ","まほうつかい","魔法の杖","魔法","棒","魔女","魔法使い"]},{"category":"activity","char":"🪅","name":"pinata","keywords":["ぴにゃーた","おいわい","ぱーてぃー","ぴなーた","ピニャータ","お祝い","パーティー","ピナータ"]},{"category":"activity","char":"🪆","name":"nesting_dolls","keywords":["いれこにんぎょう","にんぎょう","いれこ","ろしあ","入れ子人形","人形","入れ子","ロシア"]},{"category":"activity","char":"🪬","name":"hamsa","keywords":["はむさ","おまもり","ふぁてぃま","て","めありー","みりあむ","ほご","ハムサ","お守り","ファティマ","手","メアリー","ミリアム","保護"]},{"category":"activity","char":"🪩","name":"mirror_ball","keywords":["みらーぼーる","だんす","でぃすこ","かがやき","ぱーてぃー","ミラーボール","ダンス","ディスコ","輝き","パーティー"]},{"category":"travel_and_places","char":"🚗","name":"red_car","keywords":["じどうしゃ","くるま","のりもの","自動車","車","乗り物","red","transportation","vehicle"]},{"category":"travel_and_places","char":"🚕","name":"taxi","keywords":["たくしー","のりもの","タクシー","乗り物","uber","vehicle","cars","transportation"]},{"category":"travel_and_places","char":"🚙","name":"blue_car","keywords":["きゃんぴんぐかー","れくりえーしょん","RV","のりもの","キャンピングカー","レクリエーション","RV","乗り物","transportation","vehicle"]},{"category":"travel_and_places","char":"🚌","name":"bus","keywords":["ばす","のりもの","バス","乗り物","car","vehicle","transportation"]},{"category":"travel_and_places","char":"🚎","name":"trolleybus","keywords":["とろりーばす","ばす","ろめんでんしゃ","しがいでんしゃ","のりもの","トロリーバス","バス","路面電車","市街電車","乗り物","bart","transportation","vehicle"]},{"category":"travel_and_places","char":"🏎","name":"racing_car","keywords":["れーしんぐかー","くるま","きょうそう","レーシングカー","車","競争","sports","race","fast","formula","f1"]},{"category":"travel_and_places","char":"🚓","name":"police_car","keywords":["ぱとかー","くるま","ぱとろーる","けいさつ","のりもの","パトカー","車","パトロール","警察","乗り物","vehicle","cars","transportation","law","legal","enforcement"]},{"category":"travel_and_places","char":"🚑","name":"ambulance","keywords":["きゅうきゅうしゃ","のりもの","救急車","乗り物","health","911","hospital"]},{"category":"travel_and_places","char":"🚒","name":"fire_engine","keywords":["しょうぼうしゃ","えんじん","えん","とらっく","のりもの","消防車","エンジン","炎","トラック","乗り物","transportation","cars","vehicle"]},{"category":"travel_and_places","char":"🚐","name":"minibus","keywords":["まいくろばす","ばす","のりもの","マイクロバス","バス","乗り物","vehicle","car","transportation"]},{"category":"travel_and_places","char":"🚚","name":"truck","keywords":["はいたつようとらっく","はいたつ","とらっく","のりもの","配達用トラック","配達","トラック","乗り物","cars","transportation"]},{"category":"travel_and_places","char":"🚛","name":"articulated_lorry","keywords":["とれーらー","おおがたとらっく","せみ","とらっく","のりもの","トレーラー","大型トラック","セミ","トラック","乗り物","vehicle","cars","transportation","express"]},{"category":"travel_and_places","char":"🚜","name":"tractor","keywords":["とらくたー","のりもの","トラクター","乗り物","vehicle","car","farming","agriculture"]},{"category":"travel_and_places","char":"🛴","name":"kick_scooter","keywords":["きっくぼーど","きっく","すくーたー","キックボード","キック","スクーター","vehicle","kick","razor"]},{"category":"travel_and_places","char":"🏍","name":"motorcycle","keywords":["れーすばいく","おーとばい","れーす","レースバイク","オートバイ","レース","race","sports","fast"]},{"category":"travel_and_places","char":"🚲","name":"bike","keywords":["じてんしゃ","ばいく","のりもの","自転車","バイク","乗り物","sports","bicycle","exercise","hipster"]},{"category":"travel_and_places","char":"🛵","name":"motor_scooter","keywords":["すくーたー","もーたー","スクーター","モーター","vehicle","vespa","sasha"]},{"category":"travel_and_places","char":"🦽","name":"manual_wheelchair","keywords":["しゅどうくるまいす","あくせしびりてぃ","くるまいす","手動車いす","アクセシビリティ","車いす","vehicle"]},{"category":"travel_and_places","char":"🦼","name":"motorized_wheelchair","keywords":["でんどうくるまいす","あくせしびりてぃ","くるまいす","電動車いす","アクセシビリティ","車いす","vehicle"]},{"category":"travel_and_places","char":"🛺","name":"auto_rickshaw","keywords":["おーとりきしゃ","じんりきしゃ","とぅくとぅく","オートリキシャ","人力車","トゥクトゥク","vehicle"]},{"category":"travel_and_places","char":"🪂","name":"parachute","keywords":["ぱらしゅーと","ぱらせーる","すかいだいぶ","はんぐぐらいだー","パラシュート","パラセール","スカイダイブ","ハンググライダー","vehicle"]},{"category":"travel_and_places","char":"🚨","name":"rotating_light","keywords":["ぱとらいと","くるま","ひかり","けいさつ","かいてん","のりもの","さいれん","けいこく","パトライト","車","光","警察","回転","乗り物","サイレン","警告","police","ambulance","911","emergency","alert","error","pinged","law","legal"]},{"category":"travel_and_places","char":"🚔","name":"oncoming_police_car","keywords":["ぱとかー","くるま","たいこうしゃ","けいさつ","のりもの","パトカー","車","対向車","警察","乗り物","vehicle","law","legal","enforcement","911"]},{"category":"travel_and_places","char":"🚍","name":"oncoming_bus","keywords":["ばす","たいこうしゃ","のりもの","バス","対向車","乗り物","vehicle","transportation"]},{"category":"travel_and_places","char":"🚘","name":"oncoming_automobile","keywords":["たいこうしゃ","じどうしゃ","くるま","のりもの","対向車","自動車","車","乗り物","car","vehicle","transportation"]},{"category":"travel_and_places","char":"🚖","name":"oncoming_taxi","keywords":["たくしー","たいこうしゃ","のりもの","タクシー","対向車","乗り物","vehicle","cars","uber"]},{"category":"travel_and_places","char":"🚡","name":"aerial_tramway","keywords":["ろーぷうぇい","くうちゅう","けーぶる","くるま","ごんどら","とらむうぇい","のりもの","ロープウェイ","空中","ケーブル","車","ゴンドラ","トラムウェイ","乗り物","transportation","vehicle","ski"]},{"category":"travel_and_places","char":"🚠","name":"mountain_cableway","keywords":["ろーぷうぇい","けーぶる","ごんどら","やま","のりもの","ロープウェイ","ケーブル","ゴンドラ","山","乗り物","transportation","vehicle","ski"]},{"category":"travel_and_places","char":"🚟","name":"suspension_railway","keywords":["こうかてつどう","てつどう","のりもの","高架鉄道","鉄道","乗り物","vehicle","transportation"]},{"category":"travel_and_places","char":"🚃","name":"railway_car","keywords":["てつどうしゃりょう","くるま","でんき","てつどう","れっしゃ","ろめん","とろりーばす","のりもの","鉄道車両","車","電気","鉄道","列車","路面","トロリーバス","乗り物","transportation","vehicle","train"]},{"category":"travel_and_places","char":"🚋","name":"train","keywords":["ろめんでんしゃ","くるま","ろめん","とろりーばす","のりもの","路面電車","車","路面","トロリーバス","乗り物","transportation","vehicle","carriage","public","travel"]},{"category":"travel_and_places","char":"🚝","name":"monorail","keywords":["ものれーる","のりもの","モノレール","乗り物","transportation","vehicle"]},{"category":"travel_and_places","char":"🚄","name":"bullettrain_side","keywords":["しんかんせん","てつどう","こうそく","れっしゃ","のりもの","新幹線","鉄道","高速","列車","乗り物","transportation","vehicle"]},{"category":"travel_and_places","char":"🚅","name":"bullettrain_front","keywords":["しんかんせん","だんがん","てつどう","こうそく","れっしゃ","のりもの","新幹線","弾丸","鉄道","高速","列車","乗り物","transportation","vehicle","speed","fast","public","travel"]},{"category":"travel_and_places","char":"🚈","name":"light_rail","keywords":["らいとれーる","てつどう","のりもの","ライトレール","鉄道","乗り物","transportation","vehicle"]},{"category":"travel_and_places","char":"🚞","name":"mountain_railway","keywords":["さんがくてつどう","くるま","やま","てつどう","のりもの","山岳鉄道","車","山","鉄道","乗り物","transportation","vehicle"]},{"category":"travel_and_places","char":"🚂","name":"steam_locomotive","keywords":["じょうききかんしゃ","えんじん","きかんしゃ","てつどう","じょうき","れっしゃ","のりもの","蒸気機関車","エンジン","機関車","鉄道","蒸気","列車","乗り物","transportation","vehicle","train"]},{"category":"travel_and_places","char":"🚆","name":"train2","keywords":["でんしゃ","せんろ","のりもの","電車","線路","乗り物","transportation","vehicle"]},{"category":"travel_and_places","char":"🚇","name":"metro","keywords":["ちかてつ","めとろ","のりもの","地下鉄","メトロ","乗り物","transportation","blue-square","mrt","underground","tube"]},{"category":"travel_and_places","char":"🚊","name":"tram","keywords":["ろめんでんしゃ","とろりーばす","のりもの","路面電車","トロリーバス","乗り物","transportation","vehicle"]},{"category":"travel_and_places","char":"🚉","name":"station","keywords":["えき","せんろ","でんしゃ","のりもの","駅","線路","電車","乗り物","transportation","vehicle","public"]},{"category":"travel_and_places","char":"🛸","name":"flying_saucer","keywords":["そらとぶえんばん","UFO","うちゅうじん","いほしじん","うちゅう","くうそう","空飛ぶ円盤","UFO","宇宙人","異星人","宇宙","空想","transportation","vehicle","ufo"]},{"category":"travel_and_places","char":"🚁","name":"helicopter","keywords":["へりこぷたー","のりもの","ヘリコプター","乗り物","transportation","vehicle","fly"]},{"category":"travel_and_places","char":"🛩","name":"small_airplane","keywords":["こがたこうくうき","ひこうき","のりもの","小型航空機","飛行機","乗り物","flight","transportation","fly","vehicle"]},{"category":"travel_and_places","char":"✈️","name":"airplane","keywords":["ひこうき","のりもの","飛行機","乗り物","vehicle","transportation","flight","fly"]},{"category":"travel_and_places","char":"🛫","name":"flight_departure","keywords":["ひこうきのりりく","ひこうき","ちぇっくいん","しゅっぱつ","のりもの","飛行機の離陸","飛行機","チェックイン","出発","乗り物","airport","flight","landing"]},{"category":"travel_and_places","char":"🛬","name":"flight_arrival","keywords":["ひこうきのちゃくりく","ひこうき","とうちゃく","ちゃくりく","のりもの","飛行機の着陸","飛行機","到着","着陸","乗り物","airport","flight","boarding"]},{"category":"travel_and_places","char":"⛵","name":"sailboat","keywords":["よっと","ぼーと","りぞーと","うみ","のりもの","ヨット","ボート","リゾート","海","乗り物","ship","summer","transportation","water","sailing"]},{"category":"travel_and_places","char":"🛥","name":"motor_boat","keywords":["もーたーぼーと","ぼーと","のりもの","モーターボート","ボート","乗り物","ship"]},{"category":"travel_and_places","char":"🚤","name":"speedboat","keywords":["すぴーどぼーと","ぼーと","のりもの","スピードボート","ボート","乗り物","ship","transportation","vehicle","summer"]},{"category":"travel_and_places","char":"⛴","name":"ferry","keywords":["ふぇりー","ぼーと","フェリー","ボート","boat","ship","yacht"]},{"category":"travel_and_places","char":"🛳","name":"passenger_ship","keywords":["りょかくせん","りょかく","ふね","のりもの","旅客船","旅客","船","乗り物","yacht","cruise","ferry"]},{"category":"travel_and_places","char":"🚀","name":"rocket","keywords":["ろけっと","うちゅう","のりもの","ロケット","宇宙","乗り物","launch","ship","staffmode","NASA","outer space","outer_space","fly"]},{"category":"travel_and_places","char":"🛰","name":"artificial_satellite","keywords":["さてらいと","えいせい","うちゅう","のりもの","サテライト","衛星","宇宙","乗り物","communication","gps","orbit","spaceflight","NASA","ISS"]},{"category":"travel_and_places","char":"🛻","name":"pickup_truck","keywords":["ぴっくあっぷとらっく","ぴっくあっぷ","とらっく","のりもの","ピックアップトラック","ピックアップ","トラック","乗り物","car"]},{"category":"travel_and_places","char":"🛼","name":"roller_skate","keywords":["ろーらーすけーと","ろーらー","すけーと","ローラースケート","ローラー","スケート"]},{"category":"travel_and_places","char":"💺","name":"seat","keywords":["ざせき","いす","座席","椅子","sit","airplane","transport","bus","flight","fly"]},{"category":"travel_and_places","char":"🛶","name":"canoe","keywords":["かぬー","ぼーと","カヌー","ボート","boat","paddle","water","ship"]},{"category":"travel_and_places","char":"⚓","name":"anchor","keywords":["いかり","ふね","つーる","いかり","船","ツール","ship","ferry","sea","boat"]},{"category":"travel_and_places","char":"🚧","name":"construction","keywords":["こうじちゅう","こうじようふぇんす","けんせつこうじ","工事中","工事用フェンス","建設工事","wip","progress","caution","warning"]},{"category":"travel_and_places","char":"⛽","name":"fuelpump","keywords":["がそりんすたんど","ねんりょう","がそりん","きゅうゆき","さーびすすてーしょん","ガソリンスタンド","燃料","ガソリン","給油機","サービスステーション","gas station","petroleum"]},{"category":"travel_and_places","char":"🚏","name":"busstop","keywords":["ばすてい","ばす","ていし","バス停","バス","停止","transportation","wait"]},{"category":"travel_and_places","char":"🚦","name":"vertical_traffic_light","keywords":["たてむきのしんごうき","しんごうき","しんごう","こうつう","縦向きの信号機","信号機","信号","交通","transportation","driving"]},{"category":"travel_and_places","char":"🚥","name":"traffic_light","keywords":["よこむきのしんごうき","しんごうき","しんごう","こうつう","横向きの信号機","信号機","信号","交通","transportation","signal"]},{"category":"travel_and_places","char":"🏁","name":"checkered_flag","keywords":["ちぇっかーふらっぐ","いちまつもよう","はた","れーす","チェッカーフラッグ","市松模様","旗","レース","contest","finishline","race","gokart"]},{"category":"travel_and_places","char":"🚢","name":"ship","keywords":["ふね","のりもの","船","乗り物","transportation","titanic","deploy"]},{"category":"travel_and_places","char":"🎡","name":"ferris_wheel","keywords":["かんらんしゃ","あくてぃびてぃ","ゆうえんち","えんたーていめんと","ふぇりす","観覧車","アクティビティ","遊園地","エンターテイメント","フェリス","photo","carnival","londoneye"]},{"category":"travel_and_places","char":"🎢","name":"roller_coaster","keywords":["じぇっとこーすたー","あくてぃびてぃ","ゆうえんち","こーすたー","えんたーていめんと","ろーらー","ジェットコースター","アクティビティ","遊園地","コースター","エンターテイメント","ローラー","carnival","playground","photo","fun"]},{"category":"travel_and_places","char":"🎠","name":"carousel_horse","keywords":["めりーごーらんど","あくてぃびてぃ","めりーごーらうんど","えんたーていめんと","うま","メリーゴーランド","アクティビティ","メリーゴーラウンド","エンターテイメント","馬","photo","carnival"]},{"category":"travel_and_places","char":"🏗","name":"building_construction","keywords":["けんせつちゅう","たてもの","けんせつ","建設中","建物","建設","wip","working","progress"]},{"category":"travel_and_places","char":"🌁","name":"foggy","keywords":["きり","てんき","霧","天気","photo","mountain"]},{"category":"travel_and_places","char":"🏭","name":"factory","keywords":["こうじょう","たてもの","工場","建物","building","industry","pollution","smoke"]},{"category":"travel_and_places","char":"⛲","name":"fountain","keywords":["ふんすい","噴水","photo","summer","water","fresh"]},{"category":"travel_and_places","char":"🎑","name":"rice_scene","keywords":["おつきみ","あくてぃびてぃ","おいわい","じゅしょうしき","えんたーていめんと","つき","お月見","アクティビティ","お祝い","授賞式","エンターテイメント","月","photo","japan","asia","tsukimi"]},{"category":"travel_and_places","char":"⛰","name":"mountain","keywords":["やま","山","photo","nature","environment"]},{"category":"travel_and_places","char":"🏔","name":"mountain_snow","keywords":["ゆきやま","さむい","やま","ゆき","雪山","寒い","山","雪","photo","nature","environment","winter","cold"]},{"category":"travel_and_places","char":"🗻","name":"mount_fuji","keywords":["ふじさん","やま","富士山","山","photo","mountain","nature","japanese"]},{"category":"travel_and_places","char":"🌋","name":"volcano","keywords":["かざん","ふんか","やま","きしょう","火山","噴火","山","気象","photo","nature","disaster"]},{"category":"travel_and_places","char":"🗾","name":"japan","keywords":["にっぽんれっとう","にっぽん","ちず","日本列島","日本","地図","nation","country","japanese","asia"]},{"category":"travel_and_places","char":"🏕","name":"camping","keywords":["きゃんぷ","キャンプ","photo","outdoors","tent"]},{"category":"travel_and_places","char":"⛺","name":"tent","keywords":["てんと","きゃんぷ","テント","キャンプ","photo","camping","outdoors"]},{"category":"travel_and_places","char":"🏞","name":"national_park","keywords":["こくりつこうえん","こうえん","国立公園","公園","photo","environment","nature"]},{"category":"travel_and_places","char":"🛣","name":"motorway","keywords":["こうそくどうろ","はいうぇい","どうろ","高速道路","ハイウェイ","道路","road","cupertino","interstate","highway"]},{"category":"travel_and_places","char":"🛤","name":"railway_track","keywords":["せんろ","てつどう","でんしゃ","線路","鉄道","電車","train","transportation"]},{"category":"travel_and_places","char":"🌅","name":"sunrise","keywords":["ひので","あさ","たいよう","てんこう","日の出","朝","太陽","天候","morning","view","vacation","photo"]},{"category":"travel_and_places","char":"🌄","name":"sunrise_over_mountains","keywords":["やまからのひので","あさ","やま","たいよう","ひので","てんこう","山からの日の出","朝","山","太陽","日の出","天候","view","vacation","photo"]},{"category":"travel_and_places","char":"🏜","name":"desert","keywords":["さばく","砂漠","photo","warm","saharah"]},{"category":"travel_and_places","char":"🏖","name":"beach_umbrella","keywords":["びーちとかさ","びーち","かさ","ぱらそる","ビーチと傘","ビーチ","傘","パラソル","weather","summer","sunny","sand","mojito"]},{"category":"travel_and_places","char":"🏝","name":"desert_island","keywords":["むじんとう","さばく","しま","無人島","砂漠","島","photo","tropical","mojito"]},{"category":"travel_and_places","char":"🌇","name":"city_sunrise","keywords":["びるにしずむゆうひ","たてもの","ゆうぐれ","たいよう","ゆうひ","てんき","ビルに沈む夕陽","建物","夕暮れ","太陽","夕日","天気","photo","good morning","dawn"]},{"category":"travel_and_places","char":"🌆","name":"city_sunset","keywords":["ゆうぐれのまちなみ","たてもの","まち","ゆうぐれ","ひぐれ","ふうけい","たいよう","ゆうひ","てんき","夕暮れの街並み","建物","街","夕暮れ","日暮れ","風景","太陽","夕日","天気","photo","evening","sky","buildings"]},{"category":"travel_and_places","char":"🏙","name":"cityscape","keywords":["まちなみ","たてもの","まち","街並み","建物","街","photo","night life","urban"]},{"category":"travel_and_places","char":"🌃","name":"night_with_stars","keywords":["ほしぞら","よる","ほし","てんき","星空","夜","星","天気","evening","city","downtown"]},{"category":"travel_and_places","char":"🌉","name":"bridge_at_night","keywords":["よるのはし","はし","よる","てんき","夜の橋","橋","夜","天気","photo","sanfrancisco"]},{"category":"travel_and_places","char":"🌌","name":"milky_way","keywords":["あまのがわ","うちゅう","てんき","天の川","宇宙","天気","photo","space","stars"]},{"category":"travel_and_places","char":"🌠","name":"stars","keywords":["ながれぼし","あくてぃびてぃ","らっか","ながれる","うちゅう","ほし","流れ星","アクティビティ","落下","流れる","宇宙","星","night","photo"]},{"category":"travel_and_places","char":"🎇","name":"sparkler","keywords":["せんこうはなび","あくてぃびてぃ","おいわい","えんたーていめんと","はなび","きらきら","線香花火","アクティビティ","お祝い","エンターテイメント","花火","キラキラ","stars","night","shine"]},{"category":"travel_and_places","char":"🎆","name":"fireworks","keywords":["はなび","あくてぃびてぃ","おいわい","えんたーていめんと","花火","アクティビティ","お祝い","エンターテイメント","photo","festival","carnival","congratulations"]},{"category":"travel_and_places","char":"🌈","name":"rainbow","keywords":["にじ","あめ","れいんぼー","てんき","ぷらいど","lgbt","虹","雨","レインボー","天気","プライド","lgbt","nature","happy","unicorn_face","photo","sky","spring"]},{"category":"travel_and_places","char":"🏘","name":"houses","keywords":["いえ","たてもの","家","建物","buildings","photo"]},{"category":"travel_and_places","char":"🏰","name":"european_castle","keywords":["せいようのしろ","たてもの","しろ","よーろっぱ","西洋の城","建物","城","ヨーロッパ","building","royalty","history"]},{"category":"travel_and_places","char":"🏯","name":"japanese_castle","keywords":["にっぽんのしろ","たてもの","しろ","にっぽん","日本の城","建物","城","日本","photo","building"]},{"category":"travel_and_places","char":"🗼","name":"tokyo_tower","keywords":["とうきょうたわー","とうきょう","たわー","東京タワー","東京","タワー","photo","japanese"]},{"category":"travel_and_places","char":"","name":"shibuya_109","keywords":["しぶや109","SHIBUYA109","109","渋谷109","SHIBUYA109","109","photo","japanese"]},{"category":"travel_and_places","char":"🏟","name":"stadium","keywords":["すたじあむ","スタジアム","photo","place","sports","concert","venue"]},{"category":"travel_and_places","char":"🗽","name":"statue_of_liberty","keywords":["じゆうのめがみ","じゆう","ぞう","自由の女神","自由","像","american","newyork"]},{"category":"travel_and_places","char":"🏠","name":"house","keywords":["いえ","たてもの","じたく","家","建物","自宅","building","home"]},{"category":"travel_and_places","char":"🏡","name":"house_with_garden","keywords":["にわつきのいえ","たてもの","にわ","じたく","いえ","庭付きの家","建物","庭","自宅","家","home","plant","nature"]},{"category":"travel_and_places","char":"🏚","name":"derelict_house","keywords":["はいきょ","たてもの","はいおく","いえ","廃墟","建物","廃屋","家","abandon","evict","broken","building"]},{"category":"travel_and_places","char":"🏢","name":"office","keywords":["おふぃすびる","たてもの","オフィスビル","建物","building","bureau","work"]},{"category":"travel_and_places","char":"🏬","name":"department_store","keywords":["でぱーと","たてもの","てん","デパート","建物","店","building","shopping","mall"]},{"category":"travel_and_places","char":"🏣","name":"post_office","keywords":["にっぽんのゆうびんきょく","たてもの","にっぽん","ぽすと","日本の郵便局","建物","日本","ポスト","building","envelope","communication"]},{"category":"travel_and_places","char":"🏤","name":"european_post_office","keywords":["よーろっぱのゆうびんきょく","たてもの","よーろっぱ","ぽすと","ヨーロッパの郵便局","建物","ヨーロッパ","ポスト","building","email"]},{"category":"travel_and_places","char":"🏥","name":"hospital","keywords":["びょういん","たてもの","いし","くすり","病院","建物","医師","薬","building","health","surgery","doctor"]},{"category":"travel_and_places","char":"🏦","name":"bank","keywords":["ぎんこう","たてもの","銀行","建物","building","money","sales","cash","business","enterprise"]},{"category":"travel_and_places","char":"🏨","name":"hotel","keywords":["ほてる","たてもの","ホテル","建物","building","accomodation","checkin"]},{"category":"travel_and_places","char":"🏪","name":"convenience_store","keywords":["こんびにえんすすとあ","たてもの","こんびにえんす","すとあ","コンビニエンスストア","建物","コンビニエンス","ストア","building","shopping","groceries"]},{"category":"travel_and_places","char":"🏫","name":"school","keywords":["がっこう","たてもの","学校","建物","building","student","education","learn","teach"]},{"category":"travel_and_places","char":"🏩","name":"love_hotel","keywords":["らぶほてる","たてもの","ほてる","らぶ","ラブホテル","建物","ホテル","ラブ","like","affection","dating"]},{"category":"travel_and_places","char":"💒","name":"wedding","keywords":["けっこんしき","あくてぃびてぃ","ちゃぺる","ろまんす","結婚式","アクティビティ","チャペル","ロマンス","love","like","affection","couple","marriage","bride","groom"]},{"category":"travel_and_places","char":"🏛","name":"classical_building","keywords":["れきしてきなたてもの","たてもの","れきしてきな","歴史的な建物","建物","歴史的な","art","culture","history"]},{"category":"travel_and_places","char":"⛪","name":"church","keywords":["きょうかい","たてもの","くりすちゃん","じゅうじか","しゅうきょう","教会","建物","クリスチャン","十字架","宗教","building","religion","christ"]},{"category":"travel_and_places","char":"🕌","name":"mosque","keywords":["もすく","いすらむ","むすりむ","しゅうきょう","モスク","イスラム","ムスリム","宗教","islam","worship","minaret"]},{"category":"travel_and_places","char":"🕍","name":"synagogue","keywords":["しなごーぐ","ゆだやじん","ゆだやきょう","しゅうきょう","かいどう","シナゴーグ","ユダヤ人","ユダヤ教","宗教","会堂","judaism","worship","temple","jewish"]},{"category":"travel_and_places","char":"🕋","name":"kaaba","keywords":["かあば","いすらむ","むすりむ","しゅうきょう","カアバ","イスラム","ムスリム","宗教","mecca","mosque","islam"]},{"category":"travel_and_places","char":"⛩","name":"shinto_shrine","keywords":["じんじゃ","しゅうきょう","しんとう","神社","宗教","神道","temple","japan","kyoto"]},{"category":"travel_and_places","char":"🛕","name":"hindu_temple","keywords":["ひんどぅーきょうじいん","ひんどぅーきょう","じいん","しゅうきょう","ヒンドゥー教寺院","ヒンドゥー教","寺院","宗教","temple"]},{"category":"travel_and_places","char":"🪨","name":"rock","keywords":["ろっく","いわ","けんぞうぶつ","おもい","こたい","いし","ロック","岩","建造物","重い","固体","石"]},{"category":"travel_and_places","char":"🪵","name":"wood","keywords":["もくざい","けんぞうぶつ","まるた","ざいもく","はた","木材","建造物","丸太","材木","木"]},{"category":"travel_and_places","char":"🛖","name":"hut","keywords":["こや","いえ","せんけいこ","ぱお","小屋","家","扇形庫","パオ"]},{"category":"travel_and_places","char":"🛝","name":"playground_slide","keywords":["すべりだい","ゆうえんち","あそび","滑り台","遊園地","遊び"]},{"category":"travel_and_places","char":"🛞","name":"wheel","keywords":["しゃりん","えん","たいや","かいてん","車輪","円","タイヤ","回転"]},{"category":"travel_and_places","char":"🛟","name":"ring_buoy","keywords":["きゅうめいうきわ","うきわ","らいふじゃけっと","らいふせーばー","きゅうじょ","あんぜん","救命浮き輪","浮き輪","ライフジャケット","ライフセーバー","救助","安全"]},{"category":"objects","char":"⌚","name":"watch","keywords":["うでどけい","とけい","腕時計","時計","time","accessories"]},{"category":"objects","char":"📱","name":"iphone","keywords":["けいたいでんわ","けいたい","こみゅにけーしょん","もばいる","でんわ","携帯電話","携帯","コミュニケーション","モバイル","電話","technology","apple","gadgets","dial"]},{"category":"objects","char":"📲","name":"calling","keywords":["ちゃくしんちゅう","やじるし","つうわ","けいたい","こみゅにけーしょん","もばいる","けいたいでんわ","じゅしん","でんわ","着信中","矢印","通話","携帯","コミュニケーション","モバイル","携帯電話","受信","電話","iphone","incoming"]},{"category":"objects","char":"💻","name":"computer","keywords":["ぱそこん","のーとぱそこん","こんぴゅーたー","ぱーそなる","パソコン","ノートパソコン","コンピューター","パーソナル","technology","laptop","screen","display","monitor"]},{"category":"objects","char":"⌨","name":"keyboard","keywords":["きーぼーど","こんぴゅーたー","キーボード","コンピューター","technology","computer","type","input","text"]},{"category":"objects","char":"🖥","name":"desktop_computer","keywords":["ですくとっぷぱそこん","こんぴゅーたー","ですくとっぷ","デスクトップパソコン","コンピューター","デスクトップ","technology","computing","screen"]},{"category":"objects","char":"🖨","name":"printer","keywords":["ぷりんたー","こんぴゅーたー","プリンター","コンピューター","paper","ink"]},{"category":"objects","char":"🖱","name":"computer_mouse","keywords":["3ぼたんまうす","3","ぼたん","こんぴゅーたー","まうす","さん","3ボタンマウス","3","ボタン","コンピューター","マウス","三","click"]},{"category":"objects","char":"🖲","name":"trackball","keywords":["とらっくぼーる","こんぴゅーたー","トラックボール","コンピューター","technology","trackpad"]},{"category":"objects","char":"🕹","name":"joystick","keywords":["じょいすてぃっく","えんたーていめんと","げーむ","びでおげーむ","ジョイスティック","エンターテイメント","ゲーム","ビデオゲーム","game","play"]},{"category":"objects","char":"🗜","name":"clamp","keywords":["あっしゅく","つーる","けっかん","圧縮","ツール","欠陥","tool"]},{"category":"objects","char":"💽","name":"minidisc","keywords":["MD","ぱそこん","ひかりでぃすく","えんたーていめんと","みにでぃすく","こうがく","MD","パソコン","光ディスク","エンターテイメント","ミニディスク","光学","technology","record","data","disk","90s"]},{"category":"objects","char":"💾","name":"floppy_disk","keywords":["ふろっぴーでぃすく","こんぴゅーたー","でぃすく","ふろっぴー","フロッピーディスク","コンピューター","ディスク","フロッピー","oldschool","technology","save","90s","80s"]},{"category":"objects","char":"💿","name":"cd","keywords":["CDでぃすく","ぶるーれい","CD","こんぴゅーたー","でぃすく","DVD","こうがく","CDディスク","ブルーレイ","CD","コンピューター","ディスク","DVD","光学","technology","dvd","disk","disc","90s"]},{"category":"objects","char":"📀","name":"dvd","keywords":["DVD","ぶるーれい","CD","こんぴゅーたー","でぃすく","えんたーていめんと","こうがく","DVD","ブルーレイ","CD","コンピューター","ディスク","エンターテイメント","光学","cd","disk","disc"]},{"category":"objects","char":"📼","name":"vhs","keywords":["びでおてーぷ","えんたーていめんと","てーぷ","VHS","びでお","びでおかせっと","ビデオテープ","エンターテイメント","テープ","VHS","ビデオ","ビデオカセット","record","video","oldschool","90s","80s"]},{"category":"objects","char":"📷","name":"camera","keywords":["かめら","えんたーていめんと","びでお","カメラ","エンターテイメント","ビデオ","gadgets","photography"]},{"category":"objects","char":"📸","name":"camera_flash","keywords":["ふらっしゅをたいたかめら","かめら","ふらっしゅ","びでお","フラッシュを焚いたカメラ","カメラ","フラッシュ","ビデオ","photography","gadgets"]},{"category":"objects","char":"📹","name":"video_camera","keywords":["びでおかめら","かめら","えんたーていめんと","びでお","ビデオカメラ","カメラ","エンターテイメント","ビデオ","film","record"]},{"category":"objects","char":"🎥","name":"movie_camera","keywords":["びでおかめら","あくてぃびてぃ","かめら","しねま","えんたーていめんと","えいが","ビデオカメラ","アクティビティ","カメラ","シネマ","エンターテイメント","映画","film","record"]},{"category":"objects","char":"📽","name":"film_projector","keywords":["えいしゃき","しねま","ごらく","ふぃるむ","えいが","ぷろじぇくたー","びでお","映写機","シネマ","娯楽","フィルム","映画","プロジェクター","ビデオ","video","tape","record","movie"]},{"category":"objects","char":"🎞","name":"film_strip","keywords":["ふぃるむのふれーむ","しねま","えんたーていめんと","ふぃるむ","ふれーむ","えいが","フィルムのフレーム","シネマ","エンターテイメント","フィルム","フレーム","映画","movie"]},{"category":"objects","char":"📞","name":"telephone_receiver","keywords":["じゅわき","こみゅにけーしょん","でんわ","じゅしんき","受話器","コミュニケーション","電話","受信機","technology","communication","dial"]},{"category":"objects","char":"☎️","name":"phone","keywords":["でんわ","けいたいでんわ","電話","携帯電話","technology","communication","dial","telephone"]},{"category":"objects","char":"📟","name":"pager","keywords":["ぽけっとべる","こみゅにけーしょん","ぽけべる","ポケットベル","コミュニケーション","ポケベル","bbcall","oldschool","90s"]},{"category":"objects","char":"📠","name":"fax","keywords":["FAX","こみゅにけーしょん; fAX","FAX","コミュニケーション; fAX","communication","technology"]},{"category":"objects","char":"📺","name":"tv","keywords":["てれび","えんたーていめんと","TV","びでお","テレビ","エンターテイメント","TV","ビデオ","technology","program","oldschool","show","television"]},{"category":"objects","char":"📻","name":"radio","keywords":["らじお","えんたーていめんと","びでお","ラジオ","エンターテイメント","ビデオ","communication","music","podcast","program"]},{"category":"objects","char":"🎙","name":"studio_microphone","keywords":["すたじおまいく","まいく","おんがく","すたじお","スタジオマイク","マイク","音楽","スタジオ","sing","recording","artist","talkshow"]},{"category":"objects","char":"🎚","name":"level_slider","keywords":["ちょうせつばー","ちょうせつ","おんがく","ばー","調節バー","調節","音楽","バー","scale"]},{"category":"objects","char":"🎛","name":"control_knobs","keywords":["こんとろーるのぶ","こんとろーる","つまみ","おんがく","コントロールノブ","コントロール","つまみ","音楽","dial"]},{"category":"objects","char":"🧭","name":"compass","keywords":["こんぱす","じしゃく","なびげーしょん","おりえんてーりんぐ","コンパス","磁石","ナビゲーション","オリエンテーリング","magnetic","navigation","orienteering"]},{"category":"objects","char":"⏱","name":"stopwatch","keywords":["すとっぷうぉっち","とけい","ストップウォッチ","時計","time","deadline"]},{"category":"objects","char":"⏲","name":"timer_clock","keywords":["たいまーとけい","とけい","たいまー","タイマー時計","時計","タイマー","alarm"]},{"category":"objects","char":"⏰","name":"alarm_clock","keywords":["めざましとけい","あらーむ","とけい","目覚まし時計","アラーム","時計","time","wake"]},{"category":"objects","char":"🕰","name":"mantelpiece_clock","keywords":["おきどけい","とけい","置き時計","時計","time"]},{"category":"objects","char":"⏳","name":"hourglass_flowing_sand","keywords":["すなどけい","すな","たいまー","砂時計","砂","タイマー","oldschool","time","countdown"]},{"category":"objects","char":"⌛","name":"hourglass","keywords":["すなどけい","すな","たいまー","砂時計","砂","タイマー","time","clock","oldschool","limit","exam","quiz","test"]},{"category":"objects","char":"📡","name":"satellite","keywords":["えいせいあんてな","あんてな","こみゅにけーしょん","ぱらぼらあんてな","えいせい","衛星アンテナ","アンテナ","コミュニケーション","パラボラアンテナ","衛星","communication","future","radio","space"]},{"category":"objects","char":"🔋","name":"battery","keywords":["でんち","ばってりー","でんし","だかえねるぎー","電池","バッテリー","電子","高エネルギー","power","energy","sustain"]},{"category":"objects","char":"🪫","name":"low_battery","keywords":["ばってりーざんりょうしょう","ばってりー","でんし","ていえねるぎー","バッテリー残量少","バッテリー","電子","低エネルギー"]},{"category":"objects","char":"🔌","name":"electric_plug","keywords":["こんせんと","でんき","ぷらぐ","コンセント","電気","プラグ","charger","power"]},{"category":"objects","char":"💡","name":"bulb","keywords":["でんきゅう","まんが","でんき","ひらめき","ひかり","電球","漫画","電気","ひらめき","光","light","electricity","idea"]},{"category":"objects","char":"🔦","name":"flashlight","keywords":["かいちゅうでんとう","でんき","ひかり","どうぐ","たいまつ","懐中電灯","電気","光","道具","たいまつ","dark","camping","sight","night"]},{"category":"objects","char":"🕯","name":"candle","keywords":["ろうそく","ひかり","ろうそく","光","fire","wax"]},{"category":"objects","char":"🧯","name":"fire_extinguisher","keywords":["しょうかき","しょうか","ひ","けす","消火器","消火","火","消す","quench"]},{"category":"objects","char":"🗑","name":"wastebasket","keywords":["ごみばこ","ごみ","かん","びん","ごみ箱","ゴミ箱","ごみ","ゴミ","缶","ビン","bin","trash","rubbish","garbage","toss"]},{"category":"objects","char":"🛢","name":"oil_drum","keywords":["どらむかん","どらむ","おいる","ドラム缶","ドラム","オイル","barrell"]},{"category":"objects","char":"💸","name":"money_with_wings","keywords":["はねのはえたおさつ","ぎんこう","しへい","せいきゅうしょ","どる","とぶ","おかね","はね","羽の生えたお札","銀行","紙幣","請求書","ドル","飛ぶ","お金","羽","dollar","bills","payment","sale"]},{"category":"objects","char":"💵","name":"dollar","keywords":["どるさつ","ぎんこう","しへい","おさつ","つうか","どる","おかね","ドル札","銀行","紙幣","お札","通貨","ドル","お金","money","sales","bill","currency"]},{"category":"objects","char":"💴","name":"yen","keywords":["えんきごうのはいったこぎって","ぎんこう","しへい","おさつ","つうか","おかね","えん","円記号の入った小切手","銀行","紙幣","お札","通貨","お金","円","money","sales","japanese","dollar","currency"]},{"category":"objects","char":"💶","name":"euro","keywords":["ゆーろさつ","ぎんこう","しへい","おさつ","つうか","ゆーろ","おかね","ユーロ札","銀行","紙幣","お札","通貨","ユーロ","お金","money","sales","dollar","currency"]},{"category":"objects","char":"💷","name":"pound","keywords":["ぽんどさつ","ぎんこう","しへい","おさつ","つうか","おかね","ぽんど","ポンド札","銀行","紙幣","お札","通貨","お金","ポンド","british","sterling","money","sales","bills","uk","england","currency"]},{"category":"objects","char":"💰","name":"moneybag","keywords":["どるぶくろ","ばっぐ","どる","おかね","ドル袋","バッグ","ドル","お金","dollar","payment","coins","sale"]},{"category":"objects","char":"🪙","name":"coin","keywords":["こいん","きん","きんぞく","おかね","ぎん","たから","コイン","金","金属","お金","銀","宝","dollar","payment","coins","sale"]},{"category":"objects","char":"💳","name":"credit_card","keywords":["くれじっとかーど","ぎんこう","かーど","くれじっと","おかね","クレジットカード","銀行","カード","クレジット","お金","money","sales","dollar","bill","payment","shopping"]},{"category":"objects","char":"🪪","name":"identification_card","keywords":["みぶんしょうめいしょ","しかくじょうほう","ID","らいせんす","せきゅりてぃ","身分証明書","資格情報","ID","ライセンス","セキュリティ"]},{"category":"objects","char":"💎","name":"gem","keywords":["ほうせき","だいあもんど","じゅえる","ろまんす","宝石","ダイアモンド","ジュエル","ロマンス","blue","ruby","diamond","jewelry"]},{"category":"objects","char":"⚖","name":"balance_scale","keywords":["はかり","てんびん","こうせい","てんびんざ","ものさし","どうぐ","じゅうりょう","せいざ","はかり","天秤","公正","てんびん座","物差し","道具","重量","星座","law","fairness","weight"]},{"category":"objects","char":"🧰","name":"toolbox","keywords":["どうぐばこ","むね","せいびし","こうぐ","道具箱","胸","整備士","工具","tools","diy","fix","maintainer","mechanic"]},{"category":"objects","char":"🔧","name":"wrench","keywords":["れんち","どうぐ","レンチ","道具","tools","diy","ikea","fix","maintainer"]},{"category":"objects","char":"🔨","name":"hammer","keywords":["はんまー","どうぐ","ハンマー","道具","tools","build","create"]},{"category":"objects","char":"⚒","name":"hammer_and_pick","keywords":["はんまーとつるはし","はんまー","つるはし","どうぐ","ハンマーとつるはし","ハンマー","つるはし","道具","tools","build","create"]},{"category":"objects","char":"🛠","name":"hammer_and_wrench","keywords":["はんまーとれんち","はんまー","どうぐ","れんち","ハンマーとレンチ","ハンマー","道具","レンチ","tools","build","create"]},{"category":"objects","char":"⛏","name":"pick","keywords":["つるはし","さいくつ","どうぐ","つるはし","採掘","道具","tools","dig"]},{"category":"objects","char":"🪓","name":"axe","keywords":["おの","たたきぎり","ておの","われる","もくざい","こうぐ","斧","たたき切り","手斧","割る","木材","工具","tools"]},{"category":"objects","char":"🦯","name":"probing_cane","keywords":["しろつえ","あくせしびりてぃ","めがふじゆう","白杖","アクセシビリティ","目が不自由","tools"]},{"category":"objects","char":"🔩","name":"nut_and_bolt","keywords":["なっととぼると","ぼると","なっと","どうぐ","ナットとボルト","ボルト","ナット","道具","handy","tools","fix"]},{"category":"objects","char":"⚙","name":"gear","keywords":["はぐるま","ぎあ","どうぐ","歯車","ギア","道具","cog"]},{"category":"objects","char":"🪃","name":"boomerang","keywords":["ぶーめらん","おーすとらりあ","ぎゃくもどり","はねかえり","ブーメラン","オーストラリア","逆戻り","跳ね返り","tool"]},{"category":"objects","char":"🪚","name":"carpentry_saw","keywords":["もっこうようのこぎり","だいく","ざいもく","のこぎり","こうぐ","木工用のこぎり","大工","材木","のこぎり","工具","tool"]},{"category":"objects","char":"🪛","name":"screwdriver","keywords":["どらいばー","ねじ","こうぐ","ドライバー","ねじ","工具","tool"]},{"category":"objects","char":"🪝","name":"hook","keywords":["ふっく","わな","いかさま","ぺてん","ゆうわく","ふぃっしんぐ","つーる","フック","わな","いかさま","ペテン","誘惑","フィッシング","ツール","tool"]},{"category":"objects","char":"🪜","name":"ladder","keywords":["はしご","のぼる","よこぎ","だん","こうぐ","はしご","登る","横木","段","工具","tool"]},{"category":"objects","char":"🧱","name":"brick","keywords":["れんが","ねんど","けんせつ","もるたる","かべ","れんが","粘土","建設","モルタル","壁","bricks"]},{"category":"objects","char":"⛓","name":"chains","keywords":["くさり","鎖","lock","arrest"]},{"category":"objects","char":"🧲","name":"magnet","keywords":["じしゃく","あとらくしょん","ばてい","磁石","アトラクション","馬蹄","attraction","magnetic"]},{"category":"objects","char":"🔫","name":"gun","keywords":["みずでっぽう","みず","ぴすとる","ふんしゃき","じゅう","水鉄砲","水","ピストル","噴射器","銃","violence","weapon","pistol","revolver"]},{"category":"objects","char":"💣","name":"bomb","keywords":["ばくだん","爆弾","boom","explode","explosion","terrorism"]},{"category":"objects","char":"🧨","name":"firecracker","keywords":["ばくちく","だいなまいと","かやく","はなび","爆竹","ダイナマイト","火薬","花火","dynamite","boom","explode","explosion","explosive"]},{"category":"objects","char":"🔪","name":"hocho","keywords":["ほうちょう","きっちんないふ","ちょうり","ないふ","包丁","キッチンナイフ","調理","ナイフ","knife","blade","cutlery","kitchen","weapon"]},{"category":"objects","char":"🗡","name":"dagger","keywords":["たんけん","ないふ","短剣","ナイフ","weapon"]},{"category":"objects","char":"⚔","name":"crossed_swords","keywords":["こうさしたけん","こうさ","けん","交差した剣","交差","剣","weapon"]},{"category":"objects","char":"🛡","name":"shield","keywords":["たて","盾","protection","security"]},{"category":"objects","char":"🚬","name":"smoking","keywords":["きつえんまーく","あくてぃびてぃ","きつえん","喫煙マーク","アクティビティ","喫煙","kills","tobacco","cigarette","joint","smoke"]},{"category":"objects","char":"☠","name":"skull_and_crossbones","keywords":["どくろまーく","からだ","こうさしたほね","し","かお","もんすたー","がいこつ","はろうぃーん","ドクロマーク","体","交差した骨","死","顔","モンスター","骸骨","ハロウィーン","poison","danger","deadly","scary","death","pirate","evil"]},{"category":"objects","char":"⚰","name":"coffin","keywords":["かん","し","棺","死","vampire","dead","die","death","rip","graveyard","cemetery","casket","funeral","box"]},{"category":"objects","char":"⚱","name":"funeral_urn","keywords":["こつつぼ","し","そうぎ","骨壷","死","葬儀","dead","die","death","rip","ashes"]},{"category":"objects","char":"🏺","name":"amphora","keywords":["あんふぉら","みずがめざ","りょうり","のみもの","みずさし","どうぐ","せいざ","アンフォラ","みずがめ座","料理","飲み物","水差し","道具","星座","vase","jar"]},{"category":"objects","char":"🔮","name":"crystal_ball","keywords":["すいしょうだま","たま","すいしょう","おとぎばなし","ふぁんたじー","うらない","どうぐ","水晶玉","玉","水晶","おとぎ話","ファンタジー","占い","道具","disco","party","magic","circus","fortune_teller"]},{"category":"objects","char":"📿","name":"prayer_beads","keywords":["じゅずじょうのいのりのようぐ","じゅず","いるい","ねっくれす","いのり","しゅうきょう","数珠状の祈りの用具","数珠","衣類","ネックレス","祈り","宗教","dhikr","religious"]},{"category":"objects","char":"🧿","name":"nazar_amulet","keywords":["なざーるのおまもり","じゅずだま","おまもり","よこしまし","なざーる","ごふ","ナザールのお守り","数珠玉","お守り","邪視","ナザール","護符","bead","charm"]},{"category":"objects","char":"💈","name":"barber","keywords":["りはつてんのかんばんばしら","りはつてん","とこや","さんぱつ","かんばんばしら","理髪店の看板柱","理髪店","床屋","散髪","看板柱","hair","salon","style"]},{"category":"objects","char":"⚗","name":"alembic","keywords":["じょうりゅうき","かがく","じっけん","どうぐ","蒸留器","化学","実験","道具","distilling","science","experiment","chemistry"]},{"category":"objects","char":"🔭","name":"telescope","keywords":["ぼうえんきょう","つーる","望遠鏡","ツール","stars","space","zoom","science","astronomy"]},{"category":"objects","char":"🔬","name":"microscope","keywords":["けんびきょう","つーる","顕微鏡","ツール","laboratory","experiment","zoomin","science","study"]},{"category":"objects","char":"🕳","name":"hole","keywords":["あな","穴","embarrassing"]},{"category":"objects","char":"💊","name":"pill","keywords":["くすり","いし","ぴる","びょうき","薬","医師","ピル","病気","health","medicine","doctor","pharmacy","drug"]},{"category":"objects","char":"💉","name":"syringe","keywords":["ちゅうしゃき","いし","くすり","ちゅうしゃはり","ちゅうしゃ","びょうき","どうぐ","わくちん","注射器","医師","薬","注射針","注射","病気","道具","ワクチン","health","hospital","drugs","blood","medicine","needle","doctor","nurse"]},{"category":"objects","char":"🩸","name":"drop_of_blood","keywords":["ち1てき","いし","くすり","けつえき","せいり","血1滴","医師","薬","血液","生理","health","hospital","medicine","needle","doctor","nurse"]},{"category":"objects","char":"🩹","name":"adhesive_bandage","keywords":["がーぜつきばんそうこう","いし","くすり","ばんどえいど","ほうたい","ばんそうこう","ガーゼ付きばんそうこう","医師","薬","バンドエイド","包帯","ばんそうこう","health","hospital","medicine","needle","doctor","nurse"]},{"category":"objects","char":"🩺","name":"stethoscope","keywords":["ちょうしんき","いし","くすり","しんぞう","聴診器","医師","薬","心臓","health","hospital","medicine","needle","doctor","nurse"]},{"category":"objects","char":"🪒","name":"razor","keywords":["かみそり","するどい","ひげすり","カミソリ","鋭い","髭剃り","health"]},{"category":"objects","char":"🪮","name":"hair_pick","keywords":["へあぴっく","あふろ","くし","かみ","ぴっく","ヘアピック","アフロ","くし","髪","ピック","afro","comb","hair","pick"]},{"category":"objects","char":"🩻","name":"xray","keywords":["Xせん","ほね","いし","いりょう","こっかく","X線","骨","医師","医療","骨格"]},{"category":"objects","char":"🩼","name":"crutch","keywords":["まつばづえ","つえ","しょうがい","けが","いどうほじょ","ぼう","松葉杖","杖","障碍","怪我","移動補助","棒"]},{"category":"objects","char":"🧬","name":"dna","keywords":["DNA","せいぶつがくしゃ","しんか","いでんし","いでんしがく","せいめい","DNA","生物学者","進化","遺伝子","遺伝子学","生命","biologist","genetics","life"]},{"category":"objects","char":"🧫","name":"petri_dish","keywords":["ぺとりさら","ばくてりあ","せいぶつがくしゃ","せいぶつがく","ぶんか","じっけんしつ","ペトリ皿","バクテリア","生物学者","生物学","文化","実験室","bacteria","biology","culture","lab"]},{"category":"objects","char":"🧪","name":"test_tube","keywords":["しけんかん","かがくしゃ","かがく","じっけん","じっけんしつ","試験管","化学者","化学","実験","実験室","科学","chemistry","experiment","lab","science"]},{"category":"objects","char":"🌡","name":"thermometer","keywords":["おんどけい","てんき","おんど","温度計","天気","温度","weather","temperature","hot","cold"]},{"category":"objects","char":"🧹","name":"broom","keywords":["ほうき","くりーにんぐ","そうじ","まじょ","ほうき","クリーニング","掃除","魔女","cleaning","sweeping","witch"]},{"category":"objects","char":"🧺","name":"basket","keywords":["ばすけっと","のうぎょう","らんどりー","ぴくにっく","バスケット","農業","ランドリー","ピクニック","laundry"]},{"category":"objects","char":"🧻","name":"toilet_paper","keywords":["ぺーぱーろーる","ぺーぱーたおる","といれっとぺーぱー","ペーパーロール","ペーパータオル","トイレットペーパー","roll"]},{"category":"objects","char":"🏷","name":"label","keywords":["らべる","にふだ","ラベル","荷札","sale","tag"]},{"category":"objects","char":"🔖","name":"bookmark","keywords":["ぶっくまーく","しおり","しるし","ブックマーク","しおり","印","favorite","label","save"]},{"category":"objects","char":"🚽","name":"toilet","keywords":["といれ","トイレ","restroom","wc","washroom","bathroom","potty"]},{"category":"objects","char":"🚿","name":"shower","keywords":["しゃわー","みず","シャワー","水","clean","water","bathroom"]},{"category":"objects","char":"🛁","name":"bathtub","keywords":["ばすたぶ","ふろ","よくそう","バスタブ","風呂","浴槽","clean","shower","bathroom"]},{"category":"objects","char":"🧼","name":"soap","keywords":["せっけん","ぼう","みずあび","くりーにんぐ","あわ","せっけんいれ","せっけん","棒","水浴び","クリーニング","泡","せっけん入れ","bar","bathing","cleaning","lather"]},{"category":"objects","char":"🧽","name":"sponge","keywords":["すぽんじ","きゅうしゅう","くりーにんぐ","たこうせい","スポンジ","吸収","クリーニング","多孔性","absorbing","cleaning","porous"]},{"category":"objects","char":"🧴","name":"lotion_bottle","keywords":["ろーしょんぼとる","ろーしょん","ほしめざい","しゃんぷー","ひやけとめ","ローションボトル","ローション","保湿剤","シャンプー","日焼け止め","moisturizer","sunscreen"]},{"category":"objects","char":"🔑","name":"key","keywords":["かぎ","じょう","ぱすわーど","鍵","錠","パスワード","lock","door","password"]},{"category":"objects","char":"🗝","name":"old_key","keywords":["ふるいかぎ","かぎ","じょう","ふるい","古い鍵","かぎ","鍵","錠","古い","lock","door","password"]},{"category":"objects","char":"🛋","name":"couch_and_lamp","keywords":["そふぁーとらんぷ","そふぁー","ほてる","らんぷ","ソファーとランプ","ソファー","ホテル","ランプ","read","chill"]},{"category":"objects","char":"🪔","name":"diya_Lamp","keywords":["でぃやらんぷ","でぃや","らんぷ","おいる","ディヤランプ","ディヤ","ランプ","オイル","light","oil"]},{"category":"objects","char":"🛌","name":"sleeping_bed","keywords":["しゅくはくしせつ","ねる","ほてる","すいみん","べっど","宿泊施設","寝る","ホテル","睡眠","ベッド","bed","rest"]},{"category":"objects","char":"🛏","name":"bed","keywords":["べっど","ほてる","すいみん","ベッド","ホテル","睡眠","sleep","rest"]},{"category":"objects","char":"🚪","name":"door","keywords":["どあ","とびら","ドア","扉","house","entry","exit"]},{"category":"objects","char":"🪑","name":"chair","keywords":["いす","ざせき","すわる","椅子","座席","座る","house","desk"]},{"category":"objects","char":"🛎","name":"bellhop_bell","keywords":["たくじょうべる","べる","ほてる","卓上ベル","ベル","ホテル","service"]},{"category":"objects","char":"🧸","name":"teddy_bear","keywords":["てでぃべあ","おもちゃ","びろーど","ぬいぐるみ","テディベア","玩具","ビロード","ぬいぐるみ","おもちゃ","plush","stuffed"]},{"category":"objects","char":"🖼","name":"framed_picture","keywords":["がくにはいったしゃしん","あーと","がくぶち","びじゅつかん","かいが","しゃしん","額に入った写真","アート","額縁","美術館","絵画","写真","photography"]},{"category":"objects","char":"🗺","name":"world_map","keywords":["せかいちず","ちず","せかい","世界地図","地図","世界","location","direction"]},{"category":"objects","char":"🛗","name":"elevator","keywords":["えれべーたー","あくせしびりてぃ","ひきあげ","しょうこうき","エレベーター","アクセシビリティ","引き上げ","昇降機","household"]},{"category":"objects","char":"🪞","name":"mirror","keywords":["かがみ","はんしゃ","はんしゃたい","はんしゃきょう","鏡","反射","反射体","反射鏡","household"]},{"category":"objects","char":"🪟","name":"window","keywords":["まど","わく","しんせんなくうき","がらす","かいこうぶ","とうめい","しかい","窓","枠","新鮮な空気","ガラス","開口部","透明","視界","household"]},{"category":"objects","char":"🪠","name":"plunger","keywords":["ぷらんじゃー","ふぉーすかっぷ","はいかんこう","きゅういん","といれ","プランジャー","フォースカップ","配管工","吸引","トイレ","household"]},{"category":"objects","char":"🪤","name":"mouse_trap","keywords":["ねずみとりき","えさ","ねずみ","かじはどうぶつ","わなわ","わな","ネズミ捕り器","餌","ネズミ","齧歯動物","輪なわ","わな","household"]},{"category":"objects","char":"🪣","name":"bucket","keywords":["ばけつ","たる","ておけ","おおだる","バケツ","たる","手桶","大だる","household"]},{"category":"objects","char":"🪥","name":"toothbrush","keywords":["はぶらし","ばするーむ","ぶらし","きれい","はいしゃ","えいせい","は","歯ブラシ","バスルーム","ブラシ","きれい","歯医者","衛生","歯","household"]},{"category":"objects","char":"🫧","name":"bubbles","keywords":["ばぶる","げっぷ","きれい","せっけん","すいちゅう","バブル","げっぷ","きれい","せっけん","水中"]},{"category":"objects","char":"⛱","name":"parasol_on_ground","keywords":["たてられたぱらそる","あめ","はれ","かさ","てんき","立てられたパラソル","雨","晴れ","傘","天気","weather","summer"]},{"category":"objects","char":"🗿","name":"moyai","keywords":["もやいぞう","もあいぞう","かお","ぞう","モヤイ像","モアイ像","顔","像","rock","easter island","moai"]},{"category":"objects","char":"🛍","name":"shopping","keywords":["かいものぶくろ","かばん","ほてる","かいもの","買い物袋","鞄","ホテル","買い物","mall","buy","purchase"]},{"category":"objects","char":"🛒","name":"shopping_cart","keywords":["しょっぴんぐかーと","かーと","しょっぴんぐ","とろりー","ショッピングカート","カート","ショッピング","トロリー","trolley"]},{"category":"objects","char":"🎈","name":"balloon","keywords":["ふうせん","あくてぃびてぃ","おいわい","えんたーていめんと","風船","アクティビティ","お祝い","エンターテイメント","party","celebration","birthday","circus"]},{"category":"objects","char":"🎏","name":"flags","keywords":["こいのぼり","あくてぃびてぃ","こい","おいわい","えんたーていめんと","はた","ふきながし","こいのぼり","アクティビティ","鯉","お祝い","エンターテイメント","旗","吹流し","fish","japanese","koinobori","carp","banner"]},{"category":"objects","char":"🎀","name":"ribbon","keywords":["りぼん","おいわい","リボン","お祝い","decoration","pink","girl","bowtie"]},{"category":"objects","char":"🎁","name":"gift","keywords":["ぷれぜんと","はこ","おいわい","えんたーていめんと","おくりもの","ほうそう","プレゼント","箱","お祝い","エンターテイメント","贈り物","包装","present","birthday","christmas","xmas"]},{"category":"objects","char":"🎊","name":"confetti_ball","keywords":["くすだま","あくてぃびてぃ","おいわい","かみふぶき","えんたーていめんと","くす玉","アクティビティ","お祝い","紙吹雪","エンターテイメント","festival","party","birthday","circus"]},{"category":"objects","char":"🎉","name":"tada","keywords":["くらっかー","あくてぃびてぃ","おいわい","えんたーていめんと","ぱーてぃー","じゃーん","クラッカー","アクティビティ","お祝い","エンターテイメント","パーティー","ジャーン","party","congratulations","birthday","magic","circus","celebration"]},{"category":"objects","char":"🎎","name":"dolls","keywords":["ひなまつり","あくてぃびてぃ","おいわい","にんぎょう","えんたーていめんと","まつり","にっぽん","ひな祭り","アクティビティ","お祝い","人形","エンターテイメント","祭り","日本","japanese","toy","kimono"]},{"category":"objects","char":"🪭","name":"folding_hand_fan","keywords":["おりたたみせんす","れいきゃく","えんりょがち","だんす","ふぁん","ふらったー","ねつ","あつい","うちき","ひろがる","折り畳み扇子","冷却","遠慮がち","ダンス","ファン","フラッター","熱","熱い","内気","広がる","cooling","dance","fan","flutter","hot","shy"]},{"category":"objects","char":"🎐","name":"wind_chime","keywords":["ふうりん","あくてぃびてぃ","かね","おいわい","えんたーていめんと","ふう","風鈴","アクティビティ","鐘","お祝い","エンターテイメント","風","nature","ding","spring","bell"]},{"category":"objects","char":"🎌","name":"crossed_flags","keywords":["こうさき","あくてぃびてぃ","おいわい","こうさ","こうさした","はた","にっぽん","交差旗","アクティビティ","お祝い","交差","交差した","旗","日本","japanese","nation","country","border"]},{"category":"objects","char":"🏮","name":"izakaya_lantern","keywords":["いざかやのちょうちん","あかちょうちん","いざかや","にっぽん","ちょうちん","あかり","あか","居酒屋の提灯","赤ちょうちん","居酒屋","日本","提灯","灯り","赤","light","paper","halloween","spooky"]},{"category":"objects","char":"🧧","name":"red_envelope","keywords":["あかいふうとう","ぎふと","こううん","ほんばお","らいしー","おかね","赤い封筒","ギフト","幸運","紅包","利是","お金","gift"]},{"category":"objects","char":"✉️","name":"email","keywords":["ふうとう","Eめーる","でんしめーる","封筒","Eメール","電子メール","letter","postal","inbox","communication"]},{"category":"objects","char":"📩","name":"envelope_with_arrow","keywords":["めーるじゅしんちゅう","やじるし","こみゅにけーしょん","した","Eめーる","でんしめーる","ふうとう","てがみ","めーる","おくる","そうしん","メール受信中","矢印","コミュニケーション","下","Eメール","電子メール","封筒","手紙","メール","送る","送信","email","communication"]},{"category":"objects","char":"📨","name":"incoming_envelope","keywords":["めーるじゅしん","こみゅにけーしょん","Eめーる","でんしめーる","ふうとう","うけとる","てがみ","めーる","じゅしん","メール受信","コミュニケーション","Eメール","電子メール","封筒","受け取る","手紙","メール","受信","email","inbox"]},{"category":"objects","char":"📧","name":"e-mail","keywords":["Eめーる","こみゅにけーしょん","でんしめーる","てがみ","めーる","Eメール","コミュニケーション","電子メール","手紙","メール","communication","inbox"]},{"category":"objects","char":"💌","name":"love_letter","keywords":["らぶれたー","はーと","てがみ","あい","めーる","ろまんす","ラブレター","ハート","手紙","愛","メール","ロマンス","email","like","affection","envelope","valentines"]},{"category":"objects","char":"📮","name":"postbox","keywords":["ぽすと","こみゅにけーしょん","めーる","ゆうびんうけ","ポスト","コミュニケーション","メール","郵便受け","email","letter","envelope"]},{"category":"objects","char":"📪","name":"mailbox_closed","keywords":["はたがさがっていてとじているじょうたいのゆうびんうけ","とじる","こみゅにけーしょん","はた","さがった","めーる","ぽすと","ゆうびんうけ","旗が下がっていて閉じている状態の郵便受け","閉じる","コミュニケーション","旗","下がった","メール","ポスト","郵便受け","email","communication","inbox"]},{"category":"objects","char":"📫","name":"mailbox","keywords":["はたがあがっていてとじているじょうたいのゆうびんうけ","とじる","こみゅにけーしょん","はた","めーる","ゆうびんうけ","ぽすと","旗が上がっていて閉じている状態の郵便受け","閉じる","コミュニケーション","旗","メール","郵便受け","ポスト","email","inbox","communication"]},{"category":"objects","char":"📬","name":"mailbox_with_mail","keywords":["はたがあがっていてひらいているじょうたいのゆうびんうけ","こみゅにけーしょん","はた","めーる","ぽすと","あける","ゆうびんうけ","旗が上がっていて開いている状態の郵便受け","コミュニケーション","旗","メール","ポスト","開ける","郵便受け","email","inbox","communication"]},{"category":"objects","char":"📭","name":"mailbox_with_no_mail","keywords":["はたがさがっていてひらいているゆうびんうけ","こみゅにけーしょん","はた","さげ","めーる","めーるぼっくす","あける","ゆうびんうけ","旗が下がっていて開いている郵便受け","コミュニケーション","旗","下げ","メール","メールボックス","開ける","郵便受け","email","inbox"]},{"category":"objects","char":"📦","name":"package","keywords":["にもつ","はこ","こみゅにけーしょん","ぱっけーじ","こづつみ","荷物","箱","コミュニケーション","パッケージ","小包","mail","gift","cardboard","box","moving"]},{"category":"objects","char":"📯","name":"postal_horn","keywords":["ゆうびんらっぱ","こみゅにけーしょん","えんたーていめんと","かく","ぽすと","ゆうびん","郵便ラッパ","コミュニケーション","エンターテイメント","角","ポスト","郵便","instrument","music"]},{"category":"objects","char":"📥","name":"inbox_tray","keywords":["じゅしんとれい","はこ","こみゅにけーしょん","てがみ","めーる","じゅしん","とれい","受信トレイ","箱","コミュニケーション","手紙","メール","受信","トレイ","email","documents"]},{"category":"objects","char":"📤","name":"outbox_tray","keywords":["そうしんとれい","はこ","こみゅにけーしょん","てがみ","めーる","そうしん","とれい","送信トレイ","箱","コミュニケーション","手紙","メール","送信","トレイ","inbox","email"]},{"category":"objects","char":"📜","name":"scroll","keywords":["まきもの","かみ","巻物","紙","documents","ancient","history","paper"]},{"category":"objects","char":"📃","name":"page_with_curl","keywords":["げんこう","かーる","どきゅめんと","ぺーじ","原稿","カール","ドキュメント","ページ","documents","office","paper"]},{"category":"objects","char":"📑","name":"bookmark_tabs","keywords":["ぶっくまーくたぶ","ぶっくまーく","まーく","まーかー","たぶ","ブックマークタブ","ブックマーク","マーク","マーカー","タブ","favorite","save","order","tidy"]},{"category":"objects","char":"🧾","name":"receipt","keywords":["りょうしゅうしょ","かいけい","ぼき","しょうこ","しょうめい","領収書","会計","簿記","証拠","証明","accounting","expenses"]},{"category":"objects","char":"📊","name":"bar_chart","keywords":["ぼうぐらふ","ばー","ちゃーと","ぐらふ","棒グラフ","バー","チャート","グラフ","graph","presentation","stats"]},{"category":"objects","char":"📈","name":"chart_with_upwards_trend","keywords":["じょうしょうするぐらふ","じょうしょうちゃーと","ちゃーと","ぐらふ","せいちょう","とれんど","うわむき","上昇するグラフ","上昇チャート","チャート","グラフ","成長","トレンド","上向き","graph","presentation","stats","recovery","business","economics","money","sales","good","success"]},{"category":"objects","char":"📉","name":"chart_with_downwards_trend","keywords":["かこうするぐらふ","かこうちゃーと","ちゃーと","うえ","ぐらふ","とれんど","下降するグラフ","下降チャート","チャート","下","グラフ","トレンド","graph","presentation","stats","recession","business","economics","money","sales","bad","failure"]},{"category":"objects","char":"📄","name":"page_facing_up","keywords":["ぶんしょ","ぺーじ","文書","ページ","documents","office","paper","information"]},{"category":"objects","char":"📅","name":"date","keywords":["かれんだー","ひづけ","カレンダー","日付","calendar","schedule"]},{"category":"objects","char":"📆","name":"calendar","keywords":["ひめくりかれんだー","かれんだー","日めくりカレンダー","カレンダー","schedule","date","planning"]},{"category":"objects","char":"🗓","name":"spiral_calendar","keywords":["りんぐかれんだー","かれんだー","ぱっど","らせんじょう","リングカレンダー","カレンダー","パッド","らせん状","date","schedule","planning"]},{"category":"objects","char":"📇","name":"card_index","keywords":["めいしふぉるだ","かーど","さくいん","ろーらでっくす","名刺フォルダ","カード","索引","ローラデックス","business","stationery"]},{"category":"objects","char":"🗃","name":"card_file_box","keywords":["かーどふぁいる","はこ","かーど","ふぁいる","カードファイル","箱","カード","ファイル","business","stationery"]},{"category":"objects","char":"🗳","name":"ballot_box","keywords":["とうひょうようしととうひょうばこ","とうひょうようし","はこ","ひょう","とうひょう","投票用紙と投票箱","投票用紙","箱","票","投票","election","vote"]},{"category":"objects","char":"🗄","name":"file_cabinet","keywords":["ふぁいるしゅうのうこ","しゅうのう","ふぁいる","ファイル収納庫","収納","ファイル","filing","organizing"]},{"category":"objects","char":"📋","name":"clipboard","keywords":["くりっぷぼーど","クリップボード","stationery","documents"]},{"category":"objects","char":"🗒","name":"spiral_notepad","keywords":["りんぐのーと","のーと","ぱっど","らせんじょう","リングノート","ノート","パッド","らせん状","memo","stationery"]},{"category":"objects","char":"📁","name":"file_folder","keywords":["ふぉるだ","ふぁいる","フォルダ","ファイル","documents","business","office"]},{"category":"objects","char":"📂","name":"open_file_folder","keywords":["ひらいたふぉるだ","ふぁいる","ふぉるだ","ひらいた","開いたフォルダ","ファイル","フォルダ","開いた","documents","load"]},{"category":"objects","char":"🗂","name":"card_index_dividers","keywords":["しきりかーど","かーど","しきり","さくいん","仕切りカード","カード","仕切り","索引","organizing","business","stationery"]},{"category":"objects","char":"🗞","name":"newspaper_roll","keywords":["まるめたしんぶん","にゅーす","しんぶん","かみ","まるめた","丸めた新聞","ニュース","新聞","紙","丸めた","press","headline"]},{"category":"objects","char":"📰","name":"newspaper","keywords":["しんぶん","こみゅにけーしょん","にゅーす","かみ","新聞","コミュニケーション","ニュース","紙","press","headline"]},{"category":"objects","char":"📓","name":"notebook","keywords":["のーと","ノート","stationery","record","notes","paper","study"]},{"category":"objects","char":"📕","name":"closed_book","keywords":["とじたほん","ほん","とじている","閉じた本","本","閉じている","read","library","knowledge","textbook","learn"]},{"category":"objects","char":"📗","name":"green_book","keywords":["みどりいろのほん","ほん","みどり","緑色の本","本","緑","read","library","knowledge","study"]},{"category":"objects","char":"📘","name":"blue_book","keywords":["あおいほん","あお","ほん","青い本","青","本","read","library","knowledge","learn","study"]},{"category":"objects","char":"📙","name":"orange_book","keywords":["おれんじいろのほん","ほん","おれんじ","オレンジ色の本","本","オレンジ","read","library","knowledge","textbook","study"]},{"category":"objects","char":"📔","name":"notebook_with_decorative_cover","keywords":["そうしょくかばーののーと","ほん","かばー","そうしょく","のーと","装飾カバーのノート","本","カバー","装飾","ノート","classroom","notes","record","paper","study"]},{"category":"objects","char":"📒","name":"ledger","keywords":["ちょうぼ","もとちょう","のーと","帳簿","元帳","ノート","notes","paper"]},{"category":"objects","char":"📚","name":"books","keywords":["しょせき","ほん","書籍","本","literature","library","study"]},{"category":"objects","char":"📖","name":"open_book","keywords":["ひらいたほん","ほん","ひらいた","開いた本","本","開いた","book","read","library","knowledge","literature","learn","study"]},{"category":"objects","char":"🧷","name":"safety_pin","keywords":["あんぜんぴん","おむつ","ぱんくろっく","安全ピン","おむつ","パンクロック","diaper"]},{"category":"objects","char":"🔗","name":"link","keywords":["りんく","リンク","rings","url"]},{"category":"objects","char":"📎","name":"paperclip","keywords":["くりっぷ","ぺーぱーくりっぷ","クリップ","ペーパークリップ","documents","stationery"]},{"category":"objects","char":"🖇","name":"paperclips","keywords":["つながったぺーぱーくりっぷ","こみゅにけーしょん","りんく","ぺーぱーくりっぷ","繋がったペーパークリップ","コミュニケーション","リンク","ペーパークリップ","documents","stationery"]},{"category":"objects","char":"✂️","name":"scissors","keywords":["はさみ","どうぐ","ハサミ","はさみ","道具","stationery","cut"]},{"category":"objects","char":"📐","name":"triangular_ruler","keywords":["さんかくじょうぎ","じょうぎ","はいち","さんかく","三角定規","定規","配置","三角","stationery","math","architect","sketch"]},{"category":"objects","char":"📏","name":"straight_ruler","keywords":["じょうぎ","ちょくじょうぎ","定規","直定規","stationery","calculate","length","math","school","drawing","architect","sketch"]},{"category":"objects","char":"🧮","name":"abacus","keywords":["そろばん","けいさん","かうんと","しゅうけいひょう","すうがく","そろばん","計算","カウント","集計表","数学","calculation"]},{"category":"objects","char":"📌","name":"pushpin","keywords":["がびょう","ぴん","画鋲","ピン","stationery","mark","here"]},{"category":"objects","char":"📍","name":"round_pushpin","keywords":["がびょう","ぴん","画鋲","ピン","stationery","location","map","here"]},{"category":"objects","char":"🚩","name":"triangular_flag_on_post","keywords":["さんかくはた","はた","ぽすと","三角旗","旗","ポスト","mark","milestone","place"]},{"category":"objects","char":"🏳","name":"white_flag","keywords":["なびくしろはた","はた","なびく","なびく白旗","旗","なびく","losing","loser","lost","surrender","give up","fail"]},{"category":"objects","char":"🏴","name":"black_flag","keywords":["なびくくろはた","はた","なびく","なびく黒旗","旗","なびく","pirate"]},{"category":"objects","char":"🏳️‍🌈","name":"rainbow_flag","keywords":["れいんぼーふらっぐ","ふらっぐ","れいんぼー","ぷらいど","lgbt","レインボーフラッグ","フラッグ","レインボー","プライド","lgbt","flag","rainbow","pride","gay","lgbt","glbt","queer","homosexual","lesbian","bisexual","transgender"]},{"category":"objects","char":"🏳️‍⚧️","name":"transgender_flag","keywords":["とらすじぇんだーふらっぐ","ふらっぐ","とらんすじぇんだー","ぷらいど","lgbt","トラスジェンダーフラッグ","フラッグ","トランスジェンダー","プライド","lgbt","flag","transgender"]},{"category":"objects","char":"🔐","name":"closed_lock_with_key","keywords":["こいんろっかー","しまっている","かぎ","せじょう","ぼうはん","コインロッカー","閉まっている","鍵","施錠","防犯","security","privacy"]},{"category":"objects","char":"🔒","name":"lock","keywords":["かぎ","とじられた","せじょう","鍵","閉じられた","施錠","security","password","padlock"]},{"category":"objects","char":"🔓","name":"unlock","keywords":["かいじょう","せじょう","あける","解錠","施錠","開ける","privacy","security"]},{"category":"objects","char":"🔏","name":"lock_with_ink_pen","keywords":["じょうまえとぺん","いんく","じょう","ぺんさき","ぺん","ぷらいばしー","錠前とペン","インク","錠","ペン先","ペン","プライバシー","security","secret"]},{"category":"objects","char":"🖊","name":"pen","keywords":["ひだりしたむきのぼーるぺん","ぼーるぺん","こみゅにけーしょん","ぺん","左下向きのボールペン","ボールペン","コミュニケーション","ペン","stationery","writing","write"]},{"category":"objects","char":"🖋","name":"fountain_pen","keywords":["ひだりしたむきのまんねんひつ","こみゅにけーしょん","まんねんひつ","ぺん","左下向きの万年筆","コミュニケーション","万年筆","ペン","stationery","writing","write"]},{"category":"objects","char":"✒️","name":"black_nib","keywords":["ぺんさき","ぺん","ペン先","ペン","pen","stationery","writing","write"]},{"category":"objects","char":"📝","name":"memo","keywords":["めも","こみゅにけーしょん","えんぴつ","メモ","コミュニケーション","鉛筆","write","documents","stationery","pencil","paper","writing","legal","exam","quiz","test","study","compose"]},{"category":"objects","char":"✏️","name":"pencil2","keywords":["えんぴつ","鉛筆","stationery","write","paper","writing","school","study"]},{"category":"objects","char":"🖍","name":"crayon","keywords":["ひだりしたむきのくれよん","こみゅにけーしょん","くれよん","左下向きのクレヨン","コミュニケーション","クレヨン","drawing","creativity"]},{"category":"objects","char":"🖌","name":"paintbrush","keywords":["ひだりしたむきのぶらし","こみゅにけーしょん","ぺいんとぶらし","え","左下向きのブラシ","コミュニケーション","ペイントブラシ","絵","drawing","creativity","art"]},{"category":"objects","char":"🔍","name":"mag","keywords":["ひだりむきむしめがね","めがね","かくだい","けんさく","つーる","左向き虫眼鏡","眼鏡","拡大","検索","ツール","search","zoom","find","detective"]},{"category":"objects","char":"🔎","name":"mag_right","keywords":["みぎむきむしめがね","めがね","かくだい","けんさく","つーる","右向き虫眼鏡","眼鏡","拡大","検索","ツール","search","zoom","find","detective"]},{"category":"objects","char":"🪦","name":"headstone","keywords":["はかいし","ぼち","し","ぼ","はかば","はろうぃーん","墓石","墓地","死","墓","墓場","ハロウィーン"]},{"category":"objects","char":"🪧","name":"placard","keywords":["ぷらかーど","でも","しがらみ","こうぎ","かんばん","プラカード","デモ","柵","抗議","看板"]},{"category":"symbols","char":"💯","name":"100","keywords":["100てん","100","ふる","ひゃく","すこあ","100点","100","フル","百","スコア","score","perfect","numbers","century","exam","quiz","test","pass","hundred"]},{"category":"symbols","char":"🔢","name":"1234","keywords":["ばんごうのにゅうりょくきごう","1234","にゅうりょく","すうじ","番号の入力記号","1234","入力","数字","numbers","blue-square"]},{"category":"symbols","char":"🩷","name":"pink_heart","keywords":["ぴんくのはーと","かわいい","はーと","すき","あい","ぴんく","ピンクのハート","かわいい","ハート","好き","愛","ピンク","love","like","affection","valentines"]},{"category":"symbols","char":"❤️","name":"heart","keywords":["あかいろのはーと","はーと","赤色のハート","ハート","love","like","affection","valentines"]},{"category":"symbols","char":"🧡","name":"orange_heart","keywords":["おれんじいろのはーと","はーと","おれんじいろ","オレンジ色のハート","ハート","オレンジ色","love","like","affection","valentines"]},{"category":"symbols","char":"💛","name":"yellow_heart","keywords":["きいろのはーと","はーと","きいろ","黄色のハート","ハート","黄色","love","like","affection","valentines"]},{"category":"symbols","char":"💚","name":"green_heart","keywords":["みどりのはーと","はーと","みどり","緑のハート","ハート","緑","love","like","affection","valentines"]},{"category":"symbols","char":"🩵","name":"light_blue_heart","keywords":["らいとぶるーのはーと","しあん","はーと","らいとぶるー","こがも","ライトブルーのハート","シアン","ハート","ライトブルー","コガモ","love","like","affection","valentines"]},{"category":"symbols","char":"💙","name":"blue_heart","keywords":["あおのはーと","はーと","あお","青のハート","ハート","青","love","like","affection","valentines"]},{"category":"symbols","char":"💜","name":"purple_heart","keywords":["むらさきのはーと","はーと","むらさき","紫のハート","ハート","紫","love","like","affection","valentines"]},{"category":"symbols","char":"🤎","name":"brown_heart","keywords":["ちゃいろのはーと","はーと","ちゃいろ","茶色のハート","ハート","茶色","love","like","affection","valentines"]},{"category":"symbols","char":"🖤","name":"black_heart","keywords":["くろいはーと","はーと","くろ","あく","わるもの","黒いハート","ハート","黒","悪","悪者","love","like","affection","valentines"]},{"category":"symbols","char":"🩶","name":"grey_heart","keywords":["ぐれーのはーと","ぐれー","はーと","しるばー","すれーと","グレーのハート","グレー","ハート","シルバー","スレート","love","like","affection","valentines"]},{"category":"symbols","char":"🤍","name":"white_heart","keywords":["しろのはーと","はーと","しろ","白のハート","ハート","白","love","like","affection","valentines"]},{"category":"symbols","char":"💔","name":"broken_heart","keywords":["われたはーと","はーと","こわれる","はきょく","割れたハート","ハート","壊れる","破局","sad","sorry","break","heart","heartbreak"]},{"category":"symbols","char":"❣","name":"heavy_heart_exclamation","keywords":["はーとのびっくりまーく","はーと","びっくりまーく","きごう","ハートのビックリマーク","ハート","ビックリマーク","記号","decoration","love"]},{"category":"symbols","char":"💕","name":"two_hearts","keywords":["2つのはーと","はーと","あい","2つのハート","ハート","愛","love","like","affection","valentines","heart"]},{"category":"symbols","char":"💞","name":"revolving_hearts","keywords":["かいてんするはーと","はーと","かいてん","回転するハート","ハート","回転","love","like","affection","valentines"]},{"category":"symbols","char":"💓","name":"heartbeat","keywords":["こどうするはーと","はーと","こどう","どきどき","鼓動するハート","ハート","鼓動","ドキドキ","love","like","affection","valentines","pink","heart"]},{"category":"symbols","char":"💗","name":"heartpulse","keywords":["ひかるはーと","はーと","わくわく","ひかる","こどう","きんちょう","光るハート","ハート","ワクワク","光る","鼓動","緊張","like","love","affection","valentines","pink"]},{"category":"symbols","char":"💖","name":"sparkling_heart","keywords":["きらめくはーと","はーと","わくわく","きらきら","きらめくハート","ハート","ワクワク","キラキラ","love","like","affection","valentines"]},{"category":"symbols","char":"💘","name":"cupid","keywords":["いぬかれたはーと","はーと","や","きゅーぴっど","ろまんす","射抜かれたハート","ハート","矢","キューピッド","ロマンス","love","like","heart","affection","valentines"]},{"category":"symbols","char":"💝","name":"gift_heart","keywords":["りぼんつきのはーと","はーと","りぼん","ばれんたいん","リボン付きのハート","ハート","リボン","バレンタイン","love","valentines"]},{"category":"symbols","char":"💟","name":"heart_decoration","keywords":["はーとのでこれーしょん","はーと","ハートのデコレーション","ハート","purple-square","love","like"]},{"category":"symbols","char":"❤️‍🔥","name":"heart_on_fire","keywords":["もえているはーと","はーと","ひ","もえる","あい","ねつじょう","しんせいなはーと","燃えているハート","ハート","火","燃える","愛","熱情","神聖なハート"]},{"category":"symbols","char":"❤️‍🩹","name":"mending_heart","keywords":["てあてしているはーと","はーと","けんこうになる","かいぜんしている","てあてしている","かいふくしている","やみあがり","げんき","手当しているハート","ハート","健康になる","改善している","手当している","回復している","病み上がり","元気"]},{"category":"symbols","char":"☮","name":"peace_symbol","keywords":["ぴーすまーく","へいわ","ピースマーク","平和","hippie"]},{"category":"symbols","char":"✝","name":"latin_cross","keywords":["らてんじゅうじ","くりすちゃん","じゅうじか","しゅうきょう","ラテン十字","クリスチャン","十字架","宗教","christianity"]},{"category":"symbols","char":"☪","name":"star_and_crescent","keywords":["ほしとみかづき","いすらむ","むすりむ","しゅうきょう","星と三日月","イスラム","ムスリム","宗教","islam"]},{"category":"symbols","char":"🕉","name":"om","keywords":["おーむまーく","ひんどぅーきょう","おーむ","しゅうきょう","オームマーク","ヒンドゥー教","オーム","宗教","hinduism","buddhism","sikhism","jainism"]},{"category":"symbols","char":"☸","name":"wheel_of_dharma","keywords":["ほうりん","ぶっきょうと","だーま","しゅうきょう","法輪","仏教徒","ダーマ","宗教","hinduism","buddhism","sikhism","jainism"]},{"category":"symbols","char":"🪯","name":"khanda","keywords":["かんだ","しゅうきょう","しーくきょうと","カンダ","宗教","シーク教徒","religion","sikh"]},{"category":"symbols","char":"✡","name":"star_of_david","keywords":["だびでのほし","だびで","ゆだやじん","ゆだやきょう","しゅうきょう","ほし","ダビデの星","ダビデ","ユダヤ人","ユダヤ教","宗教","星","judaism"]},{"category":"symbols","char":"🔯","name":"six_pointed_star","keywords":["ろくぼうせい","うらない","ほし","六芒星","占い","星","purple-square","religion","jewish","hexagram"]},{"category":"symbols","char":"🕎","name":"menorah","keywords":["はぬっきーやー","しょくだい","めのーらー","しゅうきょう","ハヌッキーヤー","燭台","メノーラー","宗教","hanukkah","candles","jewish"]},{"category":"symbols","char":"☯","name":"yin_yang","keywords":["いんよう","しゅうきょう","どう","どうか","ひ","かげ","陰陽","宗教","道","道家","陽","陰","balance"]},{"category":"symbols","char":"☦","name":"orthodox_cross","keywords":["はったんじゅうじか","くりすちゃん","じゅうじか","しゅうきょう","八端十字架","クリスチャン","十字架","宗教","suppedaneum","religion"]},{"category":"symbols","char":"🛐","name":"place_of_worship","keywords":["れいはいしょ","しゅうきょう","れいはい","礼拝所","宗教","礼拝","religion","church","temple","prayer"]},{"category":"symbols","char":"⛎","name":"ophiuchus","keywords":["へびつかいざ","うんぱんにん","へび","せいざ","へびつかい座","運搬人","蛇","ヘビ","星座","sign","purple-square","constellation","astrology"]},{"category":"symbols","char":"♈","name":"aries","keywords":["おひつじざ","こひつじ","せいざ","おひつじ座","仔羊","星座","sign","purple-square","zodiac","astrology"]},{"category":"symbols","char":"♉","name":"taurus","keywords":["おうしざ","おすうし","ゆううし","せいざ","おうし座","牡牛","雄牛","星座","purple-square","sign","zodiac","astrology"]},{"category":"symbols","char":"♊","name":"gemini","keywords":["ふたござ","ふたご","せいざ","ふたご座","ふたご","星座","sign","zodiac","purple-square","astrology"]},{"category":"symbols","char":"♋","name":"cancer","keywords":["がん","かにざ","かに","せいざ","ガン","かに座","カニ","蟹","星座","sign","zodiac","purple-square","astrology"]},{"category":"symbols","char":"♌","name":"leo","keywords":["ししざ","らいおん","せいざ","しし座","ライオン","星座","sign","purple-square","zodiac","astrology"]},{"category":"symbols","char":"♍","name":"virgo","keywords":["おとめざ","おとめ","しょじょ","せいざ","おとめ座","乙女","処女","星座","sign","zodiac","purple-square","astrology"]},{"category":"symbols","char":"♎","name":"libra","keywords":["てんびんざ","てんびん","こうせい","はかり","せいざ","てんびん座","天秤","公正","はかり","星座","sign","purple-square","zodiac","astrology"]},{"category":"symbols","char":"♏","name":"scorpius","keywords":["さそりざ","さそり","せいざ","さそり座","さそり","サソリ","星座","sign","zodiac","purple-square","astrology","scorpio"]},{"category":"symbols","char":"♐","name":"sagittarius","keywords":["いてざ","しゃしゅ","しゃしゅざ","せいざ","いて座","射手","射手座","星座","sign","zodiac","purple-square","astrology"]},{"category":"symbols","char":"♑","name":"capricorn","keywords":["やぎざ","やぎ","せいざ","やぎ座","ヤギ","星座","sign","zodiac","purple-square","astrology"]},{"category":"symbols","char":"♒","name":"aquarius","keywords":["みずがめざ","うんぱんじん","みず","せいざ","みずがめ座","運搬人","水","星座","sign","purple-square","zodiac","astrology"]},{"category":"symbols","char":"♓","name":"pisces","keywords":["うおざ","さかな","せいざ","うお座","魚","星座","purple-square","sign","zodiac","astrology"]},{"category":"symbols","char":"🆔","name":"id","keywords":["しかくかこみID","ID","しきべつ","四角囲みID","ID","識別","purple-square","words"]},{"category":"symbols","char":"⚛","name":"atom_symbol","keywords":["げんそきごう","むしんろんしゃ","げんし","元素記号","無神論者","原子","science","physics","chemistry"]},{"category":"symbols","char":"⚧️","name":"transgender_symbol","keywords":["とらんすじぇんだーさいん","とらんすじぇんだー","ぷらいど","lgbt","トランスジェンダーサイン","トランスジェンダー","プライド","lgbt","purple-square","woman","female","toilet","loo","restroom","gender"]},{"category":"symbols","char":"🈳","name":"u7a7a","keywords":["しかくかこみそら","しかくかこみのそら","ちゅうごくご","そらしつ","あき","くうしゃ","四角囲み空","四角囲みの空","中国語","空室","空き","空車","kanji","japanese","chinese","empty","sky","blue-square","aki"]},{"category":"symbols","char":"🈹","name":"u5272","keywords":["しかくかこみわり","しかくかこみのわり","にほんご","わりびき","四角囲み割","四角囲みの割","日本語","割引","cut","divide","chinese","kanji","pink-square","waribiki"]},{"category":"symbols","char":"☢","name":"radioactive","keywords":["ほうしゃのうひょうしき","ほうしゃのう","放射能標識","放射能","nuclear","danger"]},{"category":"symbols","char":"☣","name":"biohazard","keywords":["ばいおはざーどひょうしき","せいぶつさいがい","バイオハザード標識","生物災害","danger"]},{"category":"symbols","char":"📴","name":"mobile_phone_off","keywords":["けいたいでんわでんげんおふ","けいたい","こみゅにけーしょん","もばいる","おふ","けいたいでんわ","でんわ","携帯電話電源オフ","携帯","コミュニケーション","モバイル","オフ","携帯電話","電話","mute","orange-square","silence","quiet"]},{"category":"symbols","char":"📳","name":"vibration_mode","keywords":["まなーもーど","けいたい","こみゅにけーしょん","もばいる","もーど","けいたいでんわ","でんわ","ばいぶれーしょん","マナーモード","携帯","コミュニケーション","モバイル","モード","携帯電話","電話","バイブレーション","orange-square","phone"]},{"category":"symbols","char":"🈶","name":"u6709","keywords":["しかくかこみゆう","にほんご","あり","四角囲み有","日本語","あり","orange-square","chinese","have","kanji","ari"]},{"category":"symbols","char":"🈚","name":"u7121","keywords":["しかくかこみむ","しかくかこみいな","にほんご","なし","四角囲み無","四角囲み否","日本語","なし","nothing","chinese","kanji","japanese","orange-square","nashi"]},{"category":"symbols","char":"🈸","name":"u7533","keywords":["しかくかこみしん","しかくかこみてき","ちゅうごくご","しんせい","四角囲み申","四角囲み適","中国語","申請","chinese","japanese","kanji","orange-square","moushikomi"]},{"category":"symbols","char":"🈺","name":"u55b6","keywords":["しかくかこみえい","ちゅうごくご","えいぎょう","四角囲み営","中国語","営業","japanese","opening hours","orange-square","eigyo"]},{"category":"symbols","char":"🈷️","name":"u6708","keywords":["しかくかこみつき","にほんご","つきぎめ","四角囲み月","日本語","月極","chinese","month","moon","japanese","orange-square","kanji","tsuki","tsukigime","getsugaku"]},{"category":"symbols","char":"✴️","name":"eight_pointed_black_star","keywords":["はちりょうぼし","ほし","八稜星","星","orange-square","shape","polygon"]},{"category":"symbols","char":"🆚","name":"vs","keywords":["しかくかこみVS","たい","VS","四角囲みVS","対","VS","words","orange-square"]},{"category":"symbols","char":"🉑","name":"accept","keywords":["まるかこみきょか","まるかこみか","ちゅうごくご","かのう","丸囲み許可","丸囲み可","中国語","可能","ok","good","chinese","kanji","agree","yes","orange-circle"]},{"category":"symbols","char":"💮","name":"white_flower","keywords":["しろいはな","はな","たいへんよくできました","白い花","花","たいへんよくできました","japanese","spring"]},{"category":"symbols","char":"🉐","name":"ideograph_advantage","keywords":["まるかこみとく","にほんご","とく","丸囲み得","日本語","得","chinese","kanji","obtain","get","circle"]},{"category":"symbols","char":"㊙️","name":"secret","keywords":["まるかこみひ","ちゅうごくご","ひょういもじ","ひ","丸囲み秘","中国語","表意文字","秘","privacy","chinese","sshh","kanji","red-circle"]},{"category":"symbols","char":"㊗️","name":"congratulations","keywords":["まるかこみしゅく","ちゅうごくご","おめでとう","しゅく","丸囲み祝","中国語","おめでとう","しゅく","chinese","kanji","japanese","red-circle"]},{"category":"symbols","char":"🈴","name":"u5408","keywords":["しかくかこみのごう","しかくかこみごう","ちゅうごくご","ごうかく","てきごう","四角囲みの合","四角囲み合","中国語","合格","適合","japanese","chinese","join","kanji","red-square","goukaku","pass"]},{"category":"symbols","char":"🈵","name":"u6e80","keywords":["しかくかこみまん","ちゅうごくご","まんしつ","まんしゃ","まんたん","四角囲み満","中国語","満室","満車","満タン","full","chinese","japanese","red-square","kanji","man"]},{"category":"symbols","char":"🈲","name":"u7981","keywords":["しかくかこみきん","にほんご","きんし","四角囲み禁","日本語","禁止","kanji","japanese","chinese","forbidden","limit","restricted","red-square","kinshi"]},{"category":"symbols","char":"🅰️","name":"a","keywords":["くろしかくかこみA","A","けつえきがた","黒四角囲みA","A","血液型","red-square","alphabet","letter"]},{"category":"symbols","char":"🅱️","name":"b","keywords":["くろしかくかこみB","B","けつえきがた","黒四角囲みB","B","血液型","red-square","alphabet","letter"]},{"category":"symbols","char":"🆎","name":"ab","keywords":["くろしかくかこみAB","AB","けつえきがた","黒四角囲みAB","AB","血液型","red-square","alphabet"]},{"category":"symbols","char":"🆑","name":"cl","keywords":["しかくかこみCL","CL","四角囲みCL","CL","alphabet","words","red-square"]},{"category":"symbols","char":"🅾️","name":"o2","keywords":["くろしかくかこみO","けつえきがた","O","黒四角囲みO","血液型","O","alphabet","red-square","letter"]},{"category":"symbols","char":"🆘","name":"sos","keywords":["しかくかこみSOS","へるぷ","SOS","四角囲みSOS","ヘルプ","SOS","help","red-square","words","emergency","911"]},{"category":"symbols","char":"⛔","name":"no_entry","keywords":["たちいりきんし","たちいり","きんし","だめ","できない","きんじる","こうつう","立入禁止","立ち入り","禁止","だめ","できない","禁じる","交通","limit","security","privacy","bad","denied","stop","circle"]},{"category":"symbols","char":"📛","name":"name_badge","keywords":["なふだ","ばっじ","なまえ","名札","バッジ","名前","fire","forbid"]},{"category":"symbols","char":"🚫","name":"no_entry_sign","keywords":["しんにゅうきんし","たちいり","きんし","だめ","できない","きんじる","進入禁止","立ち入り","禁止","だめ","できない","禁じる","forbid","stop","limit","denied","disallow","circle"]},{"category":"symbols","char":"❌","name":"x","keywords":["ばつしるし","きゃんせる","きごう","かけざん","じょうざん","x","バツ印","キャンセル","記号","掛け算","乗算","x","no","delete","remove","cancel","red"]},{"category":"symbols","char":"⭕","name":"o","keywords":["ふといおおきなまる","まる","O","太い大きな丸","丸","O","circle","round"]},{"category":"symbols","char":"🛑","name":"stop_sign","keywords":["いちじていしひょうしき","はっかっけい","ひょうしき","ていし","一時停止標識","八角形","標識","停止","stop"]},{"category":"symbols","char":"💢","name":"anger","keywords":["いかりまーく","いかり","まんが","げきど","怒りマーク","怒り","漫画","激怒","angry","mad"]},{"category":"symbols","char":"♨️","name":"hotsprings","keywords":["おんせん","あたたかい","わきでる","じょうき","温泉","温かい","湧き出る","蒸気","bath","warm","relax"]},{"category":"symbols","char":"🚷","name":"no_pedestrians","keywords":["ほこうしゃたちいりきんし","きんし","だめ","ない","ほこうしゃ","きんじる","歩行者立入禁止","禁止","だめ","ない","歩行者","禁じる","rules","crossing","walking","circle"]},{"category":"symbols","char":"🚯","name":"do_not_litter","keywords":["ぽいすてきんし","きんし","ごみ","だめ","ない","きんしされている","ポイ捨て禁止","禁止","ごみ","だめ","ない","禁止されている","trash","bin","garbage","circle"]},{"category":"symbols","char":"🚳","name":"no_bicycles","keywords":["じてんしゃきんし","じてんしゃ","ばいく","きんし","だめ","できない","きんじる","のりもの","自転車禁止","自転車","バイク","禁止","だめ","できない","禁じる","乗り物","cyclist","prohibited","circle"]},{"category":"symbols","char":"🚱","name":"non-potable_water","keywords":["いんようふか","ひいんりょうすい","いんりょう","きんし","だめ","ない","いんよう","きんしされている","みず","飲用不可","非飲料水","飲料","禁止","だめ","ない","飲用","禁止されている","水","drink","faucet","tap","circle"]},{"category":"symbols","char":"🔞","name":"underage","keywords":["18さいみまんきんし","18","ねんれいせいげん","じゅうはち","きんし","だめ","ない","きんしした","みせいねんしゃ","18歳未満禁止","18","年齢制限","十八","禁止","だめ","ない","禁止した","未成年者","18","drink","pub","night","minor","circle"]},{"category":"symbols","char":"📵","name":"no_mobile_phones","keywords":["けいたいでんわきんし","けいたい","つうしん","きんし","もばいる","だめ","できない","けいたいでんわ","きんしされている","でんわ","携帯電話禁止","携帯","通信","禁止","モバイル","だめ","できない","携帯電話","禁止されている","電話","iphone","mute","circle"]},{"category":"symbols","char":"❗","name":"exclamation","keywords":["あかいびっくりまーく","びっくり","まーく","きごう","赤いビックリマーク","ビックリ","マーク","記号","heavy_exclamation_mark","danger","surprise","punctuation","wow","warning"]},{"category":"symbols","char":"❕","name":"grey_exclamation","keywords":["しろいびっくりまーく","びっくり","まーく","かこみ","きごう","白いビックリマーク","ビックリ","マーク","囲み","記号","surprise","punctuation","gray","wow","warning"]},{"category":"symbols","char":"❓","name":"question","keywords":["あかいはてなまーく","まーく","きごう","はてな","赤いはてなマーク","マーク","記号","はてな","doubt","confused"]},{"category":"symbols","char":"❔","name":"grey_question","keywords":["しろいはてなまーく","まーく","かこみ","きごう","はてな","白いはてなマーク","マーク","囲み","記号","はてな","doubts","gray","huh","confused"]},{"category":"symbols","char":"‼️","name":"bangbang","keywords":["!!まーく","ばんばん","びっくり","まーく","きごう","!!マーク","バンバン","ビックリ","マーク","記号","exclamation","surprise"]},{"category":"symbols","char":"⁉️","name":"interrobang","keywords":["!?","びっくり","いんてろばんぐ","まーく","きごう","はてな","!?","ビックリ","インテロバング","マーク","記号","はてな","wat","punctuation","surprise"]},{"category":"symbols","char":"🔅","name":"low_brightness","keywords":["ていきど","あかるさ","うすぐらい","てい","低輝度","明るさ","薄暗い","低","sun","afternoon","warm","summer"]},{"category":"symbols","char":"🔆","name":"high_brightness","keywords":["こうきど","あかるい","あかるさ","高輝度","明るい","明るさ","sun","light"]},{"category":"symbols","char":"🔱","name":"trident","keywords":["とらいでんと","いかり","えんぶれむ","ふね","こうぐ","トライデント","いかり","エンブレム","船","工具","weapon","spear"]},{"category":"symbols","char":"⚜","name":"fleur_de_lis","keywords":["ゆりのもんしょう","ユリの紋章","decorative","scout"]},{"category":"symbols","char":"〽️","name":"part_alternation_mark","keywords":["いおりてん","しるし","ぶぶん","庵点","印","部分","graph","presentation","stats","business","economics","bad"]},{"category":"symbols","char":"⚠️","name":"warning","keywords":["けいこく","警告","exclamation","wip","alert","error","problem","issue"]},{"category":"symbols","char":"🚸","name":"children_crossing","keywords":["こうさてんをわたるこどもたち","こども","こうさてん","ほこうしゃ","こうつう","交差点を渡る子供たち","子供","交差点","歩行者","交通","school","warning","danger","sign","driving","yellow-diamond"]},{"category":"symbols","char":"🔰","name":"beginner","keywords":["しょしんしゃまーく","しょしんしゃ","まーく","みどり","にっぽん","わかば","どうぐ","き","初心者マーク","初心者","マーク","緑","日本","若葉","道具","黄","badge","shield"]},{"category":"symbols","char":"♻️","name":"recycle","keywords":["りさいくるまーく","りさいくる","リサイクルマーク","リサイクル","arrow","environment","garbage","trash"]},{"category":"symbols","char":"🈯","name":"u6307","keywords":["しかくかこみゆび","にほんご","四角囲み指","日本語","chinese","point","green-square","kanji","reserved","shiteiseki"]},{"category":"symbols","char":"💹","name":"chart","keywords":["じょうしょうとれんどのちゃーととえんきごう","じょうしょうちゅうえんちゃーと","ぎんこう","ちゃーと","つうか","ぐらふ","せいちょう","しじょう","おかね","じょうしょう","とれんど","うわむき","えん","上昇トレンドのチャートと円記号","上昇中円チャート","銀行","チャート","通貨","グラフ","成長","市場","お金","上昇","トレンド","上向き","円","green-square","graph","presentation","stats"]},{"category":"symbols","char":"❇️","name":"sparkle","keywords":["きらきら","キラキラ","stars","green-square","awesome","good","fireworks"]},{"category":"symbols","char":"✳️","name":"eight_spoked_asterisk","keywords":["あすたりすく (8ほんこうせい)","あすたりすく","アスタリスク (8本構成)","アスタリスク","star","sparkle","green-square"]},{"category":"symbols","char":"❎","name":"negative_squared_cross_mark","keywords":["しかくでかこまれたばつしるし","まーく","しかく","四角で囲まれたバツ印","マーク","四角","x","green-square","no","deny"]},{"category":"symbols","char":"✅","name":"white_check_mark","keywords":["しろいふとじのちぇっくまーく","ちぇっく","まーく","白い太字のチェックマーク","チェック","マーク","green-square","ok","agree","vote","election","answer","tick"]},{"category":"symbols","char":"💠","name":"diamond_shape_with_a_dot_inside","keywords":["どっともようのだいや","まんが","だいやもんど","きかがく","ないぶ","ドット模様のダイヤ","漫画","ダイヤモンド","幾何学","内部","jewel","blue","gem","crystal","fancy"]},{"category":"symbols","char":"🌀","name":"cyclone","keywords":["さいくろん","ていきあつ","めまい","たつまき","たいふう","てんき","サイクロン","低気圧","めまい","竜巻","台風","天気","weather","swirl","blue","cloud","vortex","spiral","whirlpool","spin","tornado","hurricane","typhoon"]},{"category":"symbols","char":"➿","name":"loop","keywords":["にじゅうのかーるじょうのるーぷ","かーる","だぶる","るーぷ","二重のカール状のループ","カール","ダブル","ループ","tape","cassette"]},{"category":"symbols","char":"🌐","name":"globe_with_meridians","keywords":["しごせん・けいせんのあるちきゅう","ちきゅう","ちきゅうぎ","けいせん","せかい","子午線・経線のある地球","地球","地球儀","経線","世界","earth","international","world","internet","interweb","i18n"]},{"category":"symbols","char":"Ⓜ️","name":"m","keywords":["まるかこみM","えん","M","丸囲みM","円","M","alphabet","blue-circle","letter"]},{"category":"symbols","char":"🏧","name":"atm","keywords":["ATM","ATMきごう","じどう","ぎんこう","すいとう","ATM","ATM記号","自動","銀行","出納","money","sales","cash","blue-square","payment","bank"]},{"category":"symbols","char":"🈂️","name":"sa","keywords":["しかくかこみさ","にっぽんじん","さーびす","四角囲みサ","日本人","サービス","japanese","blue-square","katakana"]},{"category":"symbols","char":"🛂","name":"passport_control","keywords":["にゅうこくしんさ","ぱすぽーと","入国審査","パスポート","custom","blue-square"]},{"category":"symbols","char":"🛃","name":"customs","keywords":["ぜいかん","税関","passport","border","blue-square"]},{"category":"symbols","char":"🛄","name":"baggage_claim","keywords":["てにもつうけとりしょ","てにもつ","うけとり","手荷物受取所","手荷物","受け取り","blue-square","airport","transport"]},{"category":"symbols","char":"🛅","name":"left_luggage","keywords":["てにもつあずかりしょ","てにもつ","ろっかー","けいこうひん","手荷物預かり所","手荷物","ロッカー","携行品","blue-square","travel"]},{"category":"symbols","char":"🛜","name":"wireless","keywords":["むせん","こんぴゅーた","いんたーねっと","ねっとわーく","Wi-Fi","せつぞく","無線","コンピュータ","インターネット","ネットワーク","Wi-Fi","接続","blue-square","computer","internet","network"]},{"category":"symbols","char":"♿","name":"wheelchair","keywords":["くるまいす","あくせす","車いす","アクセス","車椅子","blue-square","disabled","a11y","accessibility"]},{"category":"symbols","char":"🚭","name":"no_smoking","keywords":["きんえん","きんし","だめ","できない","きんしされている","きつえん","禁煙","禁止","だめ","できない","禁止されている","喫煙","cigarette","blue-square","smell","smoke"]},{"category":"symbols","char":"🚾","name":"wc","keywords":["といれ","けしょうしつ","おてあらい","みず","WC","トイレ","化粧室","お手洗い","水","WC","toilet","restroom","blue-square"]},{"category":"symbols","char":"🅿️","name":"parking","keywords":["くろしかくかこみP","ちゅうしゃじょう","黒四角囲みP","駐車場","cars","blue-square","alphabet","letter"]},{"category":"symbols","char":"🚰","name":"potable_water","keywords":["いんりょうすい","のみもの","みず","飲料水","飲み物","水","blue-square","liquid","restroom","cleaning","faucet"]},{"category":"symbols","char":"🚹","name":"mens","keywords":["だんせいのきごう","だんせいよう","といれ","おとこ","だんせい","男性の記号","男性用","トイレ","男","おとこ","男性","toilet","restroom","wc","blue-square","gender","male"]},{"category":"symbols","char":"🚺","name":"womens","keywords":["じょせいのきごう","じょせいよう","といれ","おんな","じょせい","女性の記号","女性用","トイレ","女","おんな","女性","purple-square","woman","female","toilet","loo","restroom","gender"]},{"category":"symbols","char":"🚼","name":"baby_symbol","keywords":["あかちゃんまーく","あかちゃん","おむつかえ","赤ちゃんマーク","赤ちゃん","おむつ替え","orange-square","child"]},{"category":"symbols","char":"🚻","name":"restroom","keywords":["といれ","けしょうしつ","WC","トイレ","化粧室","WC","blue-square","toilet","refresh","wc","gender"]},{"category":"symbols","char":"🚮","name":"put_litter_in_its_place","keywords":["ごみすてじょう","びんのごみすてじょう","ごみ","ごみばこ","ゴミ捨て場","ビンのゴミ捨て場","ゴミ","ゴミ箱","blue-square","sign","human","info"]},{"category":"symbols","char":"🎦","name":"cinema","keywords":["えいが","あくてぃびてぃ","かめら","えんたーていめんと","ふぃるむ","どうが","映画","アクティビティ","カメラ","エンターテイメント","フィルム","動画","blue-square","record","film","movie","curtain","stage","theater"]},{"category":"symbols","char":"📶","name":"signal_strength","keywords":["あんてな","ばー","けいたい","こみゅにけーしょん","もばいる","けいたいでんわ","しぐなる","でんわ","アンテナ","バー","携帯","コミュニケーション","モバイル","携帯電話","シグナル","電話","blue-square","reception","phone","internet","connection","wifi","bluetooth","bars"]},{"category":"symbols","char":"🈁","name":"koko","keywords":["しかくかこみここ","にっぽんじん","四角囲みココ","日本人","blue-square","here","katakana","japanese","destination"]},{"category":"symbols","char":"🆖","name":"ng","keywords":["しかくかこみNG","NG","四角囲みNG","NG","blue-square","words","shape","icon"]},{"category":"symbols","char":"🆗","name":"ok","keywords":["しかくかこみOK","OK","四角囲みOK","OK","good","agree","yes","blue-square"]},{"category":"symbols","char":"🆙","name":"up","keywords":["しかくかこみUP!","まーく","うえ","四角囲みUP!","マーク","上","blue-square","above","high"]},{"category":"symbols","char":"🆒","name":"cool","keywords":["COOL","かっこいい","くーる","COOL","かっこいい","クール","words","blue-square"]},{"category":"symbols","char":"🆕","name":"new","keywords":["しかくかこみnew","しん","四角囲みnew","新","blue-square","words","start"]},{"category":"symbols","char":"🆓","name":"free","keywords":["しかくかこみFREE","ふりー","むりょう","四角囲みFREE","フリー","無料","blue-square","words"]},{"category":"symbols","char":"0️⃣","name":"zero","keywords":["0きー","0","きー","ぜろ","0キー","0","キー","ゼロ","0","numbers","blue-square","null"]},{"category":"symbols","char":"1️⃣","name":"one","keywords":["1きー","いち","きー","1キー","1","キー","一","blue-square","numbers","1"]},{"category":"symbols","char":"2️⃣","name":"two","keywords":["2きー","2","きー","に","2キー","2","キー","ニ","numbers","2","prime","blue-square"]},{"category":"symbols","char":"3️⃣","name":"three","keywords":["3きー","3","きー","さん","3キー","3","キー","三","3","numbers","prime","blue-square"]},{"category":"symbols","char":"4️⃣","name":"four","keywords":["4きー","4","よん","きー","4キー","4","四","キー","4","numbers","blue-square"]},{"category":"symbols","char":"5️⃣","name":"five","keywords":["5きー","5","ご","きー","5キー","5","五","キー","5","numbers","blue-square","prime"]},{"category":"symbols","char":"6️⃣","name":"six","keywords":["6きー","6","きー","ろく","6キー","6","キー","六","6","numbers","blue-square"]},{"category":"symbols","char":"7️⃣","name":"seven","keywords":["7きー","7","きー","なな","7キー","7","キー","七","7","numbers","blue-square","prime"]},{"category":"symbols","char":"8️⃣","name":"eight","keywords":["8きー","8","はち","きー","8キー","8","八","キー","8","blue-square","numbers"]},{"category":"symbols","char":"9️⃣","name":"nine","keywords":["9きー","9","きー","きゅう","9キー","9","キー","九","blue-square","numbers","9"]},{"category":"symbols","char":"🔟","name":"keycap_ten","keywords":["10きー","10","きー","じゅう","10キー","10","キー","十","numbers","10","blue-square"]},{"category":"symbols","char":"*⃣","name":"asterisk","keywords":["あすたりすくきー","あすたりすく","きー","ほし","アスタリスクキー","アスタリスク","キー","星","star","keycap"]},{"category":"symbols","char":"⏏️","name":"eject_button","keywords":["とりだしまーく","とりだしぼたん","取り出しマーク","取り出しボタン","blue-square"]},{"category":"symbols","char":"▶️","name":"arrow_forward","keywords":["みぎむきさんかく","さいせいぼたん","やじるし","さいせい","みぎ","さんかっけい","右向き三角","再生ボタン","矢印","再生","右","三角形","blue-square","right","direction","play"]},{"category":"symbols","char":"⏸","name":"pause_button","keywords":["2ほんのすいちょくばー","いちじていしぼたん","ばー","2ばい","いちじていし","すいちょく","2本の垂直バー","一時停止ボタン","バー","2倍","一時停止","垂直","pause","blue-square"]},{"category":"symbols","char":"⏭","name":"next_track_button","keywords":["みぎむきのにじゅうさんかっけいとすいちょくぼう","「つぎのきょく」ぼたん","やじるし","つぎのばめん","つぎのきょく","さんかっけい","右向きの二重三角形と垂直棒","「次の曲」ボタン","矢印","次の場面","次の曲","三角形","forward","next","blue-square"]},{"category":"symbols","char":"⏹","name":"stop_button","keywords":["ていし","ていしぼたん","しかく","停止","停止ボタン","四角","blue-square"]},{"category":"symbols","char":"⏺","name":"record_button","keywords":["ろくが","ろくがぼたん","まる","録画","録画ボタン","丸","blue-square"]},{"category":"symbols","char":"⏯","name":"play_or_pause_button","keywords":["みぎむきのさんかっけいとにじゅうすいちょくぼう","さいせいまたはいちじていしぼたん","やじるし","いちじていし","さいせい","みぎ","さんかっけい","右向きの三角形と二重垂直棒","再生または一時停止ボタン","矢印","一時停止","再生","右","三角形","blue-square","play","pause"]},{"category":"symbols","char":"⏮","name":"previous_track_button","keywords":["ひだりむきのにじゅうさんかっけいとすいちょくぼう","「まえのきょく」ぼたん","やじるし","まえのばめん","まえのきょく","さんかっけい","左向きの二重三角形と垂直棒","「前の曲」ボタン","矢印","前の場面","前の曲","三角形","backward"]},{"category":"symbols","char":"⏩","name":"fast_forward","keywords":["みぎむきのにじゅうさんかっけい","はやおくりぼたん","やじるし","2ばい","こうそく","すすむ","右向きの二重三角形","早送りボタン","矢印","2倍","高速","進む","blue-square","play","speed","continue"]},{"category":"symbols","char":"⏪","name":"rewind","keywords":["ひだりむきのにじゅうさんかっけい","はやもどしぼたん","やじるし","2ばい","まきもどし","左向きの二重三角形","早戻しボタン","矢印","2倍","巻き戻し","play","blue-square"]},{"category":"symbols","char":"🔀","name":"twisted_rightwards_arrows","keywords":["ねじりみぎむきやじるしのえもじ","しゃっふる","やじるし","こうさ","ねじり右向き矢印の絵文字","シャッフル","矢印","交差","blue-square","shuffle","music","random"]},{"category":"symbols","char":"🔁","name":"repeat","keywords":["りぴーと","りぴーとぼたん","やじるし","とけいまわり","リピート","リピートボタン","矢印","時計回り","loop","record"]},{"category":"symbols","char":"🔂","name":"repeat_one","keywords":["1きょくをりぴーとさいせい","りぴーとぼたん","やじるし","とけいまわり","いちど","1曲をリピート再生","リピートボタン","矢印","時計回り","一度","blue-square","loop"]},{"category":"symbols","char":"◀️","name":"arrow_backward","keywords":["ひだりむきのさんかっけい","はんてんぼたん","やじるし","ひだり","はんてん","さんかっけい","左向きの三角形","反転ボタン","矢印","左","反転","三角形","blue-square","left","direction"]},{"category":"symbols","char":"🔼","name":"arrow_up_small","keywords":["うわむきのさんかっけい","うえぼたん","やじるし","ぼたん","うえ","上向きの三角形","上ボタン","矢印","ボタン","上","blue-square","triangle","direction","point","forward","top"]},{"category":"symbols","char":"🔽","name":"arrow_down_small","keywords":["したむきのさんかっけい","したぼたん","やじるし","ぼたん","した","下向きの三角形","下ボタン","矢印","ボタン","下","blue-square","direction","bottom"]},{"category":"symbols","char":"⏫","name":"arrow_double_up","keywords":["うわむきのにじゅうさんかっけい","こうそくじょうしょうぼたん","やじるし","だぶる","うえ","上向きの二重三角形","高速上昇ボタン","矢印","ダブル","上","blue-square","direction","top"]},{"category":"symbols","char":"⏬","name":"arrow_double_down","keywords":["したむきのにじゅうさんかっけい","こうそくだうんぼたん","やじるし","だぶる","した","下向きの二重三角形","高速ダウンボタン","矢印","ダブル","下","blue-square","direction","bottom"]},{"category":"symbols","char":"➡️","name":"arrow_right","keywords":["みぎむきやじるし","みぎやじるし","やじるし","しゅよう","ほうこう","ひがし","右向き矢印","右矢印","矢印","主要","方向","東","blue-square","next"]},{"category":"symbols","char":"⬅️","name":"arrow_left","keywords":["ひだりむきやじるし","ひだりやじるし","やじるし","しゅよう","ほうこう","にし","左向き矢印","左矢印","矢印","主要","方向","西","blue-square","previous","back"]},{"category":"symbols","char":"⬆️","name":"arrow_up","keywords":["うわむきやじるし","うえやじるし","やじるし","しゅよう","ほうこう","きた","上向き矢印","上矢印","矢印","主要","方向","北","blue-square","continue","top","direction"]},{"category":"symbols","char":"⬇️","name":"arrow_down","keywords":["したむきやじるし","したやじるし","やじるし","しゅよう","ほうこう","した","みなみ","下向き矢印","下矢印","矢印","主要","方向","下","南","blue-square","direction","bottom"]},{"category":"symbols","char":"↗️","name":"arrow_upper_right","keywords":["みぎうえやじるし","やじるし","ほうこう","ななめ","ほくとう","右上矢印","矢印","方向","斜め","北東","blue-square","point","direction","diagonal","northeast"]},{"category":"symbols","char":"↘️","name":"arrow_lower_right","keywords":["みぎしたやじるし","やじるし","ほうこう","ななめ","なんとう","右下矢印","矢印","方向","斜め","南東","blue-square","direction","diagonal","southeast"]},{"category":"symbols","char":"↙️","name":"arrow_lower_left","keywords":["ひだりしたやじるし","やじるし","ほうこう","ななめ","なんせい","左下矢印","矢印","方向","斜め","南西","blue-square","direction","diagonal","southwest"]},{"category":"symbols","char":"↖️","name":"arrow_upper_left","keywords":["ひだりうえやじるし","やじるし","ほうこう","ななめ","ほくせい","左上矢印","矢印","方向","斜め","北西","blue-square","point","direction","diagonal","northwest"]},{"category":"symbols","char":"↕️","name":"arrow_up_down","keywords":["じょうげやじるし","やじるし","ほうこう","ななめ","ほくせい","上下矢印","矢印","方向","斜め","北西","blue-square","direction","way","vertical"]},{"category":"symbols","char":"↔️","name":"left_right_arrow","keywords":["さゆうやじるし","やじるし","左右矢印","矢印","shape","direction","horizontal","sideways"]},{"category":"symbols","char":"🔄","name":"arrows_counterclockwise","keywords":["うずまきやじるし","はんとけいまわり","やじるし","ひだりまわり","うずまき矢印","反時計回り","矢印","左回り","blue-square","sync","cycle"]},{"category":"symbols","char":"↪️","name":"arrow_right_hook","keywords":["みぎむきだんつきやじるし","みぎにまがったやじるし","やじるし","右向き段付き矢印","右に曲がった矢印","矢印","blue-square","return","rotate","direction"]},{"category":"symbols","char":"↩️","name":"leftwards_arrow_with_hook","keywords":["ひだりむきだんつきやじるし","ひだりにまがったやじるし","やじるし","左向き段付き矢印","左に曲がった矢印","矢印","back","return","blue-square","undo","enter"]},{"category":"symbols","char":"⤴️","name":"arrow_heading_up","keywords":["みぎうえへかーぶするやじるし","うえへかーぶするみぎやじるし","やじるし","右上へカーブする矢印","上へカーブする右矢印","矢印","blue-square","direction","top"]},{"category":"symbols","char":"⤵️","name":"arrow_heading_down","keywords":["みぎしたへかーぶするやじるし","したにかーぶするみぎやじるし","やじるし","した","右下へカーブする矢印","下にカーブする右矢印","矢印","下","blue-square","direction","bottom"]},{"category":"symbols","char":"#️⃣","name":"hash","keywords":["#きー","はっしゅ","きー","ぽんど","#キー","ハッシュ","キー","ポンド","symbol","blue-square","twitter"]},{"category":"symbols","char":"ℹ️","name":"information_source","keywords":["じょうほうげん","i","いんふぉめーしょん","情報源","i","インフォメーション","blue-square","alphabet","letter"]},{"category":"symbols","char":"🔤","name":"abc","keywords":["あるふぁべっとにゅうりょく","abc","あるふぁべっと","にゅうりょく","らてん","もじ","アルファベット入力","abc","アルファベット","入力","ラテン","文字","blue-square","alphabet"]},{"category":"symbols","char":"🔡","name":"abcd","keywords":["あるふぁべっとこもじにゅうりょく","abcd","にゅうりょく","らてん","もじ","こもじ","アルファベット小文字入力","abcd","入力","ラテン","文字","小文字","blue-square","alphabet"]},{"category":"symbols","char":"🔠","name":"capital_abcd","keywords":["あるふぁべっとおおもじにゅうりょく","にゅうりょく","らてん","もじ","おおもじ","アルファベット大文字入力","入力","ラテン","文字","大文字","alphabet","words","blue-square"]},{"category":"symbols","char":"🔣","name":"symbols","keywords":["きごうにゅうりょく","にゅうりょく","記号入力","入力","blue-square","music","note","ampersand","percent","glyphs","characters"]},{"category":"symbols","char":"🎵","name":"musical_note","keywords":["おんぷ","あくてぃびてぃ","えんたーていめんと","おんがく","音符","アクティビティ","エンターテイメント","音楽","score","tone","sound"]},{"category":"symbols","char":"🎶","name":"notes","keywords":["ふくすうのおんぷ","あくてぃびてぃ","えんたーていめんと","おんがく","おんぷ","複数の音符","アクティビティ","エンターテイメント","音楽","音符","music","score"]},{"category":"symbols","char":"〰️","name":"wavy_dash","keywords":["はせん","だっしゅ","きごう","なみ","波線","ダッシュ","記号","波","draw","line","moustache","mustache","squiggle","scribble"]},{"category":"symbols","char":"➰","name":"curly_loop","keywords":["かーるじょうのるーぷ","かーる","るーぷ","カール状のループ","カール","ループ","scribble","draw","shape","squiggle"]},{"category":"symbols","char":"✔️","name":"heavy_check_mark","keywords":["ふとじのちぇっくまーく","ちぇっく","まーく","太字のチェックマーク","チェック","マーク","ok","nike","answer","yes","tick"]},{"category":"symbols","char":"🔃","name":"arrows_clockwise","keywords":["るーぷやじるし","とけいのはり","やじるし","とけいまわり","りろーど","ループ矢印","時計の針","矢印","時計回り","リロード","sync","cycle","round","repeat"]},{"category":"symbols","char":"➕","name":"heavy_plus_sign","keywords":["ふとじの+きごう","すうがく","ぷらす","太字の+記号","数学","プラス","math","calculation","addition","more","increase"]},{"category":"symbols","char":"➖","name":"heavy_minus_sign","keywords":["ふとじのまいなすきごう","すうがく","まいなす","太字のマイナス記号","数学","マイナス","math","calculation","subtract","less"]},{"category":"symbols","char":"➗","name":"heavy_division_sign","keywords":["ふとじのわるきごう","わりざん","すうがく","太字の÷記号","割り算","数学","divide","math","calculation"]},{"category":"symbols","char":"✖️","name":"heavy_multiplication_x","keywords":["ふとじのかけるしるし","きゃんせる","じょうざん","かける","x","太字の×印","キャンセル","乗算","かける","x","math","calculation"]},{"category":"symbols","char":"🟰","name":"heavy_equals_sign","keywords":["ふといとうごう","とうしき","すうがく","ひとしい","太い等号","等式","数学","等しい"]},{"category":"symbols","char":"♾","name":"infinity","keywords":["むげん","えいえん","ふへんてき","無限","永遠","普遍的","forever"]},{"category":"symbols","char":"💲","name":"heavy_dollar_sign","keywords":["ふとじのどるきごう","つうか","どる","おかね","太字のドル記号","通貨","ドル","お金","money","sales","payment","currency","buck"]},{"category":"symbols","char":"💱","name":"currency_exchange","keywords":["がいかりょうがえ","ぎんこう","つうか","りょうがえ","おかね","外貨両替","銀行","通貨","両替","お金","money","sales","dollar","travel"]},{"category":"symbols","char":"©️","name":"copyright","keywords":["こぴーらいとまーく","ちょさくけん","コピーライトマーク","著作権","ip","license","circle","law","legal"]},{"category":"symbols","char":"®️","name":"registered","keywords":["とうろくしょうひょうまーく","とうろくずみ","しょうひょう","登録商標マーク","登録済み","商標","alphabet","circle"]},{"category":"symbols","char":"™️","name":"tm","keywords":["しょうひょうまーく","まーく","tm","しょうひょう","商標マーク","マーク","tm","商標","trademark","brand","law","legal"]},{"category":"symbols","char":"🔚","name":"end","keywords":["ENDとひだりやじるし","やじるし","はじ","ENDと左矢印","矢印","端","words","arrow"]},{"category":"symbols","char":"🔙","name":"back","keywords":["BACKとひだりやじるし","やじるし","もどる","BACKと左矢印","矢印","戻る","arrow","words","return"]},{"category":"symbols","char":"🔛","name":"on","keywords":["ON!とさゆうやじるし","やじるし","まーく","おん","ON!と左右矢印","矢印","マーク","オン","arrow","words"]},{"category":"symbols","char":"🔝","name":"top","keywords":["TOPとうえやじるし","やじるし","とっぷ","うえ","TOPと上矢印","矢印","トップ","上","words","blue-square"]},{"category":"symbols","char":"🔜","name":"soon","keywords":["SOONとみぎやじるし","やじるし","まもなく","SOONと右矢印","矢印","まもなく","arrow","words"]},{"category":"symbols","char":"☑️","name":"ballot_box_with_check","keywords":["ちぇっくいりちぇっくぼっくす","とうひょう","ぼっくす","ちぇっく","チェック入りチェックボックス","投票","ボックス","チェック","ok","agree","confirm","black-square","vote","election","yes","tick"]},{"category":"symbols","char":"🔘","name":"radio_button","keywords":["らじおぼたん","ぼたん","きかがく","らじお","ラジオボタン","ボタン","幾何学","ラジオ","input","old","music","circle"]},{"category":"symbols","char":"⚫","name":"black_circle","keywords":["くろまる","えん","きかがく","黒丸","円","幾何学","shape","button","round"]},{"category":"symbols","char":"⚪","name":"white_circle","keywords":["しろまる","えん","きかがく","白丸","円","幾何学","shape","round"]},{"category":"symbols","char":"🔴","name":"red_circle","keywords":["あかまる","えん","きかがく","あか","赤丸","円","幾何学","赤","shape","error","danger"]},{"category":"symbols","char":"🟠","name":"orange_circle","keywords":["おれんじいろのえん","えん","きかがく","おれんじ","オレンジ色の円","円","幾何学","オレンジ","shape"]},{"category":"symbols","char":"🟡","name":"yellow_circle","keywords":["きいろのまる","えん","きかがく","ちゃいろ","黄色の丸","円","幾何学","茶色","shape"]},{"category":"symbols","char":"🟢","name":"green_circle","keywords":["みどりまる","えん","きかがく","みどり","緑丸","円","幾何学","緑","shape"]},{"category":"symbols","char":"🔵","name":"large_blue_circle","keywords":["あおまる","あお","えん","きかがく","青丸","青","円","幾何学","shape","icon","button"]},{"category":"symbols","char":"🟣","name":"purple_circle","keywords":["むらさきのまる","えん","きかがく","むらさき","紫の丸","円","幾何学","紫","shape"]},{"category":"symbols","char":"🟤","name":"brown_circle","keywords":["ちゃいろのまる","えん","きかがく","ちゃいろ","茶色の丸","円","幾何学","茶色","shape"]},{"category":"symbols","char":"🔸","name":"small_orange_diamond","keywords":["ちいさいおれんじのだいやもんど","だいやもんど","きかがく","おれんじ","小さいオレンジのダイヤモンド","ダイヤモンド","幾何学","オレンジ","shape","jewel","gem"]},{"category":"symbols","char":"🔹","name":"small_blue_diamond","keywords":["ちいさくてあおいだいやもんど","あお","だいやもんど","きかがく","小さくて青いダイヤモンド","青","ダイヤモンド","幾何学","shape","jewel","gem"]},{"category":"symbols","char":"🔶","name":"large_orange_diamond","keywords":["おおきいおれんじのだいや","だいやもんど","きかがく","おれんじ","大きいオレンジのダイヤ","ダイヤモンド","幾何学","オレンジ","shape","jewel","gem"]},{"category":"symbols","char":"🔷","name":"large_blue_diamond","keywords":["おおきくてあおいだいやもんど","あお","だいやもんど","きかがく","大きくて青いダイヤモンド","青","ダイヤモンド","幾何学","shape","jewel","gem"]},{"category":"symbols","char":"🔺","name":"small_red_triangle","keywords":["うわむきのあかいさんかっけい","うえ","きかがく","あか","上向きの赤い三角形","上","幾何学","赤","shape","direction","up","top"]},{"category":"symbols","char":"▪️","name":"black_small_square","keywords":["くろいちいさなしかく","きかがく","せいほうけい","黒い小さな四角","幾何学","正方形","shape","icon"]},{"category":"symbols","char":"▫️","name":"white_small_square","keywords":["しろいちいさなしかく","きかがく","せいほうけい","白い小さな四角","幾何学","正方形","shape","icon"]},{"category":"symbols","char":"⬛","name":"black_large_square","keywords":["くろいおおきなしかく","きかがく","せいほうけい","黒い大きな四角","幾何学","正方形","shape","icon","button"]},{"category":"symbols","char":"⬜","name":"white_large_square","keywords":["しろいおおきなしかく","きかがく","せいほうけい","白い大きな四角","幾何学","正方形","shape","icon","stone","button"]},{"category":"symbols","char":"🟥","name":"red_square","keywords":["あかのせいほうけい","せいほうけい","きかがく","あか","赤の正方形","正方形","幾何学","赤","shape"]},{"category":"symbols","char":"🟧","name":"orange_square","keywords":["おれんじしょくのせいほうけい","せいほうけい","きかがく","おれんじ","オレンジ色の正方形","正方形","幾何学","オレンジ","shape"]},{"category":"symbols","char":"🟨","name":"yellow_square","keywords":["きいろのせいほうけい","せいほうけい","きかがく","きいろ","黄色の正方形","正方形","幾何学","黄色","shape"]},{"category":"symbols","char":"🟩","name":"green_square","keywords":["みどりのせいほうけい","せいほうけい","きかがく","みどり","緑の正方形","正方形","幾何学","緑","shape"]},{"category":"symbols","char":"🟦","name":"blue_square","keywords":["あおのせいほうけい","せいほうけい","きかがく","あお","青の正方形","正方形","幾何学","青","shape"]},{"category":"symbols","char":"🟪","name":"purple_square","keywords":["むらさきのせいほうけい","せいほうけい","きかがく","むらさき","紫の正方形","正方形","幾何学","紫","shape"]},{"category":"symbols","char":"🟫","name":"brown_square","keywords":["ちゃいろのせいほうけい","せいほうけい","きかがく","ちゃいろ","茶色の正方形","正方形","幾何学","茶色","shape"]},{"category":"symbols","char":"🔻","name":"small_red_triangle_down","keywords":["したむきのさんかっけい","だうん","きかがく","あか","下向きの三角形","ダウン","幾何学","赤","shape","direction","bottom"]},{"category":"symbols","char":"◼️","name":"black_medium_square","keywords":["くろいちゅうくらいのしかく","きかがく","せいほうけい","黒い中くらいの四角","幾何学","正方形","shape","button","icon"]},{"category":"symbols","char":"◻️","name":"white_medium_square","keywords":["しろくてちゅうくらいのしかく","きかがく","せいほうけい","白くて中くらいの四角","幾何学","正方形","shape","stone","icon"]},{"category":"symbols","char":"◾","name":"black_medium_small_square","keywords":["くろくてちゅうくらいのちいさいしかく","きかがく","せいほうけい","黒くて中くらいの小さい四角","幾何学","正方形","icon","shape","button"]},{"category":"symbols","char":"◽","name":"white_medium_small_square","keywords":["しろいちゅうくらいのちいさなしかく","きかがく","せいほうけい","白い中くらいの小さな四角","幾何学","正方形","shape","stone","icon","button"]},{"category":"symbols","char":"🔲","name":"black_square_button","keywords":["くろいしかくぼたん","ぼたん","きかがく","せいほうけい","黒い四角ボタン","ボタン","幾何学","正方形","shape","input","frame"]},{"category":"symbols","char":"🔳","name":"white_square_button","keywords":["しろいしかくぼたん","ぼたん","きかがく","かこみ","しかく","白い四角ボタン","ボタン","幾何学","囲み","四角","shape","input"]},{"category":"symbols","char":"🔈","name":"speaker","keywords":["すぴーかー","おんりょう","スピーカー","音量","sound","volume","silence","broadcast"]},{"category":"symbols","char":"🔉","name":"sound","keywords":["おんりょうしょう","でんげんがはいったすぴーかー","ひくい","すぴーかー","おんりょう","なみ","音量小","電源が入ったスピーカー","低い","スピーカー","音量","波","volume","speaker","broadcast"]},{"category":"symbols","char":"🔊","name":"loud_sound","keywords":["おんりょうだい","だいおんりょうのすぴーかー","3","えんたーていめんと","たかい","おとのおおきい","すぴーかー","ぼりゅーむ","音量大","大音量のスピーカー","3","エンターテイメント","高い","音の大きい","スピーカー","ボリューム","volume","noise","noisy","speaker","broadcast"]},{"category":"symbols","char":"🔇","name":"mute","keywords":["むおんのすぴーかー","すぴーかー","おふ","みゅーと","せいおん","むおん","おんりょう","無音のスピーカー","スピーカー","オフ","ミュート","静音","無音","音量","sound","volume","silence","quiet"]},{"category":"symbols","char":"📣","name":"mega","keywords":["めがほん","おうえん","こみゅにけーしょん","かくせいき","メガホン","応援","コミュニケーション","拡声器","sound","speaker","volume"]},{"category":"symbols","char":"📢","name":"loudspeaker","keywords":["かくせいき","こみゅにけーしょん","おおごえ","すぴーかー","ぱぶりっくあどれす","めがほん","拡声器","コミュニケーション","大声","スピーカー","パブリックアドレス","メガホン","volume","sound"]},{"category":"symbols","char":"🔔","name":"bell","keywords":["べる","ベル","sound","notification","christmas","xmas","chime"]},{"category":"symbols","char":"🔕","name":"no_bell","keywords":["みゅーと","すらっしゅべる","かね","きんじられた","だめ","ない","きんし","しずか","ミュート","スラッシュベル","鐘","禁じられた","だめ","ない","禁止","静か","sound","volume","mute","quiet","silent"]},{"category":"symbols","char":"🃏","name":"black_joker","keywords":["とらんぷのじょーかー","かーど","えんたーていめんと","げーむ","じょーかー","ぷれい","トランプのジョーカー","カード","エンターテイメント","ゲーム","ジョーカー","プレイ","poker","cards","game","play","magic"]},{"category":"symbols","char":"🀄","name":"mahjong","keywords":["まーじゃんぱいのちゅう","げーむ","まーじゃん","あか","麻雀牌の中","ゲーム","麻雀","赤","game","play","chinese","kanji"]},{"category":"symbols","char":"♠️","name":"spades","keywords":["とらんぷのすぺーど","かーど","げーむ","すぺーど","すーつ","トランプのスペード","カード","ゲーム","スペード","スーツ","poker","cards","suits","magic"]},{"category":"symbols","char":"♣️","name":"clubs","keywords":["とらんぷのくらぶ","かーど","くらぶ","げーむ","すーつ","トランプのクラブ","カード","クラブ","ゲーム","スーツ","poker","cards","magic","suits"]},{"category":"symbols","char":"♥️","name":"hearts","keywords":["とらんぷのはーと","かーど","げーむ","はーと","すーつ","トランプのハート","カード","ゲーム","ハート","スーツ","poker","cards","magic","suits"]},{"category":"symbols","char":"♦️","name":"diamonds","keywords":["とらんぷのだいや","かーど","だいや","だいやもんど","げーむ","すーつ","トランプのダイヤ","カード","ダイヤ","ダイヤモンド","ゲーム","スーツ","poker","cards","magic","suits"]},{"category":"symbols","char":"🎴","name":"flower_playing_cards","keywords":["はなふだ","あくてぃびてぃ","かーど","えんたーていめんと","はな","げーむ","にっぽん","ぷれい","花札","アクティビティ","カード","エンターテイメント","花","ゲーム","日本","プレイ","game","sunset","red"]},{"category":"symbols","char":"💭","name":"thought_balloon","keywords":["かんがえふきだし","ふきだし","あわ","まんが","かんがえ","考え吹き出し","吹き出し","泡","漫画","考え","bubble","cloud","speech","thinking","dream"]},{"category":"symbols","char":"🗯","name":"right_anger_bubble","keywords":["みぎむきのいかりのふきだし","いかり","ふきだし","あわ","げきど","右向きの怒りの吹き出し","怒り","吹き出し","泡","激怒","caption","speech","thinking","mad"]},{"category":"symbols","char":"💬","name":"speech_balloon","keywords":["ふきだし","あわ","まんが","せりふ","すぴーち","吹き出し","泡","漫画","セリフ","スピーチ","bubble","words","message","talk","chatting"]},{"category":"symbols","char":"🗨","name":"left_speech_bubble","keywords":["ひだりむきのふきだし","せりふ","すぴーち","左向きの吹き出し","セリフ","スピーチ","words","message","talk","chatting"]},{"category":"symbols","char":"🕐","name":"clock1","keywords":["1じ","0ふん","1","とけい","とき","いち","1時","0分","1","時計","時","一","time","late","early","schedule"]},{"category":"symbols","char":"🕑","name":"clock2","keywords":["2じ","0ふん","2","とけい","とき","に","2時","0分","2","時計","時","二","time","late","early","schedule"]},{"category":"symbols","char":"🕒","name":"clock3","keywords":["3じ","0ふん","3","とけい","とき","さん","3時","0分","3","時計","時","三","time","late","early","schedule"]},{"category":"symbols","char":"🕓","name":"clock4","keywords":["4じ","0ふん","4","とけい","よん","とき","4時","0分","4","時計","四","時","time","late","early","schedule"]},{"category":"symbols","char":"🕔","name":"clock5","keywords":["5じ","0ふん","5","とけい","ご","とき","5時","0分","5","時計","五","時","time","late","early","schedule"]},{"category":"symbols","char":"🕕","name":"clock6","keywords":["6じ","0ふん","6","とけい","とき","ろく","6時","0分","6","時計","時","六","time","late","early","schedule","dawn","dusk"]},{"category":"symbols","char":"🕖","name":"clock7","keywords":["7じ","0ふん","7","とけい","とき","なな","7時","0分","7","時計","時","七","time","late","early","schedule"]},{"category":"symbols","char":"🕗","name":"clock8","keywords":["8じ","0ふん","8","とけい","はち","とき","8時","0分","8","時計","八","時","time","late","early","schedule"]},{"category":"symbols","char":"🕘","name":"clock9","keywords":["9じ","0ふん","9","とけい","きゅう","とき","9時","0分","9","時計","九","時","time","late","early","schedule"]},{"category":"symbols","char":"🕙","name":"clock10","keywords":["10じ","0ふん","10","とけい","とき","じゅう","10時","0分","10","時計","時","十","time","late","early","schedule"]},{"category":"symbols","char":"🕚","name":"clock11","keywords":["11じ","0ふん","11","とけい","じゅういち","とき","11時","0分","11","時計","十一","時","time","late","early","schedule"]},{"category":"symbols","char":"🕛","name":"clock12","keywords":["12じ","0ふん","12","とけい","じゅうに","とき","12時","0分","12","時計","十二","時","time","noon","midnight","midday","late","early","schedule"]},{"category":"symbols","char":"🕜","name":"clock130","keywords":["1じはん","1じ","はん","じこく","いち","30","1時半","1時","半","時刻","一","30","time","late","early","schedule"]},{"category":"symbols","char":"🕝","name":"clock230","keywords":["2じはん","2じ","はん","じこく","30","に","2時半","2時","半","時刻","30","二","time","late","early","schedule"]},{"category":"symbols","char":"🕞","name":"clock330","keywords":["3じはん","3じ","はん","じこく","30","さん","3時半","3時","半","時刻","30","三","time","late","early","schedule"]},{"category":"symbols","char":"🕟","name":"clock430","keywords":["4じはん","30","4じ","じこく","よん","はん","4時半","30","4時","時刻","四","半","time","late","early","schedule"]},{"category":"symbols","char":"🕠","name":"clock530","keywords":["5じはん","30","5じ","じこく","ご","はん","5時半","30","5時","時刻","五","半","time","late","early","schedule"]},{"category":"symbols","char":"🕡","name":"clock630","keywords":["6じはん","30","6じ","じこく","ろく","はん","6時半","30","6時","時刻","六","半","time","late","early","schedule"]},{"category":"symbols","char":"🕢","name":"clock730","keywords":["7じはん","30","7じ","じこく","なな","はん","7時半","30","7時","時刻","七","半","time","late","early","schedule"]},{"category":"symbols","char":"🕣","name":"clock830","keywords":["8じはん","30","8じ","じこく","はち","はん","8時半","30","8時","時刻","八","半","time","late","early","schedule"]},{"category":"symbols","char":"🕤","name":"clock930","keywords":["9じはん","30","9じ","じこく","きゅう","はん","9時半","30","9時","時刻","九","半","time","late","early","schedule"]},{"category":"symbols","char":"🕥","name":"clock1030","keywords":["10じはん","10じ","はん","じこく","じゅう","30","10時半","10時","半","時刻","十","30","time","late","early","schedule"]},{"category":"symbols","char":"🕦","name":"clock1130","keywords":["11じはん","11じ","はん","じこく","じゅういち","30","11時半","11時","半","時刻","十一","30","time","late","early","schedule"]},{"category":"symbols","char":"🕧","name":"clock1230","keywords":["12じはん","12じ","はん","じこく","30","じゅうに","12時半","12時","半","時刻","30","十二","time","late","early","schedule"]},{"category":"flags","char":"🇦🇫","name":"afghanistan","keywords":["あふがにすたんこっき","あふがにすたん","こっき","アフガニスタン国旗","アフガニスタン","国旗","af","afghanistan","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇽","name":"aland_islands","keywords":["おーらんどしょとうのはた","おーらんどしょとう","こっき","オーランド諸島の旗","オーランド諸島","国旗","ax","Åland","aland","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇱","name":"albania","keywords":["あるばにあこっき","あるばにあ","こっき","アルバニア国旗","アルバニア","国旗","al","albania","flag","nation","country","banner"]},{"category":"flags","char":"🇩🇿","name":"algeria","keywords":["あるじぇりあこっき","あるじぇりあ","こっき","アルジェリア国旗","アルジェリア","国旗","dz","algeria","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇸","name":"american_samoa","keywords":["あめりかりょうさもあのはた","あめりかりょう","こっき","さもあ","アメリカ領サモアの旗","アメリカ領","国旗","サモア","as","american","samoa","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇩","name":"andorra","keywords":["あんどらこっき","あんどら","こっき","アンドラ国旗","アンドラ","国旗","ad","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇴","name":"angola","keywords":["あんごらこっき","あんごら","こっき","アンゴラ国旗","アンゴラ","国旗","ao","angola","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇮","name":"anguilla","keywords":["あんぎらとうのはた","あんぎらとう","こっき","アンギラ島の旗","アンギラ島","国旗","ai","anguilla","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇶","name":"antarctica","keywords":["なんきょくたいりくのはた","なんきょくたいりく","こっき","南極大陸の旗","南極大陸","国旗","aq","antarctique","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇬","name":"antigua_barbuda","keywords":["あんてぃぐあばーぶーだこっき","あんてぃぐあ","ばーぶーだ","こっき","アンティグア・バーブーダ国旗","アンティグア","バーブーダ","国旗","ag","antigua","barbuda","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇷","name":"argentina","keywords":["あるぜんちんこっき","あるぜんちん","こっき","アルゼンチン国旗","アルゼンチン","国旗","ar","argentina","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇲","name":"armenia","keywords":["あるめにあこっき","あるめにあ","こっき","アルメニア国旗","アルメニア","国旗","am","armenia","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇼","name":"aruba","keywords":["あるばこっき","あるば","こっき","アルバ国旗","アルバ","国旗","aw","aruba","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇨","name":"ascension_island","keywords":["あせんしょんとうのはた","あせんしょん","こっき","しま","アセンション島の旗","アセンション","国旗","島","ac","ascension","island","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇺","name":"australia","keywords":["おーすとらりあこっき","おーすとらりあ","こっき","はーど","まくどなるど","オーストラリア国旗","オーストラリア","国旗","ハード","マクドナルド","au","australia","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇹","name":"austria","keywords":["おーすとりあこっき","おーすとりあ","こっき","オーストリア国旗","オーストリア","国旗","at","austria","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇿","name":"azerbaijan","keywords":["あぜるばいじゃんこっき","あぜるばいじゃん","こっき","アゼルバイジャン国旗","アゼルバイジャン","国旗","az","azerbaijan","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇸","name":"bahamas","keywords":["ばはまこっき","ばはま","こっき","バハマ国旗","バハマ","国旗","bs","bahamas","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇭","name":"bahrain","keywords":["ばーれーんこっき","ばーれーん","こっき","バーレーン国旗","バーレーン","国旗","bh","bahrain","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇩","name":"bangladesh","keywords":["ばんぐらでしゅこっき","ばんぐらでしゅ","こっき","バングラデシュ国旗","バングラデシュ","国旗","bd","bangladesh","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇧","name":"barbados","keywords":["ばるばどすこっき","ばるばどす","こっき","バルバドス国旗","バルバドス","国旗","bb","barbados","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇾","name":"belarus","keywords":["べらるーしこっき","べらるーし","こっき","ベラルーシ国旗","ベラルーシ","国旗","by","belarus","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇪","name":"belgium","keywords":["べるぎーこっき","べるぎー","こっき","ベルギー国旗","ベルギー","国旗","be","belgium","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇿","name":"belize","keywords":["べりーずこっき","べりーず","こっき","ベリーズ国旗","ベリーズ","国旗","bz","belize","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇯","name":"benin","keywords":["べなんこっき","べなん","こっき","ベナン国旗","ベナン","国旗","bj","benin","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇲","name":"bermuda","keywords":["ばみゅーだしょとうのはた","ばみゅーだしょとう","こっき","バミューダ諸島の旗","バミューダ諸島","国旗","bm","bermuda","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇹","name":"bhutan","keywords":["ぶーたんこっき","ぶーたん","こっき","ブータン国旗","ブータン","国旗","bt","bhutan","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇴","name":"bolivia","keywords":["ぼりびあこっき","ぼりびあ","こっき","ボリビア国旗","ボリビア","国旗","bo","bolivia","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇶","name":"caribbean_netherlands","keywords":["かりぶかいのおらんだりょうとうのはた","ぼねーるとう","かりぶかい","ゆーすたてぃうす","こっき","おらんだ","さば","しんと","カリブ海のオランダ領島の旗","ボネール島","カリブ海","ユースタティウス","国旗","オランダ","サバ","シント","bq","bonaire","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇦","name":"bosnia_herzegovina","keywords":["ぼすにあへるつぇごびなこっき","ぼすにあ","こっき","へるつぇごびな","ボスニア・ヘルツェゴビナ国旗","ボスニア","国旗","ヘルツェゴビナ","ba","bosnia","herzegovina","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇼","name":"botswana","keywords":["ぼつわなこっき","ぼつわな","こっき","ボツワナ国旗","ボツワナ","国旗","bw","botswana","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇷","name":"brazil","keywords":["ぶらじるこっき","ぶらじる","こっき","ブラジル国旗","ブラジル","国旗","br","brazil","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇴","name":"british_indian_ocean_territory","keywords":["いぎりすりょういんどようちいきのはた","いぎりすりょう","ちゃごす","はた","いんどよう","しま","でぃえごがるしあ","イギリス領インド洋地域の旗","イギリス領","チャゴス","旗","インド洋","島","ディエゴガルシア","io","british","indian","ocean","territory","flag","nation","country","banner"]},{"category":"flags","char":"🇻🇬","name":"british_virgin_islands","keywords":["いぎりすりょうヴぁぁーじんしょとうのはた","いぎりすりょう","こっき","しま","ヴぁーじん","イギリス領ヴァージン諸島の旗","イギリス領","国旗","島","ヴァージン","vg","british","virgin","islands","bvi","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇳","name":"brunei","keywords":["ぶるねいこっき","ぶるねい","だるさらーむ","こっき","ブルネイ国旗","ブルネイ","ダルサラーム","国旗","bn","brunei","darussalam","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇬","name":"bulgaria","keywords":["ぶるがりあこっき","ぶるがりあ","こっき","ブルガリア国旗","ブルガリア","国旗","bg","bulgaria","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇫","name":"burkina_faso","keywords":["ぶるきなふぁそこっき","ぶるきなふぁそ","こっき","ブルキナファソ国旗","ブルキナファソ","国旗","bf","burkina","faso","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇮","name":"burundi","keywords":["ぶるんじこっき","ぶるんじ","こっき","ブルンジ国旗","ブルンジ","国旗","bi","burundi","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇻","name":"cape_verde","keywords":["かーぼべるでこっき","かーぼ","けーぷ","こっき","べるで","カーボベルデ国旗","カーボ","ケープ","国旗","ベルデ","cv","cabo","verde","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇭","name":"cambodia","keywords":["かんぼじあこっき","かんぼじあ","こっき","カンボジア国旗","カンボジア","国旗","kh","cambodia","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇲","name":"cameroon","keywords":["かめるーんこっき","かめるーん","こっき","カメルーン国旗","カメルーン","国旗","cm","cameroon","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇦","name":"canada","keywords":["かなだこっき","かなだ","こっき","カナダ国旗","カナダ","国旗","ca","canada","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇨","name":"canary_islands","keywords":["かなりあしょとうのはた","かなりあ","こっき","しょとう","カナリア諸島の旗","カナリア","国旗","諸島","ic","canary","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇾","name":"cayman_islands","keywords":["けいまんしょとうのはた","けいまん","こっき","しょとう","ケイマン諸島の旗","ケイマン","国旗","諸島","ky","cayman","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇫","name":"central_african_republic","keywords":["ちゅうおうあふりかこっき","ちゅうおうあふりかきょうわこく","こっき","きょうわこく","中央アフリカ国旗","中央アフリカ共和国","国旗","共和国","cf","central","african","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇩","name":"chad","keywords":["ちゃどこっき","ちゃど","こっき","チャド国旗","チャド","国旗","td","chad","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇱","name":"chile","keywords":["ちりこっき","ちり","こっき","チリ国旗","チリ","国旗","cl","chile","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇳","name":"cn","keywords":["ちゅうごくこっき","ちゅうごく","こっき","中国国旗","中国","国旗","cn","china","chinese","prc","flag","country","nation","banner"]},{"category":"flags","char":"🇨🇽","name":"christmas_island","keywords":["くりすますとうのはた","くりすます","こっき","しま","クリスマス島の旗","クリスマス","国旗","島","cx","christmas","island","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇨","name":"cocos_islands","keywords":["ここすしょとうのはた","ここす","こっき","しょとう","きーりんぐ","ココス諸島の旗","ココス","国旗","諸島","キーリング","cc","cocos","keeling","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇴","name":"colombia","keywords":["ころんびあこっき","ころんびあ","こっき","コロンビア国旗","コロンビア","国旗","co","colombia","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇲","name":"comoros","keywords":["こもろこっき","こもろ","こっき","コモロ国旗","コモロ","国旗","km","comoros","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇬","name":"congo_brazzaville","keywords":["こんごのはた - ぶらざびる","ぶらざびる","こんご","こんごきょうわこく","こんご - ぶらざびる","こっき","きょうわこく","コンゴの旗 - ブラザビル","ブラザビル","コンゴ","コンゴ共和国","コンゴ - ブラザビル","国旗","共和国","cg","republic","congo","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇩","name":"congo_kinshasa","keywords":["こんごこっき - きんしゃさ","こんご","こんご - きんしゃさ","こんごみんしゅきょうわこく","こっき","きんしゃさ","きょうわこく","コンゴ国旗 - キンシャサ","コンゴ","コンゴ - キンシャサ","コンゴ民主共和国","国旗","キンシャサ","共和国","cd","democratic","republic","congo","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇰","name":"cook_islands","keywords":["くっくしょとうこっき","くっく","こっき","しょとう","クック諸島国旗","クック","国旗","諸島","ck","cook","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇷","name":"costa_rica","keywords":["こすたりかこっき","こすたりか","こっき","コスタリカ国旗","コスタリカ","国旗","cr","costa","rica","flag","nation","country","banner"]},{"category":"flags","char":"🇭🇷","name":"croatia","keywords":["くろあちあこっき","くろあちあ","こっき","クロアチア国旗","クロアチア","国旗","hr","croatia","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇺","name":"cuba","keywords":["きゅーばこっき","きゅーば","こっき","キューバ国旗","キューバ","国旗","cu","cuba","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇼","name":"curacao","keywords":["きゅらそーとうのはた","あんてぃるしょとう","きゅらそー","こっき","キュラソー島の旗","アンティル諸島","キュラソー","国旗","cw","curacao","curaçao","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇾","name":"cyprus","keywords":["きぷろすこっき","きぷろす","こっき","キプロス国旗","キプロス","国旗","cy","cyprus","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇿","name":"czech_republic","keywords":["ちぇここっき","ちぇこきょうわこく","こっき","チェコ国旗","チェコ共和国","国旗","cz","czech","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇩🇰","name":"denmark","keywords":["でんまーくこっき","でんまーく","こっき","デンマーク国旗","デンマーク","国旗","dk","denmark","flag","nation","country","banner"]},{"category":"flags","char":"🇩🇯","name":"djibouti","keywords":["じぶちこっき","じぶち","こっき","ジブチ国旗","ジブチ","国旗","dj","djibouti","flag","nation","country","banner"]},{"category":"flags","char":"🇩🇲","name":"dominica","keywords":["どみにかこっき","どみにか","こっき","ドミニカ国旗","ドミニカ","国旗","dm","dominica","flag","nation","country","banner"]},{"category":"flags","char":"🇩🇴","name":"dominican_republic","keywords":["どみにかきょうわこくこっき","どみにかきょうわこく","こっき","ドミニカ共和国国旗","ドミニカ共和国","国旗","do","dominican","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇪🇨","name":"ecuador","keywords":["えくあどるこっき","えくあどる","こっき","エクアドル国旗","エクアドル","国旗","ec","ecuador","flag","nation","country","banner"]},{"category":"flags","char":"🇪🇬","name":"egypt","keywords":["えじぷとこっき","えじぷと","こっき","エジプト国旗","エジプト","国旗","eg","egypt","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇻","name":"el_salvador","keywords":["えるさるばどるこっき","えるさるばどる","こっき","エルサルバドル国旗","エルサルバドル","国旗","sv","el","salvador","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇶","name":"equatorial_guinea","keywords":["せきどうぎにあこっき","せきどうぎにあ","こっき","ぎにあ","赤道ギニア国旗","赤道ギニア","国旗","ギニア","gq","equatorial","guinea","flag","nation","country","banner"]},{"category":"flags","char":"🇪🇷","name":"eritrea","keywords":["えりとりあこっき","えりとりあ","こっき","エリトリア国旗","エリトリア","国旗","er","eritrea","flag","nation","country","banner"]},{"category":"flags","char":"🇪🇪","name":"estonia","keywords":["えすとにあこっき","えすとにあ","こっき","エストニア国旗","エストニア","国旗","ee","estonia","flag","nation","country","banner"]},{"category":"flags","char":"🇪🇹","name":"ethiopia","keywords":["えちおぴあこっき","えちおぴあ","こっき","エチオピア国旗","エチオピア","国旗","et","ethiopia","flag","nation","country","banner"]},{"category":"flags","char":"🇪🇺","name":"eu","keywords":["おうしゅうはた","おうしゅうれんごう","こっき","欧州旗","欧州連合","旗","eu","european","union","flag","banner"]},{"category":"flags","char":"🇫🇰","name":"falkland_islands","keywords":["ふぉーくらんどしょとうのはた","ふぉーくらんど","ふぉーくらんどしょとう","こっき","しょとう","まるびなす","フォークランド諸島の旗","フォークランド","フォークランド諸島","国旗","諸島","マルビナス","fk","falkland","islands","malvinas","flag","nation","country","banner"]},{"category":"flags","char":"🇫🇴","name":"faroe_islands","keywords":["ふぇろーしょとうのはた","ふぇろー","はた","しょとう","フェロー諸島の旗","フェロー","旗","諸島","fo","faroe","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇫🇯","name":"fiji","keywords":["ふぃじーこっき","ふぃじー","こっき","フィジー国旗","フィジー","国旗","fj","fiji","flag","nation","country","banner"]},{"category":"flags","char":"🇫🇮","name":"finland","keywords":["ふぃんらんどこっき","ふぃんらんど","こっき","フィンランド国旗","フィンランド","国旗","fi","finland","flag","nation","country","banner"]},{"category":"flags","char":"🇫🇷","name":"fr","keywords":["ふらんすこっき","こっき","ふらんす","くりっぱーとんとう","せんと・まーちん","さん・まるたん","フランス国旗","国旗","フランス","クリッパートン島","セント・マーチン","サン・マルタン","fr","banner","flag","nation","france","french","country"]},{"category":"flags","char":"🇬🇫","name":"french_guiana","keywords":["ふらんすりょうぎあなのはた","こっき","ふらんすりょう","ぎあな","フランス領ギアナの旗","国旗","フランス領","ギアナ","gf","french","guiana","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇫","name":"french_polynesia","keywords":["ふらんすりょうぽりねしあのはた","こっき","ふらんすりょう","ぽりねしあ","フランス領ポリネシアの旗","国旗","フランス領","ポリネシア","pf","french","polynesia","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇫","name":"french_southern_territories","keywords":["ふらんすりょうなんぽう・なんきょくちいきのはた","なんきょく","こっき","ふらんすりょう","フランス領南方・南極地域の旗","南極","国旗","フランス領","tf","french","southern","territories","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇦","name":"gabon","keywords":["がぼんこっき","こっき","がぼん","ガボン国旗","国旗","ガボン","ga","gabon","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇲","name":"gambia","keywords":["がんびあこっき","こっき","がんびあ","ガンビア国旗","国旗","ガンビア","gm","gambia","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇪","name":"georgia","keywords":["じょーじあこっき","こっき","じょーじあ","ジョージア国旗","国旗","ジョージア","ge","georgia","flag","nation","country","banner"]},{"category":"flags","char":"🇩🇪","name":"de","keywords":["どいつこっき","こっき","どいつ","ドイツ国旗","国旗","ドイツ","de","deutschland","german","nation","flag","country","banner"]},{"category":"flags","char":"🇬🇭","name":"ghana","keywords":["がーなこっき","こっき","がーな","ガーナ国旗","国旗","ガーナ","gh","ghana","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇮","name":"gibraltar","keywords":["じぶらるたるこっき","こっき","じぶらるたる","ジブラルタル国旗","国旗","ジブラルタル","gi","gibraltar","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇷","name":"greece","keywords":["ぎりしゃこっき","こっき","ぎりしゃ","ギリシャ国旗","国旗","ギリシャ","gr","greece","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇱","name":"greenland","keywords":["ぐりーんらんどこっき","こっき","ぐりーんらんど","グリーンランド国旗","国旗","グリーンランド","gl","green","land","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇩","name":"grenada","keywords":["ぐれなだこっき","こっき","ぐれなだ","グレナダ国旗","国旗","グレナダ","gd","grenada","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇵","name":"guadeloupe","keywords":["ぐあどるーぷこっき","こっき","ぐあどるーぷ","グアドループ国旗","国旗","グアドループ","gp","guadeloupe","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇺","name":"guam","keywords":["ぐあむはた","こっき","ぐあむ","グアム旗","国旗","グアム","gu","guam","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇹","name":"guatemala","keywords":["ぐあてまらこっき","こっき","ぐあてまら","グアテマラ国旗","国旗","グアテマラ","gt","guatemala","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇬","name":"guernsey","keywords":["がーんじーこっき","こっき","がーんじー","ガーンジー国旗","国旗","ガーンジー","gg","guernsey","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇳","name":"guinea","keywords":["ぎにあこっき","こっき","ぎにあ","ギニア国旗","国旗","ギニア","gn","guinea","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇼","name":"guinea_bissau","keywords":["ぎにあびさうこっき","びさう","こっき","ぎにあ","ギニアビサウ国旗","ビサウ","国旗","ギニア","gw","guiana","bissau","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇾","name":"guyana","keywords":["がいあなこっき","こっき","がいあな","ガイアナ国旗","国旗","ガイアナ","gy","guyana","flag","nation","country","banner"]},{"category":"flags","char":"🇭🇹","name":"haiti","keywords":["はいちこっき","こっき","はいち","ハイチ国旗","国旗","ハイチ","ht","haiti","flag","nation","country","banner"]},{"category":"flags","char":"🇭🇳","name":"honduras","keywords":["ほんじゅらすこっき","こっき","ほんじゅらす","ホンジュラス国旗","国旗","ホンジュラス","hn","honduras","flag","nation","country","banner"]},{"category":"flags","char":"🇭🇰","name":"hong_kong","keywords":["ほんこんのはた","ちゅうごく","こっき","ほんこん","香港の旗","中国","国旗","香港","hk","hong","kong","flag","nation","country","banner"]},{"category":"flags","char":"🇭🇺","name":"hungary","keywords":["はんがりーこっき","こっき","はんがりー","ハンガリー国旗","国旗","ハンガリー","hu","hungary","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇸","name":"iceland","keywords":["あいすらんどこっき","こっき","あいすらんど","アイスランド国旗","国旗","アイスランド","is","iceland","Ísland","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇳","name":"india","keywords":["いんどこっき","こっき","いんど","インド国旗","国旗","インド","in","india","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇩","name":"indonesia","keywords":["いんどねしあこっき","こっき","いんどねしあ","インドネシア国旗","国旗","インドネシア","id","indonesia","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇷","name":"iran","keywords":["いらんこっき","こっき","いらん","イラン国旗","国旗","イラン","ir","iran","islamic","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇶","name":"iraq","keywords":["いらくこっき","こっき","いらく","イラク国旗","国旗","イラク","iq","iraq","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇪","name":"ireland","keywords":["あいるらんどこっき","こっき","あいるらんど","アイルランド国旗","国旗","アイルランド","ie","ireland","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇲","name":"isle_of_man","keywords":["まんとうのはた","こっき","まんとう","マン島の旗","国旗","マン島","im","isle","man","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇱","name":"israel","keywords":["いすらえるこっき","こっき","いすらえる","イスラエル国旗","国旗","イスラエル","il","israel","flag","nation","country","banner"]},{"category":"flags","char":"🇮🇹","name":"it","keywords":["いたりあこっき","こっき","いたりあ","イタリア国旗","国旗","イタリア","it","italy","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇮","name":"cote_divoire","keywords":["こーとじぼわーるこっき","こーとじぼわーる","こっき","コートジボワール国旗","コートジボワール","国旗","ci","cote","divoire","Côte","d'Ivoire","ivory","coast","flag","nation","country","banner"]},{"category":"flags","char":"🇯🇲","name":"jamaica","keywords":["じゃまいかこっき","こっき","じゃまいか","ジャマイカ国旗","国旗","ジャマイカ","jm","jamaica","flag","nation","country","banner"]},{"category":"flags","char":"🇯🇵","name":"jp","keywords":["にっぽんこっき","こっき","にっぽん","日本国旗","国旗","日本","jp","japan","japanese","nation","flag","country","banner"]},{"category":"flags","char":"🇯🇪","name":"jersey","keywords":["じゃーじーだいかんかんかつくのはた","こっき","じゃーじーだいかんかんかつく","ジャージー代官管轄区の旗","国旗","ジャージー代官管轄区","je","jersey","flag","nation","country","banner"]},{"category":"flags","char":"🇯🇴","name":"jordan","keywords":["よるだんこっき","こっき","よるだん","ヨルダン国旗","国旗","ヨルダン","jo","jordan","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇿","name":"kazakhstan","keywords":["かざふすたんこっき","こっき","かざふすたん","カザフスタン国旗","国旗","カザフスタン","kz","kazakhstan","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇪","name":"kenya","keywords":["けにあこっき","こっき","けにあ","ケニア国旗","国旗","ケニア","ke","kenya","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇮","name":"kiribati","keywords":["きりばすこっき","こっき","きりばす","キリバス国旗","国旗","キリバス","ki","kiribati","flag","nation","country","banner"]},{"category":"flags","char":"🇽🇰","name":"kosovo","keywords":["こそぼこっき","こっき","こそぼ","コソボ国旗","国旗","コソボ","xk","kosovo","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇼","name":"kuwait","keywords":["くうぇーとこっき","こっき","くうぇーと","クウェート国旗","国旗","クウェート","kw","kuwait","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇬","name":"kyrgyzstan","keywords":["きるぎすこっき","こっき","きるぎす","キルギス国旗","国旗","キルギス","kg","kyrgyzstan","kyrgyz","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇦","name":"laos","keywords":["らおすこっき","こっき","らおす","ラオス国旗","国旗","ラオス","la","laos","lao","democratic","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇻","name":"latvia","keywords":["らとびあこっき","こっき","らとびあ","ラトビア国旗","国旗","ラトビア","lv","latvia","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇧","name":"lebanon","keywords":["ればのんこっき","こっき","ればのん","レバノン国旗","国旗","レバノン","lb","lebanon","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇸","name":"lesotho","keywords":["れそとこっき","こっき","れそと","レソト国旗","国旗","レソト","ls","lesotho","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇷","name":"liberia","keywords":["りべりあこっき","こっき","りべりあ","リベリア国旗","国旗","リベリア","lr","liberia","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇾","name":"libya","keywords":["りびあこっき","こっき","りびあ","リビア国旗","国旗","リビア","ly","libya","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇮","name":"liechtenstein","keywords":["りひてんしゅたいんこっき","こっき","りひてんしゅたいん","リヒテンシュタイン国旗","国旗","リヒテンシュタイン","li","liechtenstein","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇹","name":"lithuania","keywords":["りとあにあこっき","こっき","りとあにあ","リトアニア国旗","国旗","リトアニア","lt","lithuania","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇺","name":"luxembourg","keywords":["るくせんぶるくこっき","こっき","るくせんぶるく","ルクセンブルク国旗","国旗","ルクセンブルク","lu","luxembourg","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇴","name":"macau","keywords":["まかおのはた","ちゅうごく","こっき","まかお","マカオの旗","中国","国旗","マカオ","mo","macao","macau","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇰","name":"macedonia","keywords":["まけどにあこっき","こっき","まけどにあ","マケドニア国旗","国旗","マケドニア","mk","north","macedonia","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇬","name":"madagascar","keywords":["まだがすかるこっき","こっき","まだがすかる","マダガスカル国旗","国旗","マダガスカル","mg","madagascar","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇼","name":"malawi","keywords":["まらういこっき","こっき","まらうい","マラウイ国旗","国旗","マラウイ","mw","malawi","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇾","name":"malaysia","keywords":["まれーしあこっき","こっき","まれーしあ","マレーシア国旗","国旗","マレーシア","my","malaysia","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇻","name":"maldives","keywords":["もるでぃぶこっき","こっき","もるでぃぶ","モルディブ国旗","国旗","モルディブ","mv","maldives","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇱","name":"mali","keywords":["まりこっき","こっき","まり","マリ国旗","国旗","マリ","ml","mali","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇹","name":"malta","keywords":["まるたこっき","こっき","まるた","マルタ国旗","国旗","マルタ","mt","malta","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇭","name":"marshall_islands","keywords":["まーしゃるしょとうこっき","こっき","しょとう","まーしゃる","マーシャル諸島国旗","国旗","諸島","マーシャル","mh","marshall","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇶","name":"martinique","keywords":["まるてぃにーくのはた","はた","まるてぃにーく","マルティニークの旗","旗","マルティニーク","mq","martinique","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇷","name":"mauritania","keywords":["もーりたにあこっき","こっき","もーりたにあ","モーリタニア国旗","国旗","モーリタニア","mr","mauritania","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇺","name":"mauritius","keywords":["もーりしゃすこっき","こっき","もーりしゃす","モーリシャス国旗","国旗","モーリシャス","mu","mauritius","flag","nation","country","banner"]},{"category":"flags","char":"🇾🇹","name":"mayotte","keywords":["まよっとのはた","こっき","まよっと","マヨットの旗","国旗","マヨット","yt","mayotte","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇽","name":"mexico","keywords":["めきしここっき","こっき","めきしこ","メキシコ国旗","国旗","メキシコ","mx","mexico","flag","nation","country","banner"]},{"category":"flags","char":"🇫🇲","name":"micronesia","keywords":["みくろねしあこっき","こっき","みくろねしあ","ミクロネシア国旗","国旗","ミクロネシア","fm","micronesia","federated","states","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇩","name":"moldova","keywords":["もるどばこっき","こっき","もるどば","モルドバ国旗","国旗","モルドバ","md","moldova","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇨","name":"monaco","keywords":["もなここっき","こっき","もなこ","モナコ国旗","国旗","モナコ","mc","monaco","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇳","name":"mongolia","keywords":["もんごるこっき","こっき","もんごる","モンゴル国旗","国旗","モンゴル","mn","mongolia","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇪","name":"montenegro","keywords":["もんてねぐろこっき","こっき","もんてねぐろ","モンテネグロ国旗","国旗","モンテネグロ","me","montenegro","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇸","name":"montserrat","keywords":["もんとせらとのはた","はた","もんとせらと","モントセラトの旗","旗","モントセラト","ms","montserrat","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇦","name":"morocco","keywords":["もろっここっき","こっき","もろっこ","モロッコ国旗","国旗","モロッコ","ma","morocco","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇿","name":"mozambique","keywords":["もざんびーくこっき","こっき","もざんびーく","モザンビーク国旗","国旗","モザンビーク","mz","mozambique","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇲","name":"myanmar","keywords":["みゃんまーこっき","びるま","こっき","みゃんまー","ミャンマー国旗","ビルマ","国旗","ミャンマー","mm","myanmar","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇦","name":"namibia","keywords":["なみびあこっき","こっき","なみびあ","ナミビア国旗","国旗","ナミビア","na","namibia","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇷","name":"nauru","keywords":["なうるこっき","こっき","なうる","ナウル国旗","国旗","ナウル","nr","nauru","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇵","name":"nepal","keywords":["ねぱーるこっき","こっき","ねぱーる","ネパール国旗","国旗","ネパール","np","nepal","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇱","name":"netherlands","keywords":["おらんだこっき","こっき","おらんだ","オランダ国旗","国旗","オランダ","nl","netherlands","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇨","name":"new_caledonia","keywords":["にゅーかれどにあのはた","こっき","にゅー","にゅーかれどにあ","ニューカレドニアの旗","国旗","ニュー","ニューカレドニア","nc","new","caledonia","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇿","name":"new_zealand","keywords":["にゅーじーらんどこっき","こっき","にゅー","にゅーじーらんど","ニュージーランド国旗","国旗","ニュー","ニュージーランド","nz","new","zealand","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇮","name":"nicaragua","keywords":["にからぐあこっき","こっき","にからぐあ","ニカラグア国旗","国旗","ニカラグア","ni","nicaragua","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇪","name":"niger","keywords":["にじぇーるこっき","こっき","にじぇーる","ニジェール国旗","国旗","ニジェール","ne","niger","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇬","name":"nigeria","keywords":["ないじぇりあこっき","こっき","ないじぇりあ","ナイジェリア国旗","国旗","ナイジェリア","ng","nigeria","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇺","name":"niue","keywords":["にうえこっき","こっき","にうえ","ニウエ国旗","国旗","ニウエ","nu","niue","flag","nation","country","banner"]},{"category":"flags","char":"🇳🇫","name":"norfolk_island","keywords":["のーふぉーくとうのはた","はた","しま","のーふぉーく","ノーフォーク島の旗","旗","島","ノーフォーク","nf","norfolk","island","flag","nation","country","banner"]},{"category":"flags","char":"🇲🇵","name":"northern_mariana_islands","keywords":["きたまりあなしょとうのはた","こっき","しょとう","まりあな","きた","きたまりあな","北マリアナ諸島の旗","国旗","諸島","マリアナ","北","北マリアナ","mp","northern","mariana","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇵","name":"north_korea","keywords":["きたちょうせんこっき","こっき","ちょうせん","きた","きたちょうせん","北朝鮮国旗","国旗","朝鮮","北","北朝鮮","kp","democratic","people","republic","north","korea","nation","flag","country","banner"]},{"category":"flags","char":"🇳🇴","name":"norway","keywords":["のるうぇーこっき","はた","のるうぇー","ぶーべ","すヴぁーるばる","やんまいえん","ノルウェー国旗","旗","ノルウェー","ブーべ","スヴァールバル","ヤンマイエン","no","norway","flag","nation","country","banner"]},{"category":"flags","char":"🇴🇲","name":"oman","keywords":["おまーんこっき","こっき","おまーん","オマーン国旗","国旗","オマーン","om","oman","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇰","name":"pakistan","keywords":["ぱきすたんこっき","こっき","ぱきすたん","パキスタン国旗","国旗","パキスタン","pk","pakistan","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇼","name":"palau","keywords":["ぱらおこっき","こっき","ぱらお","パラオ国旗","国旗","パラオ","pw","palau","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇸","name":"palestinian_territories","keywords":["ぱれすちなじちせいふのはた","こっき","ぱれすちな","パレスチナ自治政府の旗","国旗","パレスチナ","ps","palestine","palestinian","territories","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇦","name":"panama","keywords":["ぱなまこっき","こっき","ぱなま","パナマ国旗","国旗","パナマ","pa","panama","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇬","name":"papua_new_guinea","keywords":["ぱぷあにゅーぎにあこっき","こっき","ぎにあ","にゅー","ぱぷあにゅーぎにあ","パプアニューギニア国旗","国旗","ギニア","ニュー","パプアニューギニア","pg","papua","new","guinea","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇾","name":"paraguay","keywords":["ぱらぐあいこっき","こっき","ぱらぐあい","パラグアイ国旗","国旗","パラグアイ","py","paraguay","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇪","name":"peru","keywords":["ぺるーこっき","こっき","ぺるー","ペルー国旗","国旗","ペルー","pe","peru","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇭","name":"philippines","keywords":["ふぃりぴんこっき","こっき","ふぃりぴん","フィリピン国旗","国旗","フィリピン","ph","philippines","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇳","name":"pitcairn_islands","keywords":["ぴとけあんしょとうのはた","はた","しょとう","ぴとけあん","ピトケアン諸島の旗","旗","諸島","ピトケアン","pn","pitcairn","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇱","name":"poland","keywords":["ぽーらんどこっき","こっき","ぽーらんど","ポーランド国旗","国旗","ポーランド","pl","poland","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇹","name":"portugal","keywords":["ぽるとがるこっき","こっき","ぽるとがる","ポルトガル国旗","国旗","ポルトガル","pt","portugal","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇷","name":"puerto_rico","keywords":["ぷえるとりこのはた","こっき","ぷえるとりこ","プエルトリコの旗","国旗","プエルトリコ","pr","puerto","rico","flag","nation","country","banner"]},{"category":"flags","char":"🇶🇦","name":"qatar","keywords":["かたーるこっき","こっき","かたーる","カタール国旗","国旗","カタール","qa","qatar","flag","nation","country","banner"]},{"category":"flags","char":"🇷🇪","name":"reunion","keywords":["れゆにおんのはた","はた","れゆにおん","レユニオンの旗","旗","レユニオン","re","reunion","réunion","flag","nation","country","banner"]},{"category":"flags","char":"🇷🇴","name":"romania","keywords":["るーまにあこっき","こっき","るーまにあ","ルーマニア国旗","国旗","ルーマニア","ro","romania","flag","nation","country","banner"]},{"category":"flags","char":"🇷🇺","name":"ru","keywords":["ろしあこっき","こっき","ろしあ","ロシア国旗","国旗","ロシア","ru","russian","federation","flag","nation","country","banner"]},{"category":"flags","char":"🇷🇼","name":"rwanda","keywords":["るわんだこっき","こっき","るわんだ","ルワンダ国旗","国旗","ルワンダ","rw","rwanda","flag","nation","country","banner"]},{"category":"flags","char":"🇧🇱","name":"st_barthelemy","keywords":["さん・ばるてるみーとうのはた","ばるてるみー","こっき","さん","サン・バルテルミー島の旗","バルテルミー","国旗","サン","bl","saint","barthélemy","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇭","name":"st_helena","keywords":["せんとへれなとうのはた","はた","へれな","せんと","セントヘレナ島の旗","旗","ヘレナ","セント","sh","saint","helena","ascension","tristan","cunha","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇳","name":"st_kitts_nevis","keywords":["せんとくりすとふぁーねいびすこっき","こっき","きっつ","ねいびす","せんと","セントクリストファー・ネイビス国旗","国旗","キッツ","ネイビス","セント","kn","saint","kitts","nevis","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇨","name":"st_lucia","keywords":["せんとるしあこっき","こっき","せんとるしあ","セントルシア国旗","国旗","セントルシア","lc","saint","lucia","flag","nation","country","banner"]},{"category":"flags","char":"🇵🇲","name":"st_pierre_miquelon","keywords":["さんぴえーるとう・みくろんとうのはた","はた","みくろん","ぴえーる","さん","サンピエール島・ミクロン島の旗","旗","ミクロン","ピエール","サン","pm","saint","pierre","miquelon","flag","nation","country","banner"]},{"category":"flags","char":"🇻🇨","name":"st_vincent_grenadines","keywords":["せんとびんせんと・ぐれなでぃーんこっき","こっき","ぐれなでぃーんしょとう","せんと","びんせんと","セントビンセント・グレナディーン国旗","国旗","グレナディーン諸島","セント","ビンセント","vc","saint","vincent","grenadines","flag","nation","country","banner"]},{"category":"flags","char":"🇼🇸","name":"samoa","keywords":["さもあこっき","こっき","さもあ","サモア国旗","国旗","サモア","ws","western","samoa","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇲","name":"san_marino","keywords":["さんまりのこっき","こっき","さんまりの","サンマリノ国旗","国旗","サンマリノ","sm","san","marino","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇹","name":"sao_tome_principe","keywords":["さんとめぷりんしぺこっき","こっき","ぷりんしぺ","ぷりんしぴ","さんとめ","さぉんとめー","サントメ・プリンシペ国旗","国旗","プリンシペ","プリンシピ","サントメ","サォントメー","st","sao","tome","principe","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇦","name":"saudi_arabia","keywords":["さうじあらびあこっき","こっき","さうじあらびあ","サウジアラビア国旗","国旗","サウジアラビア","saudi","arabia","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇳","name":"senegal","keywords":["せねがるこっき","こっき","せねがる","セネガル国旗","国旗","セネガル","sn","senegal","flag","nation","country","banner"]},{"category":"flags","char":"🇷🇸","name":"serbia","keywords":["せるびあこっき","こっき","せるびあ","セルビア国旗","国旗","セルビア","rs","serbia","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇨","name":"seychelles","keywords":["せーしぇるこっき","こっき","せーしぇる","セーシェル国旗","国旗","セーシェル","sc","seychelles","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇱","name":"sierra_leone","keywords":["しえられおねこっき","こっき","しえられおね","シエラレオネ国旗","国旗","シエラレオネ","sl","sierra","leone","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇬","name":"singapore","keywords":["しんがぽーるこっき","こっき","しんがぽーる","シンガポール国旗","国旗","シンガポール","sg","singapore","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇽","name":"sint_maarten","keywords":["せんと・まーちんとうのはた","はた","まーちん","せんと","セント・マーチン島の旗","旗","マーチン","セント","sx","sint","maarten","dutch","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇰","name":"slovakia","keywords":["すろばきあこっき","こっき","すろばきあ","スロバキア国旗","国旗","スロバキア","sk","slovakia","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇮","name":"slovenia","keywords":["すろべにあこっき","こっき","すろべにあ","スロベニア国旗","国旗","スロベニア","si","slovenia","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇧","name":"solomon_islands","keywords":["そろもんしょとうこっき","はた","しょとう","そろもん","ソロモン諸島国旗","旗","諸島","ソロモン","sb","solomon","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇴","name":"somalia","keywords":["そまりあこっき","こっき","そまりあ","ソマリア国旗","国旗","ソマリア","so","somalia","flag","nation","country","banner"]},{"category":"flags","char":"🇿🇦","name":"south_africa","keywords":["みなみあふりかこっき","こっき","みなみ","みなみあふりか","南アフリカ国旗","国旗","南","南アフリカ","za","south","africa","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇸","name":"south_georgia_south_sandwich_islands","keywords":["さうすじょーじあ・さうすさんどうぃっちしょとうこっき","こっき","じょーじあ","しょとう","さうす","さうすじょーじあ","さうすさんどうぃっち","サウスジョージア・サウスサンドウィッチ諸島国旗","国旗","ジョージア","諸島","サウス","サウスジョージア","サウスサンドウィッチ","gs","south","georgia","sandwich","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇰🇷","name":"kr","keywords":["かんこくこっき","こっき","かんこく","みなみ","だいかんみんこく","韓国国旗","国旗","韓国","南","大韓民国","kr","south","korea","nation","flag","country","banner"]},{"category":"flags","char":"🇸🇸","name":"south_sudan","keywords":["みなみすーだんこっき","こっき","みなみ","みなみすーだん","すーだん","南スーダン国旗","国旗","南","南スーダン","スーダン","ss","south","sudan","flag","nation","country","banner"]},{"category":"flags","char":"🇪🇸","name":"es","keywords":["すぺいんこっき","こっき","すぺいん","せうた","めりりゃ","スペイン国旗","国旗","スペイン","セウタ","メリリャ","es","spain","españa","flag","nation","country","banner"]},{"category":"flags","char":"🇱🇰","name":"sri_lanka","keywords":["すりらんかこっき","こっき","すりらんか","スリランカ国旗","国旗","スリランカ","lk","sri","lanka","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇩","name":"sudan","keywords":["すーだんこっき","こっき","すーだん","スーダン国旗","国旗","スーダン","sd","sudan","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇷","name":"suriname","keywords":["すりなむこっき","こっき","すりなむ","スリナム国旗","国旗","スリナム","sr","suriname","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇿","name":"swaziland","keywords":["すわじらんどこっき","こっき","すわじらんど","スワジランド国旗","国旗","スワジランド","sz","eswatini","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇪","name":"sweden","keywords":["すうぇーでんこっき","こっき","すうぇーでん","スウェーデン国旗","国旗","スウェーデン","se","sweden","flag","nation","country","banner"]},{"category":"flags","char":"🇨🇭","name":"switzerland","keywords":["すいすこっき","こっき","すいす","スイス国旗","国旗","スイス","ch","switzerland","confoederatio","helvetica","flag","nation","country","banner"]},{"category":"flags","char":"🇸🇾","name":"syria","keywords":["しりあこっき","こっき","しりあ","シリア国旗","国旗","シリア","sy","syrian","arab","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇼","name":"taiwan","keywords":["たいわんのはた","ちゅうごく","こっき","たいわん","台湾の旗","中国","国旗","台湾","tw","taiwan","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇯","name":"tajikistan","keywords":["たじきすたんこっき","こっき","たじきすたん","タジキスタン国旗","国旗","タジキスタン","tj","tajikistan","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇿","name":"tanzania","keywords":["たんざにあこっき","こっき","たんざにあ","タンザニア国旗","国旗","タンザニア","tz","tanzania","united","republic","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇭","name":"thailand","keywords":["たいこっき","こっき","たい","タイ国旗","国旗","タイ","th","thailand","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇱","name":"timor_leste","keywords":["ひがしてぃもーるこっき","ひがし","ひがしてぃもーる","こっき","てぃもーる・れすて","東ティモール国旗","東","東ティモール","国旗","ティモール・レステ","tl","timor","leste","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇬","name":"togo","keywords":["とーごこっき","こっき","とーご","トーゴ国旗","国旗","トーゴ","tg","togo","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇰","name":"tokelau","keywords":["とけらうはた","こっき","とけらう","トケラウ旗","国旗","トケラウ","tk","tokelau","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇴","name":"tonga","keywords":["とんがこっき","こっき","とんが","トンガ国旗","国旗","トンガ","to","tonga","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇹","name":"trinidad_tobago","keywords":["とりにだーどとばごこっき","こっき","とばご","とりにだーど","トリニダード・トバゴ国旗","国旗","トバゴ","トリニダード","tt","trinidad","tobago","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇦","name":"tristan_da_cunha","keywords":["とりすたんだくーにゃのはた","はた","とりすたん・だ・くーにゃ","トリスタンダクーニャの旗","旗","トリスタン・ダ・クーニャ","ta","tristan","da","cunha","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇳","name":"tunisia","keywords":["ちゅにじあこっき","こっき","ちゅにじあ","チュニジア国旗","国旗","チュニジア","tn","tunisia","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇷","name":"tr","keywords":["とるここっき","こっき","とるこ","トルコ国旗","国旗","トルコ","tr","turkey","türkiye","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇲","name":"turkmenistan","keywords":["とるくめにすたんこっき","こっき","とるくめにすたん","トルクメニスタン国旗","国旗","トルクメニスタン","tm","turkmenistan","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇨","name":"turks_caicos_islands","keywords":["たーくす・かいこすしょとうのはた","かいこす","はた","しょとう","たーくす","タークス・カイコス諸島の旗","カイコス","旗","諸島","タークス","tc","turks","caicos","islands","flag","nation","country","banner"]},{"category":"flags","char":"🇹🇻","name":"tuvalu","keywords":["つばるこっき","こっき","つばる","ツバル国旗","国旗","ツバル","tv","tuvalu","flag","nation","country","banner"]},{"category":"flags","char":"🇺🇬","name":"uganda","keywords":["うがんだこっき","こっき","うがんだ","ウガンダ国旗","国旗","ウガンダ","ug","uganda","flag","nation","country","banner"]},{"category":"flags","char":"🇺🇦","name":"ukraine","keywords":["うくらいなこっき","こっき","うくらいな","ウクライナ国旗","国旗","ウクライナ","ua","ukraine","flag","nation","country","banner"]},{"category":"flags","char":"🇦🇪","name":"united_arab_emirates","keywords":["あらぶしゅちょうこくれんぽうこっき","しゅちょうこく","こっき","あらぶしゅちょうこくれんぽう","れんぽう","アラブ首長国連邦国旗","首長国","国旗","アラブ首長国連邦","連邦","ae","united","arab","emirates","flag","nation","country","banner"]},{"category":"flags","char":"🇬🇧","name":"uk","keywords":["いぎりすこっき","いぎりす","いぎりすりょう","こーんうぉーる","いんぐらんど","こっき","ぐれーとぶりてん","あいるらんど","きたあいるらんど","すこっとらんど","UK","ゆにおんじゃっく","れんごう","れんごうおうこく","うぇーるず","イギリス国旗","イギリス","イギリス領","コーンウォール","イングランド","国旗","グレートブリテン","アイルランド","北アイルランド","スコットランド","UK","ユニオンジャック","連合","連合王国","ウェールズ","gb","united","kingdom","great","britain","northern","ireland","flag","nation","country","banner","british","uk","english","england","union jack"]},{"category":"flags","char":"🏴󠁧󠁢󠁥󠁮󠁧󠁿","name":"england","keywords":["いんぐらんどのはた","いんぐらんど","こっき","イングランドの旗","イングランド","旗","flag","english"]},{"category":"flags","char":"🏴󠁧󠁢󠁳󠁣󠁴󠁿","name":"scotland","keywords":["すこっとらんどのはた","すこっとらんど","はた","スコットランドの旗","スコットランド","旗","flag","scottish"]},{"category":"flags","char":"🏴󠁧󠁢󠁷󠁬󠁳󠁿","name":"wales","keywords":["うぇーるずのはた","うぇーるず","はた","ウェールズの旗","ウェールズ","旗","flag","welsh"]},{"category":"flags","char":"🇺🇸","name":"us","keywords":["あめりかこっき","あめりか","はた","ごうしゅう","がっしゅうこく","あめりかがっしゅうこく","がっしゅうこくりょうゆうしょうりとう","アメリカ国旗","アメリカ","旗","合衆","合衆国","アメリカ合衆国","合衆国領有小離島","us","usa","united","states","america","flag","nation","country","banner"]},{"category":"flags","char":"🇻🇮","name":"us_virgin_islands","keywords":["あめりかりょうヴぁーじんしょとうのはた","あめりか","こっき","しま","あめりかがっしゅうこく","がっしゅうこく","ヴぁーじん","アメリカ領ヴァージン諸島の旗","アメリカ","国旗","島","アメリカ合衆国","合衆国","ヴァージン","vi","virgin","islands","us","flag","nation","country","banner"]},{"category":"flags","char":"🇺🇾","name":"uruguay","keywords":["うるぐあいこっき","こっき","うるぐあい","ウルグアイ国旗","国旗","ウルグアイ","uy","uruguay","flag","nation","country","banner"]},{"category":"flags","char":"🇺🇿","name":"uzbekistan","keywords":["うずべきすたんこっき","こっき","うずべきすたん","ウズベキスタン国旗","国旗","ウズベキスタン","uz","uzbekistan","flag","nation","country","banner"]},{"category":"flags","char":"🇻🇺","name":"vanuatu","keywords":["ばぬあつこっき","こっき","ばぬあつ","バヌアツ国旗","国旗","バヌアツ","vu","vanuatu","flag","nation","country","banner"]},{"category":"flags","char":"🇻🇦","name":"vatican_city","keywords":["ばちかんしこっき","こっき","ばちかん","バチカン市国旗","国旗","バチカン","va","vatican","city","flag","nation","country","banner"]},{"category":"flags","char":"🇻🇪","name":"venezuela","keywords":["べねずえらこっき","こっき","べねずえら","ベネズエラ国旗","国旗","ベネズエラ","ve","venezuela","flag","nation","country","banner"]},{"category":"flags","char":"🇻🇳","name":"vietnam","keywords":["べとなむこっき","こっき","べとなむ","ヴぇとなむ","ベトナム国旗","国旗","ベトナム","ヴェトナム","vn","viet","nam","flag","nation","country","banner"]},{"category":"flags","char":"🇼🇫","name":"wallis_futuna","keywords":["うぉりす・ふつなのはた","こっき","ふつな","うぉりす","ウォリス・フツナの旗","国旗","フツナ","ウォリス","wf","wallis","futuna","flag","nation","country","banner"]},{"category":"flags","char":"🇪🇭","name":"western_sahara","keywords":["にしさはらのはた","こっき","さはら","にし","にしさはら","西サハラの旗","国旗","サハラ","西","西サハラ","eh","western","sahara","flag","nation","country","banner"]},{"category":"flags","char":"🇾🇪","name":"yemen","keywords":["いえめんこっき","こっき","いえめん","イエメン国旗","国旗","イエメン","ye","yemen","flag","nation","country","banner"]},{"category":"flags","char":"🇿🇲","name":"zambia","keywords":["ざんびあこっき","こっき","ざんびあ","ザンビア国旗","国旗","ザンビア","zm","zambia","flag","nation","country","banner"]},{"category":"flags","char":"🇿🇼","name":"zimbabwe","keywords":["じんばぶえこっき","こっき","じんばぶえ","ジンバブエ国旗","国旗","ジンバブエ","zw","zimbabwe","flag","nation","country","banner"]},{"category":"flags","char":"🇺🇳","name":"united_nations","keywords":["こくれんのはた","はた","こくれん","れんごう","こくさい","国連の旗","旗","国連","連合","国際","un","united","nation","flag","banner"]},{"category":"flags","char":"🏴‍☠️","name":"pirate_flag","keywords":["かいぞくはた","はた","かいぞく","海賊旗","旗","海賊","skull","crossbones","flag","banner"]}] \ No newline at end of file diff --git a/assets/images/icon_adaptive.png b/assets/images/icon_adaptive.png deleted file mode 100644 index e546be0c8..000000000 Binary files a/assets/images/icon_adaptive.png and /dev/null differ diff --git a/assets/images/icon_adaptive_foreground.png b/assets/images/icon_adaptive_foreground.png new file mode 100644 index 000000000..466ac1fbb Binary files /dev/null and b/assets/images/icon_adaptive_foreground.png differ diff --git a/assets/images/icon_adaptive_monochrome.png b/assets/images/icon_adaptive_monochrome.png new file mode 100644 index 000000000..5a121b055 Binary files /dev/null and b/assets/images/icon_adaptive_monochrome.png differ diff --git a/assets/images/miria_error.svg b/assets/images/miria_error.svg new file mode 100644 index 000000000..bd0e738ca --- /dev/null +++ b/assets/images/miria_error.svg @@ -0,0 +1,2 @@ + + diff --git a/assets/images/unfederate.svg b/assets/images/unfederate.svg new file mode 100644 index 000000000..970e9f4eb --- /dev/null +++ b/assets/images/unfederate.svg @@ -0,0 +1 @@ +> diff --git a/assets_builder/build_themes.dart b/assets_builder/build_themes.dart index cbdee568a..5366d5343 100644 --- a/assets_builder/build_themes.dart +++ b/assets_builder/build_themes.dart @@ -1,9 +1,9 @@ -import 'dart:io'; +import "dart:io"; -import 'package:flutter/material.dart'; -import 'package:json5/json5.dart'; -import 'package:miria/model/color_theme.dart'; -import 'package:miria/model/misskey_theme.dart'; +import "package:flutter/material.dart"; +import "package:json5/json5.dart"; +import "package:miria/model/color_theme.dart"; +import "package:miria/model/misskey_theme.dart"; void main() { final themes = [ @@ -34,32 +34,32 @@ void main() { File( "assets_builder/misskey/packages/frontend/src/themes/$name.json5", ).readAsStringSync(), - ), + ) as Map, ), ), ) .map( - (theme) => 'ColorTheme(' + (theme) => "ColorTheme(" 'id: "${theme.id}", ' 'name: "${theme.name}", ' - 'isDarkTheme: ${theme.isDarkTheme}, ' - 'primary: ${theme.primary}, ' - 'primaryDarken: ${theme.primaryDarken}, ' - 'primaryLighten: ${theme.primaryLighten}, ' - 'accentedBackground: ${theme.accentedBackground}, ' - 'background: ${theme.background}, ' - 'foreground: ${theme.foreground}, ' - 'renote: ${theme.renote}, ' - 'mention: ${theme.mention}, ' - 'hashtag: ${theme.hashtag}, ' - 'link: ${theme.link}, ' - 'divider: ${theme.divider}, ' - 'buttonBackground: ${theme.buttonBackground}, ' - 'buttonGradateA: ${theme.buttonGradateA}, ' - 'buttonGradateB: ${theme.buttonGradateB}, ' - 'panel: ${theme.panel}, ' - 'panelBackground: ${theme.panelBackground}, ' - ')', + "isDarkTheme: ${theme.isDarkTheme}, " + "primary: ${theme.primary}, " + "primaryDarken: ${theme.primaryDarken}, " + "primaryLighten: ${theme.primaryLighten}, " + "accentedBackground: ${theme.accentedBackground}, " + "background: ${theme.background}, " + "foreground: ${theme.foreground}, " + "renote: ${theme.renote}, " + "mention: ${theme.mention}, " + "hashtag: ${theme.hashtag}, " + "link: ${theme.link}, " + "divider: ${theme.divider}, " + "buttonBackground: ${theme.buttonBackground}, " + "buttonGradateA: ${theme.buttonGradateA}, " + "buttonGradateB: ${theme.buttonGradateB}, " + "panel: ${theme.panel}, " + "panelBackground: ${theme.panelBackground}, " + ")", ) .toList(); // ignore: avoid_print diff --git a/assets_builder/emoji_list/builder.mjs b/assets_builder/emoji_list/builder.mjs index 82f54a4f0..fbf505da3 100644 --- a/assets_builder/emoji_list/builder.mjs +++ b/assets_builder/emoji_list/builder.mjs @@ -11,40 +11,36 @@ async function main() { const kuroshiro = new Kuroshiro.default(); await kuroshiro.init(new KuromojiAnalyzer()); - let body = fs.readFileSync("../misskey/packages/frontend/src/emojilist.json"); + const categories = ['face', 'people', 'animals_and_nature', 'food_and_drink', 'activity', 'travel_and_places', 'objects', 'symbols', 'flags']; + + const body = fs.readFileSync("../misskey/packages/frontend-shared/js/emojilist.json"); const emojiList = JSON.parse(body); + const jpHiraBody = fs.readFileSync("../misskey/packages/frontend/src/unicode-emoji-indexes/ja-JP_hira.json") + const jpHiraBodyList = JSON.parse(jpHiraBody); + const jpBody = fs.readFileSync("../misskey/packages/frontend/src/unicode-emoji-indexes/ja-JP.json"); + const jpBodyList = JSON.parse(jpBody); + const enBody = fs.readFileSync("../misskey/packages/frontend/src/unicode-emoji-indexes/en-US.json"); + const enBodyList = JSON.parse(enBody); + + // let body2 = await requestPromise("https://raw.githubusercontent.com/yagays/emoji-ja/master/data/emoji_ja.json"); + // const jpEmojiList = JSON.parse(body2); - let body2 = await requestPromise("https://raw.githubusercontent.com/yagays/emoji-ja/master/data/emoji_ja.json"); - const jpEmojiList = JSON.parse(body2); + const emojis = []; for(var i=0;i0;j--) { - - if(jpEmojiList[emojiList[i].char.substring(0, j)] !== undefined) { - findChar = emojiList[i].char.substring(0, j); - isFound = true; - break; - } - } - - if(!isFound) { - break; - } - } - - var keywords = jpEmojiList[findChar].keywords; - - for(var j=0;j= 2.7.5) + - FlutterMacOS - SwiftyGif (5.4.4) - url_launcher_ios (0.0.1): - Flutter @@ -107,6 +102,7 @@ PODS: - Flutter - webview_flutter_wkwebview (0.0.1): - Flutter + - FlutterMacOS DEPENDENCIES: - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) @@ -123,21 +119,19 @@ DEPENDENCIES: - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - receive_sharing_intent (from `.symlinks/plugins/receive_sharing_intent/ios`) - - screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preference_app_group (from `.symlinks/plugins/shared_preference_app_group/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `.symlinks/plugins/sqflite/ios`) + - sqflite_darwin (from `.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - volume_controller (from `.symlinks/plugins/volume_controller/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) - - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/ios`) + - webview_flutter_wkwebview (from `.symlinks/plugins/webview_flutter_wkwebview/darwin`) SPEC REPOS: trunk: - DKImagePickerController - DKPhotoGallery - - FMDB - libwebp - Mantle - SDWebImage @@ -173,16 +167,14 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" receive_sharing_intent: :path: ".symlinks/plugins/receive_sharing_intent/ios" - screen_brightness_ios: - :path: ".symlinks/plugins/screen_brightness_ios/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preference_app_group: :path: ".symlinks/plugins/shared_preference_app_group/ios" shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - sqflite: - :path: ".symlinks/plugins/sqflite/ios" + sqflite_darwin: + :path: ".symlinks/plugins/sqflite_darwin/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" volume_controller: @@ -190,17 +182,16 @@ EXTERNAL SOURCES: wakelock_plus: :path: ".symlinks/plugins/wakelock_plus/ios" webview_flutter_wkwebview: - :path: ".symlinks/plugins/webview_flutter_wkwebview/ios" + :path: ".symlinks/plugins/webview_flutter_wkwebview/darwin" SPEC CHECKSUMS: - device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d DKImagePickerController: b512c28220a2b8ac7419f21c491fc8534b7601ac DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 - file_picker: ce3938a0df3cc1ef404671531facef740d03f920 + file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_image_compress_common: ec1d45c362c9d30a3f6a0426c297f47c52007e3e - flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a + flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 image_editor_common: d6f6644ae4a6de80481e89fe6d0a8c49e30b4b43 image_gallery_saver: cb43cc43141711190510e92c460eb1655cd343cb libwebp: 1786c9f4ff8a279e4dac1e8f385004d5fc253009 @@ -208,23 +199,22 @@ SPEC CHECKSUMS: media_kit_libs_ios_video: a5fe24bc7875ccd6378a0978c13185e1344651c1 media_kit_native_event_loop: e6b2ab20cf0746eb1c33be961fcf79667304fa2a media_kit_video: 5da63f157170e5bf303bf85453b7ef6971218a2e - package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c - permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 - receive_sharing_intent: c0d87310754e74c0f9542947e7cbdf3a0335a3b1 - screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 + package_info_plus: c0502532a26c7662a62a356cebe2692ec5fe4ec4 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 + receive_sharing_intent: 753f808c6be5550247f6a20f2a14972466a5f33c SDWebImage: f9258c58221ed854cfa0e2b80ee4033710b1c6d3 SDWebImageWebPCoder: 633b813fca24f1de5e076bcd7f720c038b23892b - share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 - shared_preference_app_group: 83d2284f9e747839c40fc281403b5b60d83e2989 - shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 - sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a + share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad + shared_preference_app_group: 46aee3873e1da581d4904bece9876596d7f66725 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f - url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe volume_controller: 531ddf792994285c9b17f9d8a7e4dcdd29b3eae9 - wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 - webview_flutter_wkwebview: 4f3e50f7273d31e5500066ed267e3ae4309c5ae4 + wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 -PODFILE CHECKSUM: d5874a33c7eb4fc3858c862b5f42e17c95cb2c37 +PODFILE CHECKSUM: 9a5aff7d2a4fe2dcc914618b99f9ff063b14915b COCOAPODS: 1.14.3 diff --git a/ios/ShareExtension/ShareExtension.entitlements b/ios/PrivacyInfo.xcprivacy similarity index 60% rename from ios/ShareExtension/ShareExtension.entitlements rename to ios/PrivacyInfo.xcprivacy index 39e6b4910..cfbe279c7 100644 --- a/ios/ShareExtension/ShareExtension.entitlements +++ b/ios/PrivacyInfo.xcprivacy @@ -2,9 +2,7 @@ - com.apple.security.application-groups - - group.info.shiosyakeyakini.miria - + NSPrivacyTracking + diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 46edc15e3..9a5af4842 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -13,26 +13,10 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; - B670FC9B2BB4D1230071C2AC /* Flutter.xcframework in Frameworks */ = {isa = PBXBuildFile; fileRef = B670FC932BB4CC110071C2AC /* Flutter.xcframework */; }; - B670FC9C2BB4D1230071C2AC /* Flutter.xcframework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = B670FC932BB4CC110071C2AC /* Flutter.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; - B6A604652B6F867200DF5E0A /* ShareExtensionPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = B6A604642B6F867200DF5E0A /* ShareExtensionPluginRegistrant.m */; }; - B6E32F982A29F16500F51621 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B6E32F972A29F16500F51621 /* ShareViewController.swift */; }; - B6E32F9B2A29F16500F51621 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = B6E32F992A29F16500F51621 /* MainInterface.storyboard */; }; - B6E32F9F2A29F16500F51621 /* ShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = B6E32F952A29F16500F51621 /* ShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - DC8EA09EC4ED8DC1688C7FFA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 251D1F74C3951DCD4C1BBE60 /* Pods_Runner.framework */; }; - F92D0DC1549688E298B84F03 /* Pods_ShareExtension.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 913C569628226A1759913590 /* Pods_ShareExtension.framework */; }; + A4A11FC40049D71827513F6E /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E27F6288EB71147A028B4862 /* Pods_Runner.framework */; }; + B6D30E6E2CC2F993002CCBB1 /* PrivacyInfo.xcprivacy in Resources */ = {isa = PBXBuildFile; fileRef = B6D30E6D2CC2F993002CCBB1 /* PrivacyInfo.xcprivacy */; }; /* End PBXBuildFile section */ -/* Begin PBXContainerItemProxy section */ - B6E32F9D2A29F16500F51621 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = B6E32F942A29F16500F51621; - remoteInfo = ShareExtension; - }; -/* End PBXContainerItemProxy section */ - /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -44,24 +28,12 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - B670FC9D2BB4D1230071C2AC /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - B670FC9C2BB4D1230071C2AC /* Flutter.xcframework in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; B6E32FA02A29F16500F51621 /* Embed Foundation Extensions */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; dstPath = ""; dstSubfolderSpec = 13; files = ( - B6E32F9F2A29F16500F51621 /* ShareExtension.appex in Embed Foundation Extensions */, ); name = "Embed Foundation Extensions"; runOnlyForDeploymentPostprocessing = 0; @@ -69,15 +41,17 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ - 0EA8B66974C5971C959CEE41 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 116BCDFB7776CDCEBFEC299C /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 251D1F74C3951DCD4C1BBE60 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 20DC361DEB6B41C26BA7113B /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = ""; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 913C569628226A1759913590 /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 7F0DC075AE7FA24708EE4D06 /* Pods_ShareExtension.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_ShareExtension.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 852551A4B39CB6D952EED147 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; + 89D33361BA30235FB389D9A9 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -85,26 +59,17 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - AF6575481D38F1E3908DF820 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - B6577A902B6DA683001CF804 /* Flutter.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:S8QB4VV633:FLUTTER.IO LLC"; lastKnownFileType = wrapper.xcframework; name = Flutter.xcframework; path = ../../../flutter/bin/cache/artifacts/engine/ios/extension_safe/Flutter.xcframework; sourceTree = ""; }; - B670FC8F2BB22A680071C2AC /* Flutter.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:S8QB4VV633:FLUTTER.IO LLC"; lastKnownFileType = wrapper.xcframework; name = Flutter.xcframework; path = "../../../flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework"; sourceTree = ""; }; + A62470E45F0336B78BDF18A6 /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; + B6577A902B6DA683001CF804 /* Flutter.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Flutter.xcframework; path = ../../../flutter/bin/cache/artifacts/engine/ios/extension_safe/Flutter.xcframework; sourceTree = ""; }; + B670FC8F2BB22A680071C2AC /* Flutter.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Flutter.xcframework; path = "../../../flutter/bin/cache/artifacts/engine/ios-release/Flutter.xcframework"; sourceTree = ""; }; B670FC932BB4CC110071C2AC /* Flutter.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:S8QB4VV633:FLUTTER.IO LLC"; lastKnownFileType = wrapper.xcframework; name = Flutter.xcframework; path = "../../../flutter/bin/cache/artifacts/engine/ios-release/extension_safe/Flutter.xcframework"; sourceTree = ""; }; - B670FC972BB4CEA30071C2AC /* Flutter.xcframework */ = {isa = PBXFileReference; expectedSignature = "AppleDeveloperProgram:S8QB4VV633:FLUTTER.IO LLC"; lastKnownFileType = wrapper.xcframework; name = Flutter.xcframework; path = "../../../flutter/bin/cache/artifacts/engine/ios-profile/extension_safe/Flutter.xcframework"; sourceTree = ""; }; + B670FC972BB4CEA30071C2AC /* Flutter.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = Flutter.xcframework; path = "../../../flutter/bin/cache/artifacts/engine/ios-profile/extension_safe/Flutter.xcframework"; sourceTree = ""; }; B6A6044E2B6F3F4C00DF5E0A /* shared_preference_app_group.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = shared_preference_app_group.framework; sourceTree = BUILT_PRODUCTS_DIR; }; B6A604592B6F41A100DF5E0A /* shared_preference_app_group.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = shared_preference_app_group.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - B6A604632B6F862900DF5E0A /* ShareExtensionPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ShareExtensionPluginRegistrant.h; sourceTree = ""; }; - B6A604642B6F867200DF5E0A /* ShareExtensionPluginRegistrant.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ShareExtensionPluginRegistrant.m; sourceTree = ""; }; - B6A6046A2B6F895700DF5E0A /* ShareExtension-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ShareExtension-Bridging-Header.h"; sourceTree = ""; }; - B6E32F952A29F16500F51621 /* ShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = ShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; - B6E32F972A29F16500F51621 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; - B6E32F9A2A29F16500F51621 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; - B6E32F9C2A29F16500F51621 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B6E32FA52A29F2B300F51621 /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = ""; }; + B6D30E6D2CC2F993002CCBB1 /* PrivacyInfo.xcprivacy */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = PrivacyInfo.xcprivacy; sourceTree = ""; }; B6E32FA62A29F2F600F51621 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; - DC35FB6C81052B2C96716C97 /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = ""; }; - E020CEC834B6DC172253952E /* Pods-ShareExtension.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.release.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.release.xcconfig"; sourceTree = ""; }; - E14F5D9DAF6B309CD166EFF5 /* Pods-ShareExtension.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.debug.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.debug.xcconfig"; sourceTree = ""; }; - FC29FBA115E9E21D7CDF549F /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + E27F6288EB71147A028B4862 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + FE563C168BB69E3C02B5F2FF /* Pods-ShareExtension.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-ShareExtension.profile.xcconfig"; path = "Target Support Files/Pods-ShareExtension/Pods-ShareExtension.profile.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -112,16 +77,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - DC8EA09EC4ED8DC1688C7FFA /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - B6E32F922A29F16500F51621 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - B670FC9B2BB4D1230071C2AC /* Flutter.xcframework in Frameworks */, - F92D0DC1549688E298B84F03 /* Pods_ShareExtension.framework in Frameworks */, + A4A11FC40049D71827513F6E /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -131,12 +87,12 @@ 1E6AA870CBAB1C7364D7828A /* Pods */ = { isa = PBXGroup; children = ( - FC29FBA115E9E21D7CDF549F /* Pods-Runner.debug.xcconfig */, - AF6575481D38F1E3908DF820 /* Pods-Runner.release.xcconfig */, - 0EA8B66974C5971C959CEE41 /* Pods-Runner.profile.xcconfig */, - E14F5D9DAF6B309CD166EFF5 /* Pods-ShareExtension.debug.xcconfig */, - E020CEC834B6DC172253952E /* Pods-ShareExtension.release.xcconfig */, - DC35FB6C81052B2C96716C97 /* Pods-ShareExtension.profile.xcconfig */, + 89D33361BA30235FB389D9A9 /* Pods-Runner.debug.xcconfig */, + 852551A4B39CB6D952EED147 /* Pods-Runner.release.xcconfig */, + 116BCDFB7776CDCEBFEC299C /* Pods-Runner.profile.xcconfig */, + 20DC361DEB6B41C26BA7113B /* Pods-ShareExtension.debug.xcconfig */, + A62470E45F0336B78BDF18A6 /* Pods-ShareExtension.release.xcconfig */, + FE563C168BB69E3C02B5F2FF /* Pods-ShareExtension.profile.xcconfig */, ); path = Pods; sourceTree = ""; @@ -155,9 +111,9 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( + B6D30E6D2CC2F993002CCBB1 /* PrivacyInfo.xcprivacy */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, - B6E32F962A29F16500F51621 /* ShareExtension */, 97C146EF1CF9000F007C117D /* Products */, 1E6AA870CBAB1C7364D7828A /* Pods */, A4C9FFCDD3C0E6ACDCD23018 /* Frameworks */, @@ -168,7 +124,6 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, - B6E32F952A29F16500F51621 /* ShareExtension.appex */, ); name = Products; sourceTree = ""; @@ -198,26 +153,12 @@ B6A604592B6F41A100DF5E0A /* shared_preference_app_group.framework */, B6A6044E2B6F3F4C00DF5E0A /* shared_preference_app_group.framework */, B6577A902B6DA683001CF804 /* Flutter.xcframework */, - 251D1F74C3951DCD4C1BBE60 /* Pods_Runner.framework */, - 913C569628226A1759913590 /* Pods_ShareExtension.framework */, + E27F6288EB71147A028B4862 /* Pods_Runner.framework */, + 7F0DC075AE7FA24708EE4D06 /* Pods_ShareExtension.framework */, ); name = Frameworks; sourceTree = ""; }; - B6E32F962A29F16500F51621 /* ShareExtension */ = { - isa = PBXGroup; - children = ( - B6E32FA52A29F2B300F51621 /* ShareExtension.entitlements */, - B6E32F972A29F16500F51621 /* ShareViewController.swift */, - B6E32F992A29F16500F51621 /* MainInterface.storyboard */, - B6E32F9C2A29F16500F51621 /* Info.plist */, - B6A604632B6F862900DF5E0A /* ShareExtensionPluginRegistrant.h */, - B6A604642B6F867200DF5E0A /* ShareExtensionPluginRegistrant.m */, - B6A6046A2B6F895700DF5E0A /* ShareExtension-Bridging-Header.h */, - ); - path = ShareExtension; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ @@ -225,7 +166,7 @@ isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( - 35DD0A895B67416588313A22 /* [CP] Check Pods Manifest.lock */, + 0900F14891D4D9B356DC30E6 /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, @@ -233,37 +174,18 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, B6E32FA02A29F16500F51621 /* Embed Foundation Extensions */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 72427D6E1FCF5EC68D493D27 /* [CP] Embed Pods Frameworks */, + 285699A51DE7B2673C6B5D87 /* [CP] Embed Pods Frameworks */, + DD4864388383995D9127D38F /* [CP] Copy Pods Resources */, ); buildRules = ( ); dependencies = ( - B6E32F9E2A29F16500F51621 /* PBXTargetDependency */, ); name = Runner; productName = Runner; productReference = 97C146EE1CF9000F007C117D /* Runner.app */; productType = "com.apple.product-type.application"; }; - B6E32F942A29F16500F51621 /* ShareExtension */ = { - isa = PBXNativeTarget; - buildConfigurationList = B6E32FA42A29F16500F51621 /* Build configuration list for PBXNativeTarget "ShareExtension" */; - buildPhases = ( - CD3CBF31D02D68A782FA7468 /* [CP] Check Pods Manifest.lock */, - B6E32F912A29F16500F51621 /* Sources */, - B6E32F922A29F16500F51621 /* Frameworks */, - B6E32F932A29F16500F51621 /* Resources */, - B670FC9D2BB4D1230071C2AC /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = ShareExtension; - productName = ShareExtension; - productReference = B6E32F952A29F16500F51621 /* ShareExtension.appex */; - productType = "com.apple.product-type.app-extension"; - }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ @@ -279,11 +201,6 @@ LastSwiftMigration = 1100; ProvisioningStyle = Automatic; }; - B6E32F942A29F16500F51621 = { - CreatedOnToolsVersion = 14.3; - LastSwiftMigration = 1500; - ProvisioningStyle = Automatic; - }; }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; @@ -300,7 +217,6 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, - B6E32F942A29F16500F51621 /* ShareExtension */, ); }; /* End PBXProject section */ @@ -313,22 +229,15 @@ 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, + B6D30E6E2CC2F993002CCBB1 /* PrivacyInfo.xcprivacy in Resources */, 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; - B6E32F932A29F16500F51621 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B6E32F9B2A29F16500F51621 /* MainInterface.storyboard in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ - 35DD0A895B67416588313A22 /* [CP] Check Pods Manifest.lock */ = { + 0900F14891D4D9B356DC30E6 /* [CP] Check Pods Manifest.lock */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( @@ -350,38 +259,38 @@ shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; - 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { + 285699A51DE7B2673C6B5D87 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputPaths = ( - "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", ); - name = "Thin Binary"; - outputPaths = ( + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; - 72427D6E1FCF5EC68D493D27 /* [CP] Embed Pods Frameworks */ = { + 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + name = "Thin Binary"; + outputPaths = ( ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; + shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin\n"; }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; @@ -398,26 +307,21 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; - CD3CBF31D02D68A782FA7468 /* [CP] Check Pods Manifest.lock */ = { + DD4864388383995D9127D38F /* [CP] Copy Pods Resources */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; files = ( ); inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; + name = "[CP] Copy Pods Resources"; outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-ShareExtension-checkManifestLockResult.txt", + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -432,25 +336,8 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - B6E32F912A29F16500F51621 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - B6A604652B6F867200DF5E0A /* ShareExtensionPluginRegistrant.m in Sources */, - B6E32F982A29F16500F51621 /* ShareViewController.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; /* End PBXSourcesBuildPhase section */ -/* Begin PBXTargetDependency section */ - B6E32F9E2A29F16500F51621 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = B6E32F942A29F16500F51621 /* ShareExtension */; - targetProxy = B6E32F9D2A29F16500F51621 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -468,14 +355,6 @@ name = LaunchScreen.storyboard; sourceTree = ""; }; - B6E32F992A29F16500F51621 /* MainInterface.storyboard */ = { - isa = PBXVariantGroup; - children = ( - B6E32F9A2A29F16500F51621 /* Base */, - ); - name = MainInterface.storyboard; - sourceTree = ""; - }; /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ @@ -724,132 +603,6 @@ }; name = Release; }; - B6E32FA12A29F16500F51621 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 45Y496AN6Q; - GCC_C_LANGUAGE_STANDARD = gnu11; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = ShareExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.0; - MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = info.shiosyakeyakini.miria.ShareExtension; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "ShareExtension/ShareExtension-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - B6E32FA22A29F16500F51621 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 45Y496AN6Q; - GCC_C_LANGUAGE_STANDARD = gnu11; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = ShareExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.0; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = info.shiosyakeyakini.miria.ShareExtension; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "ShareExtension/ShareExtension-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Release; - }; - B6E32FA32A29F16500F51621 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_WEAK = YES; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; - CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; - CODE_SIGN_ENTITLEMENTS = ShareExtension/ShareExtension.entitlements; - CODE_SIGN_IDENTITY = "Apple Development"; - CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 1; - DEFINES_MODULE = YES; - DEVELOPMENT_TEAM = 45Y496AN6Q; - GCC_C_LANGUAGE_STANDARD = gnu11; - GENERATE_INFOPLIST_FILE = YES; - INFOPLIST_FILE = ShareExtension/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = ShareExtension; - INFOPLIST_KEY_NSHumanReadableCopyright = ""; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/Frameworks", - "@executable_path/../../Frameworks", - ); - MARKETING_VERSION = 1.0; - MTL_FAST_MATH = YES; - PRODUCT_BUNDLE_IDENTIFIER = info.shiosyakeyakini.miria.ShareExtension; - PRODUCT_NAME = "$(TARGET_NAME)"; - PROVISIONING_PROFILE_SPECIFIER = ""; - SKIP_INSTALL = YES; - SWIFT_EMIT_LOC_STRINGS = YES; - SWIFT_OBJC_BRIDGING_HEADER = "ShareExtension/ShareExtension-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Profile; - }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ @@ -873,16 +626,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - B6E32FA42A29F16500F51621 /* Build configuration list for PBXNativeTarget "ShareExtension" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - B6E32FA12A29F16500F51621 /* Debug */, - B6E32FA22A29F16500F51621 /* Release */, - B6E32FA32A29F16500F51621 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; diff --git a/ios/ShareExtension/Base.lproj/MainInterface.storyboard b/ios/ShareExtension/Base.lproj/MainInterface.storyboard deleted file mode 100644 index 4e1656c6c..000000000 --- a/ios/ShareExtension/Base.lproj/MainInterface.storyboard +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/ShareExtension/Info.plist b/ios/ShareExtension/Info.plist deleted file mode 100644 index df84f168f..000000000 --- a/ios/ShareExtension/Info.plist +++ /dev/null @@ -1,35 +0,0 @@ - - - - - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - NSExtension - - NSExtensionPrincipalClass - ShareExtension.ShareViewController - NSExtensionAttributes - - PHSupportedMediaTypes - - Image - - NSExtensionActivationRule - - NSExtensionActivationSupportsText - - NSExtensionActivationSupportsWebURLWithMaxCount - 1 - NSExtensionActivationSupportsImageWithMaxCount - 8 - NSExtensionActivationSupportsFileWithMaxCount - 1 - - - NSExtensionPointIdentifier - com.apple.share-services - - - diff --git a/ios/ShareExtension/ShareExtension-Bridging-Header.h b/ios/ShareExtension/ShareExtension-Bridging-Header.h deleted file mode 100644 index 375276579..000000000 --- a/ios/ShareExtension/ShareExtension-Bridging-Header.h +++ /dev/null @@ -1,2 +0,0 @@ -#import "ShareExtensionPluginRegistrant.h" - diff --git a/ios/ShareExtension/ShareExtensionPluginRegistrant.h b/ios/ShareExtension/ShareExtensionPluginRegistrant.h deleted file mode 100644 index 165757130..000000000 --- a/ios/ShareExtension/ShareExtensionPluginRegistrant.h +++ /dev/null @@ -1,17 +0,0 @@ -// -// ShareExtensionPluginRegistrant.h -// ShareExtension -// -// Created by Sorairo on 2024/02/04. -// - -#ifndef ShareExtensionPluginRegistrant_h -#define ShareExtensionPluginRegistrant_h - -#import - -@interface ShareExtensionPluginRegistrant : NSObject -+ (void)registerWithRegistry:(NSObject*)registry; -@end - -#endif /* ShareExtensionPluginRegistrant_h */ diff --git a/ios/ShareExtension/ShareExtensionPluginRegistrant.m b/ios/ShareExtension/ShareExtensionPluginRegistrant.m deleted file mode 100644 index 4e18f46ab..000000000 --- a/ios/ShareExtension/ShareExtensionPluginRegistrant.m +++ /dev/null @@ -1,133 +0,0 @@ -// -// ShareExtensionPluginRegistrant.m -// ShareExtension -// -// Created by Sorairo on 2024/02/04. -// - -#import -#import "ShareExtensionPluginRegistrant.h" - -//#if __has_include() -//#import -//#else -//@import shared_preference_app_group; -//#endif -// -//#if __has_include() -//#import -//#else -//@import sqflite; -//#endif - - -#if __has_include() -#import -#else -@import device_info_plus; -#endif - -#if __has_include() -#import -#else -@import flutter_image_compress_common; -#endif - -#if __has_include() -#import -#else -@import flutter_secure_storage; -#endif - -#if __has_include() -#import -#else -@import media_kit_libs_ios_video; -#endif - -#if __has_include() -#import -#else -@import media_kit_video; -#endif - -#if __has_include() -#import -#else -@import package_info_plus; -#endif - -#if __has_include() -#import -#else -@import path_provider_foundation; -#endif - -#if __has_include() -#import -#else -@import receive_sharing_intent; -#endif - -#if __has_include() -#import -#else -@import share_plus; -#endif - -#if __has_include() -#import -#else -@import shared_preference_app_group; -#endif - -#if __has_include() -#import -#else -@import sqflite; -#endif - -#if __has_include() -#import -#else -@import wakelock_plus; -#endif - -//#if __has_include() -//#import -//#else -//@import shared_preferences_foundation; -//#endif - -@implementation ShareExtensionPluginRegistrant - -+ (void)registerWithRegistry:(NSObject*)registry { - -// [SharedPreferenceAppGroupPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferenceAppGroupPlugin"]]; -// [SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]]; - - - [FPPDeviceInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPDeviceInfoPlusPlugin"]]; -// [FilePickerPlugin registerWithRegistrar:[registry registrarForPlugin:@"FilePickerPlugin"]]; - [ImageCompressPlugin registerWithRegistrar:[registry registrarForPlugin:@"ImageCompressPlugin"]]; - [FlutterSecureStoragePlugin registerWithRegistrar:[registry registrarForPlugin:@"FlutterSecureStoragePlugin"]]; -// [ImageEditorPlugin registerWithRegistrar:[registry registrarForPlugin:@"ImageEditorPlugin"]]; -// [ImageGallerySaverPlugin registerWithRegistrar:[registry registrarForPlugin:@"ImageGallerySaverPlugin"]]; - [MediaKitLibsIosVideoPlugin registerWithRegistrar:[registry registrarForPlugin:@"MediaKitLibsIosVideoPlugin"]]; - [MediaKitVideoPlugin registerWithRegistrar:[registry registrarForPlugin:@"MediaKitVideoPlugin"]]; -// [FPPPackageInfoPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPPackageInfoPlusPlugin"]]; - [PathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"PathProviderPlugin"]]; -// [PermissionHandlerPlugin registerWithRegistrar:[registry registrarForPlugin:@"PermissionHandlerPlugin"]]; - [ReceiveSharingIntentPlugin registerWithRegistrar:[registry registrarForPlugin:@"ReceiveSharingIntentPlugin"]]; -// [ScreenBrightnessIosPlugin registerWithRegistrar:[registry registrarForPlugin:@"ScreenBrightnessIosPlugin"]]; - [FPPSharePlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"FPPSharePlusPlugin"]]; - [SharedPreferenceAppGroupPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferenceAppGroupPlugin"]]; -// [SharedPreferencesPlugin registerWithRegistrar:[registry registrarForPlugin:@"SharedPreferencesPlugin"]]; - [SqflitePlugin registerWithRegistrar:[registry registrarForPlugin:@"SqflitePlugin"]]; -// [URLLauncherPlugin registerWithRegistrar:[registry registrarForPlugin:@"URLLauncherPlugin"]]; -// [VolumeControllerPlugin registerWithRegistrar:[registry registrarForPlugin:@"VolumeControllerPlugin"]]; - [WakelockPlusPlugin registerWithRegistrar:[registry registrarForPlugin:@"WakelockPlusPlugin"]]; -// [FLTWebViewFlutterPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTWebViewFlutterPlugin"]]; - -} -@end diff --git a/ios/ShareExtension/ShareViewController.swift b/ios/ShareExtension/ShareViewController.swift deleted file mode 100644 index 841da0997..000000000 --- a/ios/ShareExtension/ShareViewController.swift +++ /dev/null @@ -1,297 +0,0 @@ -import UIKit -import Flutter -import Social -import MobileCoreServices -import Photos - -class ShareViewController: UIViewController, FlutterPluginRegistry { - private let imageContentType = kUTTypeImage as String - private let videoContentType = kUTTypeMovie as String - private let textContentType = kUTTypeText as String - private let urlContentType = kUTTypeURL as String - private let fileURLType = kUTTypeFileURL as String; - private let sharedKey = "ShareKey" - private let hostAppBundleIdentifier = "info.shiosyakeyakini.miria" - - private var shareText: [String] = [] - private var shareFiles: [SharedMediaFile] = [] - - private var flutterViewController: FlutterViewController?; - - - @objc func registrar(forPlugin pluginKey: String) -> FlutterPluginRegistrar? { - return flutterViewController!.registrar(forPlugin: pluginKey) - } - - @objc func hasPlugin(_ pluginKey: String) -> Bool { - return flutterViewController!.hasPlugin(pluginKey) - } - - @objc func valuePublished(byPlugin pluginKey: String) -> NSObject? { - return flutterViewController!.valuePublished(byPlugin: pluginKey) - } - - override func viewDidLoad() { - super.viewDidLoad() - flutterViewController = FlutterViewController(project: nil, initialRoute: "/share-extension", nibName: nil, bundle: nil); - - // This is called after the user selects Post. Do the upload of contentText and/or NSExtensionContext attachments. - if let content = extensionContext!.inputItems[0] as? NSExtensionItem { - if let contents = content.attachments { - for (index, attachment) in (contents).enumerated() { - if attachment.hasItemConformingToTypeIdentifier(imageContentType) { - handleImages(content: content, attachment: attachment, index: index) - } else if attachment.hasItemConformingToTypeIdentifier(textContentType) { - handleText(content: content, attachment: attachment, index: index) - } else if attachment.hasItemConformingToTypeIdentifier(fileURLType) { - handleFiles(content: content, attachment: attachment, index: index) - } else if attachment.hasItemConformingToTypeIdentifier(urlContentType) { - let attributedTitle = content.attributedTitle?.string - let attributedContent = content.attributedContentText?.string - handleUrl(content: content, attachment: attachment, index: index, title: attributedTitle ?? attributedContent); - } else if attachment.hasItemConformingToTypeIdentifier(videoContentType) { - handleVideos(content: content, attachment: attachment, index: index) - } - } - - } - } - - let shareExtensionChannel = FlutterMethodChannel(name: "info.shiosyakeyakini.miria/share_extension", binaryMessenger: flutterViewController as! FlutterBinaryMessenger) - shareExtensionChannel.setMethodCallHandler({ - (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in - switch call.method { - case "exit": - self.extensionContext!.completeRequest(returningItems: nil) - default: - result(FlutterMethodNotImplemented) - } - - }) - - ShareExtensionPluginRegistrant.register(with: self) - - addChild(flutterViewController!) - view.addSubview(flutterViewController!.view) - flutterViewController!.view.frame = view.bounds - } - - private func save() { - let userDefaults = UserDefaults(suiteName: "group.\(self.hostAppBundleIdentifier)") - let encoder = JSONEncoder() - let json = try? encoder.encode(SendData(text: self.shareText, files: self.shareFiles)) - guard let encoded = json else { - return - } - userDefaults?.set(String(data: encoded, encoding: .utf8), forKey: self.sharedKey) - userDefaults?.synchronize() - } - - private func handleText (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { - attachment.loadItem(forTypeIdentifier: textContentType, options: nil) { [weak self] data, error in - - if error == nil, let item = data as? String, let this = self { - this.shareText.append(item) - if index == (content.attachments?.count)! - 1 { - this.save() - } - } - } - } - - private func handleUrl (content: NSExtensionItem, attachment: NSItemProvider, index: Int, title: String?) { - attachment.loadItem(forTypeIdentifier: urlContentType, options: nil) { [weak self] data, error in - - if error == nil, let item = data as? URL, let this = self { - let text: String - if(title != nil) { - text = "[\(title ?? "")](\(item.absoluteString))" - } else { - text = item.absoluteString - } - this.shareText.append(text) - if index == (content.attachments?.count)! - 1 { - this.save() - } - } - } - } - - private func handleImages (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { - attachment.loadItem(forTypeIdentifier: imageContentType, options: nil) { [weak self] data, error in - - if error == nil, let url = data as? URL, let this = self { - - // Always copy - let fileName = this.getFileName(from: url, type: .image) - let newPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! - .appendingPathComponent(fileName) - let copied = this.copyFile(at: url, to: newPath) - if(copied) { - this.shareFiles.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .image)) - } - if index == (content.attachments?.count)! - 1 { - this.save() - } - } - } - } - - private func handleVideos (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { - attachment.loadItem(forTypeIdentifier: videoContentType, options: nil) { [weak self] data, error in - - if error == nil, let url = data as? URL, let this = self { - - // Always copy - let fileName = this.getFileName(from: url, type: .video) - let newPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! - .appendingPathComponent(fileName) - let copied = this.copyFile(at: url, to: newPath) - if(copied) { - guard let sharedFile = this.getSharedMediaFile(forVideo: newPath) else { - return - } - this.shareFiles.append(sharedFile) - } - if index == (content.attachments?.count)! - 1 { - this.save() - } - } - } - } - - private func handleFiles (content: NSExtensionItem, attachment: NSItemProvider, index: Int) { - attachment.loadItem(forTypeIdentifier: fileURLType, options: nil) { [weak self] data, error in - if error == nil, let url = data as? URL, let this = self { - // Always copy - let fileName = this.getFileName(from :url, type: .file) - let newPath = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.\(this.hostAppBundleIdentifier)")! - .appendingPathComponent(fileName) - let copied = this.copyFile(at: url, to: newPath) - if (copied) { - this.shareFiles.append(SharedMediaFile(path: newPath.absoluteString, thumbnail: nil, duration: nil, type: .file)) - } - if index == (content.attachments?.count)! - 1 { - this.save() - } - } - } - } - func getExtension(from url: URL, type: SharedMediaType) -> String { - let parts = url.lastPathComponent.components(separatedBy: ".") - var ex: String? = nil - if (parts.count > 1) { - ex = parts.last - } - - if (ex == nil) { - switch type { - case .image: - ex = "PNG" - case .video: - ex = "MP4" - case .file: - ex = "TXT" - } - } - return ex ?? "Unknown" - } - - func getFileName(from url: URL, type: SharedMediaType) -> String { - var name = url.lastPathComponent - - if (name.isEmpty) { - name = UUID().uuidString + "." + getExtension(from: url, type: type) - } - - return name - } - - func copyFile(at srcURL: URL, to dstURL: URL) -> Bool { - do { - if FileManager.default.fileExists(atPath: dstURL.path) { - try FileManager.default.removeItem(at: dstURL) - } - try FileManager.default.copyItem(at: srcURL, to: dstURL) - } catch (let error) { - print("Cannot copy item at \(srcURL) to \(dstURL): \(error)") - return false - } - return true - } - - private func getSharedMediaFile(forVideo: URL) -> SharedMediaFile? { - let asset = AVAsset(url: forVideo) - let duration = (CMTimeGetSeconds(asset.duration) * 1000).rounded() - let thumbnailPath = getThumbnailPath(for: forVideo) - - if FileManager.default.fileExists(atPath: thumbnailPath.path) { - return SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) - } - - var saved = false - let assetImgGenerate = AVAssetImageGenerator(asset: asset) - assetImgGenerate.appliesPreferredTrackTransform = true - // let scale = UIScreen.main.scale - assetImgGenerate.maximumSize = CGSize(width: 360, height: 360) - do { - let img = try assetImgGenerate.copyCGImage(at: CMTimeMakeWithSeconds(600, preferredTimescale: Int32(1.0)), actualTime: nil) - try UIImage.pngData(UIImage(cgImage: img))()?.write(to: thumbnailPath) - saved = true - } catch { - saved = false - } - - return saved ? SharedMediaFile(path: forVideo.absoluteString, thumbnail: thumbnailPath.absoluteString, duration: duration, type: .video) : nil - - } - - private func getThumbnailPath(for url: URL) -> URL { - let fileName = Data(url.lastPathComponent.utf8).base64EncodedString().replacingOccurrences(of: "==", with: "") - let path = FileManager.default - .containerURL(forSecurityApplicationGroupIdentifier: "group.\(hostAppBundleIdentifier)")! - .appendingPathComponent("\(fileName).jpg") - return path - } - - - class SharedMediaFile: Codable { - var path: String; // can be image, video or url path. It can also be text content - var thumbnail: String?; // video thumbnail - var duration: Double?; // video duration in milliseconds - var type: SharedMediaType; - - - init(path: String, thumbnail: String?, duration: Double?, type: SharedMediaType) { - self.path = path - self.thumbnail = thumbnail - self.duration = duration - self.type = type - } - - // Debug method to print out SharedMediaFile details in the console - func toString() { - print("[SharedMediaFile] \n\tpath: \(self.path)\n\tthumbnail: \(self.thumbnail)\n\tduration: \(self.duration)\n\ttype: \(self.type)") - } - } - - struct SendData: Encodable { - let text: [String] - let files: [SharedMediaFile] - } - - enum SharedMediaType: Int, Codable { - case image - case video - case file - } - - func toData(data: [SharedMediaFile]) -> Data { - let encodedData = try? JSONEncoder().encode(data) - return encodedData! - } -} - diff --git a/ios/ShareExtension/SharingFile.swift b/ios/ShareExtension/SharingFile.swift deleted file mode 100644 index 1dd36d448..000000000 --- a/ios/ShareExtension/SharingFile.swift +++ /dev/null @@ -1,26 +0,0 @@ -import Foundation - -class SharingFile: Codable { - var value: String; - var thumbnail: String?; // video thumbnail - var duration: Double?; // video duration in milliseconds - var type: SharingFileType; - - - init(value: String, thumbnail: String?, duration: Double?, type: SharingFileType) { - self.value = value - self.thumbnail = thumbnail - self.duration = duration - self.type = type - } - - // toString method to print out SharingFile details in the console - func toString() { - print("[SharingFile] \n\tvalue: \(self.value)\n\tthumbnail: \(self.thumbnail ?? "--" )\n\tduration: \(self.duration ?? 0)\n\ttype: \(self.type)") - } - - func toData(data: [SharingFile]) -> Data { - let encodedData = try? JSONEncoder().encode(data) - return encodedData! - } -} \ No newline at end of file diff --git a/ios/ShareExtension/SharingFileType.swift b/ios/ShareExtension/SharingFileType.swift deleted file mode 100644 index 12aec8b37..000000000 --- a/ios/ShareExtension/SharingFileType.swift +++ /dev/null @@ -1,7 +0,0 @@ -enum SharingFileType: Int, Codable { - case text - case url - case image - case video - case file -} diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 73482c46b..c78e56bcb 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -25,6 +25,7 @@ platform :ios do build_app( skip_build_archive: true, archive_path: "../build/ios/archive/Runner.xcarchive", + scheme: "Runner", export_team_id: CredentialsManager::AppfileConfig.try_fetch_value(:team_id) ) upload_to_testflight( diff --git a/ios/fastlane/README.md b/ios/fastlane/README.md new file mode 100644 index 000000000..87ffeb24c --- /dev/null +++ b/ios/fastlane/README.md @@ -0,0 +1,32 @@ +fastlane documentation +---- + +# Installation + +Make sure you have the latest version of the Xcode command line tools installed: + +```sh +xcode-select --install +``` + +For _fastlane_ installation instructions, see [Installing _fastlane_](https://docs.fastlane.tools/#installing-fastlane) + +# Available Actions + +## iOS + +### ios release + +```sh +[bundle exec] fastlane ios release +``` + +Push a new release build to the App Store + +---- + +This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run. + +More information about _fastlane_ can be found on [fastlane.tools](https://fastlane.tools). + +The documentation of _fastlane_ can be found on [docs.fastlane.tools](https://docs.fastlane.tools). diff --git a/lib/const.dart b/lib/const.dart index 06256e7a0..98fb61994 100644 --- a/lib/const.dart +++ b/lib/const.dart @@ -1,4 +1,4 @@ -const misskeyIOReactionDelay = 1500; +const misskeyHQReactionDelay = 1500; class Font { final String displayName; diff --git a/lib/extensions/color_extension.dart b/lib/extensions/color_extension.dart index 3b0b8cc45..45e985197 100644 --- a/lib/extensions/color_extension.dart +++ b/lib/extensions/color_extension.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import "package:flutter/material.dart"; extension ColorExtension on Color { Color darken([double amount = .1]) { diff --git a/lib/extensions/color_option_extension.dart b/lib/extensions/color_option_extension.dart index 247d9dcd0..e6ac0cf5d 100644 --- a/lib/extensions/color_option_extension.dart +++ b/lib/extensions/color_option_extension.dart @@ -1,4 +1,4 @@ -import 'package:image_editor/image_editor.dart'; +import "package:image_editor/image_editor.dart"; // inspired colorfilter_generator // https://github.com/hsbijarniya/colorfilter_generator/ diff --git a/lib/extensions/date_time_extension.dart b/lib/extensions/date_time_extension.dart index f1b822f35..217d7c2fb 100644 --- a/lib/extensions/date_time_extension.dart +++ b/lib/extensions/date_time_extension.dart @@ -1,13 +1,13 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:intl/intl.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:intl/intl.dart"; extension DateTimeExtension on DateTime { Duration operator -(DateTime other) => difference(other); - operator <(DateTime other) => compareTo(other) < 0; - operator <=(DateTime other) => compareTo(other) <= 0; - operator >(DateTime other) => compareTo(other) > 0; - operator >=(DateTime other) => compareTo(other) >= 0; + bool operator <(DateTime other) => compareTo(other) < 0; + bool operator <=(DateTime other) => compareTo(other) <= 0; + bool operator >(DateTime other) => compareTo(other) > 0; + bool operator >=(DateTime other) => compareTo(other) >= 0; String format(BuildContext context) { final localeName = Localizations.localeOf(context).toLanguageTag(); diff --git a/lib/extensions/list_mfm_node_extension.dart b/lib/extensions/list_mfm_node_extension.dart index ebecf2722..22a6f044e 100644 --- a/lib/extensions/list_mfm_node_extension.dart +++ b/lib/extensions/list_mfm_node_extension.dart @@ -1,6 +1,6 @@ -import 'dart:collection'; +import "dart:collection"; -import 'package:mfm_parser/mfm_parser.dart'; +import "package:mfm_parser/mfm_parser.dart"; extension ListMfmNodeExtension on List { // https://github.com/misskey-dev/misskey/blob/2023.9.2/packages/frontend/src/scripts/extract-url-from-mfm.ts diff --git a/lib/extensions/note_extension.dart b/lib/extensions/note_extension.dart index 09cbf665d..26e98a60b 100644 --- a/lib/extensions/note_extension.dart +++ b/lib/extensions/note_extension.dart @@ -1,4 +1,4 @@ -import 'package:misskey_dart/misskey_dart.dart'; +import "package:misskey_dart/misskey_dart.dart"; extension NoteExtension on Note { bool get isEmptyRenote => diff --git a/lib/extensions/note_visibility_extension.dart b/lib/extensions/note_visibility_extension.dart index 1177320c6..2fedbd426 100644 --- a/lib/extensions/note_visibility_extension.dart +++ b/lib/extensions/note_visibility_extension.dart @@ -1,6 +1,6 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:misskey_dart/misskey_dart.dart"; extension NoteVisibilityExtension on NoteVisibility { IconData get icon { diff --git a/lib/extensions/origin_extension.dart b/lib/extensions/origin_extension.dart index 388d52d6c..218ecf54a 100644 --- a/lib/extensions/origin_extension.dart +++ b/lib/extensions/origin_extension.dart @@ -1,6 +1,6 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:misskey_dart/misskey_dart.dart"; extension OriginExtension on Origin { String displayName(BuildContext context) { diff --git a/lib/extensions/reaction_acceptance_extension.dart b/lib/extensions/reaction_acceptance_extension.dart index c9cbcdbec..be61ffe78 100644 --- a/lib/extensions/reaction_acceptance_extension.dart +++ b/lib/extensions/reaction_acceptance_extension.dart @@ -1,6 +1,6 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:misskey_dart/misskey_dart.dart"; extension ReactionAcceptanceExtension on ReactionAcceptance { String displayName(BuildContext context) { diff --git a/lib/extensions/string_extensions.dart b/lib/extensions/string_extensions.dart index 6cc0b07dd..2a682856d 100644 --- a/lib/extensions/string_extensions.dart +++ b/lib/extensions/string_extensions.dart @@ -1,9 +1,9 @@ -import 'package:flutter/widgets.dart'; +import "package:flutter/widgets.dart"; extension StringExtensions on String { String get tight { return Characters(this) - .replaceAll(Characters(''), Characters('\u{200B}')) + .replaceAll(Characters(""), Characters("\u{200B}")) .toString(); } diff --git a/lib/extensions/text_editing_controller_extension.dart b/lib/extensions/text_editing_controller_extension.dart index 8d5f89665..db4ff4283 100644 --- a/lib/extensions/text_editing_controller_extension.dart +++ b/lib/extensions/text_editing_controller_extension.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; -import 'package:miria/model/input_completion_type.dart'; +import "package:flutter/material.dart"; +import "package:miria/model/input_completion_type.dart"; extension TextEditingControllerExtension on TextEditingController { String? get textBeforeSelection { @@ -19,7 +19,7 @@ extension TextEditingControllerExtension on TextEditingController { if (lastColonIndex < 0) { return null; } - if (RegExp(r':[a-zA-z_0-9]+?:$') + if (RegExp(r":[a-zA-z_0-9]+?:$") .hasMatch(text.substring(0, lastColonIndex + 1))) { return null; } else { @@ -78,16 +78,18 @@ extension TextEditingControllerExtension on TextEditingController { } void insert(String insertText, {String? afterText}) { - final currentPosition = selection.base.offset; - final before = text.isEmpty ? "" : text.substring(0, currentPosition); - final after = (currentPosition == text.length || currentPosition == -1) - ? "" - : text.substring(currentPosition, text.length); + final start = selection.start < 0 ? 0 : selection.start; + final end = selection.end < 0 ? 0 : selection.end; + final before = text.substring(0, start); + final selectedText = text.substring(start, end); + final after = text.substring(end); value = TextEditingValue( - text: "$before$insertText${afterText ?? ""}$after", - selection: TextSelection.collapsed( - offset: (currentPosition == -1 ? 0 : currentPosition) + - insertText.length)); + text: "$before$insertText$selectedText${afterText ?? ""}$after", + selection: TextSelection( + baseOffset: start + insertText.length, + extentOffset: end + insertText.length, + ), + ); } } diff --git a/lib/extensions/user_extension.dart b/lib/extensions/user_extension.dart index dfef770b1..125123e9d 100644 --- a/lib/extensions/user_extension.dart +++ b/lib/extensions/user_extension.dart @@ -1,4 +1,4 @@ -import 'package:misskey_dart/misskey_dart.dart'; +import "package:misskey_dart/misskey_dart.dart"; extension UserExtension on User { String get acct { diff --git a/lib/extensions/users_lists_show_response_extension.dart b/lib/extensions/users_lists_show_response_extension.dart index 2525bd78c..5ea086332 100644 --- a/lib/extensions/users_lists_show_response_extension.dart +++ b/lib/extensions/users_lists_show_response_extension.dart @@ -1,4 +1,4 @@ -import 'package:misskey_dart/misskey_dart.dart'; +import "package:misskey_dart/misskey_dart.dart"; extension UsersListsShowResponseExtension on UsersListsShowResponse { UsersList toUsersList() { diff --git a/lib/extensions/users_sort_type_extension.dart b/lib/extensions/users_sort_type_extension.dart index 318600c26..82be9c04b 100644 --- a/lib/extensions/users_sort_type_extension.dart +++ b/lib/extensions/users_sort_type_extension.dart @@ -1,6 +1,6 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:misskey_dart/misskey_dart.dart"; extension UsersSortTypeExtension on UsersSortType { String displayName(BuildContext context) { diff --git a/lib/hooks/use_async.dart b/lib/hooks/use_async.dart new file mode 100644 index 000000000..7641f5ba8 --- /dev/null +++ b/lib/hooks/use_async.dart @@ -0,0 +1,54 @@ +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/log.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; + +class AsyncOperation { + final AsyncValue? value; + final Future Function() execute; + + /// onPressed: に渡し、AsyncLoading() のとき非活性に、それ以外のときは活性にするためのユーティリティ + Future Function()? get executeOrNull => + value is AsyncLoading ? null : execute; + + const AsyncOperation({required this.value, required this.execute}); +} + +AsyncOperation useAsync(Future Function() future) { + final result = useState?>(null); + + return AsyncOperation( + value: result.value, + execute: () async { + if (result.value is AsyncLoading) return; + result.value = const AsyncLoading(); + + try { + final value = await future(); + if (value is AsyncError) throw value.error; + result.value = AsyncData(value); + } catch (error, stack) { + logger + ..warning(error) + ..warning(stack); + result.value = AsyncError(error, stack); + } + }, + ); +} + +AsyncOperation useHandledFuture(Future Function() future) { + final result = useState?>(null); + final ref = ProviderScope.containerOf(useContext()); + + return AsyncOperation( + value: result.value, + execute: () async { + result + ..value = const AsyncLoading() + ..value = await ref + .read(dialogStateNotifierProvider.notifier) + .guard(() async => await future()); + }, + ); +} diff --git a/lib/l10n/app_ja-oj.arb b/lib/l10n/app_ja-oj.arb index 40a01efb1..bac68a8d4 100644 --- a/lib/l10n/app_ja-oj.arb +++ b/lib/l10n/app_ja-oj.arb @@ -72,6 +72,9 @@ "reactionNotification": "{reactionUser}らがリアクションされましてよ", "notedNotification": "{notedUser}らがノートいたしましてよ", "roleAssignedNotification": "ロール「{role}」に入れられましてよ", + "appNotification": "なにかしらのアプリからの通知のようでして", + "someoneLogined": "ログインされたようですわ", + "unknownNotification": "存じ上げない種類の通知ですの", "cancelEmojiChoosing": "やめておきますわ", "doneEmojiChoosing": "使いますわ", @@ -101,8 +104,10 @@ "pleaseSpecifyExpirationDate": "投票がいつまでか入れてくださいまし?", "pleaseSpecifyExpirationDuration": "投票期間を入れてくださいまし?", "cannotMentionToRemoteInLocalOnlyNote": "連合切られているのに他のサーバーの人がメンションに含まれているようですわ", - "cannotPublicReplyToPrivateNote": "リプライが{visibility}のようでして……パブリックにはできませんこと" + "cannotPublicReplyToPrivateNote": "リプライが{visibility}のようでして……パブリックにはできませんこと", + "unsupportedFile": "対応してないファイルのようですわ", + "failedFileSave": "ファイルの保存に失敗したようですわね…" diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index 683dfeee2..55369c6e0 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -17,8 +17,8 @@ "hide": "隠す", "note": "ノート", "mention": "メンション", - "renote": "Renote", - "quotedRenote": "引用Renote", + "renote": "リノート", + "quotedRenote": "引用", "notification": "通知", "list": "リスト", "explore": "みつける", @@ -72,6 +72,7 @@ "apiKey": "APIキー", "contentWarning": "注釈", "follower": "フォロワー", + "sending": "送信中", "antennaName": "アンテナの名前", "antennaSource": "アンテナのソース", @@ -205,12 +206,13 @@ "openBrowsers": "ブラウザで開く", "openBrowsersAsRemote": "ブラウザでリモート先を開く", "openInAnotherAccount": "別のアカウントで開く", + "openNoteInBrowsers": "ブラウザでノートを開く", "changeFullScreen": "フルスクリーンに切り替え", "shareNotes": "ノートを共有", "deleteFavorite": "お気に入り解除", "notesAfterRenote": "リノート直後のノート", "deletedRecreate": "削除してなおす", - "confirmDeletedRecreate": "このノート消してなおす?ついたリアクション、Renote、返信は消えて戻らへんで?", + "confirmDeletedRecreate": "このノート消してなおす?ついたリアクション、リノート、返信は消えて戻らへんで?", "deleteRenote": "リノートを解除する", "confirmDelete": "ほんまに消してええな?", "doDeleting": "消す!", @@ -254,7 +256,7 @@ } }, "renoted": "リノートしました。", - "renoteInSpecificChannel": "{channelName}内にRenote", + "renoteInSpecificChannel": "{channelName}内にリノート", "@renoteInSpecificChannel": { "placeholders": { "channelName": { @@ -262,7 +264,7 @@ } } }, - "quotedRenoteInSpecificChannel": "{channelName}内に引用Renote", + "quotedRenoteInSpecificChannel": "{channelName}内に引用", "@quotedRenoteInSpecificChannel": { "placeholders": { "channelName": { @@ -270,11 +272,11 @@ } } }, - "renoteInChannel": "チャンネルへRenote", - "renoteInOtherChannel": "よそのチャンネルへRenote", - "quotedRenoteInChannel": "チャンネルへ引用Renote", - "quotedRenoteInOtherChannel": "よそのチャンネルへRenote", - "renotedUsers": "Renoteしたユーザー", + "renoteInChannel": "チャンネルへリノート", + "renoteInOtherChannel": "よそのチャンネルへリノート", + "quotedRenoteInChannel": "チャンネルへ引用", + "quotedRenoteInOtherChannel": "よそのチャンネルへ引用", + "renotedUsers": "リノートしたユーザー", "otherComplementReactions": "他のん", "openAsOtherAccount": "開くアカウントを選んでや", "pickColor": "色を選んでや", @@ -329,7 +331,7 @@ "renotedUsersInNotification": "リノートしてくれはった人", "reactionUsersInNotification": "リアクションしてくれはった人", "finishedVotedNotification": "投票が終わったみたいや", - "renoteAndReactionsNotification": "{reactionUser}さんたちがリアクションしはって、{renotedUser}さんたちがRenoteしはったで", + "renoteAndReactionsNotification": "{reactionUser}さんたちがリアクションしはって、{renotedUser}さんたちがリノートしはったで", "@renoteAndReactionsNotification": { "placeholders": { "reactionUser": { @@ -340,7 +342,7 @@ } } }, - "renoteNotification": "{renoteUser}さんたちがRenoteしはったで", + "renoteNotification": "{renoteUser}さんたちがリノートしはったで", "@renoteNotification": { "placeholders": { "renoteUser": { @@ -372,6 +374,18 @@ } } }, + "appNotification": "なんかのアプリからの通知らしいわ", + "someoneLogined": "自分のアカウントにログインがあったらしいで", + "unknownNotification": "知らんタイプの通知やわ", + "messageForFollower": "フォロワーへ 「{message}」", + "@messageForFollower": { + "placeholders": { + "message": { + "type": "String" + } + } + }, + "notificationAll": "みんな", "notificationForMe": "自分宛て", "notificationDirect": "ダイレクト", @@ -777,7 +791,7 @@ "enableAnimatedMfm": "動きのあるMFM", "enableAnimatedMfmDescription": "動きのあるMFMを有効にします。", "collapseNotes": "ノートの省略", - "collapseReactionedRenotes": "リアクション済みノートのRenoteを省略します。", + "collapseReactionedRenotes": "リアクション済みノートのリノートを省略します。", "collapseLongNotes": "長いノートを省略します。", "tabPosition": "タブの位置", "tabPositionDescription": "{tabPosition}に表示する", @@ -813,13 +827,14 @@ "systemFont": "システム標準", "selectFolder": "フォルダー選択", - "importSettings": "設定のインポート", - "importSettingsDescription": "設定ファイルをドライブから読み込みます。設定ファイルには保存されたときのすべてのアカウントの設定情報が記録されていますが、そのうちこの端末でログインしているアカウントの情報のみを読み込みます。", + "settingsFileManagement": "設定ファイルの管理", + "importAndExportSettingsDescription": "現在の設定から、アカウントのログイン情報を除くすべての設定を設定ファイルに出力して管理することができます。設定ファイルは、指定したアカウントの「ドライブ」内に保存されます。", + "importSettings": "インポート", + "importSettingsDescription": "全般設定と、この端末でログインしているアカウントに対応する設定が読み込まれます。", "pleaseSelectAccount": "アカウントを選んでや", "select": "選択", - "exportSettings": "設定のエクスポート", - "exportSettingsDescription1": "設定ファイルをドライブに保存します。設定ファイルにはこの端末でログインしているすべてのアカウントの、ログイン情報以外の情報が記録されます。", - "exportSettingsDescription2": "設定ファイルは1回のエクスポートにつき1つのアカウントに対して保存されます。", + "exportSettings": "エクスポート", + "exportSettingsDescription": "全般設定と、この端末でログインしているすべてのアカウントの設定を設定ファイルに保存します。", "pleaseSelectAccountToExportSettings": "設定ファイルを保存するアカウントを選んでや", "selectAntenna": "アンテナ選択", @@ -860,7 +875,7 @@ "emojiCache": "絵文字の情報", "serverCache": "サーバーの情報", "instanceMuteDescription1": "設定したサーバーのノートを隠します。", - "instanceMuteDescription2": "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのノートとRenoteをミュートします。\n改行で区切って設定します。", + "instanceMuteDescription2": "ミュートしたサーバーのユーザーへの返信を含めて、設定したサーバーの全てのノートとリノートをミュートします。\n改行で区切って設定します。", "bulkAddReactions": "一括追加", "bulkAddReactionsDescription1": "お使いのブラウザでリアクションデッキをコピーしたいアカウントにログインしてください", "bulkAddReactionsDescription2": "同じブラウザで以下のURLにアクセスして「値 (JSON)」の内容をすべて選択してコピーしてください", @@ -900,8 +915,8 @@ "addToList": "リストに追加", "addToAntenna": "アンテナに追加", "searchNote": "ノートを検索", - "deleteRenoteMute": "Renoteのミュートを解除する", - "createRenoteMute": "Renoteをミュートする", + "deleteRenoteMute": "リノートのミュートを解除する", + "createRenoteMute": "リノートをミュートする", "deleteMute": "ミュートを解除する", "createMute": "ミュートする", "deleteBlock": "ブロックを解除する", @@ -912,7 +927,7 @@ "selectDuration": "期限を選択してください。", "confirmUnfollow": "フォロー解除してもええか?", "deleteFollow": "解除する", - "renoteMuting": "Renoteのミュート中", + "renoteMuting": "リノートのミュート中", "muting": "ミュート中", "blocking": "ブロック中", "followed": "フォローされています", @@ -930,6 +945,7 @@ "mediaOnlyShort": "ファイルつき", "displayRenotesShort": "リノートも", "showNotesBeforeThisDate": "この日までを表示", + "showNotesBeforeThisTime": "この時間までを表示", "userHighlightAvailability": "ハイライトはMisskey 2023.10.0以降の機能です。", "userInfomation": "アカウント情報", "userInfomationLocal": "アカウント情報(ローカル)", @@ -972,6 +988,9 @@ "updatedAtAscendingOrder": "更新されていない順", "updatedAtDescendingOrder": "更新された順", + "unsupportedFile": "対応してないファイルやわ", + "failedFileSave": "ファイルの保存に失敗したみたいや", + "misskeyGames": "Misskey Games", "cookieCliker": "Cookie Cliker", "bubbleGame": "Bubble Game", diff --git a/lib/licenses.dart b/lib/licenses.dart index 5a999124a..eab2d6cec 100644 --- a/lib/licenses.dart +++ b/lib/licenses.dart @@ -1,4 +1,4 @@ -import 'package:flutter/foundation.dart'; +import "package:flutter/foundation.dart"; final miriaInheritedLicenses = [ const LicenseEntryWithLineBreaks(["Blob Emoji"], """ diff --git a/lib/log.dart b/lib/log.dart new file mode 100644 index 000000000..1f2b6bf9e --- /dev/null +++ b/lib/log.dart @@ -0,0 +1,3 @@ +import "package:simple_logger/simple_logger.dart"; + +final logger = SimpleLogger(); diff --git a/lib/main.dart b/lib/main.dart index 108eb297b..e6aeb47cd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,134 +1,126 @@ -import 'dart:io'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:media_kit/media_kit.dart'; -import 'package:miria/model/desktop_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/error_dialog_listener.dart'; -import 'package:miria/view/common/sharing_intent_listener.dart'; -import 'package:miria/view/themes/app_theme_scope.dart'; -import 'package:stack_trace/stack_trace.dart' as stack_trace; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:window_manager/window_manager.dart'; +import "dart:async"; +import "dart:io"; + +import "package:flutter/foundation.dart"; +import "package:flutter/gestures.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:flutter_localizations/flutter_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:media_kit/media_kit.dart"; +import "package:miria/model/desktop_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/dialog/dialog_scope.dart"; +import "package:miria/view/common/error_dialog_listener.dart"; +import "package:miria/view/common/sharing_intent_listener.dart"; +import "package:miria/view/themes/app_theme_scope.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; +import "package:stack_trace/stack_trace.dart" as stack_trace; +import "package:window_manager/window_manager.dart"; + +part "main.g.dart"; Future main() async { WidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); if (Platform.isWindows || Platform.isMacOS || Platform.isLinux) { - windowManager.ensureInitialized(); + await windowManager.ensureInitialized(); } - FlutterError.demangleStackTrace = (StackTrace stack) { + FlutterError.demangleStackTrace = (stack) { if (stack is stack_trace.Trace) return stack.vmTrace; if (stack is stack_trace.Chain) return stack.toTrace().vmTrace; return stack; }; - runApp(ProviderScope(child: MyApp())); + runApp(const ProviderScope(child: Miria())); } -class MyApp extends ConsumerStatefulWidget { - const MyApp({super.key}); +bool get isDesktop => + Platform.isWindows || Platform.isMacOS || Platform.isLinux; - @override - ConsumerState createState() => _MyAppState(); -} - -class _MyAppState extends ConsumerState with WindowListener { - final _appRouter = AppRouter(); - final isDesktop = Platform.isWindows || Platform.isMacOS || Platform.isLinux; +class Miria extends HookConsumerWidget with WidgetsBindingObserver { + const Miria({super.key}); @override - void initState() { - if (isDesktop) { - windowManager.addListener(this); - ref - .read(desktopSettingsRepositoryProvider) - .load() - .then((_) => _initWindow()); - } - super.initState(); - } - - @override - void dispose() { - if (isDesktop) { - windowManager.removeListener(this); - } - super.dispose(); - } - - @override - void onWindowClose() async { + Future didChangePlatformBrightness() async { + super.didChangePlatformBrightness(); if (!isDesktop) return; - bool isPreventClose = await windowManager.isPreventClose(); - if (isPreventClose) { - final size = await windowManager.getSize(); - final position = await windowManager.getPosition(); - try { - final settings = ref.read(desktopSettingsRepositoryProvider).settings; - await ref.read(desktopSettingsRepositoryProvider).update( - settings.copyWith( - window: DesktopWindowSettings( - w: size.width, - h: size.height, - x: position.dx, - y: position.dy))); - } catch (e) { - if (kDebugMode) print(e); - } finally { - await windowManager.destroy(); - } - } + final brightness = + WidgetsBinding.instance.platformDispatcher.platformBrightness; + + /// Set up window brightness follow system theme mode. + await WindowManager.instance.setBrightness(brightness); + await WindowManager.instance.setTitleBarStyle(TitleBarStyle.normal); } - Future _initWindow() async { + Future _initWindow(WidgetRef ref) async { await windowManager.setPreventClose(true); - DesktopSettings config = - ref.read(desktopSettingsRepositoryProvider).settings; + final config = ref.read(desktopSettingsRepositoryProvider).settings; - Size size = (config.window.w > 0 && config.window.h > 0) + final size = (config.window.w > 0 && config.window.h > 0) ? Size(config.window.w, config.window.h) : const Size(400, 700); - Offset? position = (config.window.x != null && config.window.y != null) + final position = (config.window.x != null && config.window.y != null) ? Offset(config.window.x!, config.window.y!) : null; - WindowOptions opt = WindowOptions( + final opt = WindowOptions( size: size, - center: (position == null), - backgroundColor: Colors.transparent, + center: position == null, skipTaskbar: false, titleBarStyle: TitleBarStyle.normal, ); if (position != null) { - windowManager.setPosition(position); + await windowManager.setPosition(position); } - windowManager.waitUntilReadyToShow(opt, () async { + await windowManager.waitUntilReadyToShow(opt, () async { await windowManager.show(); await windowManager.focus(); }); } - // This widget is the root of your application. @override - Widget build(BuildContext context) { - final language = ref.watch(generalSettingsRepositoryProvider - .select((value) => value.settings.languages)); + Widget build(BuildContext context, WidgetRef ref) { + useEffect( + () { + if (!isDesktop) return null; + final windowListener = ref.read(miriaWindowListenerProvider); + WidgetsBinding.instance.addObserver(this); + windowManager.addListener(windowListener); + return () { + WidgetsBinding.instance.removeObserver(this); + windowManager.removeListener(windowListener); + }; + }, + const [], + ); + useMemoized(() { + unawaited(() async { + await ref.read(desktopSettingsRepositoryProvider).load(); + await _initWindow(ref); + }()); + }); + + final language = ref.watch( + generalSettingsRepositoryProvider + .select((value) => value.settings.languages), + ); + final appRouter = ref.watch(appRouterProvider); return MaterialApp.router( - title: 'Miria', + title: "Miria", debugShowCheckedModeBanner: false, locale: Locale(language.countryCode, language.languageCode), - supportedLocales: const [Locale("ja", "JP"), Locale("ja", "OJ"), Locale("zh", "CN")], + supportedLocales: const [ + Locale("ja", "JP"), + Locale("ja", "OJ"), + Locale("zh", "CN"), + ], scrollBehavior: AppScrollBehavior(), localizationsDelegates: const [ S.delegate, @@ -137,16 +129,18 @@ class _MyAppState extends ConsumerState with WindowListener { GlobalCupertinoLocalizations.delegate, ], builder: (context, widget) { - return AppThemeScope( - child: SharingIntentListener( - router: _appRouter, - child: ErrorDialogListener( - child: widget ?? Container(), + return DialogScope( + child: AppThemeScope( + child: SharingIntentListener( + router: appRouter, + child: ErrorDialogListener( + child: widget ?? Container(), + ), ), ), ); }, - routerConfig: _appRouter.config(), + routerConfig: appRouter.config(), ); } } @@ -159,3 +153,41 @@ class AppScrollBehavior extends MaterialScrollBehavior { PointerDeviceKind.trackpad, }; } + +@riverpod +MiriaWindowListener miriaWindowListener(MiriaWindowListenerRef ref) => + MiriaWindowListener(ref); + +class MiriaWindowListener with WindowListener { + final Ref ref; + + MiriaWindowListener(this.ref); + + @override + Future onWindowClose() async { + if (!isDesktop) return; + + final isPreventClose = await windowManager.isPreventClose(); + if (!isPreventClose) return; + + final size = await windowManager.getSize(); + final position = await windowManager.getPosition(); + try { + final settings = ref.read(desktopSettingsRepositoryProvider).settings; + await ref.read(desktopSettingsRepositoryProvider).update( + settings.copyWith( + window: DesktopWindowSettings( + w: size.width, + h: size.height, + x: position.dx, + y: position.dy, + ), + ), + ); + } catch (e) { + if (kDebugMode) print(e); + } finally { + await windowManager.destroy(); + } + } +} diff --git a/lib/main.g.dart b/lib/main.g.dart new file mode 100644 index 000000000..77f506061 --- /dev/null +++ b/lib/main.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'main.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$miriaWindowListenerHash() => + r'0e55c9140c4bdec2c79ecc178eace5c295cc29b5'; + +/// See also [miriaWindowListener]. +@ProviderFor(miriaWindowListener) +final miriaWindowListenerProvider = + AutoDisposeProvider.internal( + miriaWindowListener, + name: r'miriaWindowListenerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$miriaWindowListenerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef MiriaWindowListenerRef = AutoDisposeProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/model/account.dart b/lib/model/account.dart index 797b786de..3f3b9ea61 100644 --- a/lib/model/account.dart +++ b/lib/model/account.dart @@ -1,9 +1,9 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:miria/model/acct.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:miria/model/acct.dart"; +import "package:misskey_dart/misskey_dart.dart"; -part 'account.freezed.dart'; -part 'account.g.dart'; +part "account.freezed.dart"; +part "account.g.dart"; @Freezed(equal: false) class Account with _$Account { @@ -12,8 +12,8 @@ class Account with _$Account { const factory Account({ required String host, required String userId, - String? token, required MeDetailed i, + String? token, MetaResponse? meta, }) = _Account; @@ -38,12 +38,14 @@ class Account with _$Account { ); } + bool get isDemoAccount => userId.isEmpty; + factory Account.demoAccount(String host, MetaResponse? meta) => Account( - host: host, - userId: "", - token: null, - meta: meta, - i: MeDetailed( + host: host, + userId: "", + token: null, + meta: meta, + i: MeDetailed( id: "", username: "", createdAt: DateTime.now(), @@ -82,25 +84,27 @@ class Account with _$Account { hasUnreadSpecifiedNotes: false, mutedWords: [], mutedInstances: [], - mutingNotificationTypes: [], emailNotificationTypes: [], achievements: [], loggedInDays: 0, policies: const UserPolicies( - gtlAvailable: false, - ltlAvailable: false, - canPublicNote: false, - canInvite: false, - canManageCustomEmojis: false, - canHideAds: false, - driveCapacityMb: 0, - pinLimit: 0, - antennaLimit: 0, - wordMuteLimit: 0, - webhookLimit: 0, - clipLimit: 0, - noteEachClipsLimit: 0, - userListLimit: 0, - userEachUserListsLimit: 0, - rateLimitFactor: 0))); + gtlAvailable: false, + ltlAvailable: false, + canPublicNote: false, + canInvite: false, + canManageCustomEmojis: false, + canHideAds: false, + driveCapacityMb: 0, + pinLimit: 0, + antennaLimit: 0, + wordMuteLimit: 0, + webhookLimit: 0, + clipLimit: 0, + noteEachClipsLimit: 0, + userListLimit: 0, + userEachUserListsLimit: 0, + rateLimitFactor: 0, + ), + ), + ); } diff --git a/lib/model/account.freezed.dart b/lib/model/account.freezed.dart index c760e880d..c7cdbc9f4 100644 --- a/lib/model/account.freezed.dart +++ b/lib/model/account.freezed.dart @@ -12,7 +12,7 @@ part of 'account.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Account _$AccountFromJson(Map json) { return _Account.fromJson(json); @@ -22,12 +22,16 @@ Account _$AccountFromJson(Map json) { mixin _$Account { String get host => throw _privateConstructorUsedError; String get userId => throw _privateConstructorUsedError; - String? get token => throw _privateConstructorUsedError; MeDetailed get i => throw _privateConstructorUsedError; + String? get token => throw _privateConstructorUsedError; MetaResponse? get meta => throw _privateConstructorUsedError; + /// Serializes this Account to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of Account + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $AccountCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -39,8 +43,8 @@ abstract class $AccountCopyWith<$Res> { $Res call( {String host, String userId, - String? token, MeDetailed i, + String? token, MetaResponse? meta}); $MeDetailedCopyWith<$Res> get i; @@ -57,13 +61,15 @@ class _$AccountCopyWithImpl<$Res, $Val extends Account> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of Account + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? host = null, Object? userId = null, - Object? token = freezed, Object? i = null, + Object? token = freezed, Object? meta = freezed, }) { return _then(_value.copyWith( @@ -75,14 +81,14 @@ class _$AccountCopyWithImpl<$Res, $Val extends Account> ? _value.userId : userId // ignore: cast_nullable_to_non_nullable as String, - token: freezed == token - ? _value.token - : token // ignore: cast_nullable_to_non_nullable - as String?, i: null == i ? _value.i : i // ignore: cast_nullable_to_non_nullable as MeDetailed, + token: freezed == token + ? _value.token + : token // ignore: cast_nullable_to_non_nullable + as String?, meta: freezed == meta ? _value.meta : meta // ignore: cast_nullable_to_non_nullable @@ -90,6 +96,8 @@ class _$AccountCopyWithImpl<$Res, $Val extends Account> ) as $Val); } + /// Create a copy of Account + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $MeDetailedCopyWith<$Res> get i { @@ -98,6 +106,8 @@ class _$AccountCopyWithImpl<$Res, $Val extends Account> }); } + /// Create a copy of Account + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $MetaResponseCopyWith<$Res>? get meta { @@ -121,8 +131,8 @@ abstract class _$$AccountImplCopyWith<$Res> implements $AccountCopyWith<$Res> { $Res call( {String host, String userId, - String? token, MeDetailed i, + String? token, MetaResponse? meta}); @override @@ -139,13 +149,15 @@ class __$$AccountImplCopyWithImpl<$Res> _$AccountImpl _value, $Res Function(_$AccountImpl) _then) : super(_value, _then); + /// Create a copy of Account + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? host = null, Object? userId = null, - Object? token = freezed, Object? i = null, + Object? token = freezed, Object? meta = freezed, }) { return _then(_$AccountImpl( @@ -157,14 +169,14 @@ class __$$AccountImplCopyWithImpl<$Res> ? _value.userId : userId // ignore: cast_nullable_to_non_nullable as String, - token: freezed == token - ? _value.token - : token // ignore: cast_nullable_to_non_nullable - as String?, i: null == i ? _value.i : i // ignore: cast_nullable_to_non_nullable as MeDetailed, + token: freezed == token + ? _value.token + : token // ignore: cast_nullable_to_non_nullable + as String?, meta: freezed == meta ? _value.meta : meta // ignore: cast_nullable_to_non_nullable @@ -179,8 +191,8 @@ class _$AccountImpl extends _Account { const _$AccountImpl( {required this.host, required this.userId, - this.token, required this.i, + this.token, this.meta}) : super._(); @@ -192,18 +204,20 @@ class _$AccountImpl extends _Account { @override final String userId; @override - final String? token; - @override final MeDetailed i; @override + final String? token; + @override final MetaResponse? meta; @override String toString() { - return 'Account(host: $host, userId: $userId, token: $token, i: $i, meta: $meta)'; + return 'Account(host: $host, userId: $userId, i: $i, token: $token, meta: $meta)'; } - @JsonKey(ignore: true) + /// Create a copy of Account + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$AccountImplCopyWith<_$AccountImpl> get copyWith => @@ -221,8 +235,8 @@ abstract class _Account extends Account { const factory _Account( {required final String host, required final String userId, - final String? token, required final MeDetailed i, + final String? token, final MetaResponse? meta}) = _$AccountImpl; const _Account._() : super._(); @@ -233,13 +247,16 @@ abstract class _Account extends Account { @override String get userId; @override - String? get token; - @override MeDetailed get i; @override + String? get token; + @override MetaResponse? get meta; + + /// Create a copy of Account + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$AccountImplCopyWith<_$AccountImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/account.g.dart b/lib/model/account.g.dart index 754427110..0e07078d3 100644 --- a/lib/model/account.g.dart +++ b/lib/model/account.g.dart @@ -10,8 +10,8 @@ _$AccountImpl _$$AccountImplFromJson(Map json) => _$AccountImpl( host: json['host'] as String, userId: json['userId'] as String, - token: json['token'] as String?, i: MeDetailed.fromJson(json['i'] as Map), + token: json['token'] as String?, meta: json['meta'] == null ? null : MetaResponse.fromJson(json['meta'] as Map), @@ -21,7 +21,7 @@ Map _$$AccountImplToJson(_$AccountImpl instance) => { 'host': instance.host, 'userId': instance.userId, - 'token': instance.token, 'i': instance.i.toJson(), + 'token': instance.token, 'meta': instance.meta?.toJson(), }; diff --git a/lib/model/account_settings.dart b/lib/model/account_settings.dart index b2404ef43..a70d67cb5 100644 --- a/lib/model/account_settings.dart +++ b/lib/model/account_settings.dart @@ -1,9 +1,9 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:miria/model/acct.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:miria/model/acct.dart"; +import "package:misskey_dart/misskey_dart.dart"; -part 'account_settings.freezed.dart'; -part 'account_settings.g.dart'; +part "account_settings.freezed.dart"; +part "account_settings.g.dart"; enum CacheStrategy { whenTabChange, diff --git a/lib/model/account_settings.freezed.dart b/lib/model/account_settings.freezed.dart index 1f9473b2f..fae559c15 100644 --- a/lib/model/account_settings.freezed.dart +++ b/lib/model/account_settings.freezed.dart @@ -12,7 +12,7 @@ part of 'account_settings.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); AccountSettings _$AccountSettingsFromJson(Map json) { return _AccountSettings.fromJson(json); @@ -36,8 +36,12 @@ mixin _$AccountSettings { DateTime? get latestMetaCached => throw _privateConstructorUsedError; bool get forceShowAd => throw _privateConstructorUsedError; + /// Serializes this AccountSettings to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of AccountSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $AccountSettingsCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -74,6 +78,8 @@ class _$AccountSettingsCopyWithImpl<$Res, $Val extends AccountSettings> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of AccountSettings + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -180,6 +186,8 @@ class __$$AccountSettingsImplCopyWithImpl<$Res> _$AccountSettingsImpl _value, $Res Function(_$AccountSettingsImpl) _then) : super(_value, _then); + /// Create a copy of AccountSettings + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -355,7 +363,7 @@ class _$AccountSettingsImpl extends _AccountSettings { other.forceShowAd == forceShowAd)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, @@ -373,7 +381,9 @@ class _$AccountSettingsImpl extends _AccountSettings { latestMetaCached, forceShowAd); - @JsonKey(ignore: true) + /// Create a copy of AccountSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$AccountSettingsImplCopyWith<_$AccountSettingsImpl> get copyWith => @@ -434,8 +444,11 @@ abstract class _AccountSettings extends AccountSettings { DateTime? get latestMetaCached; @override bool get forceShowAd; + + /// Create a copy of AccountSettings + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$AccountSettingsImplCopyWith<_$AccountSettingsImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/acct.dart b/lib/model/acct.dart index e904d1015..e0d3a5a96 100644 --- a/lib/model/acct.dart +++ b/lib/model/acct.dart @@ -1,7 +1,7 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; -part 'acct.freezed.dart'; -part 'acct.g.dart'; +part "acct.freezed.dart"; +part "acct.g.dart"; @freezed class Acct with _$Acct { diff --git a/lib/model/acct.freezed.dart b/lib/model/acct.freezed.dart index 7f6164a26..8c39b645f 100644 --- a/lib/model/acct.freezed.dart +++ b/lib/model/acct.freezed.dart @@ -12,7 +12,7 @@ part of 'acct.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); Acct _$AcctFromJson(Map json) { return _Acct.fromJson(json); @@ -23,8 +23,12 @@ mixin _$Acct { String get host => throw _privateConstructorUsedError; String get username => throw _privateConstructorUsedError; + /// Serializes this Acct to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of Acct + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $AcctCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -46,6 +50,8 @@ class _$AcctCopyWithImpl<$Res, $Val extends Acct> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of Acct + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -82,6 +88,8 @@ class __$$AcctImplCopyWithImpl<$Res> __$$AcctImplCopyWithImpl(_$AcctImpl _value, $Res Function(_$AcctImpl) _then) : super(_value, _then); + /// Create a copy of Acct + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -124,11 +132,13 @@ class _$AcctImpl extends _Acct { other.username == username)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, host, username); - @JsonKey(ignore: true) + /// Create a copy of Acct + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$AcctImplCopyWith<_$AcctImpl> get copyWith => @@ -154,8 +164,11 @@ abstract class _Acct extends Acct { String get host; @override String get username; + + /// Create a copy of Acct + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$AcctImplCopyWith<_$AcctImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/antenna_settings.dart b/lib/model/antenna_settings.dart index 8cdb3ef5a..c36e1327a 100644 --- a/lib/model/antenna_settings.dart +++ b/lib/model/antenna_settings.dart @@ -1,7 +1,7 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:misskey_dart/misskey_dart.dart"; -part 'antenna_settings.freezed.dart'; +part "antenna_settings.freezed.dart"; @freezed class AntennaSettings with _$AntennaSettings { diff --git a/lib/model/antenna_settings.freezed.dart b/lib/model/antenna_settings.freezed.dart index e2b9ec1de..e5cd4ec7a 100644 --- a/lib/model/antenna_settings.freezed.dart +++ b/lib/model/antenna_settings.freezed.dart @@ -12,7 +12,7 @@ part of 'antenna_settings.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); /// @nodoc mixin _$AntennaSettings { @@ -28,7 +28,9 @@ mixin _$AntennaSettings { bool get notify => throw _privateConstructorUsedError; bool get localOnly => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of AntennaSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $AntennaSettingsCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -63,6 +65,8 @@ class _$AntennaSettingsCopyWithImpl<$Res, $Val extends AntennaSettings> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of AntennaSettings + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -157,6 +161,8 @@ class __$$AntennaSettingsImplCopyWithImpl<$Res> _$AntennaSettingsImpl _value, $Res Function(_$AntennaSettingsImpl) _then) : super(_value, _then); + /// Create a copy of AntennaSettings + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -336,7 +342,9 @@ class _$AntennaSettingsImpl extends _AntennaSettings { notify, localOnly); - @JsonKey(ignore: true) + /// Create a copy of AntennaSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$AntennaSettingsImplCopyWith<_$AntennaSettingsImpl> get copyWith => @@ -381,8 +389,11 @@ abstract class _AntennaSettings extends AntennaSettings { bool get notify; @override bool get localOnly; + + /// Create a copy of AntennaSettings + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$AntennaSettingsImplCopyWith<_$AntennaSettingsImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/clip_settings.dart b/lib/model/clip_settings.dart index dcba4f988..a04260a34 100644 --- a/lib/model/clip_settings.dart +++ b/lib/model/clip_settings.dart @@ -1,7 +1,7 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:misskey_dart/misskey_dart.dart"; -part 'clip_settings.freezed.dart'; +part "clip_settings.freezed.dart"; @freezed class ClipSettings with _$ClipSettings { diff --git a/lib/model/clip_settings.freezed.dart b/lib/model/clip_settings.freezed.dart index 0701ec4fd..59aa03af1 100644 --- a/lib/model/clip_settings.freezed.dart +++ b/lib/model/clip_settings.freezed.dart @@ -12,7 +12,7 @@ part of 'clip_settings.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); /// @nodoc mixin _$ClipSettings { @@ -20,7 +20,9 @@ mixin _$ClipSettings { String? get description => throw _privateConstructorUsedError; bool get isPublic => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of ClipSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $ClipSettingsCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -44,6 +46,8 @@ class _$ClipSettingsCopyWithImpl<$Res, $Val extends ClipSettings> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of ClipSettings + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -87,6 +91,8 @@ class __$$ClipSettingsImplCopyWithImpl<$Res> _$ClipSettingsImpl _value, $Res Function(_$ClipSettingsImpl) _then) : super(_value, _then); + /// Create a copy of ClipSettings + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -147,7 +153,9 @@ class _$ClipSettingsImpl extends _ClipSettings { @override int get hashCode => Object.hash(runtimeType, name, description, isPublic); - @JsonKey(ignore: true) + /// Create a copy of ClipSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ClipSettingsImplCopyWith<_$ClipSettingsImpl> get copyWith => @@ -167,8 +175,11 @@ abstract class _ClipSettings extends ClipSettings { String? get description; @override bool get isPublic; + + /// Create a copy of ClipSettings + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$ClipSettingsImplCopyWith<_$ClipSettingsImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/color_theme.dart b/lib/model/color_theme.dart index 9a1dd919b..a5dccf2f7 100644 --- a/lib/model/color_theme.dart +++ b/lib/model/color_theme.dart @@ -1,11 +1,11 @@ -import 'dart:ui'; +import "dart:ui"; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:miria/extensions/color_extension.dart'; -import 'package:miria/extensions/string_extensions.dart'; -import 'package:miria/model/misskey_theme.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:miria/extensions/color_extension.dart"; +import "package:miria/extensions/string_extensions.dart"; +import "package:miria/model/misskey_theme.dart"; -part 'color_theme.freezed.dart'; +part "color_theme.freezed.dart"; @freezed class ColorTheme with _$ColorTheme { @@ -34,12 +34,11 @@ class ColorTheme with _$ColorTheme { factory ColorTheme.misskey(MisskeyTheme theme) { final isDarkTheme = theme.base == "dark"; final props = { - ...isDarkTheme ? defaultDarkThemeProps : defaultLightThemeProps - }; - props.addAll(theme.props); - props - .cast() - .removeWhere((key, value) => value.startsWith('"')); + ...isDarkTheme ? defaultDarkThemeProps : defaultLightThemeProps, + } + ..addAll(theme.props) + ..cast() + .removeWhere((key, value) => value.startsWith('"')); // https://github.com/misskey-dev/misskey/blob/13.14.1/packages/frontend/src/scripts/theme.ts#L98-L124 Color getColor(String val) { @@ -68,7 +67,7 @@ class ColorTheme with _$ColorTheme { if (input.startsWith("rgb(") && input.endsWith(")")) { final rgb = input .substring(4, input.length - 1) - .split(RegExp(r"[, ]+")) + .split(RegExp("[, ]+")) .map(int.parse) .toList(); return Color.fromRGBO(rgb[0], rgb[1], rgb[2], 1); @@ -119,182 +118,182 @@ class ColorTheme with _$ColorTheme { // misskey/packages/frontend/src/themes/_light.json5 const defaultLightThemeProps = { - "accent": '#86b300', - "accentDarken": ':darken<10<@accent', - "accentLighten": ':lighten<10<@accent', - "accentedBg": ':alpha<0.15<@accent', - "focus": ':alpha<0.3<@accent', - "bg": '#fff', - "acrylicBg": ':alpha<0.5<@bg', - "fg": '#5f5f5f', - "fgTransparentWeak": ':alpha<0.75<@fg', - "fgTransparent": ':alpha<0.5<@fg', - "fgHighlighted": ':darken<3<@fg', - "fgOnAccent": '#fff', - "fgOnWhite": '#333', - "divider": 'rgba(0, 0, 0, 0.1)', - "indicator": '@accent', - "panel": ':lighten<3<@bg', - "panelHighlight": ':darken<3<@panel', - "panelHeaderBg": ':lighten<3<@panel', - "panelHeaderFg": '@fg', - "panelHeaderDivider": 'rgba(0, 0, 0, 0)', + "accent": "#86b300", + "accentDarken": ":darken<10<@accent", + "accentLighten": ":lighten<10<@accent", + "accentedBg": ":alpha<0.15<@accent", + "focus": ":alpha<0.3<@accent", + "bg": "#fff", + "acrylicBg": ":alpha<0.5<@bg", + "fg": "#5f5f5f", + "fgTransparentWeak": ":alpha<0.75<@fg", + "fgTransparent": ":alpha<0.5<@fg", + "fgHighlighted": ":darken<3<@fg", + "fgOnAccent": "#fff", + "fgOnWhite": "#333", + "divider": "rgba(0, 0, 0, 0.1)", + "indicator": "@accent", + "panel": ":lighten<3<@bg", + "panelHighlight": ":darken<3<@panel", + "panelHeaderBg": ":lighten<3<@panel", + "panelHeaderFg": "@fg", + "panelHeaderDivider": "rgba(0, 0, 0, 0)", "panelBorder": '" solid 1px var(--divider)', - "acrylicPanel": ':alpha<0.5<@panel', - "windowHeader": ':alpha<0.85<@panel', - "popup": ':lighten<3<@panel', - "shadow": 'rgba(0, 0, 0, 0.1)', - "header": ':alpha<0.7<@panel', - "navBg": '@panel', - "navFg": '@fg', - "navHoverFg": ':darken<17<@fg', - "navActive": '@accent', - "navIndicator": '@indicator', - "link": '#44a4c1', - "hashtag": '#ff9156', - "mention": '@accent', - "mentionMe": '@mention', - "renote": '#229e82', - "modalBg": 'rgba(0, 0, 0, 0.3)', - "scrollbarHandle": 'rgba(0, 0, 0, 0.2)', - "scrollbarHandleHover": 'rgba(0, 0, 0, 0.4)', - "dateLabelFg": '@fg', - "infoBg": '#e5f5ff', - "infoFg": '#72818a', - "infoWarnBg": '#fff0db', - "infoWarnFg": '#8f6e31', - "switchBg": 'rgba(0, 0, 0, 0.15)', - "cwBg": '#b1b9c1', - "cwFg": '#fff', - "cwHoverBg": '#bbc4ce', - "buttonBg": 'rgba(0, 0, 0, 0.05)', - "buttonHoverBg": 'rgba(0, 0, 0, 0.1)', - "buttonGradateA": '@accent', - "buttonGradateB": ':hue<20<@accent', - "switchOffBg": 'rgba(0, 0, 0, 0.1)', - "switchOffFg": '@panel', - "switchOnBg": '@accent', - "switchOnFg": '@fgOnAccent', - "inputBorder": 'rgba(0, 0, 0, 0.1)', - "inputBorderHover": 'rgba(0, 0, 0, 0.2)', - "listItemHoverBg": 'rgba(0, 0, 0, 0.03)', - "driveFolderBg": ':alpha<0.3<@accent', - "wallpaperOverlay": 'rgba(255, 255, 255, 0.5)', - "badge": '#31b1ce', - "messageBg": '@bg', - "success": '#86b300', - "error": '#ec4137', - "warn": '#ecb637', - "codeString": '#b98710', - "codeNumber": '#0fbbbb', - "codeBoolean": '#62b70c', - "deckBg": ':darken<3<@bg', - "htmlThemeColor": '@bg', - "X2": ':darken<2<@panel', - "X3": 'rgba(0, 0, 0, 0.05)', - "X4": 'rgba(0, 0, 0, 0.1)', - "X5": 'rgba(0, 0, 0, 0.05)', - "X6": 'rgba(0, 0, 0, 0.25)', - "X7": 'rgba(0, 0, 0, 0.05)', - "X8": ':lighten<5<@accent', - "X9": ':darken<5<@accent', - "X10": ':alpha<0.4<@accent', - "X11": 'rgba(0, 0, 0, 0.1)', - "X12": 'rgba(0, 0, 0, 0.1)', - "X13": 'rgba(0, 0, 0, 0.15)', - "X14": ':alpha<0.5<@navBg', - "X15": ':alpha<0<@panel', - "X16": ':alpha<0.7<@panel', - "X17": ':alpha<0.8<@bg', + "acrylicPanel": ":alpha<0.5<@panel", + "windowHeader": ":alpha<0.85<@panel", + "popup": ":lighten<3<@panel", + "shadow": "rgba(0, 0, 0, 0.1)", + "header": ":alpha<0.7<@panel", + "navBg": "@panel", + "navFg": "@fg", + "navHoverFg": ":darken<17<@fg", + "navActive": "@accent", + "navIndicator": "@indicator", + "link": "#44a4c1", + "hashtag": "#ff9156", + "mention": "@accent", + "mentionMe": "@mention", + "renote": "#229e82", + "modalBg": "rgba(0, 0, 0, 0.3)", + "scrollbarHandle": "rgba(0, 0, 0, 0.2)", + "scrollbarHandleHover": "rgba(0, 0, 0, 0.4)", + "dateLabelFg": "@fg", + "infoBg": "#e5f5ff", + "infoFg": "#72818a", + "infoWarnBg": "#fff0db", + "infoWarnFg": "#8f6e31", + "switchBg": "rgba(0, 0, 0, 0.15)", + "cwBg": "#b1b9c1", + "cwFg": "#fff", + "cwHoverBg": "#bbc4ce", + "buttonBg": "rgba(0, 0, 0, 0.05)", + "buttonHoverBg": "rgba(0, 0, 0, 0.1)", + "buttonGradateA": "@accent", + "buttonGradateB": ":hue<20<@accent", + "switchOffBg": "rgba(0, 0, 0, 0.1)", + "switchOffFg": "@panel", + "switchOnBg": "@accent", + "switchOnFg": "@fgOnAccent", + "inputBorder": "rgba(0, 0, 0, 0.1)", + "inputBorderHover": "rgba(0, 0, 0, 0.2)", + "listItemHoverBg": "rgba(0, 0, 0, 0.03)", + "driveFolderBg": ":alpha<0.3<@accent", + "wallpaperOverlay": "rgba(255, 255, 255, 0.5)", + "badge": "#31b1ce", + "messageBg": "@bg", + "success": "#86b300", + "error": "#ec4137", + "warn": "#ecb637", + "codeString": "#b98710", + "codeNumber": "#0fbbbb", + "codeBoolean": "#62b70c", + "deckBg": ":darken<3<@bg", + "htmlThemeColor": "@bg", + "X2": ":darken<2<@panel", + "X3": "rgba(0, 0, 0, 0.05)", + "X4": "rgba(0, 0, 0, 0.1)", + "X5": "rgba(0, 0, 0, 0.05)", + "X6": "rgba(0, 0, 0, 0.25)", + "X7": "rgba(0, 0, 0, 0.05)", + "X8": ":lighten<5<@accent", + "X9": ":darken<5<@accent", + "X10": ":alpha<0.4<@accent", + "X11": "rgba(0, 0, 0, 0.1)", + "X12": "rgba(0, 0, 0, 0.1)", + "X13": "rgba(0, 0, 0, 0.15)", + "X14": ":alpha<0.5<@navBg", + "X15": ":alpha<0<@panel", + "X16": ":alpha<0.7<@panel", + "X17": ":alpha<0.8<@bg", }; // misskey/packages/frontend/src/themes/_dark.json5 const defaultDarkThemeProps = { - "accent": '#86b300', - "accentDarken": ':darken<10<@accent', - "accentLighten": ':lighten<10<@accent', - "accentedBg": ':alpha<0.15<@accent', - "focus": ':alpha<0.3<@accent', - "bg": '#000', - "acrylicBg": ':alpha<0.5<@bg', - "fg": '#dadada', - "fgTransparentWeak": ':alpha<0.75<@fg', - "fgTransparent": ':alpha<0.5<@fg', - "fgHighlighted": ':lighten<3<@fg', - "fgOnAccent": '#fff', - "fgOnWhite": '#333', - "divider": 'rgba(255, 255, 255, 0.1)', - "indicator": '@accent', - "panel": ':lighten<3<@bg', - "panelHighlight": ':lighten<3<@panel', - "panelHeaderBg": ':lighten<3<@panel', - "panelHeaderFg": '@fg', - "panelHeaderDivider": 'rgba(0, 0, 0, 0)', + "accent": "#86b300", + "accentDarken": ":darken<10<@accent", + "accentLighten": ":lighten<10<@accent", + "accentedBg": ":alpha<0.15<@accent", + "focus": ":alpha<0.3<@accent", + "bg": "#000", + "acrylicBg": ":alpha<0.5<@bg", + "fg": "#dadada", + "fgTransparentWeak": ":alpha<0.75<@fg", + "fgTransparent": ":alpha<0.5<@fg", + "fgHighlighted": ":lighten<3<@fg", + "fgOnAccent": "#fff", + "fgOnWhite": "#333", + "divider": "rgba(255, 255, 255, 0.1)", + "indicator": "@accent", + "panel": ":lighten<3<@bg", + "panelHighlight": ":lighten<3<@panel", + "panelHeaderBg": ":lighten<3<@panel", + "panelHeaderFg": "@fg", + "panelHeaderDivider": "rgba(0, 0, 0, 0)", "panelBorder": '" solid 1px var(--divider)', - "acrylicPanel": ':alpha<0.5<@panel', - "windowHeader": ':alpha<0.85<@panel', - "popup": ':lighten<3<@panel', - "shadow": 'rgba(0, 0, 0, 0.3)', - "header": ':alpha<0.7<@panel', - "navBg": '@panel', - "navFg": '@fg', - "navHoverFg": ':lighten<17<@fg', - "navActive": '@accent', - "navIndicator": '@indicator', - "link": '#44a4c1', - "hashtag": '#ff9156', - "mention": '@accent', - "mentionMe": '@mention', - "renote": '#229e82', - "modalBg": 'rgba(0, 0, 0, 0.5)', - "scrollbarHandle": 'rgba(255, 255, 255, 0.2)', - "scrollbarHandleHover": 'rgba(255, 255, 255, 0.4)', - "dateLabelFg": '@fg', - "infoBg": '#253142', - "infoFg": '#fff', - "infoWarnBg": '#42321c', - "infoWarnFg": '#ffbd3e', - "switchBg": 'rgba(255, 255, 255, 0.15)', - "cwBg": '#687390', - "cwFg": '#393f4f', - "cwHoverBg": '#707b97', - "buttonBg": 'rgba(255, 255, 255, 0.05)', - "buttonHoverBg": 'rgba(255, 255, 255, 0.1)', - "buttonGradateA": '@accent', - "buttonGradateB": ':hue<20<@accent', - "switchOffBg": 'rgba(255, 255, 255, 0.1)', - "switchOffFg": ':alpha<0.8<@fg', - "switchOnBg": '@accentedBg', - "switchOnFg": '@accent', - "inputBorder": 'rgba(255, 255, 255, 0.1)', - "inputBorderHover": 'rgba(255, 255, 255, 0.2)', - "listItemHoverBg": 'rgba(255, 255, 255, 0.03)', - "driveFolderBg": ':alpha<0.3<@accent', - "wallpaperOverlay": 'rgba(0, 0, 0, 0.5)', - "badge": '#31b1ce', - "messageBg": '@bg', - "success": '#86b300', - "error": '#ec4137', - "warn": '#ecb637', - "codeString": '#ffb675', - "codeNumber": '#cfff9e', - "codeBoolean": '#c59eff', - "deckBg": '#000', - "htmlThemeColor": '@bg', - "X2": ':darken<2<@panel', - "X3": 'rgba(255, 255, 255, 0.05)', - "X4": 'rgba(255, 255, 255, 0.1)', - "X5": 'rgba(255, 255, 255, 0.05)', - "X6": 'rgba(255, 255, 255, 0.15)', - "X7": 'rgba(255, 255, 255, 0.05)', - "X8": ':lighten<5<@accent', - "X9": ':darken<5<@accent', - "X10": ':alpha<0.4<@accent', - "X11": 'rgba(0, 0, 0, 0.3)', - "X12": 'rgba(255, 255, 255, 0.1)', - "X13": 'rgba(255, 255, 255, 0.15)', - "X14": ':alpha<0.5<@navBg', - "X15": ':alpha<0<@panel', - "X16": ':alpha<0.7<@panel', - "X17": ':alpha<0.8<@bg', + "acrylicPanel": ":alpha<0.5<@panel", + "windowHeader": ":alpha<0.85<@panel", + "popup": ":lighten<3<@panel", + "shadow": "rgba(0, 0, 0, 0.3)", + "header": ":alpha<0.7<@panel", + "navBg": "@panel", + "navFg": "@fg", + "navHoverFg": ":lighten<17<@fg", + "navActive": "@accent", + "navIndicator": "@indicator", + "link": "#44a4c1", + "hashtag": "#ff9156", + "mention": "@accent", + "mentionMe": "@mention", + "renote": "#229e82", + "modalBg": "rgba(0, 0, 0, 0.5)", + "scrollbarHandle": "rgba(255, 255, 255, 0.2)", + "scrollbarHandleHover": "rgba(255, 255, 255, 0.4)", + "dateLabelFg": "@fg", + "infoBg": "#253142", + "infoFg": "#fff", + "infoWarnBg": "#42321c", + "infoWarnFg": "#ffbd3e", + "switchBg": "rgba(255, 255, 255, 0.15)", + "cwBg": "#687390", + "cwFg": "#393f4f", + "cwHoverBg": "#707b97", + "buttonBg": "rgba(255, 255, 255, 0.05)", + "buttonHoverBg": "rgba(255, 255, 255, 0.1)", + "buttonGradateA": "@accent", + "buttonGradateB": ":hue<20<@accent", + "switchOffBg": "rgba(255, 255, 255, 0.1)", + "switchOffFg": ":alpha<0.8<@fg", + "switchOnBg": "@accentedBg", + "switchOnFg": "@accent", + "inputBorder": "rgba(255, 255, 255, 0.1)", + "inputBorderHover": "rgba(255, 255, 255, 0.2)", + "listItemHoverBg": "rgba(255, 255, 255, 0.03)", + "driveFolderBg": ":alpha<0.3<@accent", + "wallpaperOverlay": "rgba(0, 0, 0, 0.5)", + "badge": "#31b1ce", + "messageBg": "@bg", + "success": "#86b300", + "error": "#ec4137", + "warn": "#ecb637", + "codeString": "#ffb675", + "codeNumber": "#cfff9e", + "codeBoolean": "#c59eff", + "deckBg": "#000", + "htmlThemeColor": "@bg", + "X2": ":darken<2<@panel", + "X3": "rgba(255, 255, 255, 0.05)", + "X4": "rgba(255, 255, 255, 0.1)", + "X5": "rgba(255, 255, 255, 0.05)", + "X6": "rgba(255, 255, 255, 0.15)", + "X7": "rgba(255, 255, 255, 0.05)", + "X8": ":lighten<5<@accent", + "X9": ":darken<5<@accent", + "X10": ":alpha<0.4<@accent", + "X11": "rgba(0, 0, 0, 0.3)", + "X12": "rgba(255, 255, 255, 0.1)", + "X13": "rgba(255, 255, 255, 0.15)", + "X14": ":alpha<0.5<@navBg", + "X15": ":alpha<0<@panel", + "X16": ":alpha<0.7<@panel", + "X17": ":alpha<0.8<@bg", }; diff --git a/lib/model/color_theme.freezed.dart b/lib/model/color_theme.freezed.dart index c4762888a..6f1f97cea 100644 --- a/lib/model/color_theme.freezed.dart +++ b/lib/model/color_theme.freezed.dart @@ -12,7 +12,7 @@ part of 'color_theme.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); /// @nodoc mixin _$ColorTheme { @@ -36,7 +36,9 @@ mixin _$ColorTheme { Color get panel => throw _privateConstructorUsedError; Color get panelBackground => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of ColorTheme + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $ColorThemeCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -79,6 +81,8 @@ class _$ColorThemeCopyWithImpl<$Res, $Val extends ColorTheme> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of ColorTheme + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -221,6 +225,8 @@ class __$$ColorThemeImplCopyWithImpl<$Res> _$ColorThemeImpl _value, $Res Function(_$ColorThemeImpl) _then) : super(_value, _then); + /// Create a copy of ColorTheme + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -453,7 +459,9 @@ class _$ColorThemeImpl implements _ColorTheme { panelBackground ]); - @JsonKey(ignore: true) + /// Create a copy of ColorTheme + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ColorThemeImplCopyWith<_$ColorThemeImpl> get copyWith => @@ -520,8 +528,11 @@ abstract class _ColorTheme implements ColorTheme { Color get panel; @override Color get panelBackground; + + /// Create a copy of ColorTheme + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$ColorThemeImplCopyWith<_$ColorThemeImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/converters/icon_converter.dart b/lib/model/converters/icon_converter.dart index c5825eef9..1794dcf92 100644 --- a/lib/model/converters/icon_converter.dart +++ b/lib/model/converters/icon_converter.dart @@ -1,5 +1,5 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:miria/model/tab_icon.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:miria/model/tab_icon.dart"; class IconDataConverter extends JsonConverter { const IconDataConverter(); diff --git a/lib/model/desktop_settings.dart b/lib/model/desktop_settings.dart index cf2c97596..8a526d71c 100644 --- a/lib/model/desktop_settings.dart +++ b/lib/model/desktop_settings.dart @@ -1,8 +1,7 @@ -import 'package:flutter/material.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; -part 'desktop_settings.freezed.dart'; -part 'desktop_settings.g.dart'; +part "desktop_settings.freezed.dart"; +part "desktop_settings.g.dart"; @freezed class DesktopSettings with _$DesktopSettings { diff --git a/lib/model/desktop_settings.freezed.dart b/lib/model/desktop_settings.freezed.dart index dd3e9744c..5402dd66c 100644 --- a/lib/model/desktop_settings.freezed.dart +++ b/lib/model/desktop_settings.freezed.dart @@ -12,7 +12,7 @@ part of 'desktop_settings.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); DesktopSettings _$DesktopSettingsFromJson(Map json) { return _DesktopSettings.fromJson(json); @@ -22,8 +22,12 @@ DesktopSettings _$DesktopSettingsFromJson(Map json) { mixin _$DesktopSettings { DesktopWindowSettings get window => throw _privateConstructorUsedError; + /// Serializes this DesktopSettings to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of DesktopSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $DesktopSettingsCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -49,6 +53,8 @@ class _$DesktopSettingsCopyWithImpl<$Res, $Val extends DesktopSettings> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of DesktopSettings + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -62,6 +68,8 @@ class _$DesktopSettingsCopyWithImpl<$Res, $Val extends DesktopSettings> ) as $Val); } + /// Create a copy of DesktopSettings + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $DesktopWindowSettingsCopyWith<$Res> get window { @@ -93,6 +101,8 @@ class __$$DesktopSettingsImplCopyWithImpl<$Res> _$DesktopSettingsImpl _value, $Res Function(_$DesktopSettingsImpl) _then) : super(_value, _then); + /// Create a copy of DesktopSettings + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -132,11 +142,13 @@ class _$DesktopSettingsImpl implements _DesktopSettings { (identical(other.window, window) || other.window == window)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, window); - @JsonKey(ignore: true) + /// Create a copy of DesktopSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$DesktopSettingsImplCopyWith<_$DesktopSettingsImpl> get copyWith => @@ -160,8 +172,11 @@ abstract class _DesktopSettings implements DesktopSettings { @override DesktopWindowSettings get window; + + /// Create a copy of DesktopSettings + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$DesktopSettingsImplCopyWith<_$DesktopSettingsImpl> get copyWith => throw _privateConstructorUsedError; } @@ -178,8 +193,12 @@ mixin _$DesktopWindowSettings { double get w => throw _privateConstructorUsedError; double get h => throw _privateConstructorUsedError; + /// Serializes this DesktopWindowSettings to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of DesktopWindowSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $DesktopWindowSettingsCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -204,6 +223,8 @@ class _$DesktopWindowSettingsCopyWithImpl<$Res, // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of DesktopWindowSettings + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -254,6 +275,8 @@ class __$$DesktopWindowSettingsImplCopyWithImpl<$Res> $Res Function(_$DesktopWindowSettingsImpl) _then) : super(_value, _then); + /// Create a copy of DesktopWindowSettings + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -321,11 +344,13 @@ class _$DesktopWindowSettingsImpl implements _DesktopWindowSettings { (identical(other.h, h) || other.h == h)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, x, y, w, h); - @JsonKey(ignore: true) + /// Create a copy of DesktopWindowSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$DesktopWindowSettingsImplCopyWith<_$DesktopWindowSettingsImpl> @@ -358,8 +383,11 @@ abstract class _DesktopWindowSettings implements DesktopWindowSettings { double get w; @override double get h; + + /// Create a copy of DesktopWindowSettings + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$DesktopWindowSettingsImplCopyWith<_$DesktopWindowSettingsImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/exported_setting.dart b/lib/model/exported_setting.dart index fa7a3d449..1d62231bc 100644 --- a/lib/model/exported_setting.dart +++ b/lib/model/exported_setting.dart @@ -1,16 +1,16 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:miria/model/account_settings.dart'; -import 'package:miria/model/general_settings.dart'; -import 'package:miria/model/tab_setting.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:miria/model/account_settings.dart"; +import "package:miria/model/general_settings.dart"; +import "package:miria/model/tab_setting.dart"; -part 'exported_setting.freezed.dart'; -part 'exported_setting.g.dart'; +part "exported_setting.freezed.dart"; +part "exported_setting.g.dart"; @freezed class ExportedSetting with _$ExportedSetting { const factory ExportedSetting({ - @Default([]) List accountSettings, required GeneralSettings generalSettings, + @Default([]) List accountSettings, @Default([]) List tabSettings, }) = _ExportedSetting; diff --git a/lib/model/exported_setting.freezed.dart b/lib/model/exported_setting.freezed.dart index 973d9d05b..8cc47c74b 100644 --- a/lib/model/exported_setting.freezed.dart +++ b/lib/model/exported_setting.freezed.dart @@ -12,7 +12,7 @@ part of 'exported_setting.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); ExportedSetting _$ExportedSettingFromJson(Map json) { return _ExportedSetting.fromJson(json); @@ -20,13 +20,17 @@ ExportedSetting _$ExportedSettingFromJson(Map json) { /// @nodoc mixin _$ExportedSetting { + GeneralSettings get generalSettings => throw _privateConstructorUsedError; List get accountSettings => throw _privateConstructorUsedError; - GeneralSettings get generalSettings => throw _privateConstructorUsedError; List get tabSettings => throw _privateConstructorUsedError; + /// Serializes this ExportedSetting to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of ExportedSetting + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $ExportedSettingCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -38,8 +42,8 @@ abstract class $ExportedSettingCopyWith<$Res> { _$ExportedSettingCopyWithImpl<$Res, ExportedSetting>; @useResult $Res call( - {List accountSettings, - GeneralSettings generalSettings, + {GeneralSettings generalSettings, + List accountSettings, List tabSettings}); $GeneralSettingsCopyWith<$Res> get generalSettings; @@ -55,22 +59,24 @@ class _$ExportedSettingCopyWithImpl<$Res, $Val extends ExportedSetting> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of ExportedSetting + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ - Object? accountSettings = null, Object? generalSettings = null, + Object? accountSettings = null, Object? tabSettings = null, }) { return _then(_value.copyWith( - accountSettings: null == accountSettings - ? _value.accountSettings - : accountSettings // ignore: cast_nullable_to_non_nullable - as List, generalSettings: null == generalSettings ? _value.generalSettings : generalSettings // ignore: cast_nullable_to_non_nullable as GeneralSettings, + accountSettings: null == accountSettings + ? _value.accountSettings + : accountSettings // ignore: cast_nullable_to_non_nullable + as List, tabSettings: null == tabSettings ? _value.tabSettings : tabSettings // ignore: cast_nullable_to_non_nullable @@ -78,6 +84,8 @@ class _$ExportedSettingCopyWithImpl<$Res, $Val extends ExportedSetting> ) as $Val); } + /// Create a copy of ExportedSetting + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $GeneralSettingsCopyWith<$Res> get generalSettings { @@ -96,8 +104,8 @@ abstract class _$$ExportedSettingImplCopyWith<$Res> @override @useResult $Res call( - {List accountSettings, - GeneralSettings generalSettings, + {GeneralSettings generalSettings, + List accountSettings, List tabSettings}); @override @@ -112,22 +120,24 @@ class __$$ExportedSettingImplCopyWithImpl<$Res> _$ExportedSettingImpl _value, $Res Function(_$ExportedSettingImpl) _then) : super(_value, _then); + /// Create a copy of ExportedSetting + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ - Object? accountSettings = null, Object? generalSettings = null, + Object? accountSettings = null, Object? tabSettings = null, }) { return _then(_$ExportedSettingImpl( - accountSettings: null == accountSettings - ? _value._accountSettings - : accountSettings // ignore: cast_nullable_to_non_nullable - as List, generalSettings: null == generalSettings ? _value.generalSettings : generalSettings // ignore: cast_nullable_to_non_nullable as GeneralSettings, + accountSettings: null == accountSettings + ? _value._accountSettings + : accountSettings // ignore: cast_nullable_to_non_nullable + as List, tabSettings: null == tabSettings ? _value._tabSettings : tabSettings // ignore: cast_nullable_to_non_nullable @@ -140,8 +150,8 @@ class __$$ExportedSettingImplCopyWithImpl<$Res> @JsonSerializable() class _$ExportedSettingImpl implements _ExportedSetting { const _$ExportedSettingImpl( - {final List accountSettings = const [], - required this.generalSettings, + {required this.generalSettings, + final List accountSettings = const [], final List tabSettings = const []}) : _accountSettings = accountSettings, _tabSettings = tabSettings; @@ -149,6 +159,8 @@ class _$ExportedSettingImpl implements _ExportedSetting { factory _$ExportedSettingImpl.fromJson(Map json) => _$$ExportedSettingImplFromJson(json); + @override + final GeneralSettings generalSettings; final List _accountSettings; @override @JsonKey() @@ -158,8 +170,6 @@ class _$ExportedSettingImpl implements _ExportedSetting { return EqualUnmodifiableListView(_accountSettings); } - @override - final GeneralSettings generalSettings; final List _tabSettings; @override @JsonKey() @@ -171,7 +181,7 @@ class _$ExportedSettingImpl implements _ExportedSetting { @override String toString() { - return 'ExportedSetting(accountSettings: $accountSettings, generalSettings: $generalSettings, tabSettings: $tabSettings)'; + return 'ExportedSetting(generalSettings: $generalSettings, accountSettings: $accountSettings, tabSettings: $tabSettings)'; } @override @@ -179,23 +189,25 @@ class _$ExportedSettingImpl implements _ExportedSetting { return identical(this, other) || (other.runtimeType == runtimeType && other is _$ExportedSettingImpl && - const DeepCollectionEquality() - .equals(other._accountSettings, _accountSettings) && (identical(other.generalSettings, generalSettings) || other.generalSettings == generalSettings) && + const DeepCollectionEquality() + .equals(other._accountSettings, _accountSettings) && const DeepCollectionEquality() .equals(other._tabSettings, _tabSettings)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, - const DeepCollectionEquality().hash(_accountSettings), generalSettings, + const DeepCollectionEquality().hash(_accountSettings), const DeepCollectionEquality().hash(_tabSettings)); - @JsonKey(ignore: true) + /// Create a copy of ExportedSetting + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ExportedSettingImplCopyWith<_$ExportedSettingImpl> get copyWith => @@ -212,21 +224,24 @@ class _$ExportedSettingImpl implements _ExportedSetting { abstract class _ExportedSetting implements ExportedSetting { const factory _ExportedSetting( - {final List accountSettings, - required final GeneralSettings generalSettings, + {required final GeneralSettings generalSettings, + final List accountSettings, final List tabSettings}) = _$ExportedSettingImpl; factory _ExportedSetting.fromJson(Map json) = _$ExportedSettingImpl.fromJson; - @override - List get accountSettings; @override GeneralSettings get generalSettings; @override + List get accountSettings; + @override List get tabSettings; + + /// Create a copy of ExportedSetting + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$ExportedSettingImplCopyWith<_$ExportedSettingImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/exported_setting.g.dart b/lib/model/exported_setting.g.dart index d75508041..4b1abfcd4 100644 --- a/lib/model/exported_setting.g.dart +++ b/lib/model/exported_setting.g.dart @@ -9,12 +9,12 @@ part of 'exported_setting.dart'; _$ExportedSettingImpl _$$ExportedSettingImplFromJson( Map json) => _$ExportedSettingImpl( + generalSettings: GeneralSettings.fromJson( + json['generalSettings'] as Map), accountSettings: (json['accountSettings'] as List?) ?.map((e) => AccountSettings.fromJson(e as Map)) .toList() ?? const [], - generalSettings: GeneralSettings.fromJson( - json['generalSettings'] as Map), tabSettings: (json['tabSettings'] as List?) ?.map((e) => TabSetting.fromJson(e as Map)) .toList() ?? @@ -24,8 +24,8 @@ _$ExportedSettingImpl _$$ExportedSettingImplFromJson( Map _$$ExportedSettingImplToJson( _$ExportedSettingImpl instance) => { + 'generalSettings': instance.generalSettings.toJson(), 'accountSettings': instance.accountSettings.map((e) => e.toJson()).toList(), - 'generalSettings': instance.generalSettings.toJson(), 'tabSettings': instance.tabSettings.map((e) => e.toJson()).toList(), }; diff --git a/lib/model/federation_data.dart b/lib/model/federation_data.dart index 83781834f..37ea376aa 100644 --- a/lib/model/federation_data.dart +++ b/lib/model/federation_data.dart @@ -1,11 +1,21 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "dart:async"; -part 'federation_data.freezed.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:miria/log.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +part "federation_data.freezed.dart"; +part "federation_data.g.dart"; @freezed class FederationData with _$FederationData { const factory FederationData({ + required bool isSupportedEmoji, + required bool isSupportedAnnouncement, + required bool isSupportedLocalTimeline, String? bannerUrl, String? faviconUrl, String? tosUrl, @@ -24,9 +34,143 @@ class FederationData with _$FederationData { @Default("") String softwareVersion, @Default([]) List languages, @Default([]) List ads, - required bool isSupportedEmoji, - required bool isSupportedAnnouncement, - required bool isSupportedLocalTimeline, MetaResponse? meta, }) = _FederationData; } + +@Riverpod(dependencies: [accountContext, misskeyGetContext]) +class FederationState extends _$FederationState { + @override + Future build(String host) async { + if (host == ref.read(accountContextProvider).getAccount.host) { + // 自分のサーバーの場合 + final metaResponse = await ref.read(misskeyGetContextProvider).meta(); + + // api/statsはリアクション数が十分多いサーバーでエラーを吐くことがある + StatsResponse? statsResponse; + try { + statsResponse = await ref.read(misskeyGetContextProvider).stats(); + } catch (e) { + logger.warning(e); + } + + unawaited( + ref + .read( + emojiRepositoryProvider(Account.demoAccount(host, metaResponse)), + ) + .loadFromSourceIfNeed(), + ); + return FederationData( + bannerUrl: metaResponse.bannerUrl?.toString(), + faviconUrl: metaResponse.iconUrl?.toString(), + tosUrl: metaResponse.tosUrl?.toString(), + privacyPolicyUrl: metaResponse.privacyPolicyUrl?.toString(), + impressumUrl: metaResponse.impressumUrl?.toString(), + repositoryUrl: metaResponse.repositoryUrl.toString(), + name: metaResponse.name ?? "", + description: metaResponse.description ?? "", + usersCount: statsResponse?.originalUsersCount, + notesCount: statsResponse?.originalNotesCount, + maintainerName: metaResponse.maintainerName, + maintainerEmail: metaResponse.maintainerEmail, + serverRules: metaResponse.serverRules, + reactionCount: statsResponse?.reactionsCount, + softwareName: "misskey", + softwareVersion: metaResponse.version, + languages: metaResponse.langs, + ads: metaResponse.ads, + meta: metaResponse, + + // 自分のサーバーが非対応ということはない + isSupportedAnnouncement: true, + isSupportedEmoji: true, + isSupportedLocalTimeline: true, + ); + } + final federation = await ref + .read(misskeyGetContextProvider) + .federation + .showInstance(FederationShowInstanceRequest(host: host)); + + final federateData = FederationData( + faviconUrl: federation.faviconUrl?.toString(), + isSupportedEmoji: false, + isSupportedAnnouncement: false, + isSupportedLocalTimeline: false, + name: federation.name, + usersCount: federation.usersCount, + notesCount: federation.notesCount, + softwareName: federation.softwareName ?? "", + ); + state = AsyncData(federateData); + + MetaResponse? misskeyMeta; + var isSupportedEmoji = false; + var isSupportedAnnouncement = false; + var isSupportedLocalTimeline = false; + + if (federation.softwareName == "fedibird" || + federation.softwareName == "mastodon") { + // already known unsupported software. + } else { + // Misskeyサーバーかもしれなかったら追加の情報を取得 + + try { + final misskeyServer = ref.read(misskeyWithoutAccountProvider(host)); + final (endpoints, meta) = + await (misskeyServer.endpoints(), misskeyServer.meta()).wait; + misskeyMeta = meta; + + if (endpoints.contains("announcement")) { + isSupportedAnnouncement = true; + } + + // 絵文字が取得できなければローカルタイムラインを含め非対応 + if (endpoints.contains("emojis")) { + isSupportedEmoji = true; + + if (endpoints.contains("notes/local-timeline")) { + isSupportedLocalTimeline = true; + } + + unawaited( + ref + .read( + emojiRepositoryProvider( + Account.demoAccount(host, misskeyMeta), + ), + ) + .loadFromSourceIfNeed(), + ); + } + } catch (e) { + logger.warning(e); + } + } + + return FederationData( + bannerUrl: misskeyMeta?.bannerUrl?.toString(), + faviconUrl: misskeyMeta?.iconUrl?.toString(), + tosUrl: misskeyMeta?.tosUrl?.toString(), + privacyPolicyUrl: misskeyMeta?.privacyPolicyUrl?.toString(), + impressumUrl: misskeyMeta?.impressumUrl?.toString(), + repositoryUrl: misskeyMeta?.repositoryUrl?.toString(), + name: misskeyMeta?.name ?? federation.name, + description: misskeyMeta?.description ?? federation.description ?? "", + maintainerName: misskeyMeta?.maintainerName, + maintainerEmail: misskeyMeta?.maintainerEmail, + softwareVersion: misskeyMeta?.version ?? federation.softwareVersion ?? "", + languages: misskeyMeta?.langs ?? [], + ads: misskeyMeta?.ads ?? [], + serverRules: misskeyMeta?.serverRules ?? [], + isSupportedEmoji: isSupportedEmoji, + isSupportedLocalTimeline: isSupportedLocalTimeline, + isSupportedAnnouncement: isSupportedAnnouncement, + softwareName: federation.softwareName ?? "", + usersCount: federation.usersCount, + notesCount: federation.notesCount, + meta: misskeyMeta, + ); + } +} diff --git a/lib/model/federation_data.freezed.dart b/lib/model/federation_data.freezed.dart index 8cf82979e..7ae59fa3d 100644 --- a/lib/model/federation_data.freezed.dart +++ b/lib/model/federation_data.freezed.dart @@ -12,10 +12,13 @@ part of 'federation_data.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); /// @nodoc mixin _$FederationData { + bool get isSupportedEmoji => throw _privateConstructorUsedError; + bool get isSupportedAnnouncement => throw _privateConstructorUsedError; + bool get isSupportedLocalTimeline => throw _privateConstructorUsedError; String? get bannerUrl => throw _privateConstructorUsedError; String? get faviconUrl => throw _privateConstructorUsedError; String? get tosUrl => throw _privateConstructorUsedError; @@ -34,12 +37,11 @@ mixin _$FederationData { String get softwareVersion => throw _privateConstructorUsedError; List get languages => throw _privateConstructorUsedError; List get ads => throw _privateConstructorUsedError; - bool get isSupportedEmoji => throw _privateConstructorUsedError; - bool get isSupportedAnnouncement => throw _privateConstructorUsedError; - bool get isSupportedLocalTimeline => throw _privateConstructorUsedError; MetaResponse? get meta => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of FederationData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $FederationDataCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -51,7 +53,10 @@ abstract class $FederationDataCopyWith<$Res> { _$FederationDataCopyWithImpl<$Res, FederationData>; @useResult $Res call( - {String? bannerUrl, + {bool isSupportedEmoji, + bool isSupportedAnnouncement, + bool isSupportedLocalTimeline, + String? bannerUrl, String? faviconUrl, String? tosUrl, String? privacyPolicyUrl, @@ -69,9 +74,6 @@ abstract class $FederationDataCopyWith<$Res> { String softwareVersion, List languages, List ads, - bool isSupportedEmoji, - bool isSupportedAnnouncement, - bool isSupportedLocalTimeline, MetaResponse? meta}); $MetaResponseCopyWith<$Res>? get meta; @@ -87,9 +89,14 @@ class _$FederationDataCopyWithImpl<$Res, $Val extends FederationData> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of FederationData + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ + Object? isSupportedEmoji = null, + Object? isSupportedAnnouncement = null, + Object? isSupportedLocalTimeline = null, Object? bannerUrl = freezed, Object? faviconUrl = freezed, Object? tosUrl = freezed, @@ -108,12 +115,21 @@ class _$FederationDataCopyWithImpl<$Res, $Val extends FederationData> Object? softwareVersion = null, Object? languages = null, Object? ads = null, - Object? isSupportedEmoji = null, - Object? isSupportedAnnouncement = null, - Object? isSupportedLocalTimeline = null, Object? meta = freezed, }) { return _then(_value.copyWith( + isSupportedEmoji: null == isSupportedEmoji + ? _value.isSupportedEmoji + : isSupportedEmoji // ignore: cast_nullable_to_non_nullable + as bool, + isSupportedAnnouncement: null == isSupportedAnnouncement + ? _value.isSupportedAnnouncement + : isSupportedAnnouncement // ignore: cast_nullable_to_non_nullable + as bool, + isSupportedLocalTimeline: null == isSupportedLocalTimeline + ? _value.isSupportedLocalTimeline + : isSupportedLocalTimeline // ignore: cast_nullable_to_non_nullable + as bool, bannerUrl: freezed == bannerUrl ? _value.bannerUrl : bannerUrl // ignore: cast_nullable_to_non_nullable @@ -186,18 +202,6 @@ class _$FederationDataCopyWithImpl<$Res, $Val extends FederationData> ? _value.ads : ads // ignore: cast_nullable_to_non_nullable as List, - isSupportedEmoji: null == isSupportedEmoji - ? _value.isSupportedEmoji - : isSupportedEmoji // ignore: cast_nullable_to_non_nullable - as bool, - isSupportedAnnouncement: null == isSupportedAnnouncement - ? _value.isSupportedAnnouncement - : isSupportedAnnouncement // ignore: cast_nullable_to_non_nullable - as bool, - isSupportedLocalTimeline: null == isSupportedLocalTimeline - ? _value.isSupportedLocalTimeline - : isSupportedLocalTimeline // ignore: cast_nullable_to_non_nullable - as bool, meta: freezed == meta ? _value.meta : meta // ignore: cast_nullable_to_non_nullable @@ -205,6 +209,8 @@ class _$FederationDataCopyWithImpl<$Res, $Val extends FederationData> ) as $Val); } + /// Create a copy of FederationData + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $MetaResponseCopyWith<$Res>? get meta { @@ -227,7 +233,10 @@ abstract class _$$FederationDataImplCopyWith<$Res> @override @useResult $Res call( - {String? bannerUrl, + {bool isSupportedEmoji, + bool isSupportedAnnouncement, + bool isSupportedLocalTimeline, + String? bannerUrl, String? faviconUrl, String? tosUrl, String? privacyPolicyUrl, @@ -245,9 +254,6 @@ abstract class _$$FederationDataImplCopyWith<$Res> String softwareVersion, List languages, List ads, - bool isSupportedEmoji, - bool isSupportedAnnouncement, - bool isSupportedLocalTimeline, MetaResponse? meta}); @override @@ -262,9 +268,14 @@ class __$$FederationDataImplCopyWithImpl<$Res> _$FederationDataImpl _value, $Res Function(_$FederationDataImpl) _then) : super(_value, _then); + /// Create a copy of FederationData + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ + Object? isSupportedEmoji = null, + Object? isSupportedAnnouncement = null, + Object? isSupportedLocalTimeline = null, Object? bannerUrl = freezed, Object? faviconUrl = freezed, Object? tosUrl = freezed, @@ -283,12 +294,21 @@ class __$$FederationDataImplCopyWithImpl<$Res> Object? softwareVersion = null, Object? languages = null, Object? ads = null, - Object? isSupportedEmoji = null, - Object? isSupportedAnnouncement = null, - Object? isSupportedLocalTimeline = null, Object? meta = freezed, }) { return _then(_$FederationDataImpl( + isSupportedEmoji: null == isSupportedEmoji + ? _value.isSupportedEmoji + : isSupportedEmoji // ignore: cast_nullable_to_non_nullable + as bool, + isSupportedAnnouncement: null == isSupportedAnnouncement + ? _value.isSupportedAnnouncement + : isSupportedAnnouncement // ignore: cast_nullable_to_non_nullable + as bool, + isSupportedLocalTimeline: null == isSupportedLocalTimeline + ? _value.isSupportedLocalTimeline + : isSupportedLocalTimeline // ignore: cast_nullable_to_non_nullable + as bool, bannerUrl: freezed == bannerUrl ? _value.bannerUrl : bannerUrl // ignore: cast_nullable_to_non_nullable @@ -361,18 +381,6 @@ class __$$FederationDataImplCopyWithImpl<$Res> ? _value._ads : ads // ignore: cast_nullable_to_non_nullable as List, - isSupportedEmoji: null == isSupportedEmoji - ? _value.isSupportedEmoji - : isSupportedEmoji // ignore: cast_nullable_to_non_nullable - as bool, - isSupportedAnnouncement: null == isSupportedAnnouncement - ? _value.isSupportedAnnouncement - : isSupportedAnnouncement // ignore: cast_nullable_to_non_nullable - as bool, - isSupportedLocalTimeline: null == isSupportedLocalTimeline - ? _value.isSupportedLocalTimeline - : isSupportedLocalTimeline // ignore: cast_nullable_to_non_nullable - as bool, meta: freezed == meta ? _value.meta : meta // ignore: cast_nullable_to_non_nullable @@ -385,7 +393,10 @@ class __$$FederationDataImplCopyWithImpl<$Res> class _$FederationDataImpl implements _FederationData { const _$FederationDataImpl( - {this.bannerUrl, + {required this.isSupportedEmoji, + required this.isSupportedAnnouncement, + required this.isSupportedLocalTimeline, + this.bannerUrl, this.faviconUrl, this.tosUrl, this.privacyPolicyUrl, @@ -403,14 +414,17 @@ class _$FederationDataImpl implements _FederationData { this.softwareVersion = "", final List languages = const [], final List ads = const [], - required this.isSupportedEmoji, - required this.isSupportedAnnouncement, - required this.isSupportedLocalTimeline, this.meta}) : _serverRules = serverRules, _languages = languages, _ads = ads; + @override + final bool isSupportedEmoji; + @override + final bool isSupportedAnnouncement; + @override + final bool isSupportedLocalTimeline; @override final String? bannerUrl; @override @@ -472,18 +486,12 @@ class _$FederationDataImpl implements _FederationData { return EqualUnmodifiableListView(_ads); } - @override - final bool isSupportedEmoji; - @override - final bool isSupportedAnnouncement; - @override - final bool isSupportedLocalTimeline; @override final MetaResponse? meta; @override String toString() { - return 'FederationData(bannerUrl: $bannerUrl, faviconUrl: $faviconUrl, tosUrl: $tosUrl, privacyPolicyUrl: $privacyPolicyUrl, impressumUrl: $impressumUrl, repositoryUrl: $repositoryUrl, serverRules: $serverRules, name: $name, description: $description, maintainerName: $maintainerName, maintainerEmail: $maintainerEmail, usersCount: $usersCount, notesCount: $notesCount, reactionCount: $reactionCount, softwareName: $softwareName, softwareVersion: $softwareVersion, languages: $languages, ads: $ads, isSupportedEmoji: $isSupportedEmoji, isSupportedAnnouncement: $isSupportedAnnouncement, isSupportedLocalTimeline: $isSupportedLocalTimeline, meta: $meta)'; + return 'FederationData(isSupportedEmoji: $isSupportedEmoji, isSupportedAnnouncement: $isSupportedAnnouncement, isSupportedLocalTimeline: $isSupportedLocalTimeline, bannerUrl: $bannerUrl, faviconUrl: $faviconUrl, tosUrl: $tosUrl, privacyPolicyUrl: $privacyPolicyUrl, impressumUrl: $impressumUrl, repositoryUrl: $repositoryUrl, serverRules: $serverRules, name: $name, description: $description, maintainerName: $maintainerName, maintainerEmail: $maintainerEmail, usersCount: $usersCount, notesCount: $notesCount, reactionCount: $reactionCount, softwareName: $softwareName, softwareVersion: $softwareVersion, languages: $languages, ads: $ads, meta: $meta)'; } @override @@ -491,6 +499,14 @@ class _$FederationDataImpl implements _FederationData { return identical(this, other) || (other.runtimeType == runtimeType && other is _$FederationDataImpl && + (identical(other.isSupportedEmoji, isSupportedEmoji) || + other.isSupportedEmoji == isSupportedEmoji) && + (identical( + other.isSupportedAnnouncement, isSupportedAnnouncement) || + other.isSupportedAnnouncement == isSupportedAnnouncement) && + (identical( + other.isSupportedLocalTimeline, isSupportedLocalTimeline) || + other.isSupportedLocalTimeline == isSupportedLocalTimeline) && (identical(other.bannerUrl, bannerUrl) || other.bannerUrl == bannerUrl) && (identical(other.faviconUrl, faviconUrl) || @@ -524,20 +540,15 @@ class _$FederationDataImpl implements _FederationData { const DeepCollectionEquality() .equals(other._languages, _languages) && const DeepCollectionEquality().equals(other._ads, _ads) && - (identical(other.isSupportedEmoji, isSupportedEmoji) || - other.isSupportedEmoji == isSupportedEmoji) && - (identical( - other.isSupportedAnnouncement, isSupportedAnnouncement) || - other.isSupportedAnnouncement == isSupportedAnnouncement) && - (identical( - other.isSupportedLocalTimeline, isSupportedLocalTimeline) || - other.isSupportedLocalTimeline == isSupportedLocalTimeline) && (identical(other.meta, meta) || other.meta == meta)); } @override int get hashCode => Object.hashAll([ runtimeType, + isSupportedEmoji, + isSupportedAnnouncement, + isSupportedLocalTimeline, bannerUrl, faviconUrl, tosUrl, @@ -556,13 +567,12 @@ class _$FederationDataImpl implements _FederationData { softwareVersion, const DeepCollectionEquality().hash(_languages), const DeepCollectionEquality().hash(_ads), - isSupportedEmoji, - isSupportedAnnouncement, - isSupportedLocalTimeline, meta ]); - @JsonKey(ignore: true) + /// Create a copy of FederationData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$FederationDataImplCopyWith<_$FederationDataImpl> get copyWith => @@ -572,7 +582,10 @@ class _$FederationDataImpl implements _FederationData { abstract class _FederationData implements FederationData { const factory _FederationData( - {final String? bannerUrl, + {required final bool isSupportedEmoji, + required final bool isSupportedAnnouncement, + required final bool isSupportedLocalTimeline, + final String? bannerUrl, final String? faviconUrl, final String? tosUrl, final String? privacyPolicyUrl, @@ -590,11 +603,14 @@ abstract class _FederationData implements FederationData { final String softwareVersion, final List languages, final List ads, - required final bool isSupportedEmoji, - required final bool isSupportedAnnouncement, - required final bool isSupportedLocalTimeline, final MetaResponse? meta}) = _$FederationDataImpl; + @override + bool get isSupportedEmoji; + @override + bool get isSupportedAnnouncement; + @override + bool get isSupportedLocalTimeline; @override String? get bannerUrl; @override @@ -632,15 +648,12 @@ abstract class _FederationData implements FederationData { @override List get ads; @override - bool get isSupportedEmoji; - @override - bool get isSupportedAnnouncement; - @override - bool get isSupportedLocalTimeline; - @override MetaResponse? get meta; + + /// Create a copy of FederationData + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$FederationDataImplCopyWith<_$FederationDataImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/federation_data.g.dart b/lib/model/federation_data.g.dart new file mode 100644 index 000000000..3db303d05 --- /dev/null +++ b/lib/model/federation_data.g.dart @@ -0,0 +1,225 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'federation_data.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$federationStateHash() => r'87a02d857e7326204a58ef410ad0bc068d959af7'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$FederationState + extends BuildlessAutoDisposeAsyncNotifier { + late final String host; + + FutureOr build( + String host, + ); +} + +/// See also [FederationState]. +@ProviderFor(FederationState) +const federationStateProvider = FederationStateFamily(); + +/// See also [FederationState]. +class FederationStateFamily extends Family { + /// See also [FederationState]. + const FederationStateFamily(); + + static final Iterable _dependencies = [ + accountContextProvider, + misskeyGetContextProvider + ]; + + static final Iterable _allTransitiveDependencies = + { + accountContextProvider, + ...?accountContextProvider.allTransitiveDependencies, + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies + }; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'federationStateProvider'; + + /// See also [FederationState]. + FederationStateProvider call( + String host, + ) { + return FederationStateProvider( + host, + ); + } + + @visibleForOverriding + @override + FederationStateProvider getProviderOverride( + covariant FederationStateProvider provider, + ) { + return call( + provider.host, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(FederationState Function() create) { + return _$FederationStateFamilyOverride(this, create); + } +} + +class _$FederationStateFamilyOverride implements FamilyOverride { + _$FederationStateFamilyOverride(this.overriddenFamily, this.create); + + final FederationState Function() create; + + @override + final FederationStateFamily overriddenFamily; + + @override + FederationStateProvider getProviderOverride( + covariant FederationStateProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [FederationState]. +class FederationStateProvider extends AutoDisposeAsyncNotifierProviderImpl< + FederationState, FederationData> { + /// See also [FederationState]. + FederationStateProvider( + String host, + ) : this._internal( + () => FederationState()..host = host, + from: federationStateProvider, + name: r'federationStateProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$federationStateHash, + dependencies: FederationStateFamily._dependencies, + allTransitiveDependencies: + FederationStateFamily._allTransitiveDependencies, + host: host, + ); + + FederationStateProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.host, + }) : super.internal(); + + final String host; + + @override + FutureOr runNotifierBuild( + covariant FederationState notifier, + ) { + return notifier.build( + host, + ); + } + + @override + Override overrideWith(FederationState Function() create) { + return ProviderOverride( + origin: this, + override: FederationStateProvider._internal( + () => create()..host = host, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + host: host, + ), + ); + } + + @override + (String,) get argument { + return (host,); + } + + @override + AutoDisposeAsyncNotifierProviderElement + createElement() { + return _FederationStateProviderElement(this); + } + + FederationStateProvider _copyWith( + FederationState Function() create, + ) { + return FederationStateProvider._internal( + () => create()..host = host, + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + host: host, + ); + } + + @override + bool operator ==(Object other) { + return other is FederationStateProvider && other.host == host; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, host.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin FederationStateRef + on AutoDisposeAsyncNotifierProviderRef { + /// The parameter `host` of this provider. + String get host; +} + +class _FederationStateProviderElement + extends AutoDisposeAsyncNotifierProviderElement with FederationStateRef { + _FederationStateProviderElement(super.provider); + + @override + String get host => (origin as FederationStateProvider).host; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/model/general_settings.dart b/lib/model/general_settings.dart index 017ad52d9..7d107869c 100644 --- a/lib/model/general_settings.dart +++ b/lib/model/general_settings.dart @@ -1,9 +1,9 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; -part 'general_settings.freezed.dart'; -part 'general_settings.g.dart'; +part "general_settings.freezed.dart"; +part "general_settings.g.dart"; enum ThemeColorSystem { forceLight, @@ -134,6 +134,9 @@ class GeneralSettings with _$GeneralSettings { /// 言語設定 @Default(Languages.jaJP) Languages languages, + + /// デッキモード + @Default(false) bool isDeckMode, }) = _GeneralSettings; factory GeneralSettings.fromJson(Map json) => diff --git a/lib/model/general_settings.freezed.dart b/lib/model/general_settings.freezed.dart index f66985924..62fb16ff0 100644 --- a/lib/model/general_settings.freezed.dart +++ b/lib/model/general_settings.freezed.dart @@ -12,7 +12,7 @@ part of 'general_settings.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); GeneralSettings _$GeneralSettingsFromJson(Map json) { return _GeneralSettings.fromJson(json); @@ -69,8 +69,15 @@ mixin _$GeneralSettings { /// 言語設定 Languages get languages => throw _privateConstructorUsedError; + /// デッキモード + bool get isDeckMode => throw _privateConstructorUsedError; + + /// Serializes this GeneralSettings to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of GeneralSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $GeneralSettingsCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -99,7 +106,8 @@ abstract class $GeneralSettingsCopyWith<$Res> { String monospaceFontName, String cursiveFontName, String fantasyFontName, - Languages languages}); + Languages languages, + bool isDeckMode}); } /// @nodoc @@ -112,6 +120,8 @@ class _$GeneralSettingsCopyWithImpl<$Res, $Val extends GeneralSettings> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of GeneralSettings + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -133,6 +143,7 @@ class _$GeneralSettingsCopyWithImpl<$Res, $Val extends GeneralSettings> Object? cursiveFontName = null, Object? fantasyFontName = null, Object? languages = null, + Object? isDeckMode = null, }) { return _then(_value.copyWith( lightColorThemeId: null == lightColorThemeId @@ -207,6 +218,10 @@ class _$GeneralSettingsCopyWithImpl<$Res, $Val extends GeneralSettings> ? _value.languages : languages // ignore: cast_nullable_to_non_nullable as Languages, + isDeckMode: null == isDeckMode + ? _value.isDeckMode + : isDeckMode // ignore: cast_nullable_to_non_nullable + as bool, ) as $Val); } } @@ -237,7 +252,8 @@ abstract class _$$GeneralSettingsImplCopyWith<$Res> String monospaceFontName, String cursiveFontName, String fantasyFontName, - Languages languages}); + Languages languages, + bool isDeckMode}); } /// @nodoc @@ -248,6 +264,8 @@ class __$$GeneralSettingsImplCopyWithImpl<$Res> _$GeneralSettingsImpl _value, $Res Function(_$GeneralSettingsImpl) _then) : super(_value, _then); + /// Create a copy of GeneralSettings + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -269,6 +287,7 @@ class __$$GeneralSettingsImplCopyWithImpl<$Res> Object? cursiveFontName = null, Object? fantasyFontName = null, Object? languages = null, + Object? isDeckMode = null, }) { return _then(_$GeneralSettingsImpl( lightColorThemeId: null == lightColorThemeId @@ -343,6 +362,10 @@ class __$$GeneralSettingsImplCopyWithImpl<$Res> ? _value.languages : languages // ignore: cast_nullable_to_non_nullable as Languages, + isDeckMode: null == isDeckMode + ? _value.isDeckMode + : isDeckMode // ignore: cast_nullable_to_non_nullable + as bool, )); } } @@ -368,7 +391,8 @@ class _$GeneralSettingsImpl implements _GeneralSettings { this.monospaceFontName = "", this.cursiveFontName = "", this.fantasyFontName = "", - this.languages = Languages.jaJP}); + this.languages = Languages.jaJP, + this.isDeckMode = false}); factory _$GeneralSettingsImpl.fromJson(Map json) => _$$GeneralSettingsImplFromJson(json); @@ -458,9 +482,14 @@ class _$GeneralSettingsImpl implements _GeneralSettings { @JsonKey() final Languages languages; + /// デッキモード + @override + @JsonKey() + final bool isDeckMode; + @override String toString() { - return 'GeneralSettings(lightColorThemeId: $lightColorThemeId, darkColorThemeId: $darkColorThemeId, themeColorSystem: $themeColorSystem, nsfwInherit: $nsfwInherit, enableDirectReaction: $enableDirectReaction, automaticPush: $automaticPush, enableAnimatedMFM: $enableAnimatedMFM, enableLongTextElipsed: $enableLongTextElipsed, enableFavoritedRenoteElipsed: $enableFavoritedRenoteElipsed, tabPosition: $tabPosition, textScaleFactor: $textScaleFactor, emojiType: $emojiType, defaultFontName: $defaultFontName, serifFontName: $serifFontName, monospaceFontName: $monospaceFontName, cursiveFontName: $cursiveFontName, fantasyFontName: $fantasyFontName, languages: $languages)'; + return 'GeneralSettings(lightColorThemeId: $lightColorThemeId, darkColorThemeId: $darkColorThemeId, themeColorSystem: $themeColorSystem, nsfwInherit: $nsfwInherit, enableDirectReaction: $enableDirectReaction, automaticPush: $automaticPush, enableAnimatedMFM: $enableAnimatedMFM, enableLongTextElipsed: $enableLongTextElipsed, enableFavoritedRenoteElipsed: $enableFavoritedRenoteElipsed, tabPosition: $tabPosition, textScaleFactor: $textScaleFactor, emojiType: $emojiType, defaultFontName: $defaultFontName, serifFontName: $serifFontName, monospaceFontName: $monospaceFontName, cursiveFontName: $cursiveFontName, fantasyFontName: $fantasyFontName, languages: $languages, isDeckMode: $isDeckMode)'; } @override @@ -505,33 +534,39 @@ class _$GeneralSettingsImpl implements _GeneralSettings { (identical(other.fantasyFontName, fantasyFontName) || other.fantasyFontName == fantasyFontName) && (identical(other.languages, languages) || - other.languages == languages)); + other.languages == languages) && + (identical(other.isDeckMode, isDeckMode) || + other.isDeckMode == isDeckMode)); } - @JsonKey(ignore: true) - @override - int get hashCode => Object.hash( - runtimeType, - lightColorThemeId, - darkColorThemeId, - themeColorSystem, - nsfwInherit, - enableDirectReaction, - automaticPush, - enableAnimatedMFM, - enableLongTextElipsed, - enableFavoritedRenoteElipsed, - tabPosition, - textScaleFactor, - emojiType, - defaultFontName, - serifFontName, - monospaceFontName, - cursiveFontName, - fantasyFontName, - languages); - - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hashAll([ + runtimeType, + lightColorThemeId, + darkColorThemeId, + themeColorSystem, + nsfwInherit, + enableDirectReaction, + automaticPush, + enableAnimatedMFM, + enableLongTextElipsed, + enableFavoritedRenoteElipsed, + tabPosition, + textScaleFactor, + emojiType, + defaultFontName, + serifFontName, + monospaceFontName, + cursiveFontName, + fantasyFontName, + languages, + isDeckMode + ]); + + /// Create a copy of GeneralSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$GeneralSettingsImplCopyWith<_$GeneralSettingsImpl> get copyWith => @@ -565,7 +600,8 @@ abstract class _GeneralSettings implements GeneralSettings { final String monospaceFontName, final String cursiveFontName, final String fantasyFontName, - final Languages languages}) = _$GeneralSettingsImpl; + final Languages languages, + final bool isDeckMode}) = _$GeneralSettingsImpl; factory _GeneralSettings.fromJson(Map json) = _$GeneralSettingsImpl.fromJson; @@ -576,68 +612,75 @@ abstract class _GeneralSettings implements GeneralSettings { String get darkColorThemeId; @override ThemeColorSystem get themeColorSystem; - @override /// NSFW設定を継承する - NSFWInherit get nsfwInherit; @override + NSFWInherit get nsfwInherit; /// ノートのカスタム絵文字直接タップでのリアクションを有効にする - bool get enableDirectReaction; @override + bool get enableDirectReaction; /// TLの自動更新を有効にする - AutomaticPush get automaticPush; @override + AutomaticPush get automaticPush; /// 動きのあるMFMを有効にする - bool get enableAnimatedMFM; @override + bool get enableAnimatedMFM; /// 長いノートを省略する - bool get enableLongTextElipsed; @override + bool get enableLongTextElipsed; /// リアクション済みノートを短くする - bool get enableFavoritedRenoteElipsed; @override + bool get enableFavoritedRenoteElipsed; /// タブの位置 - TabPosition get tabPosition; @override + TabPosition get tabPosition; /// 文字の大きさの倍率 - double get textScaleFactor; @override + double get textScaleFactor; /// 使用するUnicodeの絵文字種別 - EmojiType get emojiType; @override + EmojiType get emojiType; /// デフォルトのフォント名 - String get defaultFontName; @override + String get defaultFontName; /// `$[font.serif のフォント名 - String get serifFontName; @override + String get serifFontName; /// `$[font.monospace およびコードブロックのフォント名 - String get monospaceFontName; @override + String get monospaceFontName; /// `$[font.cursive のフォント名 - String get cursiveFontName; @override + String get cursiveFontName; /// `$[font.fantasy のフォント名 - String get fantasyFontName; @override + String get fantasyFontName; /// 言語設定 + @override Languages get languages; + + /// デッキモード + @override + bool get isDeckMode; + + /// Create a copy of GeneralSettings + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$GeneralSettingsImplCopyWith<_$GeneralSettingsImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/general_settings.g.dart b/lib/model/general_settings.g.dart index e74026f6e..63f9bf1d7 100644 --- a/lib/model/general_settings.g.dart +++ b/lib/model/general_settings.g.dart @@ -38,6 +38,7 @@ _$GeneralSettingsImpl _$$GeneralSettingsImplFromJson( fantasyFontName: json['fantasyFontName'] as String? ?? "", languages: $enumDecodeNullable(_$LanguagesEnumMap, json['languages']) ?? Languages.jaJP, + isDeckMode: json['isDeckMode'] as bool? ?? false, ); Map _$$GeneralSettingsImplToJson( @@ -61,6 +62,7 @@ Map _$$GeneralSettingsImplToJson( 'cursiveFontName': instance.cursiveFontName, 'fantasyFontName': instance.fantasyFontName, 'languages': _$LanguagesEnumMap[instance.languages]!, + 'isDeckMode': instance.isDeckMode, }; const _$ThemeColorSystemEnumMap = { @@ -94,4 +96,5 @@ const _$EmojiTypeEnumMap = { const _$LanguagesEnumMap = { Languages.jaJP: 'jaJP', Languages.jaOJ: 'jaOJ', + Languages.zhCN: 'zhCN', }; diff --git a/lib/model/image_file.dart b/lib/model/image_file.dart index 5ed33c7b4..e3fdb6976 100644 --- a/lib/model/image_file.dart +++ b/lib/model/image_file.dart @@ -1,4 +1,4 @@ -import 'dart:typed_data'; +import "dart:typed_data"; sealed class MisskeyPostFile { final String fileName; @@ -29,8 +29,8 @@ class ImageFileAlreadyPostedFile extends MisskeyPostFile { const ImageFileAlreadyPostedFile({ required this.data, required this.id, - this.isEdited = false, required super.fileName, + this.isEdited = false, super.isNsfw, super.caption, }); @@ -53,8 +53,8 @@ class UnknownAlreadyPostedFile extends MisskeyPostFile { const UnknownAlreadyPostedFile({ required this.url, required this.id, - this.isEdited = false, required super.fileName, + this.isEdited = false, super.isNsfw, super.caption, }); diff --git a/lib/model/misskey_emoji_data.dart b/lib/model/misskey_emoji_data.dart index 765672e4a..5f28692df 100644 --- a/lib/model/misskey_emoji_data.dart +++ b/lib/model/misskey_emoji_data.dart @@ -1,5 +1,5 @@ -import 'package:miria/repository/emoji_repository.dart'; -import 'package:collection/collection.dart'; +import "package:collection/collection.dart"; +import "package:miria/repository/emoji_repository.dart"; sealed class MisskeyEmojiData { final String baseName; @@ -19,8 +19,8 @@ sealed class MisskeyEmojiData { return UnicodeEmojiData(char: emojiName); } - final customEmojiRegExp = RegExp(r":(.+?)@(.+?):"); - final hostIncludedRegExp = RegExp(r":(.+?):"); + final customEmojiRegExp = RegExp(":(.+?)@(.+?):"); + final hostIncludedRegExp = RegExp(":(.+?):"); // よそのサーバー if (emojiInfo != null && emojiInfo.isNotEmpty) { @@ -32,22 +32,22 @@ sealed class MisskeyEmojiData { final found = emojiInfo[hostIncludedBaseName]; if (found != null) { return CustomEmojiData( - baseName: baseName, - hostedName: emojiName, - url: Uri.parse(found), - isCurrentServer: false, - isSensitive: false //TODO: 要検証 - ); + baseName: baseName, + hostedName: emojiName, + url: Uri.parse(found), + isCurrentServer: false, + isSensitive: false, + ); } } // 自分のサーバー :ai@.: if (customEmojiRegExp.hasMatch(emojiName)) { assert(repository != null); - final EmojiRepositoryData? found = repository!.emoji?.firstWhereOrNull( - (e) => - e.emoji.baseName == - (customEmojiRegExp.firstMatch(emojiName)?.group(1) ?? emojiName)); + final name = + customEmojiRegExp.firstMatch(emojiName)?.group(1) ?? emojiName; + final found = repository!.emojiMap?[name]; + if (found != null) { return found.emoji; } else { @@ -59,12 +59,9 @@ sealed class MisskeyEmojiData { final customEmojiRegExp2 = RegExp(r"^:(.+?):$"); if (customEmojiRegExp2.hasMatch(emojiName)) { assert(repository != null); - final EmojiRepositoryData? found = repository!.emoji?.firstWhereOrNull( - (e) => - e.emoji.baseName == - (customEmojiRegExp2.firstMatch(emojiName)?.group(1) ?? - emojiName)); - + final name = + customEmojiRegExp2.firstMatch(emojiName)?.group(1) ?? emojiName; + final found = repository!.emojiMap?[name]; if (found != null) { return found.emoji; } else { diff --git a/lib/model/misskey_theme.dart b/lib/model/misskey_theme.dart index a32378ab7..e07a2f306 100644 --- a/lib/model/misskey_theme.dart +++ b/lib/model/misskey_theme.dart @@ -1,17 +1,17 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; -part 'misskey_theme.freezed.dart'; -part 'misskey_theme.g.dart'; +part "misskey_theme.freezed.dart"; +part "misskey_theme.g.dart"; @freezed class MisskeyTheme with _$MisskeyTheme { const factory MisskeyTheme({ required String id, required String name, + required Map props, String? author, String? desc, String? base, - required Map props, }) = _MisskeyTheme; factory MisskeyTheme.fromJson(Map json) => diff --git a/lib/model/misskey_theme.freezed.dart b/lib/model/misskey_theme.freezed.dart index 909c9cc76..44d375884 100644 --- a/lib/model/misskey_theme.freezed.dart +++ b/lib/model/misskey_theme.freezed.dart @@ -12,7 +12,7 @@ part of 'misskey_theme.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); MisskeyTheme _$MisskeyThemeFromJson(Map json) { return _MisskeyTheme.fromJson(json); @@ -22,13 +22,17 @@ MisskeyTheme _$MisskeyThemeFromJson(Map json) { mixin _$MisskeyTheme { String get id => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError; + Map get props => throw _privateConstructorUsedError; String? get author => throw _privateConstructorUsedError; String? get desc => throw _privateConstructorUsedError; String? get base => throw _privateConstructorUsedError; - Map get props => throw _privateConstructorUsedError; + /// Serializes this MisskeyTheme to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of MisskeyTheme + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $MisskeyThemeCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -42,10 +46,10 @@ abstract class $MisskeyThemeCopyWith<$Res> { $Res call( {String id, String name, + Map props, String? author, String? desc, - String? base, - Map props}); + String? base}); } /// @nodoc @@ -58,15 +62,17 @@ class _$MisskeyThemeCopyWithImpl<$Res, $Val extends MisskeyTheme> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of MisskeyTheme + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = null, Object? name = null, + Object? props = null, Object? author = freezed, Object? desc = freezed, Object? base = freezed, - Object? props = null, }) { return _then(_value.copyWith( id: null == id @@ -77,6 +83,10 @@ class _$MisskeyThemeCopyWithImpl<$Res, $Val extends MisskeyTheme> ? _value.name : name // ignore: cast_nullable_to_non_nullable as String, + props: null == props + ? _value.props + : props // ignore: cast_nullable_to_non_nullable + as Map, author: freezed == author ? _value.author : author // ignore: cast_nullable_to_non_nullable @@ -89,10 +99,6 @@ class _$MisskeyThemeCopyWithImpl<$Res, $Val extends MisskeyTheme> ? _value.base : base // ignore: cast_nullable_to_non_nullable as String?, - props: null == props - ? _value.props - : props // ignore: cast_nullable_to_non_nullable - as Map, ) as $Val); } } @@ -108,10 +114,10 @@ abstract class _$$MisskeyThemeImplCopyWith<$Res> $Res call( {String id, String name, + Map props, String? author, String? desc, - String? base, - Map props}); + String? base}); } /// @nodoc @@ -122,15 +128,17 @@ class __$$MisskeyThemeImplCopyWithImpl<$Res> _$MisskeyThemeImpl _value, $Res Function(_$MisskeyThemeImpl) _then) : super(_value, _then); + /// Create a copy of MisskeyTheme + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? id = null, Object? name = null, + Object? props = null, Object? author = freezed, Object? desc = freezed, Object? base = freezed, - Object? props = null, }) { return _then(_$MisskeyThemeImpl( id: null == id @@ -141,6 +149,10 @@ class __$$MisskeyThemeImplCopyWithImpl<$Res> ? _value.name : name // ignore: cast_nullable_to_non_nullable as String, + props: null == props + ? _value._props + : props // ignore: cast_nullable_to_non_nullable + as Map, author: freezed == author ? _value.author : author // ignore: cast_nullable_to_non_nullable @@ -153,10 +165,6 @@ class __$$MisskeyThemeImplCopyWithImpl<$Res> ? _value.base : base // ignore: cast_nullable_to_non_nullable as String?, - props: null == props - ? _value._props - : props // ignore: cast_nullable_to_non_nullable - as Map, )); } } @@ -167,10 +175,10 @@ class _$MisskeyThemeImpl implements _MisskeyTheme { const _$MisskeyThemeImpl( {required this.id, required this.name, + required final Map props, this.author, this.desc, - this.base, - required final Map props}) + this.base}) : _props = props; factory _$MisskeyThemeImpl.fromJson(Map json) => @@ -180,12 +188,6 @@ class _$MisskeyThemeImpl implements _MisskeyTheme { final String id; @override final String name; - @override - final String? author; - @override - final String? desc; - @override - final String? base; final Map _props; @override Map get props { @@ -194,9 +196,16 @@ class _$MisskeyThemeImpl implements _MisskeyTheme { return EqualUnmodifiableMapView(_props); } + @override + final String? author; + @override + final String? desc; + @override + final String? base; + @override String toString() { - return 'MisskeyTheme(id: $id, name: $name, author: $author, desc: $desc, base: $base, props: $props)'; + return 'MisskeyTheme(id: $id, name: $name, props: $props, author: $author, desc: $desc, base: $base)'; } @override @@ -206,18 +215,20 @@ class _$MisskeyThemeImpl implements _MisskeyTheme { other is _$MisskeyThemeImpl && (identical(other.id, id) || other.id == id) && (identical(other.name, name) || other.name == name) && + const DeepCollectionEquality().equals(other._props, _props) && (identical(other.author, author) || other.author == author) && (identical(other.desc, desc) || other.desc == desc) && - (identical(other.base, base) || other.base == base) && - const DeepCollectionEquality().equals(other._props, _props)); + (identical(other.base, base) || other.base == base)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, id, name, author, desc, base, - const DeepCollectionEquality().hash(_props)); + int get hashCode => Object.hash(runtimeType, id, name, + const DeepCollectionEquality().hash(_props), author, desc, base); - @JsonKey(ignore: true) + /// Create a copy of MisskeyTheme + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$MisskeyThemeImplCopyWith<_$MisskeyThemeImpl> get copyWith => @@ -235,10 +246,10 @@ abstract class _MisskeyTheme implements MisskeyTheme { const factory _MisskeyTheme( {required final String id, required final String name, + required final Map props, final String? author, final String? desc, - final String? base, - required final Map props}) = _$MisskeyThemeImpl; + final String? base}) = _$MisskeyThemeImpl; factory _MisskeyTheme.fromJson(Map json) = _$MisskeyThemeImpl.fromJson; @@ -248,15 +259,18 @@ abstract class _MisskeyTheme implements MisskeyTheme { @override String get name; @override + Map get props; + @override String? get author; @override String? get desc; @override String? get base; + + /// Create a copy of MisskeyTheme + /// with the given fields replaced by the non-null parameter values. @override - Map get props; - @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$MisskeyThemeImplCopyWith<_$MisskeyThemeImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/misskey_theme.g.dart b/lib/model/misskey_theme.g.dart index 69868c485..278ceb7b8 100644 --- a/lib/model/misskey_theme.g.dart +++ b/lib/model/misskey_theme.g.dart @@ -10,18 +10,18 @@ _$MisskeyThemeImpl _$$MisskeyThemeImplFromJson(Map json) => _$MisskeyThemeImpl( id: json['id'] as String, name: json['name'] as String, + props: Map.from(json['props'] as Map), author: json['author'] as String?, desc: json['desc'] as String?, base: json['base'] as String?, - props: Map.from(json['props'] as Map), ); Map _$$MisskeyThemeImplToJson(_$MisskeyThemeImpl instance) => { 'id': instance.id, 'name': instance.name, + 'props': instance.props, 'author': instance.author, 'desc': instance.desc, 'base': instance.base, - 'props': instance.props, }; diff --git a/lib/model/note_search_condition.dart b/lib/model/note_search_condition.dart index e0f6024e3..2db2e573b 100644 --- a/lib/model/note_search_condition.dart +++ b/lib/model/note_search_condition.dart @@ -1,7 +1,7 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:misskey_dart/misskey_dart.dart"; -part 'note_search_condition.freezed.dart'; +part "note_search_condition.freezed.dart"; @freezed class NoteSearchCondition with _$NoteSearchCondition { diff --git a/lib/model/note_search_condition.freezed.dart b/lib/model/note_search_condition.freezed.dart index ad89ff91d..389efca0c 100644 --- a/lib/model/note_search_condition.freezed.dart +++ b/lib/model/note_search_condition.freezed.dart @@ -12,7 +12,7 @@ part of 'note_search_condition.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); /// @nodoc mixin _$NoteSearchCondition { @@ -21,7 +21,9 @@ mixin _$NoteSearchCondition { CommunityChannel? get channel => throw _privateConstructorUsedError; bool get localOnly => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of NoteSearchCondition + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $NoteSearchConditionCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -48,6 +50,8 @@ class _$NoteSearchConditionCopyWithImpl<$Res, $Val extends NoteSearchCondition> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of NoteSearchCondition + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -76,6 +80,8 @@ class _$NoteSearchConditionCopyWithImpl<$Res, $Val extends NoteSearchCondition> ) as $Val); } + /// Create a copy of NoteSearchCondition + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $CommunityChannelCopyWith<$Res>? get channel { @@ -112,6 +118,8 @@ class __$$NoteSearchConditionImplCopyWithImpl<$Res> $Res Function(_$NoteSearchConditionImpl) _then) : super(_value, _then); + /// Create a copy of NoteSearchCondition + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -178,7 +186,9 @@ class _$NoteSearchConditionImpl extends _NoteSearchCondition { @override int get hashCode => Object.hash(runtimeType, query, user, channel, localOnly); - @JsonKey(ignore: true) + /// Create a copy of NoteSearchCondition + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$NoteSearchConditionImplCopyWith<_$NoteSearchConditionImpl> get copyWith => @@ -202,8 +212,11 @@ abstract class _NoteSearchCondition extends NoteSearchCondition { CommunityChannel? get channel; @override bool get localOnly; + + /// Create a copy of NoteSearchCondition + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$NoteSearchConditionImplCopyWith<_$NoteSearchConditionImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/summaly_result.dart b/lib/model/summaly_result.dart index efd4c09af..98086c873 100644 --- a/lib/model/summaly_result.dart +++ b/lib/model/summaly_result.dart @@ -1,17 +1,17 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; -part 'summaly_result.freezed.dart'; -part 'summaly_result.g.dart'; +part "summaly_result.freezed.dart"; +part "summaly_result.g.dart"; // https://github.com/misskey-dev/summaly @freezed class SummalyResult with _$SummalyResult { const factory SummalyResult({ + required Player player, String? title, String? icon, String? description, String? thumbnail, - required Player player, String? sitename, bool? sensitive, String? url, diff --git a/lib/model/summaly_result.freezed.dart b/lib/model/summaly_result.freezed.dart index bf61b0ea2..2a6f876ba 100644 --- a/lib/model/summaly_result.freezed.dart +++ b/lib/model/summaly_result.freezed.dart @@ -12,7 +12,7 @@ part of 'summaly_result.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); SummalyResult _$SummalyResultFromJson(Map json) { return _SummalyResult.fromJson(json); @@ -20,17 +20,21 @@ SummalyResult _$SummalyResultFromJson(Map json) { /// @nodoc mixin _$SummalyResult { + Player get player => throw _privateConstructorUsedError; String? get title => throw _privateConstructorUsedError; String? get icon => throw _privateConstructorUsedError; String? get description => throw _privateConstructorUsedError; String? get thumbnail => throw _privateConstructorUsedError; - Player get player => throw _privateConstructorUsedError; String? get sitename => throw _privateConstructorUsedError; bool? get sensitive => throw _privateConstructorUsedError; String? get url => throw _privateConstructorUsedError; + /// Serializes this SummalyResult to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SummalyResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SummalyResultCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -42,11 +46,11 @@ abstract class $SummalyResultCopyWith<$Res> { _$SummalyResultCopyWithImpl<$Res, SummalyResult>; @useResult $Res call( - {String? title, + {Player player, + String? title, String? icon, String? description, String? thumbnail, - Player player, String? sitename, bool? sensitive, String? url}); @@ -64,19 +68,25 @@ class _$SummalyResultCopyWithImpl<$Res, $Val extends SummalyResult> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SummalyResult + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ + Object? player = null, Object? title = freezed, Object? icon = freezed, Object? description = freezed, Object? thumbnail = freezed, - Object? player = null, Object? sitename = freezed, Object? sensitive = freezed, Object? url = freezed, }) { return _then(_value.copyWith( + player: null == player + ? _value.player + : player // ignore: cast_nullable_to_non_nullable + as Player, title: freezed == title ? _value.title : title // ignore: cast_nullable_to_non_nullable @@ -93,10 +103,6 @@ class _$SummalyResultCopyWithImpl<$Res, $Val extends SummalyResult> ? _value.thumbnail : thumbnail // ignore: cast_nullable_to_non_nullable as String?, - player: null == player - ? _value.player - : player // ignore: cast_nullable_to_non_nullable - as Player, sitename: freezed == sitename ? _value.sitename : sitename // ignore: cast_nullable_to_non_nullable @@ -112,6 +118,8 @@ class _$SummalyResultCopyWithImpl<$Res, $Val extends SummalyResult> ) as $Val); } + /// Create a copy of SummalyResult + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $PlayerCopyWith<$Res> get player { @@ -130,11 +138,11 @@ abstract class _$$SummalyResultImplCopyWith<$Res> @override @useResult $Res call( - {String? title, + {Player player, + String? title, String? icon, String? description, String? thumbnail, - Player player, String? sitename, bool? sensitive, String? url}); @@ -151,19 +159,25 @@ class __$$SummalyResultImplCopyWithImpl<$Res> _$SummalyResultImpl _value, $Res Function(_$SummalyResultImpl) _then) : super(_value, _then); + /// Create a copy of SummalyResult + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ + Object? player = null, Object? title = freezed, Object? icon = freezed, Object? description = freezed, Object? thumbnail = freezed, - Object? player = null, Object? sitename = freezed, Object? sensitive = freezed, Object? url = freezed, }) { return _then(_$SummalyResultImpl( + player: null == player + ? _value.player + : player // ignore: cast_nullable_to_non_nullable + as Player, title: freezed == title ? _value.title : title // ignore: cast_nullable_to_non_nullable @@ -180,10 +194,6 @@ class __$$SummalyResultImplCopyWithImpl<$Res> ? _value.thumbnail : thumbnail // ignore: cast_nullable_to_non_nullable as String?, - player: null == player - ? _value.player - : player // ignore: cast_nullable_to_non_nullable - as Player, sitename: freezed == sitename ? _value.sitename : sitename // ignore: cast_nullable_to_non_nullable @@ -204,11 +214,11 @@ class __$$SummalyResultImplCopyWithImpl<$Res> @JsonSerializable() class _$SummalyResultImpl implements _SummalyResult { const _$SummalyResultImpl( - {this.title, + {required this.player, + this.title, this.icon, this.description, this.thumbnail, - required this.player, this.sitename, this.sensitive, this.url}); @@ -216,6 +226,8 @@ class _$SummalyResultImpl implements _SummalyResult { factory _$SummalyResultImpl.fromJson(Map json) => _$$SummalyResultImplFromJson(json); + @override + final Player player; @override final String? title; @override @@ -225,8 +237,6 @@ class _$SummalyResultImpl implements _SummalyResult { @override final String? thumbnail; @override - final Player player; - @override final String? sitename; @override final bool? sensitive; @@ -235,7 +245,7 @@ class _$SummalyResultImpl implements _SummalyResult { @override String toString() { - return 'SummalyResult(title: $title, icon: $icon, description: $description, thumbnail: $thumbnail, player: $player, sitename: $sitename, sensitive: $sensitive, url: $url)'; + return 'SummalyResult(player: $player, title: $title, icon: $icon, description: $description, thumbnail: $thumbnail, sitename: $sitename, sensitive: $sensitive, url: $url)'; } @override @@ -243,13 +253,13 @@ class _$SummalyResultImpl implements _SummalyResult { return identical(this, other) || (other.runtimeType == runtimeType && other is _$SummalyResultImpl && + (identical(other.player, player) || other.player == player) && (identical(other.title, title) || other.title == title) && (identical(other.icon, icon) || other.icon == icon) && (identical(other.description, description) || other.description == description) && (identical(other.thumbnail, thumbnail) || other.thumbnail == thumbnail) && - (identical(other.player, player) || other.player == player) && (identical(other.sitename, sitename) || other.sitename == sitename) && (identical(other.sensitive, sensitive) || @@ -257,12 +267,14 @@ class _$SummalyResultImpl implements _SummalyResult { (identical(other.url, url) || other.url == url)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override - int get hashCode => Object.hash(runtimeType, title, icon, description, - thumbnail, player, sitename, sensitive, url); + int get hashCode => Object.hash(runtimeType, player, title, icon, description, + thumbnail, sitename, sensitive, url); - @JsonKey(ignore: true) + /// Create a copy of SummalyResult + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SummalyResultImplCopyWith<_$SummalyResultImpl> get copyWith => @@ -278,11 +290,11 @@ class _$SummalyResultImpl implements _SummalyResult { abstract class _SummalyResult implements SummalyResult { const factory _SummalyResult( - {final String? title, + {required final Player player, + final String? title, final String? icon, final String? description, final String? thumbnail, - required final Player player, final String? sitename, final bool? sensitive, final String? url}) = _$SummalyResultImpl; @@ -290,6 +302,8 @@ abstract class _SummalyResult implements SummalyResult { factory _SummalyResult.fromJson(Map json) = _$SummalyResultImpl.fromJson; + @override + Player get player; @override String? get title; @override @@ -299,15 +313,16 @@ abstract class _SummalyResult implements SummalyResult { @override String? get thumbnail; @override - Player get player; - @override String? get sitename; @override bool? get sensitive; @override String? get url; + + /// Create a copy of SummalyResult + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SummalyResultImplCopyWith<_$SummalyResultImpl> get copyWith => throw _privateConstructorUsedError; } @@ -323,8 +338,12 @@ mixin _$Player { double? get height => throw _privateConstructorUsedError; List? get allow => throw _privateConstructorUsedError; + /// Serializes this Player to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of Player + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $PlayerCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -346,6 +365,8 @@ class _$PlayerCopyWithImpl<$Res, $Val extends Player> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of Player + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -393,6 +414,8 @@ class __$$PlayerImplCopyWithImpl<$Res> _$PlayerImpl _value, $Res Function(_$PlayerImpl) _then) : super(_value, _then); + /// Create a copy of Player + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -464,12 +487,14 @@ class _$PlayerImpl implements _Player { const DeepCollectionEquality().equals(other._allow, _allow)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, url, width, height, const DeepCollectionEquality().hash(_allow)); - @JsonKey(ignore: true) + /// Create a copy of Player + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PlayerImplCopyWith<_$PlayerImpl> get copyWith => @@ -500,8 +525,11 @@ abstract class _Player implements Player { double? get height; @override List? get allow; + + /// Create a copy of Player + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$PlayerImplCopyWith<_$PlayerImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/summaly_result.g.dart b/lib/model/summaly_result.g.dart index ebb0d8b21..417ab7265 100644 --- a/lib/model/summaly_result.g.dart +++ b/lib/model/summaly_result.g.dart @@ -8,11 +8,11 @@ part of 'summaly_result.dart'; _$SummalyResultImpl _$$SummalyResultImplFromJson(Map json) => _$SummalyResultImpl( + player: Player.fromJson(json['player'] as Map), title: json['title'] as String?, icon: json['icon'] as String?, description: json['description'] as String?, thumbnail: json['thumbnail'] as String?, - player: Player.fromJson(json['player'] as Map), sitename: json['sitename'] as String?, sensitive: json['sensitive'] as bool?, url: json['url'] as String?, @@ -20,11 +20,11 @@ _$SummalyResultImpl _$$SummalyResultImplFromJson(Map json) => Map _$$SummalyResultImplToJson(_$SummalyResultImpl instance) => { + 'player': instance.player.toJson(), 'title': instance.title, 'icon': instance.icon, 'description': instance.description, 'thumbnail': instance.thumbnail, - 'player': instance.player.toJson(), 'sitename': instance.sitename, 'sensitive': instance.sensitive, 'url': instance.url, diff --git a/lib/model/tab_icon.dart b/lib/model/tab_icon.dart index 7430cffbf..9515b6e58 100644 --- a/lib/model/tab_icon.dart +++ b/lib/model/tab_icon.dart @@ -1,7 +1,7 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; -part 'tab_icon.freezed.dart'; -part 'tab_icon.g.dart'; +part "tab_icon.freezed.dart"; +part "tab_icon.g.dart"; @freezed class TabIcon with _$TabIcon { diff --git a/lib/model/tab_icon.freezed.dart b/lib/model/tab_icon.freezed.dart index 6340f0d65..dd82a2721 100644 --- a/lib/model/tab_icon.freezed.dart +++ b/lib/model/tab_icon.freezed.dart @@ -12,7 +12,7 @@ part of 'tab_icon.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); TabIcon _$TabIconFromJson(Map json) { return _TabIcon.fromJson(json); @@ -23,8 +23,12 @@ mixin _$TabIcon { int? get codePoint => throw _privateConstructorUsedError; String? get customEmojiName => throw _privateConstructorUsedError; + /// Serializes this TabIcon to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of TabIcon + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $TabIconCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -46,6 +50,8 @@ class _$TabIconCopyWithImpl<$Res, $Val extends TabIcon> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of TabIcon + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -83,6 +89,8 @@ class __$$TabIconImplCopyWithImpl<$Res> _$TabIconImpl _value, $Res Function(_$TabIconImpl) _then) : super(_value, _then); + /// Create a copy of TabIcon + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -131,11 +139,13 @@ class _$TabIconImpl implements _TabIcon { other.customEmojiName == customEmojiName)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, codePoint, customEmojiName); - @JsonKey(ignore: true) + /// Create a copy of TabIcon + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$TabIconImplCopyWith<_$TabIconImpl> get copyWith => @@ -159,8 +169,11 @@ abstract class _TabIcon implements TabIcon { int? get codePoint; @override String? get customEmojiName; + + /// Create a copy of TabIcon + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$TabIconImplCopyWith<_$TabIconImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/tab_icon.g.dart b/lib/model/tab_icon.g.dart index f966dfc09..b554eee11 100644 --- a/lib/model/tab_icon.g.dart +++ b/lib/model/tab_icon.g.dart @@ -8,7 +8,7 @@ part of 'tab_icon.dart'; _$TabIconImpl _$$TabIconImplFromJson(Map json) => _$TabIconImpl( - codePoint: json['codePoint'] as int?, + codePoint: (json['codePoint'] as num?)?.toInt(), customEmojiName: json['customEmojiName'] as String?, ); diff --git a/lib/model/tab_setting.dart b/lib/model/tab_setting.dart index ceb677686..4a331bd51 100644 --- a/lib/model/tab_setting.dart +++ b/lib/model/tab_setting.dart @@ -1,16 +1,14 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:miria/model/acct.dart'; -import 'package:miria/model/converters/icon_converter.dart'; -import 'package:miria/model/tab_icon.dart'; -import 'package:miria/model/tab_type.dart'; -import 'package:miria/repository/time_line_repository.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:miria/model/acct.dart"; +import "package:miria/model/converters/icon_converter.dart"; +import "package:miria/model/tab_icon.dart"; +import "package:miria/model/tab_type.dart"; -part 'tab_setting.freezed.dart'; -part 'tab_setting.g.dart'; +part "tab_setting.freezed.dart"; +part "tab_setting.g.dart"; Map _readAcct(Map json, String name) { - final account = json["account"]; + final account = json["account"] as Map?; if (account != null) { return { "host": account["host"], @@ -24,15 +22,17 @@ Map _readAcct(Map json, String name) { class TabSetting with _$TabSetting { const TabSetting._(); - ChangeNotifierProvider get timelineProvider => - tabType.timelineProvider(this); - const factory TabSetting({ @IconDataConverter() required TabIcon icon, /// タブ種別 required TabType tabType, + /// アカウント情報 + // https://github.com/rrousselGit/freezed/issues/488 + // ignore: invalid_annotation_target + @JsonKey(readValue: _readAcct) required Acct acct, + /// ロールタイムラインのノートの場合、ロールID String? roleId, @@ -57,11 +57,6 @@ class TabSetting with _$TabSetting { /// タブ名 String? name, - /// アカウント情報 - // https://github.com/rrousselGit/freezed/issues/488 - // ignore: invalid_annotation_target - @JsonKey(readValue: _readAcct) required Acct acct, - /// Renoteを表示するかどうか @Default(true) bool renoteDisplay, }) = _TabSetting; diff --git a/lib/model/tab_setting.freezed.dart b/lib/model/tab_setting.freezed.dart index 7d10d14c4..7e199e141 100644 --- a/lib/model/tab_setting.freezed.dart +++ b/lib/model/tab_setting.freezed.dart @@ -12,7 +12,7 @@ part of 'tab_setting.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); TabSetting _$TabSettingFromJson(Map json) { return _TabSetting.fromJson(json); @@ -26,6 +26,12 @@ mixin _$TabSetting { /// タブ種別 TabType get tabType => throw _privateConstructorUsedError; + /// アカウント情報 +// https://github.com/rrousselGit/freezed/issues/488 +// ignore: invalid_annotation_target + @JsonKey(readValue: _readAcct) + Acct get acct => throw _privateConstructorUsedError; + /// ロールタイムラインのノートの場合、ロールID String? get roleId => throw _privateConstructorUsedError; @@ -50,17 +56,15 @@ mixin _$TabSetting { /// タブ名 String? get name => throw _privateConstructorUsedError; - /// アカウント情報 -// https://github.com/rrousselGit/freezed/issues/488 -// ignore: invalid_annotation_target - @JsonKey(readValue: _readAcct) - Acct get acct => throw _privateConstructorUsedError; - /// Renoteを表示するかどうか bool get renoteDisplay => throw _privateConstructorUsedError; + /// Serializes this TabSetting to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of TabSetting + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $TabSettingCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -74,6 +78,7 @@ abstract class $TabSettingCopyWith<$Res> { $Res call( {@IconDataConverter() TabIcon icon, TabType tabType, + @JsonKey(readValue: _readAcct) Acct acct, String? roleId, String? channelId, String? listId, @@ -82,7 +87,6 @@ abstract class $TabSettingCopyWith<$Res> { bool isIncludeReplies, bool isMediaOnly, String? name, - @JsonKey(readValue: _readAcct) Acct acct, bool renoteDisplay}); $TabIconCopyWith<$Res> get icon; @@ -99,11 +103,14 @@ class _$TabSettingCopyWithImpl<$Res, $Val extends TabSetting> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of TabSetting + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? icon = null, Object? tabType = null, + Object? acct = null, Object? roleId = freezed, Object? channelId = freezed, Object? listId = freezed, @@ -112,7 +119,6 @@ class _$TabSettingCopyWithImpl<$Res, $Val extends TabSetting> Object? isIncludeReplies = null, Object? isMediaOnly = null, Object? name = freezed, - Object? acct = null, Object? renoteDisplay = null, }) { return _then(_value.copyWith( @@ -124,6 +130,10 @@ class _$TabSettingCopyWithImpl<$Res, $Val extends TabSetting> ? _value.tabType : tabType // ignore: cast_nullable_to_non_nullable as TabType, + acct: null == acct + ? _value.acct + : acct // ignore: cast_nullable_to_non_nullable + as Acct, roleId: freezed == roleId ? _value.roleId : roleId // ignore: cast_nullable_to_non_nullable @@ -156,10 +166,6 @@ class _$TabSettingCopyWithImpl<$Res, $Val extends TabSetting> ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, - acct: null == acct - ? _value.acct - : acct // ignore: cast_nullable_to_non_nullable - as Acct, renoteDisplay: null == renoteDisplay ? _value.renoteDisplay : renoteDisplay // ignore: cast_nullable_to_non_nullable @@ -167,6 +173,8 @@ class _$TabSettingCopyWithImpl<$Res, $Val extends TabSetting> ) as $Val); } + /// Create a copy of TabSetting + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $TabIconCopyWith<$Res> get icon { @@ -175,6 +183,8 @@ class _$TabSettingCopyWithImpl<$Res, $Val extends TabSetting> }); } + /// Create a copy of TabSetting + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $AcctCopyWith<$Res> get acct { @@ -195,6 +205,7 @@ abstract class _$$TabSettingImplCopyWith<$Res> $Res call( {@IconDataConverter() TabIcon icon, TabType tabType, + @JsonKey(readValue: _readAcct) Acct acct, String? roleId, String? channelId, String? listId, @@ -203,7 +214,6 @@ abstract class _$$TabSettingImplCopyWith<$Res> bool isIncludeReplies, bool isMediaOnly, String? name, - @JsonKey(readValue: _readAcct) Acct acct, bool renoteDisplay}); @override @@ -220,11 +230,14 @@ class __$$TabSettingImplCopyWithImpl<$Res> _$TabSettingImpl _value, $Res Function(_$TabSettingImpl) _then) : super(_value, _then); + /// Create a copy of TabSetting + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ Object? icon = null, Object? tabType = null, + Object? acct = null, Object? roleId = freezed, Object? channelId = freezed, Object? listId = freezed, @@ -233,7 +246,6 @@ class __$$TabSettingImplCopyWithImpl<$Res> Object? isIncludeReplies = null, Object? isMediaOnly = null, Object? name = freezed, - Object? acct = null, Object? renoteDisplay = null, }) { return _then(_$TabSettingImpl( @@ -245,6 +257,10 @@ class __$$TabSettingImplCopyWithImpl<$Res> ? _value.tabType : tabType // ignore: cast_nullable_to_non_nullable as TabType, + acct: null == acct + ? _value.acct + : acct // ignore: cast_nullable_to_non_nullable + as Acct, roleId: freezed == roleId ? _value.roleId : roleId // ignore: cast_nullable_to_non_nullable @@ -277,10 +293,6 @@ class __$$TabSettingImplCopyWithImpl<$Res> ? _value.name : name // ignore: cast_nullable_to_non_nullable as String?, - acct: null == acct - ? _value.acct - : acct // ignore: cast_nullable_to_non_nullable - as Acct, renoteDisplay: null == renoteDisplay ? _value.renoteDisplay : renoteDisplay // ignore: cast_nullable_to_non_nullable @@ -295,6 +307,7 @@ class _$TabSettingImpl extends _TabSetting { const _$TabSettingImpl( {@IconDataConverter() required this.icon, required this.tabType, + @JsonKey(readValue: _readAcct) required this.acct, this.roleId, this.channelId, this.listId, @@ -303,7 +316,6 @@ class _$TabSettingImpl extends _TabSetting { this.isIncludeReplies = true, this.isMediaOnly = false, this.name, - @JsonKey(readValue: _readAcct) required this.acct, this.renoteDisplay = true}) : super._(); @@ -318,6 +330,13 @@ class _$TabSettingImpl extends _TabSetting { @override final TabType tabType; + /// アカウント情報 +// https://github.com/rrousselGit/freezed/issues/488 +// ignore: invalid_annotation_target + @override + @JsonKey(readValue: _readAcct) + final Acct acct; + /// ロールタイムラインのノートの場合、ロールID @override final String? roleId; @@ -353,13 +372,6 @@ class _$TabSettingImpl extends _TabSetting { @override final String? name; - /// アカウント情報 -// https://github.com/rrousselGit/freezed/issues/488 -// ignore: invalid_annotation_target - @override - @JsonKey(readValue: _readAcct) - final Acct acct; - /// Renoteを表示するかどうか @override @JsonKey() @@ -367,7 +379,7 @@ class _$TabSettingImpl extends _TabSetting { @override String toString() { - return 'TabSetting(icon: $icon, tabType: $tabType, roleId: $roleId, channelId: $channelId, listId: $listId, antennaId: $antennaId, isSubscribe: $isSubscribe, isIncludeReplies: $isIncludeReplies, isMediaOnly: $isMediaOnly, name: $name, acct: $acct, renoteDisplay: $renoteDisplay)'; + return 'TabSetting(icon: $icon, tabType: $tabType, acct: $acct, roleId: $roleId, channelId: $channelId, listId: $listId, antennaId: $antennaId, isSubscribe: $isSubscribe, isIncludeReplies: $isIncludeReplies, isMediaOnly: $isMediaOnly, name: $name, renoteDisplay: $renoteDisplay)'; } @override @@ -377,6 +389,7 @@ class _$TabSettingImpl extends _TabSetting { other is _$TabSettingImpl && (identical(other.icon, icon) || other.icon == icon) && (identical(other.tabType, tabType) || other.tabType == tabType) && + (identical(other.acct, acct) || other.acct == acct) && (identical(other.roleId, roleId) || other.roleId == roleId) && (identical(other.channelId, channelId) || other.channelId == channelId) && @@ -390,17 +403,17 @@ class _$TabSettingImpl extends _TabSetting { (identical(other.isMediaOnly, isMediaOnly) || other.isMediaOnly == isMediaOnly) && (identical(other.name, name) || other.name == name) && - (identical(other.acct, acct) || other.acct == acct) && (identical(other.renoteDisplay, renoteDisplay) || other.renoteDisplay == renoteDisplay)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, icon, tabType, + acct, roleId, channelId, listId, @@ -409,10 +422,11 @@ class _$TabSettingImpl extends _TabSetting { isIncludeReplies, isMediaOnly, name, - acct, renoteDisplay); - @JsonKey(ignore: true) + /// Create a copy of TabSetting + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$TabSettingImplCopyWith<_$TabSettingImpl> get copyWith => @@ -430,6 +444,7 @@ abstract class _TabSetting extends TabSetting { const factory _TabSetting( {@IconDataConverter() required final TabIcon icon, required final TabType tabType, + @JsonKey(readValue: _readAcct) required final Acct acct, final String? roleId, final String? channelId, final String? listId, @@ -438,7 +453,6 @@ abstract class _TabSetting extends TabSetting { final bool isIncludeReplies, final bool isMediaOnly, final String? name, - @JsonKey(readValue: _readAcct) required final Acct acct, final bool renoteDisplay}) = _$TabSettingImpl; const _TabSetting._() : super._(); @@ -448,55 +462,58 @@ abstract class _TabSetting extends TabSetting { @override @IconDataConverter() TabIcon get icon; - @override /// タブ種別 + @override TabType get tabType; + + /// アカウント情報 +// https://github.com/rrousselGit/freezed/issues/488 +// ignore: invalid_annotation_target @override + @JsonKey(readValue: _readAcct) + Acct get acct; /// ロールタイムラインのノートの場合、ロールID - String? get roleId; @override + String? get roleId; /// チャンネルのノートの場合、チャンネルID - String? get channelId; @override + String? get channelId; /// リストのノートの場合、リストID - String? get listId; @override + String? get listId; /// アンテナのノートの場合、アンテナID - String? get antennaId; @override + String? get antennaId; /// ノートの投稿のキャプチャをするかどうか - bool get isSubscribe; @override + bool get isSubscribe; /// 返信を含むかどうか - bool get isIncludeReplies; @override + bool get isIncludeReplies; /// ファイルのみにするかどうか - bool get isMediaOnly; @override + bool get isMediaOnly; /// タブ名 - String? get name; - @override - - /// アカウント情報 -// https://github.com/rrousselGit/freezed/issues/488 -// ignore: invalid_annotation_target - @JsonKey(readValue: _readAcct) - Acct get acct; @override + String? get name; /// Renoteを表示するかどうか + @override bool get renoteDisplay; + + /// Create a copy of TabSetting + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$TabSettingImplCopyWith<_$TabSettingImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/tab_setting.g.dart b/lib/model/tab_setting.g.dart index d979170fe..194c0012e 100644 --- a/lib/model/tab_setting.g.dart +++ b/lib/model/tab_setting.g.dart @@ -10,6 +10,7 @@ _$TabSettingImpl _$$TabSettingImplFromJson(Map json) => _$TabSettingImpl( icon: const IconDataConverter().fromJson(json['icon']), tabType: $enumDecode(_$TabTypeEnumMap, json['tabType']), + acct: Acct.fromJson(_readAcct(json, 'acct') as Map), roleId: json['roleId'] as String?, channelId: json['channelId'] as String?, listId: json['listId'] as String?, @@ -18,7 +19,6 @@ _$TabSettingImpl _$$TabSettingImplFromJson(Map json) => isIncludeReplies: json['isIncludeReplies'] as bool? ?? true, isMediaOnly: json['isMediaOnly'] as bool? ?? false, name: json['name'] as String?, - acct: Acct.fromJson(_readAcct(json, 'acct') as Map), renoteDisplay: json['renoteDisplay'] as bool? ?? true, ); @@ -26,6 +26,7 @@ Map _$$TabSettingImplToJson(_$TabSettingImpl instance) => { 'icon': const IconDataConverter().toJson(instance.icon), 'tabType': _$TabTypeEnumMap[instance.tabType]!, + 'acct': instance.acct.toJson(), 'roleId': instance.roleId, 'channelId': instance.channelId, 'listId': instance.listId, @@ -34,7 +35,6 @@ Map _$$TabSettingImplToJson(_$TabSettingImpl instance) => 'isIncludeReplies': instance.isIncludeReplies, 'isMediaOnly': instance.isMediaOnly, 'name': instance.name, - 'acct': instance.acct.toJson(), 'renoteDisplay': instance.renoteDisplay, }; diff --git a/lib/model/tab_type.dart b/lib/model/tab_type.dart index 68dfb97ba..1b8adc24c 100644 --- a/lib/model/tab_type.dart +++ b/lib/model/tab_type.dart @@ -1,9 +1,5 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/tab_setting.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/repository/time_line_repository.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; enum TabType { localTimeline, @@ -28,26 +24,4 @@ enum TabType { TabType.antenna => S.of(context).antenna, }; } - - ChangeNotifierProvider timelineProvider( - TabSetting setting) { - switch (this) { - case TabType.localTimeline: - return localTimeLineProvider(setting); - case TabType.homeTimeline: - return homeTimeLineProvider(setting); - case TabType.globalTimeline: - return globalTimeLineProvider(setting); - case TabType.hybridTimeline: - return hybridTimeLineProvider(setting); //FIXME - case TabType.roleTimeline: - return roleTimelineProvider(setting); - case TabType.channel: - return channelTimelineProvider(setting); - case TabType.userList: - return userListTimelineProvider(setting); - case TabType.antenna: - return antennaTimelineProvider(setting); - } - } } diff --git a/lib/model/unicode_emoji.dart b/lib/model/unicode_emoji.dart index c6e6f766f..72ee4f4a0 100644 --- a/lib/model/unicode_emoji.dart +++ b/lib/model/unicode_emoji.dart @@ -1,7 +1,7 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; -part 'unicode_emoji.freezed.dart'; -part 'unicode_emoji.g.dart'; +part "unicode_emoji.freezed.dart"; +part "unicode_emoji.g.dart"; @freezed class UnicodeEmoji with _$UnicodeEmoji { diff --git a/lib/model/unicode_emoji.freezed.dart b/lib/model/unicode_emoji.freezed.dart index 6a82c865e..9a851952f 100644 --- a/lib/model/unicode_emoji.freezed.dart +++ b/lib/model/unicode_emoji.freezed.dart @@ -12,7 +12,7 @@ part of 'unicode_emoji.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); UnicodeEmoji _$UnicodeEmojiFromJson(Map json) { return _UnicodeEmoji.fromJson(json); @@ -25,8 +25,12 @@ mixin _$UnicodeEmoji { String get name => throw _privateConstructorUsedError; List get keywords => throw _privateConstructorUsedError; + /// Serializes this UnicodeEmoji to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of UnicodeEmoji + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $UnicodeEmojiCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -50,6 +54,8 @@ class _$UnicodeEmojiCopyWithImpl<$Res, $Val extends UnicodeEmoji> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of UnicodeEmoji + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -98,6 +104,8 @@ class __$$UnicodeEmojiImplCopyWithImpl<$Res> _$UnicodeEmojiImpl _value, $Res Function(_$UnicodeEmojiImpl) _then) : super(_value, _then); + /// Create a copy of UnicodeEmoji + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -171,12 +179,14 @@ class _$UnicodeEmojiImpl implements _UnicodeEmoji { const DeepCollectionEquality().equals(other._keywords, _keywords)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, category, char, name, const DeepCollectionEquality().hash(_keywords)); - @JsonKey(ignore: true) + /// Create a copy of UnicodeEmoji + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$UnicodeEmojiImplCopyWith<_$UnicodeEmojiImpl> get copyWith => @@ -208,8 +218,11 @@ abstract class _UnicodeEmoji implements UnicodeEmoji { String get name; @override List get keywords; + + /// Create a copy of UnicodeEmoji + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$UnicodeEmojiImplCopyWith<_$UnicodeEmojiImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/model/users_list_settings.dart b/lib/model/users_list_settings.dart index 8765b5c8b..68d672336 100644 --- a/lib/model/users_list_settings.dart +++ b/lib/model/users_list_settings.dart @@ -1,7 +1,7 @@ -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:misskey_dart/misskey_dart.dart"; -part 'users_list_settings.freezed.dart'; +part "users_list_settings.freezed.dart"; @freezed class UsersListSettings with _$UsersListSettings { diff --git a/lib/model/users_list_settings.freezed.dart b/lib/model/users_list_settings.freezed.dart index 4fdebc48f..38d603c41 100644 --- a/lib/model/users_list_settings.freezed.dart +++ b/lib/model/users_list_settings.freezed.dart @@ -12,14 +12,16 @@ part of 'users_list_settings.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); /// @nodoc mixin _$UsersListSettings { String get name => throw _privateConstructorUsedError; bool get isPublic => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of UsersListSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $UsersListSettingsCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -43,6 +45,8 @@ class _$UsersListSettingsCopyWithImpl<$Res, $Val extends UsersListSettings> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of UsersListSettings + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -81,6 +85,8 @@ class __$$UsersListSettingsImplCopyWithImpl<$Res> $Res Function(_$UsersListSettingsImpl) _then) : super(_value, _then); + /// Create a copy of UsersListSettings + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -131,7 +137,9 @@ class _$UsersListSettingsImpl extends _UsersListSettings { @override int get hashCode => Object.hash(runtimeType, name, isPublic); - @JsonKey(ignore: true) + /// Create a copy of UsersListSettings + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$UsersListSettingsImplCopyWith<_$UsersListSettingsImpl> get copyWith => @@ -148,8 +156,11 @@ abstract class _UsersListSettings extends UsersListSettings { String get name; @override bool get isPublic; + + /// Create a copy of UsersListSettings + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$UsersListSettingsImplCopyWith<_$UsersListSettingsImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/providers.dart b/lib/providers.dart index a8280f4a1..6644b632d 100644 --- a/lib/providers.dart +++ b/lib/providers.dart @@ -1,237 +1,110 @@ -import 'package:dio/dio.dart'; -import 'package:file/file.dart'; -import 'package:file/local.dart'; -import 'package:flutter/widgets.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/acct.dart'; -import 'package:miria/model/tab_setting.dart'; -import 'package:miria/repository/account_repository.dart'; -import 'package:miria/repository/account_settings_repository.dart'; -import 'package:miria/repository/antenna_timeline_repository.dart'; -import 'package:miria/repository/channel_time_line_repository.dart'; -import 'package:miria/repository/desktop_settings_repository.dart'; -import 'package:miria/repository/emoji_repository.dart'; -import 'package:miria/repository/favorite_repository.dart'; -import 'package:miria/repository/general_settings_repository.dart'; -import 'package:miria/repository/hybrid_timeline_repository.dart'; -import 'package:miria/repository/import_export_repository.dart'; -import 'package:miria/repository/main_stream_repository.dart'; -import 'package:miria/repository/global_time_line_repository.dart'; -import 'package:miria/repository/home_time_line_repository.dart'; -import 'package:miria/repository/local_time_line_repository.dart'; -import 'package:miria/repository/role_timeline_repository.dart'; -import 'package:miria/repository/note_repository.dart'; -import 'package:miria/repository/shared_preference_controller.dart'; -import 'package:miria/repository/tab_settings_repository.dart'; -import 'package:miria/repository/time_line_repository.dart'; -import 'package:miria/repository/user_list_time_line_repository.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/state_notifier/antenna_page/antennas_notifier.dart'; -import 'package:miria/state_notifier/clip_list_page/clips_notifier.dart'; -import 'package:miria/state_notifier/common/misskey_notes/misskey_note_notifier.dart'; -import 'package:miria/state_notifier/common/misskey_server_list_notifier.dart'; -import 'package:miria/state_notifier/note_create_page/note_create_state_notifier.dart'; -import 'package:miria/state_notifier/photo_edit_page/photo_edit_state_notifier.dart'; -import 'package:miria/state_notifier/user_list_page/users_lists_notifier.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; - -final dioProvider = Provider((ref) => Dio()); -final fileSystemProvider = - Provider((ref) => const LocalFileSystem()); -final misskeyProvider = Provider.family( - (ref, account) => Misskey( - token: account.token, - host: account.host, - socketConnectionTimeout: const Duration(seconds: 20), - ), -); -final misskeyWithoutAccountProvider = Provider.family( - (ref, host) => Misskey( - host: host, - token: null, - socketConnectionTimeout: const Duration(seconds: 20))); - -final localTimeLineProvider = - ChangeNotifierProvider.family( - (ref, tabSetting) { - final account = ref.watch(accountProvider(tabSetting.acct)); - return LocalTimeLineRepository( +import "package:dio/dio.dart"; +import "package:file/file.dart"; +import "package:file/local.dart"; +import "package:flutter/widgets.dart"; +import "package:flutter_cache_manager/flutter_cache_manager.dart" + hide FileSystem; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/acct.dart"; +import "package:miria/model/tab_setting.dart"; +import "package:miria/model/tab_type.dart"; +import "package:miria/repository/account_repository.dart"; +import "package:miria/repository/account_settings_repository.dart"; +import "package:miria/repository/antenna_timeline_repository.dart"; +import "package:miria/repository/channel_time_line_repository.dart"; +import "package:miria/repository/desktop_settings_repository.dart"; +import "package:miria/repository/emoji_repository.dart"; +import "package:miria/repository/favorite_repository.dart"; +import "package:miria/repository/general_settings_repository.dart"; +import "package:miria/repository/global_time_line_repository.dart"; +import "package:miria/repository/home_time_line_repository.dart"; +import "package:miria/repository/hybrid_timeline_repository.dart"; +import "package:miria/repository/import_export_repository.dart"; +import "package:miria/repository/local_time_line_repository.dart"; +import "package:miria/repository/note_repository.dart"; +import "package:miria/repository/role_timeline_repository.dart"; +import "package:miria/repository/shared_preference_controller.dart"; +import "package:miria/repository/tab_settings_repository.dart"; +import "package:miria/repository/time_line_repository.dart"; +import "package:miria/repository/user_list_time_line_repository.dart"; +import "package:miria/router/app_router.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +part "providers.freezed.dart"; +part "providers.g.dart"; + +@Riverpod(keepAlive: true) +Dio dio(DioRef ref) => Dio(); + +@Riverpod(keepAlive: true) +FileSystem fileSystem(FileSystemRef ref) => const LocalFileSystem(); + +@Riverpod(keepAlive: true) +@Deprecated( + "Most case will be replace misskeyGetContext or misskeyPostContext, but will be remain", +) +Misskey misskey(MisskeyRef ref, Account account) => Misskey( + token: account.token, + host: account.host, + socketConnectionTimeout: const Duration(seconds: 20), + ); + +@Riverpod(keepAlive: true) +Raw appRouter(AppRouterRef ref) => AppRouter(); + +@riverpod +Misskey misskeyWithoutAccount(MisskeyWithoutAccountRef ref, String host) => + Misskey( + host: host, + token: null, + socketConnectionTimeout: const Duration(seconds: 20), + ); + +final favoriteProvider = + ChangeNotifierProvider.family( + (ref, account) => FavoriteRepository( ref.read(misskeyProvider(account)), - account, ref.read(notesProvider(account)), - ref.read(mainStreamRepositoryProvider(account)), - ref.read(generalSettingsRepositoryProvider), - tabSetting, - ref.read(mainStreamRepositoryProvider(account)), - ref.read(accountRepositoryProvider.notifier), - ref.read(emojiRepositoryProvider(account)), - ); -}); - -final homeTimeLineProvider = - ChangeNotifierProvider.family( - (ref, tabSetting) { - final account = ref.watch(accountProvider(tabSetting.acct)); - return HomeTimeLineRepository( - ref.read(misskeyProvider(account)), - account, - ref.read(notesProvider(account)), - ref.read(mainStreamRepositoryProvider(account)), - ref.read(generalSettingsRepositoryProvider), - tabSetting, - ref.read(mainStreamRepositoryProvider(account)), - ref.read(accountRepositoryProvider.notifier), - ref.read(emojiRepositoryProvider(account)), - ); -}); - -final globalTimeLineProvider = - ChangeNotifierProvider.family( - (ref, tabSetting) { - final account = ref.watch(accountProvider(tabSetting.acct)); - return GlobalTimeLineRepository( - ref.read(misskeyProvider(account)), - ref.read(notesProvider(account)), - ref.read(mainStreamRepositoryProvider(account)), - ref.read(generalSettingsRepositoryProvider), - tabSetting, - ); -}); - -final hybridTimeLineProvider = - ChangeNotifierProvider.family( - (ref, tabSetting) { - final account = ref.watch(accountProvider(tabSetting.acct)); - return HybridTimelineRepository( - ref.read(misskeyProvider(account)), - account, - ref.read(notesProvider(account)), - ref.read(mainStreamRepositoryProvider(account)), - ref.read(generalSettingsRepositoryProvider), - tabSetting, - ref.read(mainStreamRepositoryProvider(account)), - ref.read(accountRepositoryProvider.notifier), - ref.read(emojiRepositoryProvider(account)), - ); -}); - -final roleTimelineProvider = - ChangeNotifierProvider.family( - (ref, tabSetting) { - final account = ref.watch(accountProvider(tabSetting.acct)); - return RoleTimelineRepository( - ref.read(misskeyProvider(account)), - account, - ref.read(notesProvider(account)), - ref.read(mainStreamRepositoryProvider(account)), - ref.read(generalSettingsRepositoryProvider), - tabSetting, - ref.read(mainStreamRepositoryProvider(account)), - ref.read(accountRepositoryProvider.notifier), - ref.read(emojiRepositoryProvider(account)), - ); -}); - -final channelTimelineProvider = - ChangeNotifierProvider.family( - (ref, tabSetting) { - final account = ref.watch(accountProvider(tabSetting.acct)); - return ChannelTimelineRepository( - ref.read(misskeyProvider(account)), - account, - ref.read(notesProvider(account)), - ref.read(mainStreamRepositoryProvider(account)), - ref.read(generalSettingsRepositoryProvider), - tabSetting, - ref.read(mainStreamRepositoryProvider(account)), - ref.read(accountRepositoryProvider.notifier), - ref.read(emojiRepositoryProvider(account)), - ); -}); - -final userListTimelineProvider = - ChangeNotifierProvider.family( - (ref, tabSetting) { - final account = ref.watch(accountProvider(tabSetting.acct)); - return UserListTimelineRepository( - ref.read(misskeyProvider(account)), - account, - ref.read(notesProvider(account)), - ref.read(mainStreamRepositoryProvider(account)), - ref.read(generalSettingsRepositoryProvider), - tabSetting, - ref.read(mainStreamRepositoryProvider(account)), - ref.read(accountRepositoryProvider.notifier), - ref.read(emojiRepositoryProvider(account)), - ); -}); - -final antennaTimelineProvider = - ChangeNotifierProvider.family( - (ref, tabSetting) { - final account = ref.watch(accountProvider(tabSetting.acct)); - return AntennaTimelineRepository( - ref.read(misskeyProvider(account)), - account, - ref.read(notesProvider(account)), - ref.read(mainStreamRepositoryProvider(account)), - ref.read(generalSettingsRepositoryProvider), - tabSetting, - ref.read(mainStreamRepositoryProvider(account)), - ref.read(accountRepositoryProvider.notifier), - ref.read(emojiRepositoryProvider(account)), - ); -}); - -final mainStreamRepositoryProvider = - ChangeNotifierProvider.family( - (ref, account) => MainStreamRepository( - ref.read(misskeyProvider(account)), - ref.read(emojiRepositoryProvider(account)), - account, - ref.read(accountRepositoryProvider.notifier))); - -final favoriteProvider = ChangeNotifierProvider.autoDispose - .family((ref, account) => FavoriteRepository( - ref.read(misskeyProvider(account)), ref.read(notesProvider(account)))); - -final notesProvider = ChangeNotifierProvider.family( - (ref, account) => - NoteRepository(ref.read(misskeyProvider(account)), account)); - -//TODO: アカウント毎である必要はない ホスト毎 -//TODO: のつもりだったけど、絵文字にロールが関係するようになるとアカウント毎になる -final emojiRepositoryProvider = Provider.family( - (ref, account) => EmojiRepositoryImpl( - misskey: ref.read(misskeyProvider(account)), - account: account, - accountSettingsRepository: ref.read(accountSettingsRepositoryProvider), - sharePreferenceController: ref.read(sharedPrefenceControllerProvider), ), ); -final accountRepositoryProvider = - NotifierProvider>(AccountRepository.new); - -final accountsProvider = - Provider>((ref) => ref.watch(accountRepositoryProvider)); +final notesProvider = ChangeNotifierProvider.family( + (ref, account) => NoteRepository(ref.read(misskeyProvider(account)), account), +); -final iProvider = Provider.family((ref, acct) { +@Riverpod(dependencies: [accountContext]) +Raw notesWith(NotesWithRef ref) { + return ref.read(notesProvider(ref.read(accountContextProvider).getAccount)); +} + +@Riverpod(keepAlive: true) +EmojiRepository emojiRepository(EmojiRepositoryRef ref, Account account) => + EmojiRepositoryImpl( + misskey: ref.read(misskeyProvider(account)), + account: account, + accountSettingsRepository: ref.read(accountSettingsRepositoryProvider), + sharePreferenceController: ref.read(sharedPrefenceControllerProvider), + ); + +@riverpod +List accounts(AccountsRef ref) => ref.watch(accountRepositoryProvider); + +@riverpod +MeDetailed i(IRef ref, Acct acct) { final accounts = ref.watch(accountsProvider); final account = accounts.firstWhere((account) => account.acct == acct); return account.i; -}); +} -final accountProvider = Provider.family( - (ref, acct) => ref.watch( - accountsProvider.select( - (accounts) => accounts.firstWhere( - (account) => account.acct == acct, +@riverpod +Account account(AccountRef ref, Acct acct) => ref.watch( + accountsProvider.select( + (accounts) => accounts.firstWhere((account) => account.acct == acct), ), - ), - ), -); + ); final tabSettingsRepositoryProvider = ChangeNotifierProvider((ref) => TabSettingsRepository()); @@ -247,62 +120,118 @@ final desktopSettingsRepositoryProvider = final errorEventProvider = StateProvider<(Object? error, BuildContext? context)>( - (ref) => (null, null)); - -final photoEditProvider = - StateNotifierProvider.autoDispose( - (ref) => PhotoEditStateNotifier(const PhotoEdit()), + (ref) => (null, null), ); -final importExportRepository = +final importExportRepositoryProvider = ChangeNotifierProvider((ref) => ImportExportRepository(ref.read)); -// TODO: 下書きの機能かんがえるときにfamilyの引数みなおす -final noteCreateProvider = StateNotifierProvider.family - .autoDispose( - (ref, account) => NoteCreateNotifier( - NoteCreate( - account: account, - noteVisibility: ref - .read(accountSettingsRepositoryProvider) - .fromAccount(account) - .defaultNoteVisibility, - localOnly: ref - .read(accountSettingsRepositoryProvider) - .fromAccount(account) - .defaultIsLocalOnly, - reactionAcceptance: ref - .read(accountSettingsRepositoryProvider) - .fromAccount(account) - .defaultReactionAcceptance), - ref.read(fileSystemProvider), - ref.read(dioProvider), - ref.read(misskeyProvider(account)), - ref.read(errorEventProvider.notifier), - ref.read(notesProvider(account))), -); +@Riverpod(keepAlive: true) +BaseCacheManager? cacheManager(CacheManagerRef ref) => null; -final misskeyServerListNotifierProvider = AsyncNotifierProvider.autoDispose< - MisskeyServerListNotifier, List>( - MisskeyServerListNotifier.new, -); +@freezed +class AccountContext with _$AccountContext { + const factory AccountContext({ + /// 他鯖を取得するなどの目的で、非ログイン状態として使用されるアカウント + required Account getAccount, + required Account postAccount, + }) = _AccountContext; -final cacheManagerProvider = Provider((ref) => null); + factory AccountContext.as(Account account) => + AccountContext(getAccount: account, postAccount: account); -final misskeyNoteNotifierProvider = - NotifierProvider.family( - MisskeyNoteNotifier.new, -); + const AccountContext._(); -final usersListsNotifierProvider = AsyncNotifierProvider.autoDispose - .family, Misskey>( - UsersListsNotifier.new, -); + bool get isSame => getAccount == postAccount; +} -final antennasNotifierProvider = AsyncNotifierProvider.autoDispose - .family, Misskey>( - AntennasNotifier.new, -); +@Riverpod(dependencies: []) +AccountContext accountContext(AccountContextRef ref) => + throw UnimplementedError(); + +@Riverpod(dependencies: [accountContext]) +Misskey misskeyGetContext(MisskeyGetContextRef ref) { + final account = + ref.read(accountContextProvider.select((value) => value.getAccount)); + return ref.read(misskeyProvider(account)); +} -final clipsNotifierProvider = AsyncNotifierProvider.autoDispose - .family, Misskey>(ClipsNotifier.new); +@Riverpod(dependencies: [accountContext]) +Misskey misskeyPostContext(MisskeyPostContextRef ref) { + final account = + ref.read(accountContextProvider.select((value) => value.postAccount)); + return ref.read(misskeyProvider(account)); +} + +final timelineProvider = + ChangeNotifierProvider.family( + (ref, setting) { + final account = ref.read(accountProvider(setting.acct)); + + return switch (setting.tabType) { + TabType.localTimeline => LocalTimelineRepository( + ref.read(misskeyProvider(account)), + account, + ref.read(notesProvider(account)), + ref.read(generalSettingsRepositoryProvider), + setting, + ref, + ), + TabType.homeTimeline => HomeTimelineRepository( + ref.read(misskeyProvider(account)), + account, + ref.read(notesProvider(account)), + ref.read(generalSettingsRepositoryProvider), + setting, + ref, + ), + TabType.globalTimeline => GlobalTimelineRepository( + ref.read(misskeyProvider(account)), + account, + ref.read(notesProvider(account)), + ref.read(generalSettingsRepositoryProvider), + setting, + ref, + ), + TabType.hybridTimeline => HybridTimelineRepository( + ref.read(misskeyProvider(account)), + account, + ref.read(notesProvider(account)), + ref.read(generalSettingsRepositoryProvider), + setting, + ref, + ), + TabType.roleTimeline => RoleTimelineRepository( + ref.read(misskeyProvider(account)), + account, + ref.read(notesProvider(account)), + ref.read(generalSettingsRepositoryProvider), + setting, + ref, + ), + TabType.channel => ChannelTimelineRepository( + ref.read(misskeyProvider(account)), + account, + ref.read(notesProvider(account)), + ref.read(generalSettingsRepositoryProvider), + setting, + ref, + ), + TabType.userList => UserListTimelineRepository( + ref.read(misskeyProvider(account)), + account, + ref.read(notesProvider(account)), + ref.read(generalSettingsRepositoryProvider), + setting, + ref, + ), + TabType.antenna => AntennaTimelineRepository( + ref.read(misskeyProvider(account)), + account, + ref.read(notesProvider(account)), + ref.read(generalSettingsRepositoryProvider), + setting, + ref, + ) + }; +}); diff --git a/lib/providers.freezed.dart b/lib/providers.freezed.dart new file mode 100644 index 000000000..6637d8c54 --- /dev/null +++ b/lib/providers.freezed.dart @@ -0,0 +1,198 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'providers.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$AccountContext { + /// 他鯖を取得するなどの目的で、非ログイン状態として使用されるアカウント + Account get getAccount => throw _privateConstructorUsedError; + Account get postAccount => throw _privateConstructorUsedError; + + /// Create a copy of AccountContext + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $AccountContextCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $AccountContextCopyWith<$Res> { + factory $AccountContextCopyWith( + AccountContext value, $Res Function(AccountContext) then) = + _$AccountContextCopyWithImpl<$Res, AccountContext>; + @useResult + $Res call({Account getAccount, Account postAccount}); + + $AccountCopyWith<$Res> get getAccount; + $AccountCopyWith<$Res> get postAccount; +} + +/// @nodoc +class _$AccountContextCopyWithImpl<$Res, $Val extends AccountContext> + implements $AccountContextCopyWith<$Res> { + _$AccountContextCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of AccountContext + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? getAccount = null, + Object? postAccount = null, + }) { + return _then(_value.copyWith( + getAccount: null == getAccount + ? _value.getAccount + : getAccount // ignore: cast_nullable_to_non_nullable + as Account, + postAccount: null == postAccount + ? _value.postAccount + : postAccount // ignore: cast_nullable_to_non_nullable + as Account, + ) as $Val); + } + + /// Create a copy of AccountContext + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AccountCopyWith<$Res> get getAccount { + return $AccountCopyWith<$Res>(_value.getAccount, (value) { + return _then(_value.copyWith(getAccount: value) as $Val); + }); + } + + /// Create a copy of AccountContext + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AccountCopyWith<$Res> get postAccount { + return $AccountCopyWith<$Res>(_value.postAccount, (value) { + return _then(_value.copyWith(postAccount: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$AccountContextImplCopyWith<$Res> + implements $AccountContextCopyWith<$Res> { + factory _$$AccountContextImplCopyWith(_$AccountContextImpl value, + $Res Function(_$AccountContextImpl) then) = + __$$AccountContextImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({Account getAccount, Account postAccount}); + + @override + $AccountCopyWith<$Res> get getAccount; + @override + $AccountCopyWith<$Res> get postAccount; +} + +/// @nodoc +class __$$AccountContextImplCopyWithImpl<$Res> + extends _$AccountContextCopyWithImpl<$Res, _$AccountContextImpl> + implements _$$AccountContextImplCopyWith<$Res> { + __$$AccountContextImplCopyWithImpl( + _$AccountContextImpl _value, $Res Function(_$AccountContextImpl) _then) + : super(_value, _then); + + /// Create a copy of AccountContext + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? getAccount = null, + Object? postAccount = null, + }) { + return _then(_$AccountContextImpl( + getAccount: null == getAccount + ? _value.getAccount + : getAccount // ignore: cast_nullable_to_non_nullable + as Account, + postAccount: null == postAccount + ? _value.postAccount + : postAccount // ignore: cast_nullable_to_non_nullable + as Account, + )); + } +} + +/// @nodoc + +class _$AccountContextImpl extends _AccountContext { + const _$AccountContextImpl( + {required this.getAccount, required this.postAccount}) + : super._(); + + /// 他鯖を取得するなどの目的で、非ログイン状態として使用されるアカウント + @override + final Account getAccount; + @override + final Account postAccount; + + @override + String toString() { + return 'AccountContext(getAccount: $getAccount, postAccount: $postAccount)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$AccountContextImpl && + (identical(other.getAccount, getAccount) || + other.getAccount == getAccount) && + (identical(other.postAccount, postAccount) || + other.postAccount == postAccount)); + } + + @override + int get hashCode => Object.hash(runtimeType, getAccount, postAccount); + + /// Create a copy of AccountContext + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$AccountContextImplCopyWith<_$AccountContextImpl> get copyWith => + __$$AccountContextImplCopyWithImpl<_$AccountContextImpl>( + this, _$identity); +} + +abstract class _AccountContext extends AccountContext { + const factory _AccountContext( + {required final Account getAccount, + required final Account postAccount}) = _$AccountContextImpl; + const _AccountContext._() : super._(); + + /// 他鯖を取得するなどの目的で、非ログイン状態として使用されるアカウント + @override + Account get getAccount; + @override + Account get postAccount; + + /// Create a copy of AccountContext + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$AccountContextImplCopyWith<_$AccountContextImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/providers.g.dart b/lib/providers.g.dart new file mode 100644 index 000000000..5a3255592 --- /dev/null +++ b/lib/providers.g.dart @@ -0,0 +1,1013 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'providers.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$dioHash() => r'41b696b35e5b56ccb124ee5abab8b893747d2153'; + +/// See also [dio]. +@ProviderFor(dio) +final dioProvider = Provider.internal( + dio, + name: r'dioProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$dioHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef DioRef = ProviderRef; +String _$fileSystemHash() => r'98684b2a2a8fd9ee5818ec713ba28d29da92c168'; + +/// See also [fileSystem]. +@ProviderFor(fileSystem) +final fileSystemProvider = Provider.internal( + fileSystem, + name: r'fileSystemProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$fileSystemHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef FileSystemRef = ProviderRef; +String _$misskeyHash() => r'796d9e849aca70f97031c38e646439a01bc5abe5'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [misskey]. +@ProviderFor(misskey) +@Deprecated( + "Most case will be replace misskeyGetContext or misskeyPostContext, but will be remain") +const misskeyProvider = MisskeyFamily(); + +/// See also [misskey]. +class MisskeyFamily extends Family { + /// See also [misskey]. + const MisskeyFamily(); + + static const Iterable? _dependencies = null; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'misskeyProvider'; + + /// See also [misskey]. + MisskeyProvider call( + Account account, + ) { + return MisskeyProvider( + account, + ); + } + + @visibleForOverriding + @override + MisskeyProvider getProviderOverride( + covariant MisskeyProvider provider, + ) { + return call( + provider.account, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(Misskey Function(MisskeyRef ref) create) { + return _$MisskeyFamilyOverride(this, create); + } +} + +class _$MisskeyFamilyOverride implements FamilyOverride { + _$MisskeyFamilyOverride(this.overriddenFamily, this.create); + + final Misskey Function(MisskeyRef ref) create; + + @override + final MisskeyFamily overriddenFamily; + + @override + MisskeyProvider getProviderOverride( + covariant MisskeyProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [misskey]. +class MisskeyProvider extends Provider { + /// See also [misskey]. + MisskeyProvider( + Account account, + ) : this._internal( + (ref) => misskey( + ref as MisskeyRef, + account, + ), + from: misskeyProvider, + name: r'misskeyProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$misskeyHash, + dependencies: MisskeyFamily._dependencies, + allTransitiveDependencies: MisskeyFamily._allTransitiveDependencies, + account: account, + ); + + MisskeyProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.account, + }) : super.internal(); + + final Account account; + + @override + Override overrideWith( + Misskey Function(MisskeyRef ref) create, + ) { + return ProviderOverride( + origin: this, + override: MisskeyProvider._internal( + (ref) => create(ref as MisskeyRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + account: account, + ), + ); + } + + @override + (Account,) get argument { + return (account,); + } + + @override + ProviderElement createElement() { + return _MisskeyProviderElement(this); + } + + MisskeyProvider _copyWith( + Misskey Function(MisskeyRef ref) create, + ) { + return MisskeyProvider._internal( + (ref) => create(ref as MisskeyRef), + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + account: account, + ); + } + + @override + bool operator ==(Object other) { + return other is MisskeyProvider && other.account == account; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, account.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin MisskeyRef on ProviderRef { + /// The parameter `account` of this provider. + Account get account; +} + +class _MisskeyProviderElement extends ProviderElement with MisskeyRef { + _MisskeyProviderElement(super.provider); + + @override + Account get account => (origin as MisskeyProvider).account; +} + +String _$appRouterHash() => r'bb30ea3f6e2863af290ea7544f9a9bd2a53f79f4'; + +/// See also [appRouter]. +@ProviderFor(appRouter) +final appRouterProvider = Provider>.internal( + appRouter, + name: r'appRouterProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$appRouterHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef AppRouterRef = ProviderRef>; +String _$misskeyWithoutAccountHash() => + r'69fd2ed57ba01ab828bd39dd8adde72f977dd91e'; + +/// See also [misskeyWithoutAccount]. +@ProviderFor(misskeyWithoutAccount) +const misskeyWithoutAccountProvider = MisskeyWithoutAccountFamily(); + +/// See also [misskeyWithoutAccount]. +class MisskeyWithoutAccountFamily extends Family { + /// See also [misskeyWithoutAccount]. + const MisskeyWithoutAccountFamily(); + + static const Iterable? _dependencies = null; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'misskeyWithoutAccountProvider'; + + /// See also [misskeyWithoutAccount]. + MisskeyWithoutAccountProvider call( + String host, + ) { + return MisskeyWithoutAccountProvider( + host, + ); + } + + @visibleForOverriding + @override + MisskeyWithoutAccountProvider getProviderOverride( + covariant MisskeyWithoutAccountProvider provider, + ) { + return call( + provider.host, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(Misskey Function(MisskeyWithoutAccountRef ref) create) { + return _$MisskeyWithoutAccountFamilyOverride(this, create); + } +} + +class _$MisskeyWithoutAccountFamilyOverride implements FamilyOverride { + _$MisskeyWithoutAccountFamilyOverride(this.overriddenFamily, this.create); + + final Misskey Function(MisskeyWithoutAccountRef ref) create; + + @override + final MisskeyWithoutAccountFamily overriddenFamily; + + @override + MisskeyWithoutAccountProvider getProviderOverride( + covariant MisskeyWithoutAccountProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [misskeyWithoutAccount]. +class MisskeyWithoutAccountProvider extends AutoDisposeProvider { + /// See also [misskeyWithoutAccount]. + MisskeyWithoutAccountProvider( + String host, + ) : this._internal( + (ref) => misskeyWithoutAccount( + ref as MisskeyWithoutAccountRef, + host, + ), + from: misskeyWithoutAccountProvider, + name: r'misskeyWithoutAccountProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$misskeyWithoutAccountHash, + dependencies: MisskeyWithoutAccountFamily._dependencies, + allTransitiveDependencies: + MisskeyWithoutAccountFamily._allTransitiveDependencies, + host: host, + ); + + MisskeyWithoutAccountProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.host, + }) : super.internal(); + + final String host; + + @override + Override overrideWith( + Misskey Function(MisskeyWithoutAccountRef ref) create, + ) { + return ProviderOverride( + origin: this, + override: MisskeyWithoutAccountProvider._internal( + (ref) => create(ref as MisskeyWithoutAccountRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + host: host, + ), + ); + } + + @override + (String,) get argument { + return (host,); + } + + @override + AutoDisposeProviderElement createElement() { + return _MisskeyWithoutAccountProviderElement(this); + } + + MisskeyWithoutAccountProvider _copyWith( + Misskey Function(MisskeyWithoutAccountRef ref) create, + ) { + return MisskeyWithoutAccountProvider._internal( + (ref) => create(ref as MisskeyWithoutAccountRef), + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + host: host, + ); + } + + @override + bool operator ==(Object other) { + return other is MisskeyWithoutAccountProvider && other.host == host; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, host.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin MisskeyWithoutAccountRef on AutoDisposeProviderRef { + /// The parameter `host` of this provider. + String get host; +} + +class _MisskeyWithoutAccountProviderElement + extends AutoDisposeProviderElement with MisskeyWithoutAccountRef { + _MisskeyWithoutAccountProviderElement(super.provider); + + @override + String get host => (origin as MisskeyWithoutAccountProvider).host; +} + +String _$notesWithHash() => r'0650987360236bb7d00f08b92ab03ebb6bfeb413'; + +/// See also [notesWith]. +@ProviderFor(notesWith) +final notesWithProvider = AutoDisposeProvider>.internal( + notesWith, + name: r'notesWithProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$notesWithHash, + dependencies: [accountContextProvider], + allTransitiveDependencies: { + accountContextProvider, + ...?accountContextProvider.allTransitiveDependencies + }, +); + +typedef NotesWithRef = AutoDisposeProviderRef>; +String _$emojiRepositoryHash() => r'cce1a6d3e6daba91779840fde7973c6e6987e471'; + +/// See also [emojiRepository]. +@ProviderFor(emojiRepository) +const emojiRepositoryProvider = EmojiRepositoryFamily(); + +/// See also [emojiRepository]. +class EmojiRepositoryFamily extends Family { + /// See also [emojiRepository]. + const EmojiRepositoryFamily(); + + static const Iterable? _dependencies = null; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'emojiRepositoryProvider'; + + /// See also [emojiRepository]. + EmojiRepositoryProvider call( + Account account, + ) { + return EmojiRepositoryProvider( + account, + ); + } + + @visibleForOverriding + @override + EmojiRepositoryProvider getProviderOverride( + covariant EmojiRepositoryProvider provider, + ) { + return call( + provider.account, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith( + EmojiRepository Function(EmojiRepositoryRef ref) create) { + return _$EmojiRepositoryFamilyOverride(this, create); + } +} + +class _$EmojiRepositoryFamilyOverride implements FamilyOverride { + _$EmojiRepositoryFamilyOverride(this.overriddenFamily, this.create); + + final EmojiRepository Function(EmojiRepositoryRef ref) create; + + @override + final EmojiRepositoryFamily overriddenFamily; + + @override + EmojiRepositoryProvider getProviderOverride( + covariant EmojiRepositoryProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [emojiRepository]. +class EmojiRepositoryProvider extends Provider { + /// See also [emojiRepository]. + EmojiRepositoryProvider( + Account account, + ) : this._internal( + (ref) => emojiRepository( + ref as EmojiRepositoryRef, + account, + ), + from: emojiRepositoryProvider, + name: r'emojiRepositoryProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$emojiRepositoryHash, + dependencies: EmojiRepositoryFamily._dependencies, + allTransitiveDependencies: + EmojiRepositoryFamily._allTransitiveDependencies, + account: account, + ); + + EmojiRepositoryProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.account, + }) : super.internal(); + + final Account account; + + @override + Override overrideWith( + EmojiRepository Function(EmojiRepositoryRef ref) create, + ) { + return ProviderOverride( + origin: this, + override: EmojiRepositoryProvider._internal( + (ref) => create(ref as EmojiRepositoryRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + account: account, + ), + ); + } + + @override + (Account,) get argument { + return (account,); + } + + @override + ProviderElement createElement() { + return _EmojiRepositoryProviderElement(this); + } + + EmojiRepositoryProvider _copyWith( + EmojiRepository Function(EmojiRepositoryRef ref) create, + ) { + return EmojiRepositoryProvider._internal( + (ref) => create(ref as EmojiRepositoryRef), + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + account: account, + ); + } + + @override + bool operator ==(Object other) { + return other is EmojiRepositoryProvider && other.account == account; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, account.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin EmojiRepositoryRef on ProviderRef { + /// The parameter `account` of this provider. + Account get account; +} + +class _EmojiRepositoryProviderElement extends ProviderElement + with EmojiRepositoryRef { + _EmojiRepositoryProviderElement(super.provider); + + @override + Account get account => (origin as EmojiRepositoryProvider).account; +} + +String _$accountsHash() => r'd70730d18b80a35f4fac0b25ec10004ce706bef9'; + +/// See also [accounts]. +@ProviderFor(accounts) +final accountsProvider = AutoDisposeProvider>.internal( + accounts, + name: r'accountsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$accountsHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef AccountsRef = AutoDisposeProviderRef>; +String _$iHash() => r'ed4b1dd720889e8c1651bba7506fcb941b45342d'; + +/// See also [i]. +@ProviderFor(i) +const iProvider = IFamily(); + +/// See also [i]. +class IFamily extends Family { + /// See also [i]. + const IFamily(); + + static const Iterable? _dependencies = null; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'iProvider'; + + /// See also [i]. + IProvider call( + Acct acct, + ) { + return IProvider( + acct, + ); + } + + @visibleForOverriding + @override + IProvider getProviderOverride( + covariant IProvider provider, + ) { + return call( + provider.acct, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(MeDetailed Function(IRef ref) create) { + return _$IFamilyOverride(this, create); + } +} + +class _$IFamilyOverride implements FamilyOverride { + _$IFamilyOverride(this.overriddenFamily, this.create); + + final MeDetailed Function(IRef ref) create; + + @override + final IFamily overriddenFamily; + + @override + IProvider getProviderOverride( + covariant IProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [i]. +class IProvider extends AutoDisposeProvider { + /// See also [i]. + IProvider( + Acct acct, + ) : this._internal( + (ref) => i( + ref as IRef, + acct, + ), + from: iProvider, + name: r'iProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$iHash, + dependencies: IFamily._dependencies, + allTransitiveDependencies: IFamily._allTransitiveDependencies, + acct: acct, + ); + + IProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.acct, + }) : super.internal(); + + final Acct acct; + + @override + Override overrideWith( + MeDetailed Function(IRef ref) create, + ) { + return ProviderOverride( + origin: this, + override: IProvider._internal( + (ref) => create(ref as IRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + acct: acct, + ), + ); + } + + @override + (Acct,) get argument { + return (acct,); + } + + @override + AutoDisposeProviderElement createElement() { + return _IProviderElement(this); + } + + IProvider _copyWith( + MeDetailed Function(IRef ref) create, + ) { + return IProvider._internal( + (ref) => create(ref as IRef), + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + acct: acct, + ); + } + + @override + bool operator ==(Object other) { + return other is IProvider && other.acct == acct; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, acct.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin IRef on AutoDisposeProviderRef { + /// The parameter `acct` of this provider. + Acct get acct; +} + +class _IProviderElement extends AutoDisposeProviderElement + with IRef { + _IProviderElement(super.provider); + + @override + Acct get acct => (origin as IProvider).acct; +} + +String _$accountHash() => r'1614cc4c66271ef2e69f4a527237ec179b58db56'; + +/// See also [account]. +@ProviderFor(account) +const accountProvider = AccountFamily(); + +/// See also [account]. +class AccountFamily extends Family { + /// See also [account]. + const AccountFamily(); + + static const Iterable? _dependencies = null; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'accountProvider'; + + /// See also [account]. + AccountProvider call( + Acct acct, + ) { + return AccountProvider( + acct, + ); + } + + @visibleForOverriding + @override + AccountProvider getProviderOverride( + covariant AccountProvider provider, + ) { + return call( + provider.acct, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(Account Function(AccountRef ref) create) { + return _$AccountFamilyOverride(this, create); + } +} + +class _$AccountFamilyOverride implements FamilyOverride { + _$AccountFamilyOverride(this.overriddenFamily, this.create); + + final Account Function(AccountRef ref) create; + + @override + final AccountFamily overriddenFamily; + + @override + AccountProvider getProviderOverride( + covariant AccountProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [account]. +class AccountProvider extends AutoDisposeProvider { + /// See also [account]. + AccountProvider( + Acct acct, + ) : this._internal( + (ref) => account( + ref as AccountRef, + acct, + ), + from: accountProvider, + name: r'accountProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$accountHash, + dependencies: AccountFamily._dependencies, + allTransitiveDependencies: AccountFamily._allTransitiveDependencies, + acct: acct, + ); + + AccountProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.acct, + }) : super.internal(); + + final Acct acct; + + @override + Override overrideWith( + Account Function(AccountRef ref) create, + ) { + return ProviderOverride( + origin: this, + override: AccountProvider._internal( + (ref) => create(ref as AccountRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + acct: acct, + ), + ); + } + + @override + (Acct,) get argument { + return (acct,); + } + + @override + AutoDisposeProviderElement createElement() { + return _AccountProviderElement(this); + } + + AccountProvider _copyWith( + Account Function(AccountRef ref) create, + ) { + return AccountProvider._internal( + (ref) => create(ref as AccountRef), + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + acct: acct, + ); + } + + @override + bool operator ==(Object other) { + return other is AccountProvider && other.acct == acct; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, acct.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin AccountRef on AutoDisposeProviderRef { + /// The parameter `acct` of this provider. + Acct get acct; +} + +class _AccountProviderElement extends AutoDisposeProviderElement + with AccountRef { + _AccountProviderElement(super.provider); + + @override + Acct get acct => (origin as AccountProvider).acct; +} + +String _$cacheManagerHash() => r'0e854572e1bd7223650c8437a463a060314d0531'; + +/// See also [cacheManager]. +@ProviderFor(cacheManager) +final cacheManagerProvider = Provider.internal( + cacheManager, + name: r'cacheManagerProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$cacheManagerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef CacheManagerRef = ProviderRef; +String _$accountContextHash() => r'1c9bff1004e7054ed091327e5a83c07d9da2c20a'; + +/// See also [accountContext]. +@ProviderFor(accountContext) +final accountContextProvider = AutoDisposeProvider.internal( + accountContext, + name: r'accountContextProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$accountContextHash, + dependencies: const [], + allTransitiveDependencies: const {}, +); + +typedef AccountContextRef = AutoDisposeProviderRef; +String _$misskeyGetContextHash() => r'8c7e8fbe8b1add5fdd1f7a42efbeaefaf417657f'; + +/// See also [misskeyGetContext]. +@ProviderFor(misskeyGetContext) +final misskeyGetContextProvider = AutoDisposeProvider.internal( + misskeyGetContext, + name: r'misskeyGetContextProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$misskeyGetContextHash, + dependencies: [accountContextProvider], + allTransitiveDependencies: { + accountContextProvider, + ...?accountContextProvider.allTransitiveDependencies + }, +); + +typedef MisskeyGetContextRef = AutoDisposeProviderRef; +String _$misskeyPostContextHash() => + r'4c78e6717ea452a6e7bf8804a34e596edc71ab88'; + +/// See also [misskeyPostContext]. +@ProviderFor(misskeyPostContext) +final misskeyPostContextProvider = AutoDisposeProvider.internal( + misskeyPostContext, + name: r'misskeyPostContextProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$misskeyPostContextHash, + dependencies: [accountContextProvider], + allTransitiveDependencies: { + accountContextProvider, + ...?accountContextProvider.allTransitiveDependencies + }, +); + +typedef MisskeyPostContextRef = AutoDisposeProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/repository/account_repository.dart b/lib/repository/account_repository.dart index c16bcaaeb..20134da2b 100644 --- a/lib/repository/account_repository.dart +++ b/lib/repository/account_repository.dart @@ -1,17 +1,22 @@ -import 'dart:convert'; - -import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/account_settings.dart'; -import 'package:miria/model/acct.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/repository/shared_preference_controller.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:shared_preference_app_group/shared_preference_app_group.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:uuid/uuid.dart'; +// ignore_for_file: avoid_dynamic_calls + +import "dart:convert"; + +import "package:dio/dio.dart"; +import "package:flutter/foundation.dart"; +import "package:miria/log.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/account_settings.dart"; +import "package:miria/model/acct.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/shared_preference_controller.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; +import "package:shared_preference_app_group/shared_preference_app_group.dart"; +import "package:url_launcher/url_launcher.dart"; +import "package:uuid/uuid.dart"; + +part "account_repository.g.dart"; sealed class ValidateMisskeyException implements Exception {} @@ -46,7 +51,8 @@ class AlreadyLoggedInException implements ValidateMisskeyException { final String acct; } -class AccountRepository extends Notifier> { +@riverpod +class AccountRepository extends _$AccountRepository { late final SharedPreferenceController sharedPreferenceController = ref.read(sharedPrefenceControllerProvider); @@ -64,24 +70,27 @@ class AccountRepository extends Notifier> { Future load() async { if (defaultTargetPlatform == TargetPlatform.iOS) { await SharedPreferenceAppGroup.setAppGroup( - "group.info.shiosyakeyakini.miria"); + "group.info.shiosyakeyakini.miria", + ); } - final String? storedData = + final storedData = await sharedPreferenceController.getStringSecure("accounts"); if (storedData == null) return; try { - final list = (jsonDecode(storedData) as List); + final list = jsonDecode(storedData) as List; final resultList = List.of(list); for (final element in list) { - if (element["meta"] == null) { + if ((element as Map)["meta"] == null) { try { final meta = await ref .read(misskeyWithoutAccountProvider(element["host"])) .meta(); element["meta"] = jsonDecode(jsonEncode(meta.toJson())); - } catch (e) {} + } catch (e) { + logger.warning(e); + } } } @@ -142,7 +151,6 @@ class AccountRepository extends Notifier> { switch (setting.iCacheStrategy) { case CacheStrategy.whenLaunch: if (!_validatedAccts.contains(acct)) await updateI(account); - break; case CacheStrategy.whenOneDay: final latestUpdated = setting.latestICached; if (latestUpdated == null || @@ -151,14 +159,12 @@ class AccountRepository extends Notifier> { } case CacheStrategy.whenTabChange: await updateI(account); - break; } }), Future(() async { switch (setting.metaChacheStrategy) { case CacheStrategy.whenLaunch: if (!_validateMetaAccts.contains(acct)) await updateMeta(account); - break; case CacheStrategy.whenOneDay: final latestUpdated = setting.latestMetaCached; if (latestUpdated == null || @@ -167,9 +173,8 @@ class AccountRepository extends Notifier> { } case CacheStrategy.whenTabChange: await updateMeta(account); - break; } - }) + }), ]); await _save(); @@ -183,7 +188,7 @@ class AccountRepository extends Notifier> { final i = state[index].i.copyWith( unreadAnnouncements: [ ...state[index].i.unreadAnnouncements, - announcement + announcement, ], ); @@ -236,9 +241,10 @@ class AccountRepository extends Notifier> { final Uri uri; try { uri = Uri( - scheme: "https", - host: server, - pathSegments: [".well-known", "nodeinfo"]); + scheme: "https", + host: server, + pathSegments: [".well-known", "nodeinfo"], + ); } catch (e) { throw InvalidServerException(server); } @@ -281,14 +287,17 @@ class AccountRepository extends Notifier> { } Future loginAsPassword( - String server, String userId, String password) async { + String server, + String userId, + String password, + ) async { final token = await MisskeyServer().loginAsPassword(server, userId, password); final i = await Misskey(token: token, host: server).i.i(); final meta = await Misskey(token: token, host: server).meta(); final account = Account(host: server, token: token, userId: userId, i: i, meta: meta); - _addAccount(account); + await _addAccount(account); } Future loginAsToken(String server, String token) async { @@ -339,16 +348,13 @@ class AccountRepository extends Notifier> { await ref .read(tabSettingsRepositoryProvider) .initializeTabSettings(account); - (); } Future reorder(int oldIndex, int newIndex) async { - if (oldIndex < newIndex) { - newIndex -= 1; - } + final actualIndex = oldIndex < newIndex ? -1 : newIndex; final newState = state.toList(); final item = newState.removeAt(oldIndex); - newState.insert(newIndex, item); + newState.insert(actualIndex, item); state = newState; await _save(); diff --git a/lib/repository/account_repository.g.dart b/lib/repository/account_repository.g.dart new file mode 100644 index 000000000..241590792 --- /dev/null +++ b/lib/repository/account_repository.g.dart @@ -0,0 +1,26 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'account_repository.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$accountRepositoryHash() => r'8d1b97fd138499b9c1431688ae5b028c8944c5b4'; + +/// See also [AccountRepository]. +@ProviderFor(AccountRepository) +final accountRepositoryProvider = + AutoDisposeNotifierProvider>.internal( + AccountRepository.new, + name: r'accountRepositoryProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$accountRepositoryHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$AccountRepository = AutoDisposeNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/repository/account_settings_repository.dart b/lib/repository/account_settings_repository.dart index 9d10e99ac..4d27aa318 100644 --- a/lib/repository/account_settings_repository.dart +++ b/lib/repository/account_settings_repository.dart @@ -1,12 +1,12 @@ -import 'dart:convert'; +import "dart:convert"; -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/account_settings.dart'; -import 'package:miria/model/acct.dart'; -import 'package:shared_preference_app_group/shared_preference_app_group.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import "package:collection/collection.dart"; +import "package:flutter/foundation.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/account_settings.dart"; +import "package:miria/model/acct.dart"; +import "package:shared_preference_app_group/shared_preference_app_group.dart"; +import "package:shared_preferences/shared_preferences.dart"; class AccountSettingsRepository extends ChangeNotifier { List _accountSettings = []; @@ -15,9 +15,8 @@ class AccountSettingsRepository extends ChangeNotifier { Future load() async { if (defaultTargetPlatform == TargetPlatform.iOS) { await SharedPreferenceAppGroup.setAppGroup( - "group.info.shiosyakeyakini.miria"); - final key = await SharedPreferenceAppGroup.get("account_settings"); - print(key); + "group.info.shiosyakeyakini.miria", + ); } final prefs = await SharedPreferences.getInstance(); @@ -34,14 +33,19 @@ class AccountSettingsRepository extends ChangeNotifier { } } else { if (defaultTargetPlatform == TargetPlatform.iOS) { - await SharedPreferenceAppGroup.setString("account_settings", storedData); + await SharedPreferenceAppGroup.setString( + "account_settings", + storedData, + ); } } try { _accountSettings ..clear() - ..addAll((jsonDecode(storedData) as List) - .map((e) => AccountSettings.fromJson(e))); + ..addAll( + (jsonDecode(storedData) as List) + .map((e) => AccountSettings.fromJson(e)), + ); } catch (e) { if (kDebugMode) print(e); } @@ -61,7 +65,7 @@ class AccountSettingsRepository extends ChangeNotifier { await prefs.setString("account_settings", value); notifyListeners(); if (defaultTargetPlatform == TargetPlatform.iOS) { - SharedPreferenceAppGroup.setString("account_settings", value); + await SharedPreferenceAppGroup.setString("account_settings", value); } } diff --git a/lib/repository/antenna_timeline_repository.dart b/lib/repository/antenna_timeline_repository.dart index e564f2aeb..3419eb498 100644 --- a/lib/repository/antenna_timeline_repository.dart +++ b/lib/repository/antenna_timeline_repository.dart @@ -1,40 +1,18 @@ -import 'dart:async'; +import "dart:async"; -import 'package:miria/repository/socket_timeline_repository.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:miria/repository/socket_timeline_repository.dart"; +import "package:misskey_dart/misskey_dart.dart"; class AntennaTimelineRepository extends SocketTimelineRepository { AntennaTimelineRepository( super.misskey, super.account, super.noteRepository, - super.globalNotificationRepository, super.generalSettingsRepository, super.tabSetting, - super.mainStreamRepository, - super.accountRepository, - super.emojiRepository, + super.ref, ); - @override - SocketController createSocketController({ - required void Function(Note note) onReceived, - required FutureOr Function(String id, TimelineReacted reaction) - onReacted, - required FutureOr Function(String id, TimelineReacted reaction) - onUnreacted, - required FutureOr Function(String id, TimelineVoted vote) onVoted, - required FutureOr Function(String id, NoteEdited note) onUpdated, - }) { - return misskey.antennaStream( - antennaId: tabSetting.antennaId!, - onNoteReceived: onReceived, - onReacted: onReacted, - onUnreacted: onUnreacted, - onVoted: onVoted, - onUpdated: onUpdated); - } - @override Future> requestNotes({String? untilId}) async { return await misskey.antennas.notes( @@ -45,4 +23,10 @@ class AntennaTimelineRepository extends SocketTimelineRepository { ), ); } + + @override + Channel get channel => Channel.antenna; + + @override + Map get parameters => {"antennaId": tabSetting.antennaId}; } diff --git a/lib/repository/channel_time_line_repository.dart b/lib/repository/channel_time_line_repository.dart index e2e8906a8..f29c3dca4 100644 --- a/lib/repository/channel_time_line_repository.dart +++ b/lib/repository/channel_time_line_repository.dart @@ -1,40 +1,18 @@ -import 'dart:async'; +import "dart:async"; -import 'package:miria/repository/socket_timeline_repository.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:miria/repository/socket_timeline_repository.dart"; +import "package:misskey_dart/misskey_dart.dart"; class ChannelTimelineRepository extends SocketTimelineRepository { ChannelTimelineRepository( super.misskey, super.account, super.noteRepository, - super.globalNotificationRepository, super.generalSettingsRepository, super.tabSetting, - super.mainStreamRepository, - super.accountRepository, - super.emojiRepository, + super.ref, ); - @override - SocketController createSocketController({ - required void Function(Note note) onReceived, - required FutureOr Function(String id, TimelineReacted reaction) - onReacted, - required FutureOr Function(String id, TimelineReacted reaction) - onUnreacted, - required FutureOr Function(String id, TimelineVoted vote) onVoted, - required FutureOr Function(String id, NoteEdited note) onUpdated, - }) { - return misskey.channelStream( - channelId: tabSetting.channelId!, - onNoteReceived: onReceived, - onReacted: onReacted, - onUnreacted: onUnreacted, - onVoted: onVoted, - onUpdated: onUpdated); - } - @override Future> requestNotes({String? untilId}) async { return await misskey.channels.timeline( @@ -45,4 +23,10 @@ class ChannelTimelineRepository extends SocketTimelineRepository { ), ); } + + @override + Channel get channel => Channel.channel; + + @override + Map get parameters => {"channelId": tabSetting.channelId}; } diff --git a/lib/repository/desktop_settings_repository.dart b/lib/repository/desktop_settings_repository.dart index e8a2fdc2c..395143140 100644 --- a/lib/repository/desktop_settings_repository.dart +++ b/lib/repository/desktop_settings_repository.dart @@ -1,10 +1,10 @@ -import 'dart:convert'; -import 'dart:io'; +import "dart:convert"; +import "dart:io"; -import 'package:flutter/foundation.dart'; -import 'package:miria/model/desktop_settings.dart'; -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; +import "package:flutter/foundation.dart"; +import "package:miria/model/desktop_settings.dart"; +import "package:path/path.dart"; +import "package:path_provider/path_provider.dart"; class DesktopSettingsRepository extends ChangeNotifier { var _settings = const DesktopSettings(); @@ -33,6 +33,8 @@ class DesktopSettingsRepository extends ChangeNotifier { Future getSettingPath() async { return join( - (await getApplicationSupportDirectory()).path, "desktopSettings.json"); + (await getApplicationSupportDirectory()).path, + "desktopSettings.json", + ); } } diff --git a/lib/repository/emoji_repository.dart b/lib/repository/emoji_repository.dart index c8b440a14..f02276d35 100644 --- a/lib/repository/emoji_repository.dart +++ b/lib/repository/emoji_repository.dart @@ -1,18 +1,20 @@ -import 'dart:convert'; - -import 'package:collection/collection.dart'; -import 'package:flutter/services.dart'; -import 'package:kana_kit/kana_kit.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/account_settings.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/model/unicode_emoji.dart'; -import 'package:miria/repository/account_settings_repository.dart'; -import 'package:miria/repository/shared_preference_controller.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "dart:collection"; +import "dart:convert"; + +import "package:collection/collection.dart"; +import "package:flutter/services.dart"; +import "package:kana_kit/kana_kit.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/account_settings.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/model/unicode_emoji.dart"; +import "package:miria/repository/account_settings_repository.dart"; +import "package:miria/repository/shared_preference_controller.dart"; +import "package:misskey_dart/misskey_dart.dart"; abstract class EmojiRepository { List? emoji; + Map? emojiMap; Future loadFromSourceIfNeed(); Future loadFromSource(); @@ -79,9 +81,11 @@ class EmojiRepositoryImpl extends EmojiRepository { jsonEncode(serverFetchData), ); - await accountSettingsRepository.save(accountSettingsRepository - .fromAccount(account) - .copyWith(latestEmojiCached: DateTime.now())); + await accountSettingsRepository.save( + accountSettingsRepository + .fromAccount(account) + .copyWith(latestEmojiCached: DateTime.now()), + ); } thisLaunchLoaded = true; } @@ -93,16 +97,13 @@ class EmojiRepositoryImpl extends EmojiRepository { switch (settings.emojiCacheStrategy) { case CacheStrategy.whenTabChange: await loadFromSource(); - break; case CacheStrategy.whenLaunch: if (thisLaunchLoaded) return; await loadFromSource(); - break; case CacheStrategy.whenOneDay: if (latestUpdated == null || latestUpdated.day != DateTime.now().day) { await loadFromSource(); } - break; } } @@ -121,36 +122,49 @@ class EmojiRepositoryImpl extends EmojiRepository { (jsonDecode(await rootBundle.loadString("assets/emoji_list.json")) as List) .map((e) => UnicodeEmoji.fromJson(e)) - .map((e) => EmojiRepositoryData( - emoji: UnicodeEmojiData(char: e.char), - kanaName: toH(format(e.char)), - kanaAliases: [e.name, ...e.keywords] - .map((e2) => toH(format(e2))) - .toList(), - aliases: [e.name, ...e.keywords], - category: e.category, - )); + .map( + (e) => EmojiRepositoryData( + emoji: UnicodeEmojiData(char: e.char), + kanaName: toH(format(e.char)), + kanaAliases: [e.name, ...e.keywords] + .map((e2) => toH(format(e2))) + .toList(), + aliases: [e.name, ...e.keywords], + category: e.category, + ), + ); emoji = response.emojis - .map((e) => EmojiRepositoryData( - emoji: CustomEmojiData( - baseName: e.name, - hostedName: ":${e.name}@.:", - url: e.url, - isCurrentServer: true, - isSensitive: e.isSensitive, - ), - category: e.category ?? "", - kanaName: toH(format(e.name)), - aliases: e.aliases, - kanaAliases: e.aliases.map((e2) => format(toH(e2))).toList(), - )) + .map( + (e) => EmojiRepositoryData( + emoji: CustomEmojiData( + baseName: e.name, + hostedName: ":${e.name}@.:", + url: e.url, + isCurrentServer: true, + isSensitive: e.isSensitive, + ), + category: e.category ?? "", + kanaName: toH(format(e.name)), + aliases: e.aliases, + kanaAliases: e.aliases.map((e2) => format(toH(e2))).toList(), + ), + ) .toList(); emoji!.addAll(unicodeEmojis); + + emojiMap = HashMap.fromIterable( + emoji!, + key: (e) => (e as EmojiRepositoryData).emoji.baseName, + value: (e) => e, + ); } bool emojiSearchCondition( - String query, String convertedQuery, EmojiRepositoryData element) { + String query, + String convertedQuery, + EmojiRepositoryData element, + ) { if (query.length == 1) { return element.emoji.baseName == query || element.aliases.any((element2) => element2 == query) || @@ -165,8 +179,10 @@ class EmojiRepositoryImpl extends EmojiRepository { } @override - Future> searchEmojis(String name, - {int limit = 30}) async { + Future> searchEmojis( + String name, { + int limit = 30, + }) async { if (name == "") { return defaultEmojis(limit: limit); } @@ -180,16 +196,16 @@ class EmojiRepositoryImpl extends EmojiRepository { if (a.emoji.baseName.contains(name)) a.emoji.baseName, ...a.aliases.where((e2) => e2.contains(name)), if (a.kanaName.contains(converted)) a.kanaName, - ...a.kanaAliases.where((e2) => e2.contains(converted)) + ...a.kanaAliases.where((e2) => e2.contains(converted)), ].map((e) => e.length); final bValue = [ if (b.emoji.baseName.contains(name)) b.emoji.baseName, ...b.aliases.where((element2) => element2.contains(name)), if (b.kanaName.contains(converted)) b.kanaName, - ...b.kanaAliases.where((e2) => e2.contains(converted)) + ...b.kanaAliases.where((e2) => e2.contains(converted)), ].map((e) => e.length); - var ret = aValue.min.compareTo(bValue.min); + final ret = aValue.min.compareTo(bValue.min); if (ret != 0) return ret; if (a.emoji is CustomEmojiData) return -1; return 0; @@ -208,8 +224,7 @@ class EmojiRepositoryImpl extends EmojiRepository { return []; } else { return reactionDeck - .map((e) => - emoji?.firstWhereOrNull((element) => element.emoji.baseName == e)) + .map((e) => emojiMap?[e]) .whereNotNull() .map((e) => e.emoji) .toList(); diff --git a/lib/repository/favorite_repository.dart b/lib/repository/favorite_repository.dart index 6e42adeb5..4f8d85256 100644 --- a/lib/repository/favorite_repository.dart +++ b/lib/repository/favorite_repository.dart @@ -1,6 +1,6 @@ -import 'package:flutter/foundation.dart'; -import 'package:miria/repository/note_repository.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/foundation.dart"; +import "package:miria/repository/note_repository.dart"; +import "package:misskey_dart/misskey_dart.dart"; class FavoriteRepository extends ChangeNotifier { final Misskey misskey; @@ -12,10 +12,12 @@ class FavoriteRepository extends ChangeNotifier { List get notes => _notes; Future getFavorites() async { - final response = await misskey.i.favorites(IFavoritesRequest( - untilId: _notes.isEmpty ? null : _notes.last.id, - limit: 50, - )); + final response = await misskey.i.favorites( + IFavoritesRequest( + untilId: _notes.isEmpty ? null : _notes.last.id, + limit: 50, + ), + ); final responseNotes = response.map((e) => e.note); _notes = [..._notes, ...responseNotes]; noteRepository.registerAll(responseNotes); diff --git a/lib/repository/general_settings_repository.dart b/lib/repository/general_settings_repository.dart index 1cf42aa12..3edd9c163 100644 --- a/lib/repository/general_settings_repository.dart +++ b/lib/repository/general_settings_repository.dart @@ -1,8 +1,8 @@ -import 'dart:convert'; +import "dart:convert"; -import 'package:flutter/foundation.dart'; -import 'package:miria/model/general_settings.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import "package:flutter/foundation.dart"; +import "package:miria/model/general_settings.dart"; +import "package:shared_preferences/shared_preferences.dart"; class GeneralSettingsRepository extends ChangeNotifier { var _settings = const GeneralSettings(); diff --git a/lib/repository/global_time_line_repository.dart b/lib/repository/global_time_line_repository.dart index 0240542f6..dd48fd3af 100644 --- a/lib/repository/global_time_line_repository.dart +++ b/lib/repository/global_time_line_repository.dart @@ -1,50 +1,31 @@ -import 'package:miria/repository/time_line_repository.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "dart:async"; -class GlobalTimeLineRepository extends TimelineRepository { - SocketController? socketController; +import "package:miria/repository/socket_timeline_repository.dart"; +import "package:misskey_dart/misskey_dart.dart"; - final Misskey misskey; - - GlobalTimeLineRepository( - this.misskey, +class GlobalTimelineRepository extends SocketTimelineRepository { + GlobalTimelineRepository( + super.misskey, + super.account, super.noteRepository, - super.globalNotificationRepository, super.generalSettingsRepository, super.tabSetting, + super.ref, ); @override - void startTimeLine() { - socketController = misskey.globalTimelineStream( - parameter: GlobalTimelineParameter( - withRenotes: tabSetting.renoteDisplay, - withFiles: tabSetting.isMediaOnly, + Future> requestNotes({String? untilId}) async { + return await misskey.notes.globalTimeline( + NotesGlobalTimelineRequest( + limit: 30, + untilId: untilId, ), - onNoteReceived: (note) { - newerNotes.add(note); - - notifyListeners(); - }, ); - misskey.startStreaming(); } @override - void disconnect() { - socketController?.disconnect(); - } + Channel get channel => Channel.globalTimeline; @override - Future reconnect() async { - await super.reconnect(); - socketController?.reconnect(); - } - - @override - void dispose() { - super.dispose(); - socketController?.disconnect(); - socketController = null; - } + Map get parameters => {"channelId": tabSetting.channelId}; } diff --git a/lib/repository/home_time_line_repository.dart b/lib/repository/home_time_line_repository.dart index db2e4000c..38f2ae543 100644 --- a/lib/repository/home_time_line_repository.dart +++ b/lib/repository/home_time_line_repository.dart @@ -1,43 +1,18 @@ -import 'dart:async'; +import "dart:async"; -import 'package:miria/repository/socket_timeline_repository.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:miria/repository/socket_timeline_repository.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class HomeTimeLineRepository extends SocketTimelineRepository { - HomeTimeLineRepository( +class HomeTimelineRepository extends SocketTimelineRepository { + HomeTimelineRepository( super.misskey, super.account, super.noteRepository, - super.globalNotificationRepository, super.generalSettingsRepository, super.tabSetting, - super.mainStreamRepository, - super.accountRepository, - super.emojiRepository, + super.ref, ); - @override - SocketController createSocketController({ - required void Function(Note note) onReceived, - required FutureOr Function(String id, TimelineReacted reaction) - onReacted, - required FutureOr Function(String id, TimelineReacted reaction) - onUnreacted, - required FutureOr Function(String id, TimelineVoted vote) onVoted, - required FutureOr Function(String id, NoteEdited note) onUpdated, - }) { - return misskey.homeTimelineStream( - parameter: HomeTimelineParameter( - withRenotes: tabSetting.renoteDisplay, - withFiles: tabSetting.isMediaOnly, - ), - onNoteReceived: onReceived, - onReacted: onReacted, - onUnreacted: onUnreacted, - onVoted: onVoted, - onUpdated: onUpdated); - } - @override Future> requestNotes({String? untilId}) async { return await misskey.notes.homeTimeline( @@ -49,4 +24,13 @@ class HomeTimeLineRepository extends SocketTimelineRepository { ), ); } + + @override + Channel get channel => Channel.homeTimeline; + + @override + Map get parameters => { + "withRenotes": tabSetting.renoteDisplay, + "withFiles": tabSetting.isMediaOnly, + }; } diff --git a/lib/repository/hybrid_timeline_repository.dart b/lib/repository/hybrid_timeline_repository.dart index 9270d7db5..0b01f1ea1 100644 --- a/lib/repository/hybrid_timeline_repository.dart +++ b/lib/repository/hybrid_timeline_repository.dart @@ -1,51 +1,33 @@ -import 'dart:async'; +import "dart:async"; -import 'package:miria/repository/socket_timeline_repository.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:miria/repository/socket_timeline_repository.dart"; +import "package:misskey_dart/misskey_dart.dart"; class HybridTimelineRepository extends SocketTimelineRepository { HybridTimelineRepository( super.misskey, super.account, super.noteRepository, - super.globalNotificationRepository, super.generalSettingsRepository, super.tabSetting, - super.mainStreamRepository, - super.accountRepository, - super.emojiRepository, + super.ref, ); @override - SocketController createSocketController({ - required void Function(Note note) onReceived, - required FutureOr Function(String id, TimelineReacted reaction) - onReacted, - required FutureOr Function(String id, TimelineReacted reaction) - onUnreacted, - required FutureOr Function(String id, TimelineVoted vote) onVoted, - required FutureOr Function(String id, NoteEdited note) onUpdated, - }) { - return misskey.hybridTimelineStream( - parameter: HybridTimelineParameter( - withRenotes: tabSetting.renoteDisplay, - withReplies: tabSetting.isIncludeReplies, - withFiles: tabSetting.isMediaOnly, - ), - onNoteReceived: onReceived, - onReacted: onReacted, - onUnreacted: onUnreacted, - onVoted: onVoted, - onUpdated: onUpdated); + Future> requestNotes({String? untilId}) async { + return await misskey.notes.hybridTimeline( + NotesHybridTimelineRequest( + untilId: untilId, + withRenotes: tabSetting.renoteDisplay, + withReplies: tabSetting.isIncludeReplies, + withFiles: tabSetting.isMediaOnly, + ), + ); } @override - Future> requestNotes({String? untilId}) async { - return await misskey.notes.hybridTimeline(NotesHybridTimelineRequest( - untilId: untilId, - withRenotes: tabSetting.renoteDisplay, - withReplies: tabSetting.isIncludeReplies, - withFiles: tabSetting.isMediaOnly, - )); - } + Channel get channel => Channel.hybridTimeline; + + @override + Map get parameters => {}; } diff --git a/lib/repository/import_export_repository.dart b/lib/repository/import_export_repository.dart index 5d1976974..9966b1765 100644 --- a/lib/repository/import_export_repository.dart +++ b/lib/repository/import_export_repository.dart @@ -1,22 +1,22 @@ -import 'dart:convert'; - -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/exported_setting.dart'; -import 'package:miria/model/tab_setting.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; -import 'package:miria/view/dialogs/simple_message_dialog.dart'; -import 'package:miria/view/settings_page/import_export_page/folder_select_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:package_info_plus/package_info_plus.dart'; +import "dart:convert"; + +import "package:auto_route/auto_route.dart"; +import "package:collection/collection.dart"; +import "package:dio/dio.dart"; +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/exported_setting.dart"; +import "package:miria/model/tab_setting.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/dialogs/simple_confirm_dialog.dart"; +import "package:miria/view/dialogs/simple_message_dialog.dart"; +import "package:miria/view/settings_page/import_export_page/folder_select_dialog.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:package_info_plus/package_info_plus.dart"; class ImportExportRepository extends ChangeNotifier { final T Function(ProviderListenable provider) reader; @@ -45,9 +45,8 @@ class ImportExportRepository extends ChangeNotifier { } Future import(BuildContext context, Account account) async { - final result = await showDialog( - context: context, - builder: (context2) => FolderSelectDialog( + final result = await context.pushRoute( + FolderSelectRoute( account: account, fileShowTarget: const ["miria.json", "miria.json.unknown"], confirmationText: S.of(context).importFromThisFolder, @@ -62,7 +61,9 @@ class ImportExportRepository extends ChangeNotifier { if (!context.mounted) return; if (alreadyExists.isEmpty) { await SimpleMessageDialog.show( - context, S.of(context).exportedFileNotFound); + context, + S.of(context).exportedFileNotFound, + ); return; } @@ -80,12 +81,12 @@ class ImportExportRepository extends ChangeNotifier { for (final accountSetting in importedSettings.accountSettings) { // この端末でログイン済みのアカウントであれば if (accounts.any((account) => account.acct == accountSetting.acct)) { - reader(accountSettingsRepositoryProvider).save(accountSetting); + await reader(accountSettingsRepositoryProvider).save(accountSetting); } } // 全般設定 - reader(generalSettingsRepositoryProvider) + await reader(generalSettingsRepositoryProvider) .update(importedSettings.generalSettings); // タブ設定 @@ -101,21 +102,19 @@ class ImportExportRepository extends ChangeNotifier { tabSettings.add(tabSetting); } - reader(tabSettingsRepositoryProvider).save(tabSettings); + await reader(tabSettingsRepositoryProvider).save(tabSettings); if (!context.mounted) return; await SimpleMessageDialog.show(context, S.of(context).importCompleted); if (!context.mounted) return; - context.router - ..removeWhere((route) => true) - ..push(const SplashRoute()); + context.router.removeWhere((route) => true); + await context.router.push(const SplashRoute()); } Future export(BuildContext context, Account account) async { - final result = await showDialog( - context: context, - builder: (context2) => FolderSelectDialog( + final result = await context.pushRoute( + FolderSelectRoute( account: account, fileShowTarget: const ["miria.json", "miria.json.unknown"], confirmationText: S.of(context).exportToThisFolder, diff --git a/lib/repository/local_time_line_repository.dart b/lib/repository/local_time_line_repository.dart index e1c696dcd..9ada3a002 100644 --- a/lib/repository/local_time_line_repository.dart +++ b/lib/repository/local_time_line_repository.dart @@ -1,52 +1,37 @@ -import 'dart:async'; +import "dart:async"; -import 'package:miria/repository/socket_timeline_repository.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:miria/repository/socket_timeline_repository.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class LocalTimeLineRepository extends SocketTimelineRepository { - LocalTimeLineRepository( +class LocalTimelineRepository extends SocketTimelineRepository { + LocalTimelineRepository( super.misskey, super.account, super.noteRepository, - super.globalNotificationRepository, super.generalSettingsRepository, super.tabSetting, - super.mainStreamRepository, - super.accountRepository, - super.emojiRepository, + super.ref, ); @override - SocketController createSocketController({ - required void Function(Note note) onReceived, - required FutureOr Function(String id, TimelineReacted reaction) - onReacted, - required FutureOr Function(String id, TimelineReacted reaction) - onUnreacted, - required FutureOr Function(String id, TimelineVoted vote) onVoted, - required FutureOr Function(String id, NoteEdited note) onUpdated, - }) { - return misskey.localTimelineStream( - parameter: LocalTimelineParameter( + Future> requestNotes({String? untilId}) async { + return await misskey.notes.localTimeline( + NotesLocalTimelineRequest( + untilId: untilId, withRenotes: tabSetting.renoteDisplay, withReplies: tabSetting.isIncludeReplies, withFiles: tabSetting.isMediaOnly, ), - onNoteReceived: onReceived, - onReacted: onReacted, - onUnreacted: onUnreacted, - onVoted: onVoted, - onUpdated: onUpdated, ); } @override - Future> requestNotes({String? untilId}) async { - return await misskey.notes.localTimeline(NotesLocalTimelineRequest( - untilId: untilId, - withRenotes: tabSetting.renoteDisplay, - withReplies: tabSetting.isIncludeReplies, - withFiles: tabSetting.isMediaOnly, - )); - } + Channel get channel => Channel.localTimeline; + + @override + Map get parameters => { + "withRenotes": tabSetting.renoteDisplay, + "withReplies": tabSetting.isIncludeReplies, + "withFiles": tabSetting.isMediaOnly, + }; } diff --git a/lib/repository/main_stream_repository.dart b/lib/repository/main_stream_repository.dart deleted file mode 100644 index 6488bc473..000000000 --- a/lib/repository/main_stream_repository.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:flutter/widgets.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/repository/account_repository.dart'; -import 'package:miria/repository/emoji_repository.dart'; -import 'package:misskey_dart/misskey_dart.dart'; - -class MainStreamRepository extends ChangeNotifier { - var hasUnreadNotification = false; - - final Misskey misskey; - final EmojiRepository emojiRepository; - final Account account; - final AccountRepository accountRepository; - SocketController? socketController; - bool isReconnecting = false; - - MainStreamRepository( - this.misskey, - this.emojiRepository, - this.account, - this.accountRepository, - ); - - Future confirmNotification() async { - await accountRepository.updateI(account); - - notifyListeners(); - } - - Future connect() async { - socketController = misskey.mainStream( - onReadAllNotifications: () { - accountRepository.readAllNotification(account); - }, - onUnreadNotification: (_) { - accountRepository.addUnreadNotification(account); - }, - onReadAllAnnouncements: () { - accountRepository.removeUnreadAnnouncement(account); - }, - onEmojiAdded: (_) { - emojiRepository.loadFromSource(); - }, - onEmojiUpdated: (_) { - emojiRepository.loadFromSource(); - }, - onAnnouncementCreated: (announcement) { - accountRepository.createUnreadAnnouncement(account, announcement); - }, - ); - await misskey.startStreaming(); - confirmNotification(); - } - - Future reconnect() async { - if (isReconnecting) { - // 排他制御11 - while (isReconnecting) { - await Future.delayed(const Duration(milliseconds: 100)); - } - return; - } - isReconnecting = true; - try { - print("main stream repository's socket controller will be disconnect"); - socketController?.disconnect(); - socketController = null; - await misskey.streamingService.restart(); - await connect(); - } finally { - isReconnecting = false; - } - } -} diff --git a/lib/repository/note_repository.dart b/lib/repository/note_repository.dart index db0099290..b24686452 100644 --- a/lib/repository/note_repository.dart +++ b/lib/repository/note_repository.dart @@ -1,10 +1,11 @@ -import 'package:flutter/foundation.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:miria/extensions/note_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/foundation.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:miria/extensions/note_extension.dart"; +import "package:miria/log.dart"; +import "package:miria/model/account.dart"; +import "package:misskey_dart/misskey_dart.dart"; -part 'note_repository.freezed.dart'; +part "note_repository.freezed.dart"; @freezed class NoteStatus with _$NoteStatus { @@ -42,20 +43,22 @@ class NoteRepository extends ChangeNotifier { softMuteWordContents.add(content); } if (regExp != null) { - final regExpAndFlags = RegExp(r'^\/(.+)\/(.*)$').firstMatch(regExp); + final regExpAndFlags = RegExp(r"^\/(.+)\/(.*)$").firstMatch(regExp); if (regExpAndFlags != null) { try { - final flags = regExpAndFlags[2] ?? ''; + final flags = regExpAndFlags[2] ?? ""; softMuteWordRegExps.add( RegExp( regExpAndFlags[1]!, - multiLine: flags.contains('m'), - caseSensitive: !flags.contains('i'), - unicode: flags.contains('u'), - dotAll: flags.contains('s'), + multiLine: flags.contains("m"), + caseSensitive: !flags.contains("i"), + unicode: flags.contains("u"), + dotAll: flags.contains("s"), ), ); - } catch (_) {} + } catch (e) { + logger.warning(e); + } } } } @@ -67,21 +70,10 @@ class NoteRepository extends ChangeNotifier { hardMuteWordContents.add(content); } if (regExp != null) { - final regExpAndFlags = RegExp(r'^\/(.+)\/(.*)$').firstMatch(regExp); - if (regExpAndFlags != null) { - try { - final flags = regExpAndFlags[2] ?? ''; - hardMuteWordRegExps.add( - RegExp( - regExpAndFlags[1]!, - multiLine: flags.contains('m'), - caseSensitive: !flags.contains('i'), - unicode: flags.contains('u'), - dotAll: flags.contains('s'), - ), - ); - } catch (_) {} - } + try { + hardMuteWordRegExps + .add(RegExp(regExp.substring(1, regExp.length - 1))); + } catch (e) {} } } } @@ -91,8 +83,10 @@ class NoteRepository extends ChangeNotifier { Map get noteStatuses => _noteStatuses; void updateNoteStatus( - String id, NoteStatus Function(NoteStatus status) statusPredicate, - {bool isNotify = true}) { + String id, + NoteStatus Function(NoteStatus status) statusPredicate, { + bool isNotify = true, + }) { _noteStatuses[id] = statusPredicate.call(_noteStatuses[id]!); if (isNotify) notifyListeners(); } @@ -125,15 +119,16 @@ class NoteRepository extends ChangeNotifier { (note.reactions.isNotEmpty ? registeredNote?.myReaction : null)), ); _noteStatuses[note.id] ??= NoteStatus( - isCwOpened: false, - isLongVisible: false, - isReactionedRenote: false, - isLongVisibleInitialized: false, - isIncludeMuteWord: - (note.user.host != null || note.user.id != account.i.id) && - softMuteWordContents.any((e) => e.every(isMuteTarget)) || - softMuteWordRegExps.any(isMuteTarget), - isMuteOpened: false); + isCwOpened: false, + isLongVisible: false, + isReactionedRenote: false, + isLongVisibleInitialized: false, + isIncludeMuteWord: + (note.user.host != null || note.user.id != account.i.id) && + softMuteWordContents.any((e) => e.every(isMuteTarget)) || + softMuteWordRegExps.any(isMuteTarget), + isMuteOpened: false, + ); final renote = note.renote; final reply = note.reply; if (renote != null) { diff --git a/lib/repository/note_repository.freezed.dart b/lib/repository/note_repository.freezed.dart index c245591f4..af43c6526 100644 --- a/lib/repository/note_repository.freezed.dart +++ b/lib/repository/note_repository.freezed.dart @@ -12,7 +12,7 @@ part of 'note_repository.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); /// @nodoc mixin _$NoteStatus { @@ -23,7 +23,9 @@ mixin _$NoteStatus { bool get isIncludeMuteWord => throw _privateConstructorUsedError; bool get isMuteOpened => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of NoteStatus + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $NoteStatusCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -53,6 +55,8 @@ class _$NoteStatusCopyWithImpl<$Res, $Val extends NoteStatus> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of NoteStatus + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -117,6 +121,8 @@ class __$$NoteStatusImplCopyWithImpl<$Res> _$NoteStatusImpl _value, $Res Function(_$NoteStatusImpl) _then) : super(_value, _then); + /// Create a copy of NoteStatus + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -229,7 +235,9 @@ class _$NoteStatusImpl with DiagnosticableTreeMixin implements _NoteStatus { isIncludeMuteWord, isMuteOpened); - @JsonKey(ignore: true) + /// Create a copy of NoteStatus + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$NoteStatusImplCopyWith<_$NoteStatusImpl> get copyWith => @@ -257,8 +265,11 @@ abstract class _NoteStatus implements NoteStatus { bool get isIncludeMuteWord; @override bool get isMuteOpened; + + /// Create a copy of NoteStatus + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$NoteStatusImplCopyWith<_$NoteStatusImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/repository/role_timeline_repository.dart b/lib/repository/role_timeline_repository.dart index 74a7645c9..fbed7b157 100644 --- a/lib/repository/role_timeline_repository.dart +++ b/lib/repository/role_timeline_repository.dart @@ -1,45 +1,32 @@ -import 'dart:async'; +import "dart:async"; -import 'package:miria/repository/socket_timeline_repository.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:miria/repository/socket_timeline_repository.dart"; +import "package:misskey_dart/misskey_dart.dart"; class RoleTimelineRepository extends SocketTimelineRepository { RoleTimelineRepository( super.misskey, super.account, super.noteRepository, - super.globalNotificationRepository, super.generalSettingsRepository, super.tabSetting, - super.mainStreamRepository, - super.accountRepository, - super.emojiRepository, + super.ref, ); @override - SocketController createSocketController({ - required void Function(Note note) onReceived, - required FutureOr Function(String id, TimelineReacted reaction) - onReacted, - required FutureOr Function(String id, TimelineReacted reaction) - onUnreacted, - required FutureOr Function(String id, TimelineVoted vote) onVoted, - required FutureOr Function(String id, NoteEdited note) onUpdated, - }) { - return misskey.roleTimelineStream( - roleId: tabSetting.roleId!, - onNoteReceived: onReceived, - onReacted: onReacted, - onVoted: onVoted, + Future> requestNotes({String? untilId}) async { + return await misskey.roles.notes( + RolesNotesRequest( + roleId: tabSetting.roleId!, + limit: 30, + untilId: untilId, + ), ); } @override - Future> requestNotes({String? untilId}) async { - return await misskey.roles.notes(RolesNotesRequest( - roleId: tabSetting.roleId!, - limit: 30, - untilId: untilId, - )); - } + Channel get channel => Channel.roleTimeline; + + @override + Map get parameters => {"roleId": tabSetting.roleId}; } diff --git a/lib/repository/shared_preference_controller.dart b/lib/repository/shared_preference_controller.dart index 3b31cd5a0..75c893b43 100644 --- a/lib/repository/shared_preference_controller.dart +++ b/lib/repository/shared_preference_controller.dart @@ -1,9 +1,9 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:miria/view/share_extension_page/share_extension_page.dart'; -import 'package:shared_preference_app_group/shared_preference_app_group.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import "package:flutter/foundation.dart"; +import "package:flutter_secure_storage/flutter_secure_storage.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/view/share_extension_page/share_extension_page.dart"; +import "package:shared_preference_app_group/shared_preference_app_group.dart"; +import "package:shared_preferences/shared_preferences.dart"; class SharedPreferenceController { final bool isShareExtensionContext; @@ -52,7 +52,7 @@ class SharedPreferenceController { // 共有エクステンションのコンテクストでは書かない return; } - prefs.write(key: key, value: value); + await prefs.write(key: key, value: value); if (defaultTargetPlatform == TargetPlatform.iOS) { await SharedPreferenceAppGroup.setString(key, value); } diff --git a/lib/repository/socket_timeline_repository.dart b/lib/repository/socket_timeline_repository.dart index f757d12d0..bed407a27 100644 --- a/lib/repository/socket_timeline_repository.dart +++ b/lib/repository/socket_timeline_repository.dart @@ -1,55 +1,63 @@ -import 'dart:async'; -import 'dart:math'; +import "dart:async"; +import "dart:math"; -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:miria/extensions/date_time_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/repository/account_repository.dart'; -import 'package:miria/repository/emoji_repository.dart'; -import 'package:miria/repository/main_stream_repository.dart'; -import 'package:miria/repository/time_line_repository.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:collection/collection.dart"; +import "package:flutter/foundation.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/date_time_extension.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/account_repository.dart"; +import "package:miria/repository/emoji_repository.dart"; +import "package:miria/repository/time_line_repository.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; +import "package:uuid/uuid.dart"; + +part "socket_timeline_repository.g.dart"; + +@Riverpod(keepAlive: true) +Future misskeyStreaming( + MisskeyStreamingRef ref, + Misskey misskey, +) async { + return await misskey.streamingService.stream(); +} abstract class SocketTimelineRepository extends TimelineRepository { - SocketController? socketController; final Misskey misskey; final Account account; - final MainStreamRepository mainStreamRepository; - final AccountRepository accountRepository; - final EmojiRepository emojiRepository; + late final EmojiRepository emojiRepository = + ref.read(emojiRepositoryProvider(account)); bool isReconnecting = false; + late final AccountRepository accountRepository = + ref.read(accountRepositoryProvider.notifier); + StreamingController? streamingController; bool isLoading = true; (Object?, StackTrace)? error; + Channel get channel; + Map get parameters; + String? timelineId; + String? mainId; + Ref ref; + StreamSubscription? timelineSubscription; + StreamSubscription? mainSubscription; SocketTimelineRepository( this.misskey, this.account, super.noteRepository, - super.globalNotificationRepository, super.generalSettingsRepository, super.tabSetting, - this.mainStreamRepository, - this.accountRepository, - this.emojiRepository, + this.ref, ); Future> requestNotes({String? untilId}); - - SocketController createSocketController({ - required void Function(Note note) onReceived, - required FutureOr Function(String id, TimelineReacted reaction) - onReacted, - required FutureOr Function(String id, TimelineReacted reaction) - onUnreacted, - required FutureOr Function(String id, TimelineVoted vote) onVoted, - required FutureOr Function(String id, NoteEdited vote) onUpdated, - }); - void reloadLatestNotes() { moveToOlder(); - requestNotes().then((resultNotes) { + unawaited(() async { + final resultNotes = await requestNotes(); if (olderNotes.isEmpty) { olderNotes.addAll(resultNotes); notifyListeners(); @@ -75,7 +83,7 @@ abstract class SocketTimelineRepository extends TimelineRepository { noteRepository.registerNote(note); } notifyListeners(); - }); + }()); } @override @@ -83,10 +91,7 @@ abstract class SocketTimelineRepository extends TimelineRepository { try { await emojiRepository.loadFromSourceIfNeed(); // api/iおよびapi/metaはawaitしない - unawaited(Future(() async { - await accountRepository.loadFromSourceIfNeed(tabSetting.acct); - })); - await mainStreamRepository.reconnect(); + unawaited(accountRepository.loadFromSourceIfNeed(tabSetting.acct)); isLoading = false; error = null; notifyListeners(); @@ -96,74 +101,15 @@ abstract class SocketTimelineRepository extends TimelineRepository { notifyListeners(); } - if (socketController != null) { - socketController?.disconnect(); + if (misskey.streamingService.isClosed) { + streamingController = + await ref.refresh(misskeyStreamingProvider(misskey).future); + } else { + streamingController = + await ref.read(misskeyStreamingProvider(misskey).future); } - - socketController = createSocketController( - onReceived: (note) { - newerNotes.add(note); - - notifyListeners(); - }, - onReacted: (id, value) { - final registeredNote = noteRepository.notes[id]; - if (registeredNote == null) return; - final reaction = Map.of(registeredNote.reactions); - reaction[value.reaction] = (reaction[value.reaction] ?? 0) + 1; - final emoji = value.emoji; - final reactionEmojis = Map.of(registeredNote.reactionEmojis); - if (emoji != null && !value.reaction.endsWith("@.:")) { - reactionEmojis[emoji.name] = emoji.url; - } - noteRepository.registerNote(registeredNote.copyWith( - reactions: reaction, - reactionEmojis: reactionEmojis, - myReaction: value.userId == account.i.id - ? (emoji?.name != null ? ":${emoji?.name}:" : null) - : registeredNote.myReaction)); - }, - onUnreacted: (id, value) { - final registeredNote = noteRepository.notes[id]; - if (registeredNote == null) return; - final reaction = Map.of(registeredNote.reactions); - reaction[value.reaction] = max((reaction[value.reaction] ?? 0) - 1, 0); - if (reaction[value.reaction] == 0) { - reaction.remove(value.reaction); - } - final emoji = value.emoji; - final reactionEmojis = Map.of(registeredNote.reactionEmojis); - if (emoji != null && !value.reaction.endsWith("@.:")) { - reactionEmojis[emoji.name] = emoji.url; - } - noteRepository.registerNote(registeredNote.copyWith( - reactions: reaction, - reactionEmojis: reactionEmojis, - myReaction: - value.userId == account.i.id ? "" : registeredNote.myReaction)); - }, - onVoted: (id, value) { - final registeredNote = noteRepository.notes[id]; - if (registeredNote == null) return; - - final poll = registeredNote.poll; - if (poll == null) return; - - final choices = poll.choices.toList(); - choices[value.choice] = choices[value.choice] - .copyWith(votes: choices[value.choice].votes + 1); - noteRepository.registerNote( - registeredNote.copyWith(poll: poll.copyWith(choices: choices))); - }, - onUpdated: (id, value) { - final note = noteRepository.notes[id]; - if (note == null) return; - noteRepository.registerNote(note.copyWith( - text: value.text, cw: value.cw, updatedAt: DateTime.now())); - }, - ); - Future.wait([ - Future(() async => await misskey.startStreaming()), + await _listenStreaming(); + await Future.wait([ Future(() async { if (olderNotes.isEmpty) { try { @@ -179,26 +125,53 @@ abstract class SocketTimelineRepository extends TimelineRepository { } else { reloadLatestNotes(); } - }) + }), ]); } @override - void disconnect() { - socketController?.disconnect(); + Future disconnect() async { + final id = timelineId; + if (id != null) { + await streamingController?.removeChannel(id); + await timelineSubscription?.cancel(); + } + final id2 = mainId; + if (id2 != null) { + await streamingController?.removeChannel(id2); + await mainSubscription?.cancel(); + } } @override Future reconnect() async { if (isReconnecting) return; isReconnecting = true; - notifyListeners(); try { - await super.reconnect(); - socketController = null; - await startTimeLine(); + await ( + disconnect(), + timelineSubscription?.cancel() ?? Future.value(), + mainSubscription?.cancel() ?? Future.value() + ).wait; + } catch (e) { + print(e); + } + + try { + await ( + () async { + await misskey.streamingService.reconnect(); + await _listenStreaming(); + }(), + () async { + reloadLatestNotes(); + }(), + ).wait; error = null; - } finally { + isReconnecting = false; + notifyListeners(); + } catch (e, s) { + error = (e, s); isReconnecting = false; notifyListeners(); } @@ -220,24 +193,35 @@ abstract class SocketTimelineRepository extends TimelineRepository { @override void dispose() { super.dispose(); - socketController?.disconnect(); - socketController = null; + unawaited(() async { + final id = timelineId; + if (id != null) { + await streamingController?.removeChannel(id); + } + final id2 = mainId; + if (id2 != null) { + await streamingController?.removeChannel(id2); + } + }()); } @override - void subscribe(SubscribeItem item) { + Future subscribe(SubscribeItem item) async { if (!tabSetting.isSubscribe) return; + await ref.read(misskeyStreamingProvider(misskey).future); final index = subscribedList.indexWhere((element) => element.noteId == item.noteId); - final isSubscribed = subscribedList.indexWhere((element) => - element.noteId == item.noteId || - element.renoteId == item.noteId || - element.replyId == item.noteId); + final isSubscribed = subscribedList.indexWhere( + (element) => + element.noteId == item.noteId || + element.renoteId == item.noteId || + element.replyId == item.noteId, + ); if (index == -1) { subscribedList.add(item); if (isSubscribed == -1) { - socketController?.subNote(item.noteId); + streamingController?.subNote(item.noteId); } } else { subscribedList[index] = item; @@ -246,31 +230,229 @@ abstract class SocketTimelineRepository extends TimelineRepository { final renoteId = item.renoteId; if (renoteId != null) { - final isRenoteSubscribed = subscribedList.indexWhere((element) => - element.noteId == renoteId || - element.renoteId == renoteId || - element.replyId == renoteId); + final isRenoteSubscribed = subscribedList.indexWhere( + (element) => + element.noteId == renoteId || + element.renoteId == renoteId || + element.replyId == renoteId, + ); if (isRenoteSubscribed == -1) { - socketController?.subNote(renoteId); + streamingController?.subNote(renoteId); } } final replyId = item.replyId; if (replyId != null) { - socketController?.subNote(replyId); - final isRenoteSubscribed = subscribedList.indexWhere((element) => - element.noteId == replyId || - element.renoteId == replyId || - element.replyId == replyId); + streamingController?.subNote(replyId); + final isRenoteSubscribed = subscribedList.indexWhere( + (element) => + element.noteId == replyId || + element.renoteId == replyId || + element.replyId == replyId, + ); if (isRenoteSubscribed == -1) { - socketController?.subNote(replyId); + streamingController?.subNote(replyId); } } } @override - void describe(String id) { + Future describe(String id) async { if (!tabSetting.isSubscribe) return; - socketController?.unsubNote(id); + await ref.read(misskeyStreamingProvider(misskey).future); + streamingController?.unsubNote(id); + } + + Future _listenStreaming() async { + final generatedId = const Uuid().v4(); + timelineId = generatedId; + final generatedId2 = const Uuid().v4(); + mainId = generatedId2; + + timelineSubscription = streamingController + ?.addChannel(channel, parameters, generatedId) + .listen(listenTimeline); + mainSubscription = + streamingController?.mainStream(id: generatedId2).listen(listenMain); + } + + Future listenMain(StreamingResponse response) async { + switch (response) { + case StreamingChannelResponse(:final body): + switch (body) { + case ReadAllNotificationsChannelEvent(): + await accountRepository.readAllNotification(account); + case UnreadNotificationChannelEvent(): + await accountRepository.addUnreadNotification(account); + case ReadAllAnnouncementsChannelEvent(): + await accountRepository.removeUnreadAnnouncement(account); + case AnnouncementCreatedChannelEvent(): + case NoteChannelEvent(): + case StatsLogChannelEvent(): + case StatsChannelEvent(): + case UserAddedChannelEvent(): + case UserRemovedChannelEvent(): + case NotificationChannelEvent(): + case MentionChannelEvent(): + case ReplyChannelEvent(): + case RenoteChannelEvent(): + case FollowChannelEvent(): + case FollowedChannelEvent(): + case UnfollowChannelEvent(): + case MeUpdatedChannelEvent(): + case PageEventChannelEvent(): + case UrlUploadFinishedChannelEvent(): + case UnreadMentionChannelEvent(): + case ReadAllUnreadMentionsChannelEvent(): + case NotificationFlushedChannelEvent(): + case UnreadSpecifiedNoteChannelEvent(): + case ReadAllUnreadSpecifiedNotesChannelEvent(): + case ReadAllAntennasChannelEvent(): + case UnreadAntennaChannelEvent(): + case MyTokenRegeneratedChannelEvent(): + case SigninChannelEvent(): + case RegistryUpdatedChannelEvent(): + case DriveFileCreatedChannelEvent(): + case ReadAntennaChannelEvent(): + case ReceiveFollowRequestChannelEvent(): + case FallbackChannelEvent(): + case ReactedChannelEvent(): + case UnreactedChannelEvent(): + case DeletedChannelEvent(): + case PollVotedChannelEvent(): + case UpdatedChannelEvent(): + } + case StreamingChannelEmojiAddedResponse(): + case StreamingChannelEmojiUpdatedResponse(): + case StreamingChannelEmojiDeletedResponse(): + await emojiRepository.loadFromSource(); + + case StreamingChannelAnnouncementCreatedResponse(:final body): + await accountRepository.createUnreadAnnouncement( + account, + body.announcement, + ); + case StreamingChannelNoteUpdatedResponse(): + case StreamingChannelUnknownResponse(): + // TODO: Handle this case. + } + } + + Future listenTimeline(StreamingResponse response) async { + switch (response) { + case StreamingChannelResponse(:final body): + switch (body) { + case NoteChannelEvent(:final body): + newerNotes.add(body); + notifyListeners(); + case ReadAllNotificationsChannelEvent(): + case UnreadNotificationChannelEvent(): + case ReadAllAnnouncementsChannelEvent(): + case AnnouncementCreatedChannelEvent(): + case StatsLogChannelEvent(): + case StatsChannelEvent(): + case UserAddedChannelEvent(): + case UserRemovedChannelEvent(): + case NotificationChannelEvent(): + case MentionChannelEvent(): + case ReplyChannelEvent(): + case RenoteChannelEvent(): + case FollowChannelEvent(): + case FollowedChannelEvent(): + case UnfollowChannelEvent(): + case MeUpdatedChannelEvent(): + case PageEventChannelEvent(): + case UrlUploadFinishedChannelEvent(): + case UnreadMentionChannelEvent(): + case ReadAllUnreadMentionsChannelEvent(): + case NotificationFlushedChannelEvent(): + case UnreadSpecifiedNoteChannelEvent(): + case ReadAllUnreadSpecifiedNotesChannelEvent(): + case ReadAllAntennasChannelEvent(): + case UnreadAntennaChannelEvent(): + case MyTokenRegeneratedChannelEvent(): + case SigninChannelEvent(): + case RegistryUpdatedChannelEvent(): + case DriveFileCreatedChannelEvent(): + case ReadAntennaChannelEvent(): + case ReceiveFollowRequestChannelEvent(): + case FallbackChannelEvent(): + } + case StreamingChannelNoteUpdatedResponse(:final body): + switch (body) { + case ReactedChannelEvent(:final id, :final body): + final registeredNote = noteRepository.notes[id]; + if (registeredNote == null) return; + final reaction = Map.of(registeredNote.reactions); + reaction[body.reaction] = (reaction[body.reaction] ?? 0) + 1; + final emoji = body.emoji; + final reactionEmojis = Map.of(registeredNote.reactionEmojis); + if (emoji != null && !body.reaction.endsWith("@.:")) { + reactionEmojis[emoji.name] = emoji.url; + } + noteRepository.registerNote( + registeredNote.copyWith( + reactions: reaction, + reactionEmojis: reactionEmojis, + myReaction: body.userId == account.i.id + ? (emoji?.name != null ? ":${emoji?.name}:" : null) + : registeredNote.myReaction, + ), + ); + case UnreactedChannelEvent(:final body, :final id): + final registeredNote = noteRepository.notes[id]; + if (registeredNote == null) return; + final reaction = Map.of(registeredNote.reactions); + reaction[body.reaction] = + max((reaction[body.reaction] ?? 0) - 1, 0); + if (reaction[body.reaction] == 0) { + reaction.remove(body.reaction); + } + final emoji = body.emoji; + final reactionEmojis = Map.of(registeredNote.reactionEmojis); + if (emoji != null && !body.reaction.endsWith("@.:")) { + reactionEmojis[emoji.name] = emoji.url; + } + noteRepository.registerNote( + registeredNote.copyWith( + reactions: reaction, + reactionEmojis: reactionEmojis, + myReaction: body.userId == account.i.id + ? "" + : registeredNote.myReaction, + ), + ); + case PollVotedChannelEvent(:final body, :final id): + final registeredNote = noteRepository.notes[id]; + if (registeredNote == null) return; + + final poll = registeredNote.poll; + if (poll == null) return; + + final choices = poll.choices.toList(); + choices[body.choice] = choices[body.choice] + .copyWith(votes: choices[body.choice].votes + 1); + noteRepository.registerNote( + registeredNote.copyWith(poll: poll.copyWith(choices: choices)), + ); + case UpdatedChannelEvent(:final body): + final note = noteRepository.notes[timelineId]; + if (note == null) return; + noteRepository.registerNote( + note.copyWith( + text: body.text, + cw: body.cw, + updatedAt: DateTime.now(), + ), + ); + case DeletedChannelEvent(): + } + case StreamingChannelEmojiAddedResponse(): + case StreamingChannelEmojiUpdatedResponse(): + case StreamingChannelEmojiDeletedResponse(): + await emojiRepository.loadFromSource(); + case StreamingChannelUnknownResponse(): + case StreamingChannelAnnouncementCreatedResponse(): + } } } diff --git a/lib/repository/socket_timeline_repository.g.dart b/lib/repository/socket_timeline_repository.g.dart new file mode 100644 index 000000000..d0f719dcd --- /dev/null +++ b/lib/repository/socket_timeline_repository.g.dart @@ -0,0 +1,201 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'socket_timeline_repository.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$misskeyStreamingHash() => r'f56a883b14e639e71e8ec44952e56c0a527447c1'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [misskeyStreaming]. +@ProviderFor(misskeyStreaming) +const misskeyStreamingProvider = MisskeyStreamingFamily(); + +/// See also [misskeyStreaming]. +class MisskeyStreamingFamily extends Family { + /// See also [misskeyStreaming]. + const MisskeyStreamingFamily(); + + static const Iterable? _dependencies = null; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'misskeyStreamingProvider'; + + /// See also [misskeyStreaming]. + MisskeyStreamingProvider call( + Misskey misskey, + ) { + return MisskeyStreamingProvider( + misskey, + ); + } + + @visibleForOverriding + @override + MisskeyStreamingProvider getProviderOverride( + covariant MisskeyStreamingProvider provider, + ) { + return call( + provider.misskey, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith( + FutureOr Function(MisskeyStreamingRef ref) create) { + return _$MisskeyStreamingFamilyOverride(this, create); + } +} + +class _$MisskeyStreamingFamilyOverride implements FamilyOverride { + _$MisskeyStreamingFamilyOverride(this.overriddenFamily, this.create); + + final FutureOr Function(MisskeyStreamingRef ref) create; + + @override + final MisskeyStreamingFamily overriddenFamily; + + @override + MisskeyStreamingProvider getProviderOverride( + covariant MisskeyStreamingProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [misskeyStreaming]. +class MisskeyStreamingProvider extends FutureProvider { + /// See also [misskeyStreaming]. + MisskeyStreamingProvider( + Misskey misskey, + ) : this._internal( + (ref) => misskeyStreaming( + ref as MisskeyStreamingRef, + misskey, + ), + from: misskeyStreamingProvider, + name: r'misskeyStreamingProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$misskeyStreamingHash, + dependencies: MisskeyStreamingFamily._dependencies, + allTransitiveDependencies: + MisskeyStreamingFamily._allTransitiveDependencies, + misskey: misskey, + ); + + MisskeyStreamingProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.misskey, + }) : super.internal(); + + final Misskey misskey; + + @override + Override overrideWith( + FutureOr Function(MisskeyStreamingRef ref) create, + ) { + return ProviderOverride( + origin: this, + override: MisskeyStreamingProvider._internal( + (ref) => create(ref as MisskeyStreamingRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + misskey: misskey, + ), + ); + } + + @override + (Misskey,) get argument { + return (misskey,); + } + + @override + FutureProviderElement createElement() { + return _MisskeyStreamingProviderElement(this); + } + + MisskeyStreamingProvider _copyWith( + FutureOr Function(MisskeyStreamingRef ref) create, + ) { + return MisskeyStreamingProvider._internal( + (ref) => create(ref as MisskeyStreamingRef), + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + misskey: misskey, + ); + } + + @override + bool operator ==(Object other) { + return other is MisskeyStreamingProvider && other.misskey == misskey; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, misskey.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin MisskeyStreamingRef on FutureProviderRef { + /// The parameter `misskey` of this provider. + Misskey get misskey; +} + +class _MisskeyStreamingProviderElement + extends FutureProviderElement + with MisskeyStreamingRef { + _MisskeyStreamingProviderElement(super.provider); + + @override + Misskey get misskey => (origin as MisskeyStreamingProvider).misskey; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/repository/tab_settings_repository.dart b/lib/repository/tab_settings_repository.dart index 807ca3af7..b3fd18e67 100644 --- a/lib/repository/tab_settings_repository.dart +++ b/lib/repository/tab_settings_repository.dart @@ -1,12 +1,12 @@ -import 'dart:convert'; +import "dart:convert"; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/tab_icon.dart'; -import 'package:miria/model/tab_setting.dart'; -import 'package:miria/model/tab_type.dart'; -import 'package:shared_preferences/shared_preferences.dart'; +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/tab_icon.dart"; +import "package:miria/model/tab_setting.dart"; +import "package:miria/model/tab_type.dart"; +import "package:shared_preferences/shared_preferences.dart"; class TabSettingsRepository extends ChangeNotifier { List _tabSettings = []; @@ -24,8 +24,9 @@ class TabSettingsRepository extends ChangeNotifier { try { _tabSettings ..clear() - ..addAll((jsonDecode(storedData) as List) - .map((e) => TabSetting.fromJson(e))); + ..addAll( + (jsonDecode(storedData) as List).map((e) => TabSetting.fromJson(e)), + ); } catch (e) { if (kDebugMode) print(e); } @@ -34,8 +35,10 @@ class TabSettingsRepository extends ChangeNotifier { Future save(List tabSettings) async { _tabSettings = tabSettings.toList(); final prefs = await SharedPreferences.getInstance(); - await prefs.setString("tab_settings", - jsonEncode(_tabSettings.map((e) => e.toJson()).toList())); + await prefs.setString( + "tab_settings", + jsonEncode(_tabSettings.map((e) => e.toJson()).toList()), + ); notifyListeners(); } diff --git a/lib/repository/time_line_repository.dart b/lib/repository/time_line_repository.dart index fca45b8f6..1884030d2 100644 --- a/lib/repository/time_line_repository.dart +++ b/lib/repository/time_line_repository.dart @@ -1,13 +1,12 @@ -import 'dart:async'; +import "dart:async"; -import 'package:collection/collection.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:miria/model/general_settings.dart'; -import 'package:miria/model/tab_setting.dart'; -import 'package:miria/repository/general_settings_repository.dart'; -import 'package:miria/repository/main_stream_repository.dart'; -import 'package:miria/repository/note_repository.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:collection/collection.dart"; +import "package:flutter/cupertino.dart"; +import "package:miria/model/general_settings.dart"; +import "package:miria/model/tab_setting.dart"; +import "package:miria/repository/general_settings_repository.dart"; +import "package:miria/repository/note_repository.dart"; +import "package:misskey_dart/misskey_dart.dart"; class NotifierQueueList extends QueueList { final NoteRepository noteRepository; @@ -21,7 +20,7 @@ class NotifierQueueList extends QueueList { ); bool filterAs(Note element) { - if (tabSetting.renoteDisplay == false && + if (!tabSetting.renoteDisplay && element.text == null && element.cw == null && element.renoteId != null) { @@ -92,7 +91,6 @@ class SubscribeItem { abstract class TimelineRepository extends ChangeNotifier { final NoteRepository noteRepository; - final MainStreamRepository globalNotificationRepository; final GeneralSettingsRepository generalSettingsRepository; final TabSetting tabSetting; @@ -101,7 +99,6 @@ abstract class TimelineRepository extends ChangeNotifier { TimelineRepository( this.noteRepository, - this.globalNotificationRepository, this.generalSettingsRepository, this.tabSetting, ) { @@ -115,28 +112,33 @@ abstract class TimelineRepository extends ChangeNotifier { for (final item in subscribedList.where(condition)) { // 他に参照がなければ、購読を解除する if (subscribedList.every( - (e) => e.renoteId != item.noteId && e.replyId != item.noteId)) { + (e) => e.renoteId != item.noteId && e.replyId != item.noteId, + )) { describe(item.noteId); } final renoteId = item.renoteId; if (renoteId != null) { - if (subscribedList.every((e) => - (e.noteId != item.renoteId && - e.replyId != item.renoteId && - (e.noteId != item.noteId && e.renoteId != item.renoteId)) || - e.noteId == item.noteId)) { + if (subscribedList.every( + (e) => + (e.noteId != item.renoteId && + e.replyId != item.renoteId && + (e.noteId != item.noteId && e.renoteId != item.renoteId)) || + e.noteId == item.noteId, + )) { describe(renoteId); } } final replyId = item.replyId; if (replyId != null) { - if (subscribedList.every((e) => - (e.noteId != item.replyId && - e.replyId != item.replyId && - (e.noteId != item.noteId && e.replyId != item.replyId)) || - e.noteId == item.noteId)) { + if (subscribedList.every( + (e) => + (e.noteId != item.replyId && + e.replyId != item.replyId && + (e.noteId != item.noteId && e.replyId != item.replyId)) || + e.noteId == item.noteId, + )) { describe(replyId); } } @@ -161,10 +163,6 @@ abstract class TimelineRepository extends ChangeNotifier { timer.cancel(); } - Future reconnect() async { - await globalNotificationRepository.reconnect(); - } - void updateNote(Note newNote) { var isChanged = false; newerNotes.forEachIndexed((index, element) { @@ -224,4 +222,6 @@ abstract class TimelineRepository extends ChangeNotifier { } void describe(String id) {} + + Future reconnect() async {} } diff --git a/lib/repository/user_list_time_line_repository.dart b/lib/repository/user_list_time_line_repository.dart index 66ffe0562..2a1b860a1 100644 --- a/lib/repository/user_list_time_line_repository.dart +++ b/lib/repository/user_list_time_line_repository.dart @@ -1,40 +1,18 @@ -import 'dart:async'; +import "dart:async"; -import 'package:miria/repository/socket_timeline_repository.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:miria/repository/socket_timeline_repository.dart"; +import "package:misskey_dart/misskey_dart.dart"; class UserListTimelineRepository extends SocketTimelineRepository { UserListTimelineRepository( super.misskey, super.account, super.noteRepository, - super.globalNotificationRepository, super.generalSettingsRepository, super.tabSetting, - super.mainStreamRepository, - super.accountRepository, - super.emojiRepository, + super.ref, ); - @override - SocketController createSocketController({ - required void Function(Note note) onReceived, - required FutureOr Function(String id, TimelineReacted reaction) - onReacted, - required FutureOr Function(String id, TimelineReacted reaction) - onUnreacted, - required FutureOr Function(String id, TimelineVoted vote) onVoted, - required FutureOr Function(String id, NoteEdited note) onUpdated, - }) { - return misskey.userListStream( - listId: tabSetting.listId!, - onNoteReceived: onReceived, - onReacted: onReacted, - onVoted: onVoted, - onUpdated: onUpdated, - ); - } - @override Future> requestNotes({String? untilId}) async { return await misskey.notes.userListTimeline( @@ -46,4 +24,12 @@ class UserListTimelineRepository extends SocketTimelineRepository { ), ); } + + @override + // TODO: implement channel + Channel get channel => Channel.userList; + + @override + // TODO: implement parameters + Map get parameters => {"listId": tabSetting.listId}; } diff --git a/lib/router/app_router.dart b/lib/router/app_router.dart index 496e4923b..194b63d0e 100644 --- a/lib/router/app_router.dart +++ b/lib/router/app_router.dart @@ -1,61 +1,93 @@ -import 'dart:typed_data'; +import "dart:typed_data"; -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart' hide Page; -import 'package:miria/model/account.dart'; -import 'package:miria/model/image_file.dart'; -import 'package:miria/model/note_search_condition.dart'; -import 'package:miria/model/tab_setting.dart'; -import 'package:miria/view/announcements_page/announcements_page.dart'; -import 'package:miria/view/antenna_page/antenna_page.dart'; -import 'package:miria/view/channels_page/channels_page.dart'; -import 'package:miria/view/clip_list_page/clip_detail_page.dart'; -import 'package:miria/view/clip_list_page/clip_list_page.dart'; -import 'package:miria/view/explore_page/explore_page.dart'; -import 'package:miria/view/explore_page/explore_role_users_page.dart'; -import 'package:miria/view/favorited_note_page/favorited_note_page.dart'; -import 'package:miria/view/federation_page/federation_page.dart'; -import 'package:miria/view/games_page/misskey_games_page.dart'; -import 'package:miria/view/hashtag_page/hashtag_page.dart'; -import 'package:miria/view/misskey_page_page/misskey_page_page.dart'; -import 'package:miria/view/note_create_page/note_create_page.dart'; -import 'package:miria/view/note_detail_page/note_detail_page.dart'; -import 'package:miria/view/notes_after_renote_page/notes_after_renote_page.dart'; -import 'package:miria/view/notification_page/notification_page.dart'; -import 'package:miria/view/search_page/search_page.dart'; -import 'package:miria/view/photo_edit_page/photo_edit_page.dart'; -import 'package:miria/view/settings_page/account_settings_page/account_list.dart'; -import 'package:miria/view/settings_page/app_info_page/app_info_page.dart'; -import 'package:miria/view/settings_page/general_settings_page/general_settings_page.dart'; -import 'package:miria/view/settings_page/import_export_page/import_export_page.dart'; -import 'package:miria/view/settings_page/tab_settings_page/tab_settings_list_page.dart'; -import 'package:miria/view/several_account_settings_page/cache_management_page/cache_management_page.dart'; -import 'package:miria/view/several_account_settings_page/word_mute_page/word_mute_page.dart'; -import 'package:miria/view/several_account_settings_page/instance_mute_page/instance_mute_page.dart'; -import 'package:miria/view/several_account_settings_page/reaction_deck_page/reaction_deck_page.dart'; -import 'package:miria/view/several_account_settings_page/several_account_general_settings_page/several_account_general_settings_page.dart'; -import 'package:miria/view/several_account_settings_page/several_account_settings_page.dart'; -import 'package:miria/view/share_extension_page/share_extension_page.dart'; -import 'package:miria/view/sharing_account_select_page/account_select_page.dart'; -import 'package:miria/view/time_line_page/time_line_page.dart'; -import 'package:miria/view/user_page/user_followee.dart'; -import 'package:miria/view/user_page/user_follower.dart'; -import 'package:miria/view/user_page/user_page.dart'; -import 'package:miria/view/users_list_page/users_list_detail_page.dart'; -import 'package:miria/view/users_list_page/users_list_page.dart'; -import 'package:miria/view/users_list_page/users_list_timeline_page.dart'; -import 'package:miria/view/splash_page/splash_page.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart" hide Page; +import "package:miria/model/account.dart"; +import "package:miria/model/antenna_settings.dart"; +import "package:miria/model/clip_settings.dart"; +import "package:miria/model/image_file.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/model/note_search_condition.dart"; +import "package:miria/model/tab_setting.dart"; +import "package:miria/model/users_list_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/abuse_dialog/abuse_dialog.dart"; +import "package:miria/view/announcements_page/announcements_page.dart"; +import "package:miria/view/antenna_page/antenna_notes_page.dart"; +import "package:miria/view/antenna_page/antenna_page.dart"; +import "package:miria/view/antenna_page/antenna_settings_dialog.dart"; +import "package:miria/view/channel_description_dialog.dart"; +import "package:miria/view/channels_page/channel_detail_page.dart"; +import "package:miria/view/channels_page/channels_page.dart"; +import "package:miria/view/clip_list_page/clip_detail_page.dart"; +import "package:miria/view/clip_list_page/clip_list_page.dart"; +import "package:miria/view/clip_list_page/clip_settings_dialog.dart"; +import "package:miria/view/clip_modal_sheet/clip_modal_sheet.dart"; +import "package:miria/view/common/account_select_dialog.dart"; +import "package:miria/view/common/color_picker_dialog.dart"; +import "package:miria/view/common/misskey_notes/reaction_user_dialog.dart"; +import "package:miria/view/common/misskey_notes/renote_modal_sheet.dart"; +import "package:miria/view/common/misskey_notes/renote_user_dialog.dart"; +import "package:miria/view/explore_page/explore_page.dart"; +import "package:miria/view/explore_page/explore_role_users_page.dart"; +import "package:miria/view/favorited_note_page/favorited_note_page.dart"; +import "package:miria/view/federation_page/federation_page.dart"; +import "package:miria/view/games_page/misskey_games_page.dart"; +import "package:miria/view/hashtag_page/hashtag_page.dart"; +import "package:miria/view/login_page/login_page.dart"; +import "package:miria/view/login_page/misskey_server_list_dialog.dart"; +import "package:miria/view/misskey_page_page/misskey_page_page.dart"; +import "package:miria/view/note_create_page/drive_file_select_dialog.dart"; +import "package:miria/view/note_create_page/drive_modal_sheet.dart"; +import "package:miria/view/note_create_page/note_create_page.dart"; +import "package:miria/view/note_detail_page/note_detail_page.dart"; +import "package:miria/view/note_modal_sheet/note_modal_sheet.dart"; +import "package:miria/view/notes_after_renote_page/notes_after_renote_page.dart"; +import "package:miria/view/notification_page/notification_page.dart"; +import "package:miria/view/photo_edit_page/license_confirm_dialog.dart"; +import "package:miria/view/photo_edit_page/photo_edit_page.dart"; +import "package:miria/view/reaction_picker_dialog/reaction_picker_dialog.dart"; +import "package:miria/view/search_page/search_page.dart"; +import "package:miria/view/server_detail_dialog.dart"; +import "package:miria/view/settings_page/account_settings_page/account_list.dart"; +import "package:miria/view/settings_page/app_info_page/app_info_page.dart"; +import "package:miria/view/settings_page/general_settings_page/general_settings_page.dart"; +import "package:miria/view/settings_page/import_export_page/folder_select_dialog.dart"; +import "package:miria/view/settings_page/import_export_page/import_export_page.dart"; +import "package:miria/view/settings_page/settings_page.dart"; +import "package:miria/view/settings_page/tab_settings_page/antenna_select_dialog.dart"; +import "package:miria/view/settings_page/tab_settings_page/channel_select_dialog.dart"; +import "package:miria/view/settings_page/tab_settings_page/role_select_dialog.dart"; +import "package:miria/view/settings_page/tab_settings_page/tab_settings_list_page.dart"; +import "package:miria/view/settings_page/tab_settings_page/tab_settings_page.dart"; +import "package:miria/view/settings_page/tab_settings_page/user_list_select_dialog.dart"; +import "package:miria/view/several_account_settings_page/cache_management_page/cache_management_page.dart"; +import "package:miria/view/several_account_settings_page/instance_mute_page/instance_mute_page.dart"; +import "package:miria/view/several_account_settings_page/reaction_deck_page/reaction_deck_page.dart"; +import "package:miria/view/several_account_settings_page/several_account_general_settings_page/several_account_general_settings_page.dart"; +import "package:miria/view/several_account_settings_page/several_account_settings_page.dart"; +import "package:miria/view/several_account_settings_page/word_mute_page/word_mute_page.dart"; +import "package:miria/view/share_extension_page/share_extension_page.dart"; +import "package:miria/view/sharing_account_select_page/account_select_page.dart"; +import "package:miria/view/splash_page/splash_page.dart"; +import "package:miria/view/time_line_page/time_line_page.dart"; +import "package:miria/view/user_page/antenna_modal_sheet.dart"; +import "package:miria/view/user_page/update_memo_dialog.dart"; +import "package:miria/view/user_page/user_control_dialog.dart"; +import "package:miria/view/user_page/user_followee.dart"; +import "package:miria/view/user_page/user_follower.dart"; +import "package:miria/view/user_page/user_page.dart"; +import "package:miria/view/user_page/users_list_modal_sheet.dart"; +import "package:miria/view/user_select_dialog.dart"; +import "package:miria/view/users_list_page/users_list_detail_page.dart"; +import "package:miria/view/users_list_page/users_list_page.dart"; +import "package:miria/view/users_list_page/users_list_settings_dialog.dart"; +import "package:miria/view/users_list_page/users_list_timeline_page.dart"; +import "package:misskey_dart/misskey_dart.dart"; -import '../view/antenna_page/antenna_notes_page.dart'; -import '../view/channels_page/channel_detail_page.dart'; -import '../view/login_page/login_page.dart'; -import '../view/settings_page/settings_page.dart'; -import '../view/settings_page/tab_settings_page/tab_settings_page.dart'; +part "app_router.gr.dart"; -part 'app_router.gr.dart'; - -@AutoRouterConfig() +@AutoRouterConfig(replaceInRouteName: "Page|Dialog|Sheet,Route") class AppRouter extends _$AppRouter { @override final List routes = [ @@ -104,6 +136,72 @@ class AppRouter extends _$AppRouter { // きしょ…… AutoRoute(page: MisskeyRouteRoute.page), - AutoRoute(path: "/share-extension", page: ShareExtensionRoute.page) + AutoRoute(path: "/share-extension", page: ShareExtensionRoute.page), + + // ダイアログ + AutoDialogRoute(page: AbuseRoute.page), + AutoDialogRoute(page: RenoteUserRoute.page), + AutoDialogRoute(page: ChannelDescriptionRoute.page), + AutoDialogRoute(page: ExpireSelectRoute.page), + AutoDialogRoute(page: UpdateMemoRoute.page), + AutoDialogRoute(page: LicenseConfirmRoute.page), + AutoDialogRoute(page: ColorPickerRoute.page), + AutoDialogRoute(page: MisskeyServerListRoute.page), + AutoDialogRoute(page: ChannelDetailRoute.page), + AutoDialogRoute(page: ServerDetailRoute.page), + AutoDialogRoute(page: ReactionUserRoute.page), + AutoDialogRoute(page: ChannelSelectRoute.page), + AutoDialogRoute(page: ClipSettingsRoute.page), + AutoDialogRoute(page: ReactionPickerRoute.page), + AutoDialogRoute(page: AccountSelectRoute.page), + AutoDialogRoute(page: UserSelectRoute.page), + AutoDialogRoute(page: RoleSelectRoute.page), + AutoDialogRoute(page: AntennaSelectRoute.page), + AutoDialogRoute(page: UserListSelectRoute.page), + AutoDialogRoute(page: UsersListSettingsRoute.page), + AutoDialogRoute(page: AntennaSettingsRoute.page), + AutoDialogRoute(page: FolderSelectRoute.page), + AutoDialogRoute>(page: DriveFileSelectRoute.page), + + // モーダルシート + AutoModalRouteSheet(page: UserControlRoute.page), + AutoModalRouteSheet(page: NoteModalRoute.page), + AutoModalRouteSheet(page: RenoteModalRoute.page), + AutoModalRouteSheet(page: AntennaModalRoute.page), + AutoModalRouteSheet(page: ClipModalRoute.page), + AutoModalRouteSheet(page: UsersListModalRoute.page), + AutoModalRouteSheet(page: DriveModalRoute.page), ]; } + +/// ダイアログ +class AutoDialogRoute extends CustomRoute { + AutoDialogRoute({ + required super.page, + }) : super( + transitionsBuilder: TransitionsBuilders.fadeIn, + durationInMilliseconds: 200, + fullscreenDialog: false, + customRouteBuilder: (context, widget, page) => DialogRoute( + context: context, + builder: (context) => widget, + settings: page, + ), + ); +} + +/// モーダルボトムシート +class AutoModalRouteSheet extends CustomRoute { + AutoModalRouteSheet({ + required super.page, + }) : super( + transitionsBuilder: TransitionsBuilders.slideBottom, + durationInMilliseconds: 200, + customRouteBuilder: (context, widget, page) => + ModalBottomSheetRoute( + builder: (context) => widget, + isScrollControlled: false, + settings: page, + ), + ); +} diff --git a/lib/router/app_router.gr.dart b/lib/router/app_router.gr.dart index 639dc6fca..2477e2e3a 100644 --- a/lib/router/app_router.gr.dart +++ b/lib/router/app_router.gr.dart @@ -15,343 +15,319 @@ abstract class _$AppRouter extends RootStackRouter { @override final Map pagesMap = { - NotesAfterRenoteRoute.name: (routeData) { - final args = routeData.argsAs(); + AbuseRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: NotesAfterRenotePage( - key: args.key, - note: args.note, + child: WrappedRoute( + child: AbuseDialog( account: args.account, - ), + targetUser: args.targetUser, + key: args.key, + defaultText: args.defaultText, + )), ); }, - AntennaRoute.name: (routeData) { - final args = routeData.argsAs(); + AccountListRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, - child: AntennaPage( - account: args.account, - key: args.key, - ), + child: const AccountListPage(), ); }, - AntennaNotesRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( + AccountSelectRoute.name: (routeData) { + final args = routeData.argsAs( + orElse: () => const AccountSelectRouteArgs()); + return AutoRoutePage( routeData: routeData, - child: AntennaNotesPage( + child: AccountSelectDialog( key: args.key, - antenna: args.antenna, - account: args.account, + host: args.host, + remoteHost: args.remoteHost, ), ); }, - NotificationRoute.name: (routeData) { - final args = routeData.argsAs(); + AnnouncementRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: NotificationPage( + child: WrappedRoute( + child: AnnouncementPage( + accountContext: args.accountContext, key: args.key, - account: args.account, - ), + )), ); }, - LoginRoute.name: (routeData) { + AntennaModalRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: const LoginPage(), + child: WrappedRoute( + child: AntennaModalSheet( + account: args.account, + user: args.user, + key: args.key, + )), ); }, - ClipDetailRoute.name: (routeData) { - final args = routeData.argsAs(); + AntennaNotesRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: ClipDetailPage( + child: WrappedRoute( + child: AntennaNotesPage( + antenna: args.antenna, + accountContext: args.accountContext, key: args.key, - account: args.account, - id: args.id, - ), + )), ); }, - ClipListRoute.name: (routeData) { - final args = routeData.argsAs(); + AntennaRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: ClipListPage( + child: WrappedRoute( + child: AntennaPage( + accountContext: args.accountContext, key: args.key, - account: args.account, - ), + )), ); }, - NoteCreateRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( + AntennaSelectRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( routeData: routeData, - child: NoteCreatePage( + child: WrappedRoute( + child: AntennaSelectDialog( + account: args.account, key: args.key, - initialAccount: args.initialAccount, - initialText: args.initialText, - initialMediaFiles: args.initialMediaFiles, - exitOnNoted: args.exitOnNoted, - channel: args.channel, - reply: args.reply, - renote: args.renote, - note: args.note, - noteCreationMode: args.noteCreationMode, - ), + )), ); }, - HashtagRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( + AntennaSettingsRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( routeData: routeData, - child: HashtagPage( - key: args.key, - hashtag: args.hashtag, + child: AntennaSettingsDialog( account: args.account, + key: args.key, + title: args.title, + initialSettings: args.initialSettings, ), ); }, - UserFolloweeRoute.name: (routeData) { - final args = routeData.argsAs(); + AppInfoRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, - child: UserFolloweePage( - key: args.key, - userId: args.userId, - account: args.account, - ), + child: const AppInfoPage(), ); }, - UserRoute.name: (routeData) { - final args = routeData.argsAs(); + CacheManagementRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: UserPage( - key: args.key, - userId: args.userId, + child: CacheManagementPage( account: args.account, + key: args.key, ), ); }, - UserFollowerRoute.name: (routeData) { - final args = routeData.argsAs(); + ChannelDescriptionRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: UserFollowerPage( - key: args.key, - userId: args.userId, + child: WrappedRoute( + child: ChannelDescriptionDialog( + channelId: args.channelId, account: args.account, - ), + key: args.key, + )), ); }, - PhotoEditRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( + ChannelDetailRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( routeData: routeData, - child: PhotoEditPage( - account: args.account, - file: args.file, - onSubmit: args.onSubmit, + child: WrappedRoute( + child: ChannelDetailPage( + accountContext: args.accountContext, + channelId: args.channelId, key: args.key, - ), + )), ); }, - AnnouncementRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( + ChannelSelectRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( routeData: routeData, - child: AnnouncementPage( - key: args.key, + child: WrappedRoute( + child: ChannelSelectDialog( account: args.account, - ), + key: args.key, + )), ); }, - SplashRoute.name: (routeData) { + ChannelsRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: const SplashPage(), + child: WrappedRoute( + child: ChannelsPage( + accountContext: args.accountContext, + key: args.key, + )), ); }, - SeveralAccountGeneralSettingsRoute.name: (routeData) { - final args = routeData.argsAs(); + ClipDetailRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: SeveralAccountGeneralSettingsPage( + child: WrappedRoute( + child: ClipDetailPage( + accountContext: args.accountContext, + id: args.id, key: args.key, - account: args.account, - ), + )), ); }, - SeveralAccountSettingsRoute.name: (routeData) { - final args = routeData.argsAs(); + ClipListRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: SeveralAccountSettingsPage( + child: WrappedRoute( + child: ClipListPage( + accountContext: args.accountContext, key: args.key, - account: args.account, - ), + )), ); }, - InstanceMuteRoute.name: (routeData) { - final args = routeData.argsAs(); + ClipModalRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: InstanceMutePage( - key: args.key, + child: WrappedRoute( + child: ClipModalSheet( account: args.account, - ), + noteId: args.noteId, + key: args.key, + )), ); }, - WordMuteRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( + ClipSettingsRoute.name: (routeData) { + final args = routeData.argsAs( + orElse: () => const ClipSettingsRouteArgs()); + return AutoRoutePage( routeData: routeData, - child: WordMutePage( + child: ClipSettingsDialog( key: args.key, - account: args.account, - muteType: args.muteType, + title: args.title, + initialSettings: args.initialSettings, ), ); }, - CacheManagementRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( + ColorPickerRoute.name: (routeData) { + return AutoRoutePage( routeData: routeData, - child: CacheManagementPage( - key: args.key, - account: args.account, - ), + child: const ColorPickerDialog(), ); }, - ReactionDeckRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( + DriveFileSelectRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage>( routeData: routeData, - child: ReactionDeckPage( - key: args.key, + child: WrappedRoute( + child: DriveFileSelectDialog( account: args.account, - ), + key: args.key, + allowMultiple: args.allowMultiple, + )), ); }, - UsersListTimelineRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( + DriveModalRoute.name: (routeData) { + return AutoRoutePage( routeData: routeData, - child: UsersListTimelinePage( - args.account, - args.list, - key: args.key, - ), + child: const DriveModalSheet(), ); }, - UsersListRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( + ExpireSelectRoute.name: (routeData) { + return AutoRoutePage( routeData: routeData, - child: UsersListPage( - args.account, - key: args.key, - ), + child: const ExpireSelectDialog(), ); }, - UsersListDetailRoute.name: (routeData) { - final args = routeData.argsAs(); + ExploreRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: UsersListDetailPage( + child: WrappedRoute( + child: ExplorePage( + accountContext: args.accountContext, key: args.key, - account: args.account, - listId: args.listId, - ), + )), ); }, - ChannelDetailRoute.name: (routeData) { - final args = routeData.argsAs(); + ExploreRoleUsersRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: ChannelDetailPage( + child: WrappedRoute( + child: ExploreRoleUsersPage( + item: args.item, + accountContext: args.accountContext, key: args.key, - account: args.account, - channelId: args.channelId, - ), + )), ); }, - ChannelsRoute.name: (routeData) { - final args = routeData.argsAs(); + FavoritedNoteRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: ChannelsPage( + child: WrappedRoute( + child: FavoritedNotePage( + accountContext: args.accountContext, key: args.key, - account: args.account, - ), + )), ); }, FederationRoute.name: (routeData) { final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: FederationPage( - key: args.key, - account: args.account, + child: WrappedRoute( + child: FederationPage( + accountContext: args.accountContext, host: args.host, - ), - ); - }, - NoteDetailRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: NoteDetailPage( key: args.key, - note: args.note, - account: args.account, - ), + )), ); }, - SearchRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( + FolderSelectRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( routeData: routeData, - child: SearchPage( - key: args.key, - initialNoteSearchCondition: args.initialNoteSearchCondition, + child: FolderSelectDialog( account: args.account, - ), - ); - }, - SharingAccountSelectRoute.name: (routeData) { - final args = routeData.argsAs( - orElse: () => const SharingAccountSelectRouteArgs()); - return AutoRoutePage( - routeData: routeData, - child: SharingAccountSelectPage( + fileShowTarget: args.fileShowTarget, + confirmationText: args.confirmationText, key: args.key, - sharingText: args.sharingText, - filePath: args.filePath, ), ); }, - MisskeyRouteRoute.name: (routeData) { - final args = routeData.argsAs(); + GeneralSettingsRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, - child: MisskeyPagePage( - key: args.key, - account: args.account, - page: args.page, - ), + child: const GeneralSettingsPage(), ); }, - MisskeyGamesRoute.name: (routeData) { - final args = routeData.argsAs(); + HashtagRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: MisskeyGamesPage( + child: WrappedRoute( + child: HashtagPage( + hashtag: args.hashtag, + accountContext: args.accountContext, key: args.key, - account: args.account, - ), + )), ); }, ImportExportRoute.name: (routeData) { @@ -360,65 +336,301 @@ abstract class _$AppRouter extends RootStackRouter { child: const ImportExportPage(), ); }, - GeneralSettingsRoute.name: (routeData) { + InstanceMuteRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: const GeneralSettingsPage(), + child: WrappedRoute( + child: InstanceMutePage( + account: args.account, + key: args.key, + )), ); }, - TabSettingsRoute.name: (routeData) { - final args = routeData.argsAs( - orElse: () => const TabSettingsRouteArgs()); - return AutoRoutePage( + LicenseConfirmRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( routeData: routeData, - child: TabSettingsPage( + child: WrappedRoute( + child: LicenseConfirmDialog( + emoji: args.emoji, + account: args.account, key: args.key, - tabIndex: args.tabIndex, - ), + )), ); }, - TabSettingsListRoute.name: (routeData) { + LoginRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, - child: const TabSettingsListPage(), + child: const LoginPage(), ); }, - AppInfoRoute.name: (routeData) { + MisskeyGamesRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: const AppInfoPage(), + child: WrappedRoute( + child: MisskeyGamesPage( + accountContext: args.accountContext, + key: args.key, + )), ); }, - SettingsRoute.name: (routeData) { + MisskeyRouteRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: const SettingsPage(), + child: WrappedRoute( + child: MisskeyPagePage( + accountContext: args.accountContext, + page: args.page, + key: args.key, + )), ); }, - AccountListRoute.name: (routeData) { - return AutoRoutePage( + MisskeyServerListRoute.name: (routeData) { + return AutoRoutePage( routeData: routeData, - child: const AccountListPage(), + child: const MisskeyServerListDialog(), ); }, - ExploreRoleUsersRoute.name: (routeData) { - final args = routeData.argsAs(); + NoteCreateRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: ExploreRoleUsersPage( + child: WrappedRoute( + child: NoteCreatePage( + initialAccount: args.initialAccount, key: args.key, - item: args.item, + initialText: args.initialText, + initialMediaFiles: args.initialMediaFiles, + exitOnNoted: args.exitOnNoted, + channel: args.channel, + reply: args.reply, + renote: args.renote, + note: args.note, + noteCreationMode: args.noteCreationMode, + )), + ); + }, + NoteDetailRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: NoteDetailPage( + note: args.note, + accountContext: args.accountContext, + key: args.key, + )), + ); + }, + NoteModalRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: NoteModalSheet( + baseNote: args.baseNote, + targetNote: args.targetNote, + accountContext: args.accountContext, + noteBoundaryKey: args.noteBoundaryKey, + key: args.key, + )), + ); + }, + NotesAfterRenoteRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: NotesAfterRenotePage( + note: args.note, + accountContext: args.accountContext, + key: args.key, + )), + ); + }, + NotificationRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: NotificationPage( + accountContext: args.accountContext, + key: args.key, + )), + ); + }, + PhotoEditRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: PhotoEditPage( + accountContext: args.accountContext, + file: args.file, + onSubmit: args.onSubmit, + key: args.key, + )), + ); + }, + ReactionDeckRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: ReactionDeckPage( account: args.account, + key: args.key, ), ); }, - ExploreRoute.name: (routeData) { - final args = routeData.argsAs(); + ReactionPickerRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: ReactionPickerDialog( + account: args.account, + isAcceptSensitive: args.isAcceptSensitive, + key: args.key, + )), + ); + }, + ReactionUserRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: ExplorePage( + child: WrappedRoute( + child: ReactionUserDialog( + accountContext: args.accountContext, + emojiData: args.emojiData, + noteId: args.noteId, key: args.key, + )), + ); + }, + RenoteModalRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: RenoteModalSheet( + note: args.note, + account: args.account, + key: args.key, + ), + ); + }, + RenoteUserRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: RenoteUserDialog( + account: args.account, + noteId: args.noteId, + key: args.key, + )), + ); + }, + RoleSelectRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: RoleSelectDialog( + account: args.account, + key: args.key, + )), + ); + }, + SearchRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: SearchPage( + accountContext: args.accountContext, + key: args.key, + initialNoteSearchCondition: args.initialNoteSearchCondition, + )), + ); + }, + ServerDetailRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: ServerDetailDialog( + accountContext: args.accountContext, + key: args.key, + )), + ); + }, + SettingsRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const SettingsPage(), + ); + }, + SeveralAccountGeneralSettingsRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: SeveralAccountGeneralSettingsPage( + account: args.account, + key: args.key, + ), + ); + }, + SeveralAccountSettingsRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: SeveralAccountSettingsPage( account: args.account, + key: args.key, + ), + ); + }, + ShareExtensionRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const ShareExtensionPage(), + ); + }, + SharingAccountSelectRoute.name: (routeData) { + final args = routeData.argsAs( + orElse: () => const SharingAccountSelectRouteArgs()); + return AutoRoutePage( + routeData: routeData, + child: SharingAccountSelectPage( + key: args.key, + sharingText: args.sharingText, + filePath: args.filePath, + ), + ); + }, + SplashRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const SplashPage(), + ); + }, + TabSettingsListRoute.name: (routeData) { + return AutoRoutePage( + routeData: routeData, + child: const TabSettingsListPage(), + ); + }, + TabSettingsRoute.name: (routeData) { + final args = routeData.argsAs( + orElse: () => const TabSettingsRouteArgs()); + return AutoRoutePage( + routeData: routeData, + child: TabSettingsPage( + key: args.key, + tabIndex: args.tabIndex, ), ); }, @@ -427,189 +639,1347 @@ abstract class _$AppRouter extends RootStackRouter { return AutoRoutePage( routeData: routeData, child: TimeLinePage( - key: args.key, initialTabSetting: args.initialTabSetting, + key: args.key, ), ); }, - ShareExtensionRoute.name: (routeData) { + UpdateMemoRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: const ShareExtensionPage(), + child: WrappedRoute( + child: UpdateMemoDialog( + accountContext: args.accountContext, + initialMemo: args.initialMemo, + userId: args.userId, + key: args.key, + )), ); }, - FavoritedNoteRoute.name: (routeData) { - final args = routeData.argsAs(); + UserControlRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: UserControlDialog( + account: args.account, + response: args.response, + key: args.key, + )), + ); + }, + UserFolloweeRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: UserFolloweePage( + userId: args.userId, + accountContext: args.accountContext, + key: args.key, + )), + ); + }, + UserFollowerRoute.name: (routeData) { + final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: FavoritedNotePage( + child: WrappedRoute( + child: UserFollowerPage( + userId: args.userId, + accountContext: args.accountContext, key: args.key, + )), + ); + }, + UserListSelectRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: UserListSelectDialog( account: args.account, - ), + key: args.key, + )), + ); + }, + UserRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: UserPage( + userId: args.userId, + accountContext: args.accountContext, + key: args.key, + )), + ); + }, + UserSelectRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: UserSelectDialog( + accountContext: args.accountContext, + key: args.key, + )), + ); + }, + UsersListDetailRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: UsersListDetailPage( + accountContext: args.accountContext, + listId: args.listId, + key: args.key, + )), + ); + }, + UsersListModalRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: UsersListModalSheet( + account: args.account, + user: args.user, + key: args.key, + )), + ); + }, + UsersListRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: UsersListPage( + args.accountContext, + key: args.key, + )), + ); + }, + UsersListSettingsRoute.name: (routeData) { + final args = routeData.argsAs( + orElse: () => const UsersListSettingsRouteArgs()); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: UsersListSettingsDialog( + key: args.key, + title: args.title, + initialSettings: args.initialSettings, + )), + ); + }, + UsersListTimelineRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: UsersListTimelinePage( + args.accountContext, + args.list, + key: args.key, + )), + ); + }, + WordMuteRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: WrappedRoute( + child: WordMutePage( + account: args.account, + muteType: args.muteType, + key: args.key, + )), ); }, }; } /// generated route for -/// [NotesAfterRenotePage] -class NotesAfterRenoteRoute extends PageRouteInfo { - NotesAfterRenoteRoute({ +/// [AbuseDialog] +class AbuseRoute extends PageRouteInfo { + AbuseRoute({ + required Account account, + required User targetUser, + Key? key, + String? defaultText, + List? children, + }) : super( + AbuseRoute.name, + args: AbuseRouteArgs( + account: account, + targetUser: targetUser, + key: key, + defaultText: defaultText, + ), + initialChildren: children, + ); + + static const String name = 'AbuseRoute'; + + static const PageInfo page = PageInfo(name); +} + +class AbuseRouteArgs { + const AbuseRouteArgs({ + required this.account, + required this.targetUser, + this.key, + this.defaultText, + }); + + final Account account; + + final User targetUser; + + final Key? key; + + final String? defaultText; + + @override + String toString() { + return 'AbuseRouteArgs{account: $account, targetUser: $targetUser, key: $key, defaultText: $defaultText}'; + } +} + +/// generated route for +/// [AccountListPage] +class AccountListRoute extends PageRouteInfo { + const AccountListRoute({List? children}) + : super( + AccountListRoute.name, + initialChildren: children, + ); + + static const String name = 'AccountListRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [AccountSelectDialog] +class AccountSelectRoute extends PageRouteInfo { + AccountSelectRoute({ + Key? key, + String? host, + String? remoteHost, + List? children, + }) : super( + AccountSelectRoute.name, + args: AccountSelectRouteArgs( + key: key, + host: host, + remoteHost: remoteHost, + ), + initialChildren: children, + ); + + static const String name = 'AccountSelectRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class AccountSelectRouteArgs { + const AccountSelectRouteArgs({ + this.key, + this.host, + this.remoteHost, + }); + + final Key? key; + + final String? host; + + final String? remoteHost; + + @override + String toString() { + return 'AccountSelectRouteArgs{key: $key, host: $host, remoteHost: $remoteHost}'; + } +} + +/// generated route for +/// [AnnouncementPage] +class AnnouncementRoute extends PageRouteInfo { + AnnouncementRoute({ + required AccountContext accountContext, + Key? key, + List? children, + }) : super( + AnnouncementRoute.name, + args: AnnouncementRouteArgs( + accountContext: accountContext, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'AnnouncementRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class AnnouncementRouteArgs { + const AnnouncementRouteArgs({ + required this.accountContext, + this.key, + }); + + final AccountContext accountContext; + + final Key? key; + + @override + String toString() { + return 'AnnouncementRouteArgs{accountContext: $accountContext, key: $key}'; + } +} + +/// generated route for +/// [AntennaModalSheet] +class AntennaModalRoute extends PageRouteInfo { + AntennaModalRoute({ + required Account account, + required User user, + Key? key, + List? children, + }) : super( + AntennaModalRoute.name, + args: AntennaModalRouteArgs( + account: account, + user: user, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'AntennaModalRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class AntennaModalRouteArgs { + const AntennaModalRouteArgs({ + required this.account, + required this.user, + this.key, + }); + + final Account account; + + final User user; + + final Key? key; + + @override + String toString() { + return 'AntennaModalRouteArgs{account: $account, user: $user, key: $key}'; + } +} + +/// generated route for +/// [AntennaNotesPage] +class AntennaNotesRoute extends PageRouteInfo { + AntennaNotesRoute({ + required Antenna antenna, + required AccountContext accountContext, + Key? key, + List? children, + }) : super( + AntennaNotesRoute.name, + args: AntennaNotesRouteArgs( + antenna: antenna, + accountContext: accountContext, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'AntennaNotesRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class AntennaNotesRouteArgs { + const AntennaNotesRouteArgs({ + required this.antenna, + required this.accountContext, + this.key, + }); + + final Antenna antenna; + + final AccountContext accountContext; + + final Key? key; + + @override + String toString() { + return 'AntennaNotesRouteArgs{antenna: $antenna, accountContext: $accountContext, key: $key}'; + } +} + +/// generated route for +/// [AntennaPage] +class AntennaRoute extends PageRouteInfo { + AntennaRoute({ + required AccountContext accountContext, + Key? key, + List? children, + }) : super( + AntennaRoute.name, + args: AntennaRouteArgs( + accountContext: accountContext, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'AntennaRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class AntennaRouteArgs { + const AntennaRouteArgs({ + required this.accountContext, + this.key, + }); + + final AccountContext accountContext; + + final Key? key; + + @override + String toString() { + return 'AntennaRouteArgs{accountContext: $accountContext, key: $key}'; + } +} + +/// generated route for +/// [AntennaSelectDialog] +class AntennaSelectRoute extends PageRouteInfo { + AntennaSelectRoute({ + required Account account, + Key? key, + List? children, + }) : super( + AntennaSelectRoute.name, + args: AntennaSelectRouteArgs( + account: account, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'AntennaSelectRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class AntennaSelectRouteArgs { + const AntennaSelectRouteArgs({ + required this.account, + this.key, + }); + + final Account account; + + final Key? key; + + @override + String toString() { + return 'AntennaSelectRouteArgs{account: $account, key: $key}'; + } +} + +/// generated route for +/// [AntennaSettingsDialog] +class AntennaSettingsRoute extends PageRouteInfo { + AntennaSettingsRoute({ + required Account account, + Key? key, + Widget? title, + AntennaSettings initialSettings = const AntennaSettings(), + List? children, + }) : super( + AntennaSettingsRoute.name, + args: AntennaSettingsRouteArgs( + account: account, + key: key, + title: title, + initialSettings: initialSettings, + ), + initialChildren: children, + ); + + static const String name = 'AntennaSettingsRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class AntennaSettingsRouteArgs { + const AntennaSettingsRouteArgs({ + required this.account, + this.key, + this.title, + this.initialSettings = const AntennaSettings(), + }); + + final Account account; + + final Key? key; + + final Widget? title; + + final AntennaSettings initialSettings; + + @override + String toString() { + return 'AntennaSettingsRouteArgs{account: $account, key: $key, title: $title, initialSettings: $initialSettings}'; + } +} + +/// generated route for +/// [AppInfoPage] +class AppInfoRoute extends PageRouteInfo { + const AppInfoRoute({List? children}) + : super( + AppInfoRoute.name, + initialChildren: children, + ); + + static const String name = 'AppInfoRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [CacheManagementPage] +class CacheManagementRoute extends PageRouteInfo { + CacheManagementRoute({ + required Account account, + Key? key, + List? children, + }) : super( + CacheManagementRoute.name, + args: CacheManagementRouteArgs( + account: account, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'CacheManagementRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class CacheManagementRouteArgs { + const CacheManagementRouteArgs({ + required this.account, + this.key, + }); + + final Account account; + + final Key? key; + + @override + String toString() { + return 'CacheManagementRouteArgs{account: $account, key: $key}'; + } +} + +/// generated route for +/// [ChannelDescriptionDialog] +class ChannelDescriptionRoute + extends PageRouteInfo { + ChannelDescriptionRoute({ + required String channelId, + required Account account, + Key? key, + List? children, + }) : super( + ChannelDescriptionRoute.name, + args: ChannelDescriptionRouteArgs( + channelId: channelId, + account: account, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'ChannelDescriptionRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class ChannelDescriptionRouteArgs { + const ChannelDescriptionRouteArgs({ + required this.channelId, + required this.account, + this.key, + }); + + final String channelId; + + final Account account; + + final Key? key; + + @override + String toString() { + return 'ChannelDescriptionRouteArgs{channelId: $channelId, account: $account, key: $key}'; + } +} + +/// generated route for +/// [ChannelDetailPage] +class ChannelDetailRoute extends PageRouteInfo { + ChannelDetailRoute({ + required AccountContext accountContext, + required String channelId, + Key? key, + List? children, + }) : super( + ChannelDetailRoute.name, + args: ChannelDetailRouteArgs( + accountContext: accountContext, + channelId: channelId, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'ChannelDetailRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class ChannelDetailRouteArgs { + const ChannelDetailRouteArgs({ + required this.accountContext, + required this.channelId, + this.key, + }); + + final AccountContext accountContext; + + final String channelId; + + final Key? key; + + @override + String toString() { + return 'ChannelDetailRouteArgs{accountContext: $accountContext, channelId: $channelId, key: $key}'; + } +} + +/// generated route for +/// [ChannelSelectDialog] +class ChannelSelectRoute extends PageRouteInfo { + ChannelSelectRoute({ + required Account account, + Key? key, + List? children, + }) : super( + ChannelSelectRoute.name, + args: ChannelSelectRouteArgs( + account: account, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'ChannelSelectRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class ChannelSelectRouteArgs { + const ChannelSelectRouteArgs({ + required this.account, + this.key, + }); + + final Account account; + + final Key? key; + + @override + String toString() { + return 'ChannelSelectRouteArgs{account: $account, key: $key}'; + } +} + +/// generated route for +/// [ChannelsPage] +class ChannelsRoute extends PageRouteInfo { + ChannelsRoute({ + required AccountContext accountContext, + Key? key, + List? children, + }) : super( + ChannelsRoute.name, + args: ChannelsRouteArgs( + accountContext: accountContext, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'ChannelsRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class ChannelsRouteArgs { + const ChannelsRouteArgs({ + required this.accountContext, + this.key, + }); + + final AccountContext accountContext; + + final Key? key; + + @override + String toString() { + return 'ChannelsRouteArgs{accountContext: $accountContext, key: $key}'; + } +} + +/// generated route for +/// [ClipDetailPage] +class ClipDetailRoute extends PageRouteInfo { + ClipDetailRoute({ + required AccountContext accountContext, + required String id, + Key? key, + List? children, + }) : super( + ClipDetailRoute.name, + args: ClipDetailRouteArgs( + accountContext: accountContext, + id: id, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'ClipDetailRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class ClipDetailRouteArgs { + const ClipDetailRouteArgs({ + required this.accountContext, + required this.id, + this.key, + }); + + final AccountContext accountContext; + + final String id; + + final Key? key; + + @override + String toString() { + return 'ClipDetailRouteArgs{accountContext: $accountContext, id: $id, key: $key}'; + } +} + +/// generated route for +/// [ClipListPage] +class ClipListRoute extends PageRouteInfo { + ClipListRoute({ + required AccountContext accountContext, + Key? key, + List? children, + }) : super( + ClipListRoute.name, + args: ClipListRouteArgs( + accountContext: accountContext, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'ClipListRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class ClipListRouteArgs { + const ClipListRouteArgs({ + required this.accountContext, + this.key, + }); + + final AccountContext accountContext; + + final Key? key; + + @override + String toString() { + return 'ClipListRouteArgs{accountContext: $accountContext, key: $key}'; + } +} + +/// generated route for +/// [ClipModalSheet] +class ClipModalRoute extends PageRouteInfo { + ClipModalRoute({ + required Account account, + required String noteId, + Key? key, + List? children, + }) : super( + ClipModalRoute.name, + args: ClipModalRouteArgs( + account: account, + noteId: noteId, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'ClipModalRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class ClipModalRouteArgs { + const ClipModalRouteArgs({ + required this.account, + required this.noteId, + this.key, + }); + + final Account account; + + final String noteId; + + final Key? key; + + @override + String toString() { + return 'ClipModalRouteArgs{account: $account, noteId: $noteId, key: $key}'; + } +} + +/// generated route for +/// [ClipSettingsDialog] +class ClipSettingsRoute extends PageRouteInfo { + ClipSettingsRoute({ + Key? key, + Widget? title, + ClipSettings initialSettings = const ClipSettings(), + List? children, + }) : super( + ClipSettingsRoute.name, + args: ClipSettingsRouteArgs( + key: key, + title: title, + initialSettings: initialSettings, + ), + initialChildren: children, + ); + + static const String name = 'ClipSettingsRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class ClipSettingsRouteArgs { + const ClipSettingsRouteArgs({ + this.key, + this.title, + this.initialSettings = const ClipSettings(), + }); + + final Key? key; + + final Widget? title; + + final ClipSettings initialSettings; + + @override + String toString() { + return 'ClipSettingsRouteArgs{key: $key, title: $title, initialSettings: $initialSettings}'; + } +} + +/// generated route for +/// [ColorPickerDialog] +class ColorPickerRoute extends PageRouteInfo { + const ColorPickerRoute({List? children}) + : super( + ColorPickerRoute.name, + initialChildren: children, + ); + + static const String name = 'ColorPickerRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [DriveFileSelectDialog] +class DriveFileSelectRoute extends PageRouteInfo { + DriveFileSelectRoute({ + required Account account, + Key? key, + bool allowMultiple = false, + List? children, + }) : super( + DriveFileSelectRoute.name, + args: DriveFileSelectRouteArgs( + account: account, + key: key, + allowMultiple: allowMultiple, + ), + initialChildren: children, + ); + + static const String name = 'DriveFileSelectRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class DriveFileSelectRouteArgs { + const DriveFileSelectRouteArgs({ + required this.account, + this.key, + this.allowMultiple = false, + }); + + final Account account; + + final Key? key; + + final bool allowMultiple; + + @override + String toString() { + return 'DriveFileSelectRouteArgs{account: $account, key: $key, allowMultiple: $allowMultiple}'; + } +} + +/// generated route for +/// [DriveModalSheet] +class DriveModalRoute extends PageRouteInfo { + const DriveModalRoute({List? children}) + : super( + DriveModalRoute.name, + initialChildren: children, + ); + + static const String name = 'DriveModalRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [ExpireSelectDialog] +class ExpireSelectRoute extends PageRouteInfo { + const ExpireSelectRoute({List? children}) + : super( + ExpireSelectRoute.name, + initialChildren: children, + ); + + static const String name = 'ExpireSelectRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [ExplorePage] +class ExploreRoute extends PageRouteInfo { + ExploreRoute({ + required AccountContext accountContext, + Key? key, + List? children, + }) : super( + ExploreRoute.name, + args: ExploreRouteArgs( + accountContext: accountContext, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'ExploreRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class ExploreRouteArgs { + const ExploreRouteArgs({ + required this.accountContext, + this.key, + }); + + final AccountContext accountContext; + + final Key? key; + + @override + String toString() { + return 'ExploreRouteArgs{accountContext: $accountContext, key: $key}'; + } +} + +/// generated route for +/// [ExploreRoleUsersPage] +class ExploreRoleUsersRoute extends PageRouteInfo { + ExploreRoleUsersRoute({ + required RolesListResponse item, + required AccountContext accountContext, Key? key, - required Note note, - required Account account, List? children, }) : super( - NotesAfterRenoteRoute.name, - args: NotesAfterRenoteRouteArgs( + ExploreRoleUsersRoute.name, + args: ExploreRoleUsersRouteArgs( + item: item, + accountContext: accountContext, key: key, - note: note, - account: account, ), initialChildren: children, ); - static const String name = 'NotesAfterRenoteRoute'; + static const String name = 'ExploreRoleUsersRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class NotesAfterRenoteRouteArgs { - const NotesAfterRenoteRouteArgs({ +class ExploreRoleUsersRouteArgs { + const ExploreRoleUsersRouteArgs({ + required this.item, + required this.accountContext, this.key, - required this.note, - required this.account, }); + final RolesListResponse item; + + final AccountContext accountContext; + final Key? key; - final Note note; + @override + String toString() { + return 'ExploreRoleUsersRouteArgs{item: $item, accountContext: $accountContext, key: $key}'; + } +} - final Account account; +/// generated route for +/// [FavoritedNotePage] +class FavoritedNoteRoute extends PageRouteInfo { + FavoritedNoteRoute({ + required AccountContext accountContext, + Key? key, + List? children, + }) : super( + FavoritedNoteRoute.name, + args: FavoritedNoteRouteArgs( + accountContext: accountContext, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'FavoritedNoteRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class FavoritedNoteRouteArgs { + const FavoritedNoteRouteArgs({ + required this.accountContext, + this.key, + }); + + final AccountContext accountContext; + + final Key? key; @override String toString() { - return 'NotesAfterRenoteRouteArgs{key: $key, note: $note, account: $account}'; + return 'FavoritedNoteRouteArgs{accountContext: $accountContext, key: $key}'; } } /// generated route for -/// [AntennaPage] -class AntennaRoute extends PageRouteInfo { - AntennaRoute({ +/// [FederationPage] +class FederationRoute extends PageRouteInfo { + FederationRoute({ + required AccountContext accountContext, + required String host, + Key? key, + List? children, + }) : super( + FederationRoute.name, + args: FederationRouteArgs( + accountContext: accountContext, + host: host, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'FederationRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class FederationRouteArgs { + const FederationRouteArgs({ + required this.accountContext, + required this.host, + this.key, + }); + + final AccountContext accountContext; + + final String host; + + final Key? key; + + @override + String toString() { + return 'FederationRouteArgs{accountContext: $accountContext, host: $host, key: $key}'; + } +} + +/// generated route for +/// [FolderSelectDialog] +class FolderSelectRoute extends PageRouteInfo { + FolderSelectRoute({ required Account account, + required List? fileShowTarget, + required String confirmationText, Key? key, List? children, }) : super( - AntennaRoute.name, - args: AntennaRouteArgs( + FolderSelectRoute.name, + args: FolderSelectRouteArgs( account: account, + fileShowTarget: fileShowTarget, + confirmationText: confirmationText, key: key, ), initialChildren: children, ); - static const String name = 'AntennaRoute'; + static const String name = 'FolderSelectRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class AntennaRouteArgs { - const AntennaRouteArgs({ +class FolderSelectRouteArgs { + const FolderSelectRouteArgs({ required this.account, + required this.fileShowTarget, + required this.confirmationText, this.key, }); final Account account; + final List? fileShowTarget; + + final String confirmationText; + final Key? key; @override String toString() { - return 'AntennaRouteArgs{account: $account, key: $key}'; + return 'FolderSelectRouteArgs{account: $account, fileShowTarget: $fileShowTarget, confirmationText: $confirmationText, key: $key}'; } } /// generated route for -/// [AntennaNotesPage] -class AntennaNotesRoute extends PageRouteInfo { - AntennaNotesRoute({ +/// [GeneralSettingsPage] +class GeneralSettingsRoute extends PageRouteInfo { + const GeneralSettingsRoute({List? children}) + : super( + GeneralSettingsRoute.name, + initialChildren: children, + ); + + static const String name = 'GeneralSettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [HashtagPage] +class HashtagRoute extends PageRouteInfo { + HashtagRoute({ + required String hashtag, + required AccountContext accountContext, Key? key, - required Antenna antenna, - required Account account, List? children, }) : super( - AntennaNotesRoute.name, - args: AntennaNotesRouteArgs( + HashtagRoute.name, + args: HashtagRouteArgs( + hashtag: hashtag, + accountContext: accountContext, key: key, - antenna: antenna, - account: account, ), initialChildren: children, ); - static const String name = 'AntennaNotesRoute'; + static const String name = 'HashtagRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class AntennaNotesRouteArgs { - const AntennaNotesRouteArgs({ +class HashtagRouteArgs { + const HashtagRouteArgs({ + required this.hashtag, + required this.accountContext, this.key, - required this.antenna, - required this.account, }); + final String hashtag; + + final AccountContext accountContext; + final Key? key; - final Antenna antenna; + @override + String toString() { + return 'HashtagRouteArgs{hashtag: $hashtag, accountContext: $accountContext, key: $key}'; + } +} + +/// generated route for +/// [ImportExportPage] +class ImportExportRoute extends PageRouteInfo { + const ImportExportRoute({List? children}) + : super( + ImportExportRoute.name, + initialChildren: children, + ); + + static const String name = 'ImportExportRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [InstanceMutePage] +class InstanceMuteRoute extends PageRouteInfo { + InstanceMuteRoute({ + required Account account, + Key? key, + List? children, + }) : super( + InstanceMuteRoute.name, + args: InstanceMuteRouteArgs( + account: account, + key: key, + ), + initialChildren: children, + ); + + static const String name = 'InstanceMuteRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class InstanceMuteRouteArgs { + const InstanceMuteRouteArgs({ + required this.account, + this.key, + }); final Account account; + final Key? key; + @override String toString() { - return 'AntennaNotesRouteArgs{key: $key, antenna: $antenna, account: $account}'; + return 'InstanceMuteRouteArgs{account: $account, key: $key}'; } } /// generated route for -/// [NotificationPage] -class NotificationRoute extends PageRouteInfo { - NotificationRoute({ - Key? key, +/// [LicenseConfirmDialog] +class LicenseConfirmRoute extends PageRouteInfo { + LicenseConfirmRoute({ + required String emoji, required Account account, + Key? key, List? children, }) : super( - NotificationRoute.name, - args: NotificationRouteArgs( - key: key, + LicenseConfirmRoute.name, + args: LicenseConfirmRouteArgs( + emoji: emoji, account: account, + key: key, ), initialChildren: children, ); - static const String name = 'NotificationRoute'; + static const String name = 'LicenseConfirmRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class NotificationRouteArgs { - const NotificationRouteArgs({ - this.key, +class LicenseConfirmRouteArgs { + const LicenseConfirmRouteArgs({ + required this.emoji, required this.account, + this.key, }); - final Key? key; + final String emoji; final Account account; + final Key? key; + @override String toString() { - return 'NotificationRouteArgs{key: $key, account: $account}'; + return 'LicenseConfirmRouteArgs{emoji: $emoji, account: $account, key: $key}'; } } @@ -628,92 +1998,106 @@ class LoginRoute extends PageRouteInfo { } /// generated route for -/// [ClipDetailPage] -class ClipDetailRoute extends PageRouteInfo { - ClipDetailRoute({ - Key? key, - required Account account, - required String id, +/// [MisskeyGamesPage] +class MisskeyGamesRoute extends PageRouteInfo { + MisskeyGamesRoute({ + required AccountContext accountContext, + Key? key, List? children, }) : super( - ClipDetailRoute.name, - args: ClipDetailRouteArgs( + MisskeyGamesRoute.name, + args: MisskeyGamesRouteArgs( + accountContext: accountContext, key: key, - account: account, - id: id, ), initialChildren: children, ); - static const String name = 'ClipDetailRoute'; + static const String name = 'MisskeyGamesRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class ClipDetailRouteArgs { - const ClipDetailRouteArgs({ +class MisskeyGamesRouteArgs { + const MisskeyGamesRouteArgs({ + required this.accountContext, this.key, - required this.account, - required this.id, }); - final Key? key; - - final Account account; + final AccountContext accountContext; - final String id; + final Key? key; @override String toString() { - return 'ClipDetailRouteArgs{key: $key, account: $account, id: $id}'; + return 'MisskeyGamesRouteArgs{accountContext: $accountContext, key: $key}'; } } /// generated route for -/// [ClipListPage] -class ClipListRoute extends PageRouteInfo { - ClipListRoute({ +/// [MisskeyPagePage] +class MisskeyRouteRoute extends PageRouteInfo { + MisskeyRouteRoute({ + required AccountContext accountContext, + required Page page, Key? key, - required Account account, List? children, }) : super( - ClipListRoute.name, - args: ClipListRouteArgs( + MisskeyRouteRoute.name, + args: MisskeyRouteRouteArgs( + accountContext: accountContext, + page: page, key: key, - account: account, ), initialChildren: children, ); - static const String name = 'ClipListRoute'; + static const String name = 'MisskeyRouteRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class ClipListRouteArgs { - const ClipListRouteArgs({ +class MisskeyRouteRouteArgs { + const MisskeyRouteRouteArgs({ + required this.accountContext, + required this.page, this.key, - required this.account, }); - final Key? key; + final AccountContext accountContext; - final Account account; + final Page page; + + final Key? key; @override String toString() { - return 'ClipListRouteArgs{key: $key, account: $account}'; + return 'MisskeyRouteRouteArgs{accountContext: $accountContext, page: $page, key: $key}'; } } +/// generated route for +/// [MisskeyServerListDialog] +class MisskeyServerListRoute extends PageRouteInfo { + const MisskeyServerListRoute({List? children}) + : super( + MisskeyServerListRoute.name, + initialChildren: children, + ); + + static const String name = 'MisskeyServerListRoute'; + + static const PageInfo page = PageInfo(name); +} + /// generated route for /// [NoteCreatePage] class NoteCreateRoute extends PageRouteInfo { NoteCreateRoute({ - Key? key, required Account initialAccount, + Key? key, String? initialText, List? initialMediaFiles, bool exitOnNoted = false, @@ -726,8 +2110,8 @@ class NoteCreateRoute extends PageRouteInfo { }) : super( NoteCreateRoute.name, args: NoteCreateRouteArgs( - key: key, initialAccount: initialAccount, + key: key, initialText: initialText, initialMediaFiles: initialMediaFiles, exitOnNoted: exitOnNoted, @@ -748,8 +2132,8 @@ class NoteCreateRoute extends PageRouteInfo { class NoteCreateRouteArgs { const NoteCreateRouteArgs({ - this.key, required this.initialAccount, + this.key, this.initialText, this.initialMediaFiles, this.exitOnNoted = false, @@ -760,10 +2144,10 @@ class NoteCreateRouteArgs { this.noteCreationMode, }); - final Key? key; - final Account initialAccount; + final Key? key; + final String? initialText; final List? initialMediaFiles; @@ -782,178 +2166,184 @@ class NoteCreateRouteArgs { @override String toString() { - return 'NoteCreateRouteArgs{key: $key, initialAccount: $initialAccount, initialText: $initialText, initialMediaFiles: $initialMediaFiles, exitOnNoted: $exitOnNoted, channel: $channel, reply: $reply, renote: $renote, note: $note, noteCreationMode: $noteCreationMode}'; + return 'NoteCreateRouteArgs{initialAccount: $initialAccount, key: $key, initialText: $initialText, initialMediaFiles: $initialMediaFiles, exitOnNoted: $exitOnNoted, channel: $channel, reply: $reply, renote: $renote, note: $note, noteCreationMode: $noteCreationMode}'; } } /// generated route for -/// [HashtagPage] -class HashtagRoute extends PageRouteInfo { - HashtagRoute({ +/// [NoteDetailPage] +class NoteDetailRoute extends PageRouteInfo { + NoteDetailRoute({ + required Note note, + required AccountContext accountContext, Key? key, - required String hashtag, - required Account account, List? children, }) : super( - HashtagRoute.name, - args: HashtagRouteArgs( + NoteDetailRoute.name, + args: NoteDetailRouteArgs( + note: note, + accountContext: accountContext, key: key, - hashtag: hashtag, - account: account, ), initialChildren: children, ); - static const String name = 'HashtagRoute'; + static const String name = 'NoteDetailRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class HashtagRouteArgs { - const HashtagRouteArgs({ +class NoteDetailRouteArgs { + const NoteDetailRouteArgs({ + required this.note, + required this.accountContext, this.key, - required this.hashtag, - required this.account, }); - final Key? key; + final Note note; - final String hashtag; + final AccountContext accountContext; - final Account account; + final Key? key; @override String toString() { - return 'HashtagRouteArgs{key: $key, hashtag: $hashtag, account: $account}'; + return 'NoteDetailRouteArgs{note: $note, accountContext: $accountContext, key: $key}'; } } /// generated route for -/// [UserFolloweePage] -class UserFolloweeRoute extends PageRouteInfo { - UserFolloweeRoute({ +/// [NoteModalSheet] +class NoteModalRoute extends PageRouteInfo { + NoteModalRoute({ + required Note baseNote, + required Note targetNote, + required AccountContext accountContext, + required GlobalKey> noteBoundaryKey, Key? key, - required String userId, - required Account account, List? children, }) : super( - UserFolloweeRoute.name, - args: UserFolloweeRouteArgs( + NoteModalRoute.name, + args: NoteModalRouteArgs( + baseNote: baseNote, + targetNote: targetNote, + accountContext: accountContext, + noteBoundaryKey: noteBoundaryKey, key: key, - userId: userId, - account: account, ), initialChildren: children, ); - static const String name = 'UserFolloweeRoute'; + static const String name = 'NoteModalRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class UserFolloweeRouteArgs { - const UserFolloweeRouteArgs({ +class NoteModalRouteArgs { + const NoteModalRouteArgs({ + required this.baseNote, + required this.targetNote, + required this.accountContext, + required this.noteBoundaryKey, this.key, - required this.userId, - required this.account, }); - final Key? key; + final Note baseNote; - final String userId; + final Note targetNote; - final Account account; + final AccountContext accountContext; + + final GlobalKey> noteBoundaryKey; + + final Key? key; @override String toString() { - return 'UserFolloweeRouteArgs{key: $key, userId: $userId, account: $account}'; + return 'NoteModalRouteArgs{baseNote: $baseNote, targetNote: $targetNote, accountContext: $accountContext, noteBoundaryKey: $noteBoundaryKey, key: $key}'; } } /// generated route for -/// [UserPage] -class UserRoute extends PageRouteInfo { - UserRoute({ +/// [NotesAfterRenotePage] +class NotesAfterRenoteRoute extends PageRouteInfo { + NotesAfterRenoteRoute({ + required Note note, + required AccountContext accountContext, Key? key, - required String userId, - required Account account, List? children, }) : super( - UserRoute.name, - args: UserRouteArgs( + NotesAfterRenoteRoute.name, + args: NotesAfterRenoteRouteArgs( + note: note, + accountContext: accountContext, key: key, - userId: userId, - account: account, ), initialChildren: children, ); - static const String name = 'UserRoute'; + static const String name = 'NotesAfterRenoteRoute'; - static const PageInfo page = PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class UserRouteArgs { - const UserRouteArgs({ +class NotesAfterRenoteRouteArgs { + const NotesAfterRenoteRouteArgs({ + required this.note, + required this.accountContext, this.key, - required this.userId, - required this.account, }); - final Key? key; + final Note note; - final String userId; + final AccountContext accountContext; - final Account account; + final Key? key; @override String toString() { - return 'UserRouteArgs{key: $key, userId: $userId, account: $account}'; + return 'NotesAfterRenoteRouteArgs{note: $note, accountContext: $accountContext, key: $key}'; } } /// generated route for -/// [UserFollowerPage] -class UserFollowerRoute extends PageRouteInfo { - UserFollowerRoute({ +/// [NotificationPage] +class NotificationRoute extends PageRouteInfo { + NotificationRoute({ + required AccountContext accountContext, Key? key, - required String userId, - required Account account, List? children, }) : super( - UserFollowerRoute.name, - args: UserFollowerRouteArgs( + NotificationRoute.name, + args: NotificationRouteArgs( + accountContext: accountContext, key: key, - userId: userId, - account: account, ), initialChildren: children, ); - static const String name = 'UserFollowerRoute'; + static const String name = 'NotificationRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class UserFollowerRouteArgs { - const UserFollowerRouteArgs({ +class NotificationRouteArgs { + const NotificationRouteArgs({ + required this.accountContext, this.key, - required this.userId, - required this.account, }); - final Key? key; - - final String userId; + final AccountContext accountContext; - final Account account; + final Key? key; @override String toString() { - return 'UserFollowerRouteArgs{key: $key, userId: $userId, account: $account}'; + return 'NotificationRouteArgs{accountContext: $accountContext, key: $key}'; } } @@ -961,7 +2351,7 @@ class UserFollowerRouteArgs { /// [PhotoEditPage] class PhotoEditRoute extends PageRouteInfo { PhotoEditRoute({ - required Account account, + required AccountContext accountContext, required MisskeyPostFile file, required void Function(Uint8List) onSubmit, Key? key, @@ -969,7 +2359,7 @@ class PhotoEditRoute extends PageRouteInfo { }) : super( PhotoEditRoute.name, args: PhotoEditRouteArgs( - account: account, + accountContext: accountContext, file: file, onSubmit: onSubmit, key: key, @@ -985,13 +2375,13 @@ class PhotoEditRoute extends PageRouteInfo { class PhotoEditRouteArgs { const PhotoEditRouteArgs({ - required this.account, + required this.accountContext, required this.file, required this.onSubmit, this.key, }); - final Account account; + final AccountContext accountContext; final MisskeyPostFile file; @@ -1001,364 +2391,421 @@ class PhotoEditRouteArgs { @override String toString() { - return 'PhotoEditRouteArgs{account: $account, file: $file, onSubmit: $onSubmit, key: $key}'; + return 'PhotoEditRouteArgs{accountContext: $accountContext, file: $file, onSubmit: $onSubmit, key: $key}'; } } /// generated route for -/// [AnnouncementPage] -class AnnouncementRoute extends PageRouteInfo { - AnnouncementRoute({ - Key? key, +/// [ReactionDeckPage] +class ReactionDeckRoute extends PageRouteInfo { + ReactionDeckRoute({ required Account account, + Key? key, List? children, }) : super( - AnnouncementRoute.name, - args: AnnouncementRouteArgs( - key: key, + ReactionDeckRoute.name, + args: ReactionDeckRouteArgs( account: account, + key: key, ), initialChildren: children, ); - static const String name = 'AnnouncementRoute'; + static const String name = 'ReactionDeckRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class AnnouncementRouteArgs { - const AnnouncementRouteArgs({ - this.key, +class ReactionDeckRouteArgs { + const ReactionDeckRouteArgs({ required this.account, + this.key, }); - final Key? key; - final Account account; + final Key? key; + @override String toString() { - return 'AnnouncementRouteArgs{key: $key, account: $account}'; + return 'ReactionDeckRouteArgs{account: $account, key: $key}'; } } /// generated route for -/// [SplashPage] -class SplashRoute extends PageRouteInfo { - const SplashRoute({List? children}) - : super( - SplashRoute.name, +/// [ReactionPickerDialog] +class ReactionPickerRoute extends PageRouteInfo { + ReactionPickerRoute({ + required Account account, + required bool isAcceptSensitive, + Key? key, + List? children, + }) : super( + ReactionPickerRoute.name, + args: ReactionPickerRouteArgs( + account: account, + isAcceptSensitive: isAcceptSensitive, + key: key, + ), initialChildren: children, ); - static const String name = 'SplashRoute'; + static const String name = 'ReactionPickerRoute'; - static const PageInfo page = PageInfo(name); + static const PageInfo page = + PageInfo(name); +} + +class ReactionPickerRouteArgs { + const ReactionPickerRouteArgs({ + required this.account, + required this.isAcceptSensitive, + this.key, + }); + + final Account account; + + final bool isAcceptSensitive; + + final Key? key; + + @override + String toString() { + return 'ReactionPickerRouteArgs{account: $account, isAcceptSensitive: $isAcceptSensitive, key: $key}'; + } } /// generated route for -/// [SeveralAccountGeneralSettingsPage] -class SeveralAccountGeneralSettingsRoute - extends PageRouteInfo { - SeveralAccountGeneralSettingsRoute({ +/// [ReactionUserDialog] +class ReactionUserRoute extends PageRouteInfo { + ReactionUserRoute({ + required AccountContext accountContext, + required MisskeyEmojiData emojiData, + required String noteId, Key? key, - required Account account, List? children, }) : super( - SeveralAccountGeneralSettingsRoute.name, - args: SeveralAccountGeneralSettingsRouteArgs( + ReactionUserRoute.name, + args: ReactionUserRouteArgs( + accountContext: accountContext, + emojiData: emojiData, + noteId: noteId, key: key, - account: account, ), initialChildren: children, ); - static const String name = 'SeveralAccountGeneralSettingsRoute'; + static const String name = 'ReactionUserRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class SeveralAccountGeneralSettingsRouteArgs { - const SeveralAccountGeneralSettingsRouteArgs({ +class ReactionUserRouteArgs { + const ReactionUserRouteArgs({ + required this.accountContext, + required this.emojiData, + required this.noteId, this.key, - required this.account, }); - final Key? key; + final AccountContext accountContext; - final Account account; + final MisskeyEmojiData emojiData; + + final String noteId; + + final Key? key; @override String toString() { - return 'SeveralAccountGeneralSettingsRouteArgs{key: $key, account: $account}'; + return 'ReactionUserRouteArgs{accountContext: $accountContext, emojiData: $emojiData, noteId: $noteId, key: $key}'; } } /// generated route for -/// [SeveralAccountSettingsPage] -class SeveralAccountSettingsRoute - extends PageRouteInfo { - SeveralAccountSettingsRoute({ - Key? key, +/// [RenoteModalSheet] +class RenoteModalRoute extends PageRouteInfo { + RenoteModalRoute({ + required Note note, required Account account, + Key? key, List? children, }) : super( - SeveralAccountSettingsRoute.name, - args: SeveralAccountSettingsRouteArgs( - key: key, + RenoteModalRoute.name, + args: RenoteModalRouteArgs( + note: note, account: account, + key: key, ), initialChildren: children, ); - static const String name = 'SeveralAccountSettingsRoute'; + static const String name = 'RenoteModalRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class SeveralAccountSettingsRouteArgs { - const SeveralAccountSettingsRouteArgs({ - this.key, +class RenoteModalRouteArgs { + const RenoteModalRouteArgs({ + required this.note, required this.account, + this.key, }); - final Key? key; + final Note note; final Account account; + final Key? key; + @override String toString() { - return 'SeveralAccountSettingsRouteArgs{key: $key, account: $account}'; + return 'RenoteModalRouteArgs{note: $note, account: $account, key: $key}'; } } /// generated route for -/// [InstanceMutePage] -class InstanceMuteRoute extends PageRouteInfo { - InstanceMuteRoute({ - Key? key, +/// [RenoteUserDialog] +class RenoteUserRoute extends PageRouteInfo { + RenoteUserRoute({ required Account account, + required String noteId, + Key? key, List? children, }) : super( - InstanceMuteRoute.name, - args: InstanceMuteRouteArgs( - key: key, + RenoteUserRoute.name, + args: RenoteUserRouteArgs( account: account, + noteId: noteId, + key: key, ), initialChildren: children, ); - static const String name = 'InstanceMuteRoute'; + static const String name = 'RenoteUserRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class InstanceMuteRouteArgs { - const InstanceMuteRouteArgs({ - this.key, +class RenoteUserRouteArgs { + const RenoteUserRouteArgs({ required this.account, + required this.noteId, + this.key, }); - final Key? key; - final Account account; + final String noteId; + + final Key? key; + @override String toString() { - return 'InstanceMuteRouteArgs{key: $key, account: $account}'; + return 'RenoteUserRouteArgs{account: $account, noteId: $noteId, key: $key}'; } } /// generated route for -/// [WordMutePage] -class WordMuteRoute extends PageRouteInfo { - WordMuteRoute({ - Key? key, +/// [RoleSelectDialog] +class RoleSelectRoute extends PageRouteInfo { + RoleSelectRoute({ required Account account, - required MuteType muteType, + Key? key, List? children, }) : super( - WordMuteRoute.name, - args: WordMuteRouteArgs( - key: key, + RoleSelectRoute.name, + args: RoleSelectRouteArgs( account: account, - muteType: muteType, + key: key, ), initialChildren: children, ); - static const String name = 'WordMuteRoute'; + static const String name = 'RoleSelectRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class WordMuteRouteArgs { - const WordMuteRouteArgs({ - this.key, +class RoleSelectRouteArgs { + const RoleSelectRouteArgs({ required this.account, - required this.muteType, + this.key, }); - final Key? key; - final Account account; - final MuteType muteType; + final Key? key; @override String toString() { - return 'WordMuteRouteArgs{key: $key, account: $account, muteType: $muteType}'; + return 'RoleSelectRouteArgs{account: $account, key: $key}'; } } /// generated route for -/// [CacheManagementPage] -class CacheManagementRoute extends PageRouteInfo { - CacheManagementRoute({ +/// [SearchPage] +class SearchRoute extends PageRouteInfo { + SearchRoute({ + required AccountContext accountContext, Key? key, - required Account account, + NoteSearchCondition? initialNoteSearchCondition, List? children, }) : super( - CacheManagementRoute.name, - args: CacheManagementRouteArgs( + SearchRoute.name, + args: SearchRouteArgs( + accountContext: accountContext, key: key, - account: account, + initialNoteSearchCondition: initialNoteSearchCondition, ), initialChildren: children, ); - static const String name = 'CacheManagementRoute'; + static const String name = 'SearchRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = PageInfo(name); } -class CacheManagementRouteArgs { - const CacheManagementRouteArgs({ +class SearchRouteArgs { + const SearchRouteArgs({ + required this.accountContext, this.key, - required this.account, + this.initialNoteSearchCondition, }); + final AccountContext accountContext; + final Key? key; - final Account account; + final NoteSearchCondition? initialNoteSearchCondition; @override String toString() { - return 'CacheManagementRouteArgs{key: $key, account: $account}'; + return 'SearchRouteArgs{accountContext: $accountContext, key: $key, initialNoteSearchCondition: $initialNoteSearchCondition}'; } } /// generated route for -/// [ReactionDeckPage] -class ReactionDeckRoute extends PageRouteInfo { - ReactionDeckRoute({ +/// [ServerDetailDialog] +class ServerDetailRoute extends PageRouteInfo { + ServerDetailRoute({ + required AccountContext accountContext, Key? key, - required Account account, List? children, }) : super( - ReactionDeckRoute.name, - args: ReactionDeckRouteArgs( + ServerDetailRoute.name, + args: ServerDetailRouteArgs( + accountContext: accountContext, key: key, - account: account, ), initialChildren: children, ); - static const String name = 'ReactionDeckRoute'; + static const String name = 'ServerDetailRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class ReactionDeckRouteArgs { - const ReactionDeckRouteArgs({ +class ServerDetailRouteArgs { + const ServerDetailRouteArgs({ + required this.accountContext, this.key, - required this.account, }); - final Key? key; + final AccountContext accountContext; - final Account account; + final Key? key; @override String toString() { - return 'ReactionDeckRouteArgs{key: $key, account: $account}'; + return 'ServerDetailRouteArgs{accountContext: $accountContext, key: $key}'; } } /// generated route for -/// [UsersListTimelinePage] -class UsersListTimelineRoute extends PageRouteInfo { - UsersListTimelineRoute({ +/// [SettingsPage] +class SettingsRoute extends PageRouteInfo { + const SettingsRoute({List? children}) + : super( + SettingsRoute.name, + initialChildren: children, + ); + + static const String name = 'SettingsRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [SeveralAccountGeneralSettingsPage] +class SeveralAccountGeneralSettingsRoute + extends PageRouteInfo { + SeveralAccountGeneralSettingsRoute({ required Account account, - required UsersList list, Key? key, List? children, }) : super( - UsersListTimelineRoute.name, - args: UsersListTimelineRouteArgs( + SeveralAccountGeneralSettingsRoute.name, + args: SeveralAccountGeneralSettingsRouteArgs( account: account, - list: list, key: key, ), initialChildren: children, ); - static const String name = 'UsersListTimelineRoute'; + static const String name = 'SeveralAccountGeneralSettingsRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class UsersListTimelineRouteArgs { - const UsersListTimelineRouteArgs({ +class SeveralAccountGeneralSettingsRouteArgs { + const SeveralAccountGeneralSettingsRouteArgs({ required this.account, - required this.list, this.key, }); final Account account; - final UsersList list; - final Key? key; @override String toString() { - return 'UsersListTimelineRouteArgs{account: $account, list: $list, key: $key}'; + return 'SeveralAccountGeneralSettingsRouteArgs{account: $account, key: $key}'; } } /// generated route for -/// [UsersListPage] -class UsersListRoute extends PageRouteInfo { - UsersListRoute({ +/// [SeveralAccountSettingsPage] +class SeveralAccountSettingsRoute + extends PageRouteInfo { + SeveralAccountSettingsRoute({ required Account account, Key? key, List? children, }) : super( - UsersListRoute.name, - args: UsersListRouteArgs( + SeveralAccountSettingsRoute.name, + args: SeveralAccountSettingsRouteArgs( account: account, key: key, ), initialChildren: children, ); - static const String name = 'UsersListRoute'; + static const String name = 'SeveralAccountSettingsRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class UsersListRouteArgs { - const UsersListRouteArgs({ +class SeveralAccountSettingsRouteArgs { + const SeveralAccountSettingsRouteArgs({ required this.account, this.key, }); @@ -1369,676 +2816,716 @@ class UsersListRouteArgs { @override String toString() { - return 'UsersListRouteArgs{account: $account, key: $key}'; + return 'SeveralAccountSettingsRouteArgs{account: $account, key: $key}'; } } /// generated route for -/// [UsersListDetailPage] -class UsersListDetailRoute extends PageRouteInfo { - UsersListDetailRoute({ +/// [ShareExtensionPage] +class ShareExtensionRoute extends PageRouteInfo { + const ShareExtensionRoute({List? children}) + : super( + ShareExtensionRoute.name, + initialChildren: children, + ); + + static const String name = 'ShareExtensionRoute'; + + static const PageInfo page = PageInfo(name); +} + +/// generated route for +/// [SharingAccountSelectPage] +class SharingAccountSelectRoute + extends PageRouteInfo { + SharingAccountSelectRoute({ Key? key, - required Account account, - required String listId, + String? sharingText, + List? filePath, List? children, }) : super( - UsersListDetailRoute.name, - args: UsersListDetailRouteArgs( + SharingAccountSelectRoute.name, + args: SharingAccountSelectRouteArgs( key: key, - account: account, - listId: listId, + sharingText: sharingText, + filePath: filePath, ), initialChildren: children, ); - static const String name = 'UsersListDetailRoute'; + static const String name = 'SharingAccountSelectRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class SharingAccountSelectRouteArgs { + const SharingAccountSelectRouteArgs({ + this.key, + this.sharingText, + this.filePath, + }); + + final Key? key; + + final String? sharingText; + + final List? filePath; + + @override + String toString() { + return 'SharingAccountSelectRouteArgs{key: $key, sharingText: $sharingText, filePath: $filePath}'; + } +} + +/// generated route for +/// [SplashPage] +class SplashRoute extends PageRouteInfo { + const SplashRoute({List? children}) + : super( + SplashRoute.name, + initialChildren: children, + ); + + static const String name = 'SplashRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = PageInfo(name); } -class UsersListDetailRouteArgs { - const UsersListDetailRouteArgs({ - this.key, - required this.account, - required this.listId, - }); - - final Key? key; - - final Account account; +/// generated route for +/// [TabSettingsListPage] +class TabSettingsListRoute extends PageRouteInfo { + const TabSettingsListRoute({List? children}) + : super( + TabSettingsListRoute.name, + initialChildren: children, + ); - final String listId; + static const String name = 'TabSettingsListRoute'; - @override - String toString() { - return 'UsersListDetailRouteArgs{key: $key, account: $account, listId: $listId}'; - } + static const PageInfo page = PageInfo(name); } /// generated route for -/// [ChannelDetailPage] -class ChannelDetailRoute extends PageRouteInfo { - ChannelDetailRoute({ +/// [TabSettingsPage] +class TabSettingsRoute extends PageRouteInfo { + TabSettingsRoute({ Key? key, - required Account account, - required String channelId, + int? tabIndex, List? children, }) : super( - ChannelDetailRoute.name, - args: ChannelDetailRouteArgs( + TabSettingsRoute.name, + args: TabSettingsRouteArgs( key: key, - account: account, - channelId: channelId, + tabIndex: tabIndex, ), initialChildren: children, ); - static const String name = 'ChannelDetailRoute'; + static const String name = 'TabSettingsRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class ChannelDetailRouteArgs { - const ChannelDetailRouteArgs({ +class TabSettingsRouteArgs { + const TabSettingsRouteArgs({ this.key, - required this.account, - required this.channelId, + this.tabIndex, }); final Key? key; - final Account account; - - final String channelId; + final int? tabIndex; @override String toString() { - return 'ChannelDetailRouteArgs{key: $key, account: $account, channelId: $channelId}'; + return 'TabSettingsRouteArgs{key: $key, tabIndex: $tabIndex}'; } } /// generated route for -/// [ChannelsPage] -class ChannelsRoute extends PageRouteInfo { - ChannelsRoute({ +/// [TimeLinePage] +class TimeLineRoute extends PageRouteInfo { + TimeLineRoute({ + required TabSetting initialTabSetting, Key? key, - required Account account, List? children, }) : super( - ChannelsRoute.name, - args: ChannelsRouteArgs( + TimeLineRoute.name, + args: TimeLineRouteArgs( + initialTabSetting: initialTabSetting, key: key, - account: account, ), initialChildren: children, ); - static const String name = 'ChannelsRoute'; + static const String name = 'TimeLineRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class ChannelsRouteArgs { - const ChannelsRouteArgs({ +class TimeLineRouteArgs { + const TimeLineRouteArgs({ + required this.initialTabSetting, this.key, - required this.account, }); - final Key? key; + final TabSetting initialTabSetting; - final Account account; + final Key? key; @override String toString() { - return 'ChannelsRouteArgs{key: $key, account: $account}'; + return 'TimeLineRouteArgs{initialTabSetting: $initialTabSetting, key: $key}'; } } /// generated route for -/// [FederationPage] -class FederationRoute extends PageRouteInfo { - FederationRoute({ +/// [UpdateMemoDialog] +class UpdateMemoRoute extends PageRouteInfo { + UpdateMemoRoute({ + required AccountContext accountContext, + required String initialMemo, + required String userId, Key? key, - required Account account, - required String host, List? children, }) : super( - FederationRoute.name, - args: FederationRouteArgs( + UpdateMemoRoute.name, + args: UpdateMemoRouteArgs( + accountContext: accountContext, + initialMemo: initialMemo, + userId: userId, key: key, - account: account, - host: host, ), initialChildren: children, ); - static const String name = 'FederationRoute'; + static const String name = 'UpdateMemoRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class FederationRouteArgs { - const FederationRouteArgs({ +class UpdateMemoRouteArgs { + const UpdateMemoRouteArgs({ + required this.accountContext, + required this.initialMemo, + required this.userId, this.key, - required this.account, - required this.host, }); - final Key? key; + final AccountContext accountContext; - final Account account; + final String initialMemo; - final String host; + final String userId; + + final Key? key; @override String toString() { - return 'FederationRouteArgs{key: $key, account: $account, host: $host}'; + return 'UpdateMemoRouteArgs{accountContext: $accountContext, initialMemo: $initialMemo, userId: $userId, key: $key}'; } } /// generated route for -/// [NoteDetailPage] -class NoteDetailRoute extends PageRouteInfo { - NoteDetailRoute({ - Key? key, - required Note note, +/// [UserControlDialog] +class UserControlRoute extends PageRouteInfo { + UserControlRoute({ required Account account, + required UserDetailed response, + Key? key, List? children, }) : super( - NoteDetailRoute.name, - args: NoteDetailRouteArgs( - key: key, - note: note, + UserControlRoute.name, + args: UserControlRouteArgs( account: account, + response: response, + key: key, ), initialChildren: children, ); - static const String name = 'NoteDetailRoute'; + static const String name = 'UserControlRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class NoteDetailRouteArgs { - const NoteDetailRouteArgs({ - this.key, - required this.note, +class UserControlRouteArgs { + const UserControlRouteArgs({ required this.account, + required this.response, + this.key, }); - final Key? key; + final Account account; - final Note note; + final UserDetailed response; - final Account account; + final Key? key; @override String toString() { - return 'NoteDetailRouteArgs{key: $key, note: $note, account: $account}'; + return 'UserControlRouteArgs{account: $account, response: $response, key: $key}'; } } /// generated route for -/// [SearchPage] -class SearchRoute extends PageRouteInfo { - SearchRoute({ +/// [UserFolloweePage] +class UserFolloweeRoute extends PageRouteInfo { + UserFolloweeRoute({ + required String userId, + required AccountContext accountContext, Key? key, - NoteSearchCondition? initialNoteSearchCondition, - required Account account, List? children, }) : super( - SearchRoute.name, - args: SearchRouteArgs( + UserFolloweeRoute.name, + args: UserFolloweeRouteArgs( + userId: userId, + accountContext: accountContext, key: key, - initialNoteSearchCondition: initialNoteSearchCondition, - account: account, ), initialChildren: children, ); - static const String name = 'SearchRoute'; + static const String name = 'UserFolloweeRoute'; - static const PageInfo page = PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class SearchRouteArgs { - const SearchRouteArgs({ +class UserFolloweeRouteArgs { + const UserFolloweeRouteArgs({ + required this.userId, + required this.accountContext, this.key, - this.initialNoteSearchCondition, - required this.account, }); - final Key? key; + final String userId; - final NoteSearchCondition? initialNoteSearchCondition; + final AccountContext accountContext; - final Account account; + final Key? key; @override String toString() { - return 'SearchRouteArgs{key: $key, initialNoteSearchCondition: $initialNoteSearchCondition, account: $account}'; + return 'UserFolloweeRouteArgs{userId: $userId, accountContext: $accountContext, key: $key}'; } } /// generated route for -/// [SharingAccountSelectPage] -class SharingAccountSelectRoute - extends PageRouteInfo { - SharingAccountSelectRoute({ +/// [UserFollowerPage] +class UserFollowerRoute extends PageRouteInfo { + UserFollowerRoute({ + required String userId, + required AccountContext accountContext, Key? key, - String? sharingText, - List? filePath, List? children, }) : super( - SharingAccountSelectRoute.name, - args: SharingAccountSelectRouteArgs( + UserFollowerRoute.name, + args: UserFollowerRouteArgs( + userId: userId, + accountContext: accountContext, key: key, - sharingText: sharingText, - filePath: filePath, ), initialChildren: children, ); - static const String name = 'SharingAccountSelectRoute'; + static const String name = 'UserFollowerRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class SharingAccountSelectRouteArgs { - const SharingAccountSelectRouteArgs({ +class UserFollowerRouteArgs { + const UserFollowerRouteArgs({ + required this.userId, + required this.accountContext, this.key, - this.sharingText, - this.filePath, }); - final Key? key; + final String userId; - final String? sharingText; + final AccountContext accountContext; - final List? filePath; + final Key? key; @override String toString() { - return 'SharingAccountSelectRouteArgs{key: $key, sharingText: $sharingText, filePath: $filePath}'; + return 'UserFollowerRouteArgs{userId: $userId, accountContext: $accountContext, key: $key}'; } } /// generated route for -/// [MisskeyPagePage] -class MisskeyRouteRoute extends PageRouteInfo { - MisskeyRouteRoute({ - Key? key, +/// [UserListSelectDialog] +class UserListSelectRoute extends PageRouteInfo { + UserListSelectRoute({ required Account account, - required Page page, + Key? key, List? children, }) : super( - MisskeyRouteRoute.name, - args: MisskeyRouteRouteArgs( - key: key, + UserListSelectRoute.name, + args: UserListSelectRouteArgs( account: account, - page: page, + key: key, ), initialChildren: children, ); - static const String name = 'MisskeyRouteRoute'; + static const String name = 'UserListSelectRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class MisskeyRouteRouteArgs { - const MisskeyRouteRouteArgs({ - this.key, +class UserListSelectRouteArgs { + const UserListSelectRouteArgs({ required this.account, - required this.page, + this.key, }); - final Key? key; - final Account account; - final Page page; + final Key? key; @override String toString() { - return 'MisskeyRouteRouteArgs{key: $key, account: $account, page: $page}'; + return 'UserListSelectRouteArgs{account: $account, key: $key}'; } } /// generated route for -/// [MisskeyGamesPage] -class MisskeyGamesRoute extends PageRouteInfo { - MisskeyGamesRoute({ +/// [UserPage] +class UserRoute extends PageRouteInfo { + UserRoute({ + required String userId, + required AccountContext accountContext, Key? key, - required Account account, List? children, }) : super( - MisskeyGamesRoute.name, - args: MisskeyGamesRouteArgs( + UserRoute.name, + args: UserRouteArgs( + userId: userId, + accountContext: accountContext, key: key, - account: account, ), initialChildren: children, ); - static const String name = 'MisskeyGamesRoute'; + static const String name = 'UserRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = PageInfo(name); } -class MisskeyGamesRouteArgs { - const MisskeyGamesRouteArgs({ +class UserRouteArgs { + const UserRouteArgs({ + required this.userId, + required this.accountContext, this.key, - required this.account, }); - final Key? key; + final String userId; - final Account account; + final AccountContext accountContext; + + final Key? key; @override String toString() { - return 'MisskeyGamesRouteArgs{key: $key, account: $account}'; + return 'UserRouteArgs{userId: $userId, accountContext: $accountContext, key: $key}'; } } /// generated route for -/// [ImportExportPage] -class ImportExportRoute extends PageRouteInfo { - const ImportExportRoute({List? children}) - : super( - ImportExportRoute.name, +/// [UserSelectDialog] +class UserSelectRoute extends PageRouteInfo { + UserSelectRoute({ + required AccountContext accountContext, + Key? key, + List? children, + }) : super( + UserSelectRoute.name, + args: UserSelectRouteArgs( + accountContext: accountContext, + key: key, + ), initialChildren: children, ); - static const String name = 'ImportExportRoute'; + static const String name = 'UserSelectRoute'; - static const PageInfo page = PageInfo(name); + static const PageInfo page = + PageInfo(name); } -/// generated route for -/// [GeneralSettingsPage] -class GeneralSettingsRoute extends PageRouteInfo { - const GeneralSettingsRoute({List? children}) - : super( - GeneralSettingsRoute.name, - initialChildren: children, - ); +class UserSelectRouteArgs { + const UserSelectRouteArgs({ + required this.accountContext, + this.key, + }); - static const String name = 'GeneralSettingsRoute'; + final AccountContext accountContext; - static const PageInfo page = PageInfo(name); + final Key? key; + + @override + String toString() { + return 'UserSelectRouteArgs{accountContext: $accountContext, key: $key}'; + } } /// generated route for -/// [TabSettingsPage] -class TabSettingsRoute extends PageRouteInfo { - TabSettingsRoute({ +/// [UsersListDetailPage] +class UsersListDetailRoute extends PageRouteInfo { + UsersListDetailRoute({ + required AccountContext accountContext, + required String listId, Key? key, - int? tabIndex, List? children, }) : super( - TabSettingsRoute.name, - args: TabSettingsRouteArgs( + UsersListDetailRoute.name, + args: UsersListDetailRouteArgs( + accountContext: accountContext, + listId: listId, key: key, - tabIndex: tabIndex, ), initialChildren: children, ); - static const String name = 'TabSettingsRoute'; + static const String name = 'UsersListDetailRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class TabSettingsRouteArgs { - const TabSettingsRouteArgs({ +class UsersListDetailRouteArgs { + const UsersListDetailRouteArgs({ + required this.accountContext, + required this.listId, this.key, - this.tabIndex, }); - final Key? key; + final AccountContext accountContext; - final int? tabIndex; + final String listId; + + final Key? key; @override String toString() { - return 'TabSettingsRouteArgs{key: $key, tabIndex: $tabIndex}'; + return 'UsersListDetailRouteArgs{accountContext: $accountContext, listId: $listId, key: $key}'; } } /// generated route for -/// [TabSettingsListPage] -class TabSettingsListRoute extends PageRouteInfo { - const TabSettingsListRoute({List? children}) - : super( - TabSettingsListRoute.name, - initialChildren: children, - ); - - static const String name = 'TabSettingsListRoute'; - - static const PageInfo page = PageInfo(name); -} - -/// generated route for -/// [AppInfoPage] -class AppInfoRoute extends PageRouteInfo { - const AppInfoRoute({List? children}) - : super( - AppInfoRoute.name, +/// [UsersListModalSheet] +class UsersListModalRoute extends PageRouteInfo { + UsersListModalRoute({ + required Account account, + required User user, + Key? key, + List? children, + }) : super( + UsersListModalRoute.name, + args: UsersListModalRouteArgs( + account: account, + user: user, + key: key, + ), initialChildren: children, ); - static const String name = 'AppInfoRoute'; + static const String name = 'UsersListModalRoute'; - static const PageInfo page = PageInfo(name); + static const PageInfo page = + PageInfo(name); } -/// generated route for -/// [SettingsPage] -class SettingsRoute extends PageRouteInfo { - const SettingsRoute({List? children}) - : super( - SettingsRoute.name, - initialChildren: children, - ); - - static const String name = 'SettingsRoute'; +class UsersListModalRouteArgs { + const UsersListModalRouteArgs({ + required this.account, + required this.user, + this.key, + }); - static const PageInfo page = PageInfo(name); -} + final Account account; -/// generated route for -/// [AccountListPage] -class AccountListRoute extends PageRouteInfo { - const AccountListRoute({List? children}) - : super( - AccountListRoute.name, - initialChildren: children, - ); + final User user; - static const String name = 'AccountListRoute'; + final Key? key; - static const PageInfo page = PageInfo(name); + @override + String toString() { + return 'UsersListModalRouteArgs{account: $account, user: $user, key: $key}'; + } } /// generated route for -/// [ExploreRoleUsersPage] -class ExploreRoleUsersRoute extends PageRouteInfo { - ExploreRoleUsersRoute({ +/// [UsersListPage] +class UsersListRoute extends PageRouteInfo { + UsersListRoute({ + required AccountContext accountContext, Key? key, - required RolesListResponse item, - required Account account, List? children, }) : super( - ExploreRoleUsersRoute.name, - args: ExploreRoleUsersRouteArgs( + UsersListRoute.name, + args: UsersListRouteArgs( + accountContext: accountContext, key: key, - item: item, - account: account, ), initialChildren: children, ); - static const String name = 'ExploreRoleUsersRoute'; + static const String name = 'UsersListRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class ExploreRoleUsersRouteArgs { - const ExploreRoleUsersRouteArgs({ +class UsersListRouteArgs { + const UsersListRouteArgs({ + required this.accountContext, this.key, - required this.item, - required this.account, }); - final Key? key; - - final RolesListResponse item; + final AccountContext accountContext; - final Account account; + final Key? key; @override String toString() { - return 'ExploreRoleUsersRouteArgs{key: $key, item: $item, account: $account}'; + return 'UsersListRouteArgs{accountContext: $accountContext, key: $key}'; } } /// generated route for -/// [ExplorePage] -class ExploreRoute extends PageRouteInfo { - ExploreRoute({ +/// [UsersListSettingsDialog] +class UsersListSettingsRoute extends PageRouteInfo { + UsersListSettingsRoute({ Key? key, - required Account account, + Widget? title, + UsersListSettings initialSettings = const UsersListSettings(), List? children, }) : super( - ExploreRoute.name, - args: ExploreRouteArgs( + UsersListSettingsRoute.name, + args: UsersListSettingsRouteArgs( key: key, - account: account, + title: title, + initialSettings: initialSettings, ), initialChildren: children, ); - static const String name = 'ExploreRoute'; + static const String name = 'UsersListSettingsRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class ExploreRouteArgs { - const ExploreRouteArgs({ +class UsersListSettingsRouteArgs { + const UsersListSettingsRouteArgs({ this.key, - required this.account, + this.title, + this.initialSettings = const UsersListSettings(), }); final Key? key; - final Account account; + final Widget? title; + + final UsersListSettings initialSettings; @override String toString() { - return 'ExploreRouteArgs{key: $key, account: $account}'; + return 'UsersListSettingsRouteArgs{key: $key, title: $title, initialSettings: $initialSettings}'; } } /// generated route for -/// [TimeLinePage] -class TimeLineRoute extends PageRouteInfo { - TimeLineRoute({ +/// [UsersListTimelinePage] +class UsersListTimelineRoute extends PageRouteInfo { + UsersListTimelineRoute({ + required AccountContext accountContext, + required UsersList list, Key? key, - required TabSetting initialTabSetting, List? children, }) : super( - TimeLineRoute.name, - args: TimeLineRouteArgs( + UsersListTimelineRoute.name, + args: UsersListTimelineRouteArgs( + accountContext: accountContext, + list: list, key: key, - initialTabSetting: initialTabSetting, ), initialChildren: children, ); - static const String name = 'TimeLineRoute'; + static const String name = 'UsersListTimelineRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class TimeLineRouteArgs { - const TimeLineRouteArgs({ +class UsersListTimelineRouteArgs { + const UsersListTimelineRouteArgs({ + required this.accountContext, + required this.list, this.key, - required this.initialTabSetting, }); - final Key? key; + final AccountContext accountContext; - final TabSetting initialTabSetting; + final UsersList list; + + final Key? key; @override String toString() { - return 'TimeLineRouteArgs{key: $key, initialTabSetting: $initialTabSetting}'; + return 'UsersListTimelineRouteArgs{accountContext: $accountContext, list: $list, key: $key}'; } } /// generated route for -/// [ShareExtensionPage] -class ShareExtensionRoute extends PageRouteInfo { - const ShareExtensionRoute({List? children}) - : super( - ShareExtensionRoute.name, - initialChildren: children, - ); - - static const String name = 'ShareExtensionRoute'; - - static const PageInfo page = PageInfo(name); -} - -/// generated route for -/// [FavoritedNotePage] -class FavoritedNoteRoute extends PageRouteInfo { - FavoritedNoteRoute({ - Key? key, +/// [WordMutePage] +class WordMuteRoute extends PageRouteInfo { + WordMuteRoute({ required Account account, + required MuteType muteType, + Key? key, List? children, }) : super( - FavoritedNoteRoute.name, - args: FavoritedNoteRouteArgs( - key: key, + WordMuteRoute.name, + args: WordMuteRouteArgs( account: account, + muteType: muteType, + key: key, ), initialChildren: children, ); - static const String name = 'FavoritedNoteRoute'; + static const String name = 'WordMuteRoute'; - static const PageInfo page = - PageInfo(name); + static const PageInfo page = + PageInfo(name); } -class FavoritedNoteRouteArgs { - const FavoritedNoteRouteArgs({ - this.key, +class WordMuteRouteArgs { + const WordMuteRouteArgs({ required this.account, + required this.muteType, + this.key, }); - final Key? key; - final Account account; + final MuteType muteType; + + final Key? key; + @override String toString() { - return 'FavoritedNoteRouteArgs{key: $key, account: $account}'; + return 'WordMuteRouteArgs{account: $account, muteType: $muteType, key: $key}'; } } diff --git a/lib/state_notifier/antenna_page/antennas_notifier.dart b/lib/state_notifier/antenna_page/antennas_notifier.dart deleted file mode 100644 index 1bb172aba..000000000 --- a/lib/state_notifier/antenna_page/antennas_notifier.dart +++ /dev/null @@ -1,81 +0,0 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/antenna_settings.dart'; -import 'package:misskey_dart/misskey_dart.dart'; - -class AntennasNotifier - extends AutoDisposeFamilyAsyncNotifier, Misskey> { - @override - Future> build(Misskey arg) async { - final response = await _misskey.antennas.list(); - return response.toList(); - } - - Misskey get _misskey => arg; - - Future create(AntennaSettings settings) async { - final antenna = await _misskey.antennas.create( - AntennasCreateRequest( - name: settings.name, - src: settings.src, - keywords: settings.keywords, - excludeKeywords: settings.excludeKeywords, - users: settings.users, - caseSensitive: settings.caseSensitive, - withReplies: settings.withReplies, - withFile: settings.withFile, - notify: settings.notify, - localOnly: settings.localOnly, - ), - ); - state = AsyncValue.data([...?state.valueOrNull, antenna]); - } - - Future delete(String antennaId) async { - await _misskey.antennas.delete(AntennasDeleteRequest(antennaId: antennaId)); - state = AsyncValue.data( - state.valueOrNull?.where((e) => e.id != antennaId).toList() ?? [], - ); - } - - Future updateAntenna( - String antennaId, - AntennaSettings settings, - ) async { - await _misskey.antennas.update( - AntennasUpdateRequest( - antennaId: antennaId, - name: settings.name, - src: settings.src, - keywords: settings.keywords, - excludeKeywords: settings.excludeKeywords, - users: settings.users, - caseSensitive: settings.caseSensitive, - withReplies: settings.withReplies, - withFile: settings.withFile, - notify: settings.notify, - localOnly: settings.localOnly, - ), - ); - state = AsyncValue.data( - state.valueOrNull - ?.map( - (antenna) => (antenna.id == antennaId) - ? antenna.copyWith( - name: settings.name, - src: settings.src, - keywords: settings.keywords, - excludeKeywords: settings.excludeKeywords, - users: settings.users, - caseSensitive: settings.caseSensitive, - withReplies: settings.withReplies, - withFile: settings.withFile, - notify: settings.notify, - localOnly: settings.localOnly, - ) - : antenna, - ) - .toList() ?? - [], - ); - } -} diff --git a/lib/state_notifier/clip_list_page/clips_notifier.dart b/lib/state_notifier/clip_list_page/clips_notifier.dart index 05647692d..713d27576 100644 --- a/lib/state_notifier/clip_list_page/clips_notifier.dart +++ b/lib/state_notifier/clip_list_page/clips_notifier.dart @@ -1,54 +1,69 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/clip_settings.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:miria/model/clip_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; -class ClipsNotifier - extends AutoDisposeFamilyAsyncNotifier, Misskey> { +part "clips_notifier.g.dart"; + +@Riverpod(dependencies: [misskeyPostContext]) +class ClipsNotifier extends _$ClipsNotifier { @override - Future> build(Misskey arg) async { - final response = await _misskey.clips.list(); + Future> build() async { + final response = await ref.read(misskeyPostContextProvider).clips.list(); return response.toList(); } - Misskey get _misskey => arg; - Future create(ClipSettings settings) async { - final list = await _misskey.clips.create( - ClipsCreateRequest( - name: settings.name, - description: settings.description, - isPublic: settings.isPublic, - ), - ); - state = AsyncValue.data([...?state.valueOrNull, list]); + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + final list = await ref.read(misskeyPostContextProvider).clips.create( + ClipsCreateRequest( + name: settings.name, + description: settings.description, + isPublic: settings.isPublic, + ), + ); + state = AsyncValue.data([...?state.valueOrNull, list]); + }); } Future delete(String clipId) async { - await _misskey.clips.delete(ClipsDeleteRequest(clipId: clipId)); - state = AsyncValue.data( - state.valueOrNull?.where((e) => e.id != clipId).toList() ?? [], - ); + final result = + await ref.read(dialogStateNotifierProvider.notifier).showDialog( + message: (context) => S.of(context).confirmDeleteClip, + actions: (context) => + [S.of(context).willDelete, S.of(context).cancel], + ); + if (result != 0) return; + + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + await ref + .read(misskeyPostContextProvider) + .clips + .delete(ClipsDeleteRequest(clipId: clipId)); + state = AsyncValue.data( + [...?state.valueOrNull?.where((e) => e.id != clipId)], + ); + }); } Future updateClip( String clipId, ClipSettings settings, ) async { - final clip = await _misskey.clips.update( - ClipsUpdateRequest( - clipId: clipId, - name: settings.name, - description: settings.description, - isPublic: settings.isPublic, - ), - ); - state = AsyncValue.data( - state.valueOrNull - ?.map( - (e) => (e.id == clipId) ? clip : e, - ) - .toList() ?? - [], - ); + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + final clip = await ref.read(misskeyPostContextProvider).clips.update( + ClipsUpdateRequest( + clipId: clipId, + name: settings.name, + description: settings.description, + isPublic: settings.isPublic, + ), + ); + state = AsyncValue.data([ + for (final e in [...?state.valueOrNull]) e.id == clipId ? clip : e, + ]); + }); } } diff --git a/lib/state_notifier/clip_list_page/clips_notifier.g.dart b/lib/state_notifier/clip_list_page/clips_notifier.g.dart new file mode 100644 index 000000000..f54d5b991 --- /dev/null +++ b/lib/state_notifier/clip_list_page/clips_notifier.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'clips_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$clipsNotifierHash() => r'df24732aca182347a988675c5c7974a6a5bb587d'; + +/// See also [ClipsNotifier]. +@ProviderFor(ClipsNotifier) +final clipsNotifierProvider = + AutoDisposeAsyncNotifierProvider>.internal( + ClipsNotifier.new, + name: r'clipsNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$clipsNotifierHash, + dependencies: [misskeyPostContextProvider], + allTransitiveDependencies: { + misskeyPostContextProvider, + ...?misskeyPostContextProvider.allTransitiveDependencies + }, +); + +typedef _$ClipsNotifier = AutoDisposeAsyncNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/state_notifier/common/download_file_notifier.dart b/lib/state_notifier/common/download_file_notifier.dart new file mode 100644 index 000000000..b4c4db7d5 --- /dev/null +++ b/lib/state_notifier/common/download_file_notifier.dart @@ -0,0 +1,98 @@ +import "dart:io"; + +import "package:device_info_plus/device_info_plus.dart"; +import "package:dio/dio.dart"; +import "package:flutter/foundation.dart"; +import "package:image/image.dart"; +import "package:image_gallery_saver/image_gallery_saver.dart"; +import "package:miria/providers.dart"; +import "package:misskey_dart/misskey_dart.dart" hide Permission; +import "package:permission_handler/permission_handler.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +part "download_file_notifier.g.dart"; + +enum DownloadFileResult { succeeded, failed, permissionDenied } + +@Riverpod(keepAlive: true) +class DownloadFileNotifier extends _$DownloadFileNotifier { + @override + void build() { + return; + } + + Future downloadFile(DriveFile driveFile) async { + if (defaultTargetPlatform == TargetPlatform.android) { + final androidInfo = await DeviceInfoPlugin().androidInfo; + if (androidInfo.version.sdkInt <= 32) { + final permissionStatus = await Permission.storage.status; + if (!permissionStatus.isGranted) { + final p = await Permission.storage.request(); + if (!p.isGranted) { + return DownloadFileResult.permissionDenied; + } + } + } else { + final permissionStatus = await Permission.photos.status; + if (!permissionStatus.isGranted) { + final p = await Permission.photos.request(); + if (!p.isGranted) { + return DownloadFileResult.permissionDenied; + } + } + } + } else if (defaultTargetPlatform == TargetPlatform.iOS) { + final permissionStatus = await Permission.photosAddOnly.status; + if (!permissionStatus.isGranted) { + return DownloadFileResult.permissionDenied; + } + } + + final tempDir = ref.read(fileSystemProvider).systemTempDirectory; + var savePath = "${tempDir.path}/${driveFile.name}"; + + await ref.read(dioProvider).download( + driveFile.url, + savePath, + options: Options( + responseType: ResponseType.bytes, + ), + ); + + if (defaultTargetPlatform == TargetPlatform.iOS) { + final imageBytes = await File(savePath).readAsBytes(); + final d = findDecoderForData(imageBytes); + if (d == null) return DownloadFileResult.failed; + + if (d.format == ImageFormat.webp) { + final decoder = WebPDecoder(); + final info = decoder.startDecode(imageBytes); + final image = decoder.decode(imageBytes); + if (info == null || image == null) return DownloadFileResult.failed; + + switch (info.format) { + case WebPFormat.animated: + savePath = "$savePath.gif"; + await File(savePath).writeAsBytes(encodeGif(image)); + + case WebPFormat.lossy: + savePath = "$savePath.jpg"; + await File(savePath).writeAsBytes(encodeJpg(image)); + + case WebPFormat.lossless: + case WebPFormat.undefined: + savePath = "$savePath.png"; + await File(savePath).writeAsBytes(encodePng(image)); + + default: + return DownloadFileResult.failed; + } + } + } + await ImageGallerySaver.saveFile( + savePath, + name: driveFile.name, + ); + return DownloadFileResult.succeeded; + } +} diff --git a/lib/state_notifier/common/download_file_notifier.g.dart b/lib/state_notifier/common/download_file_notifier.g.dart new file mode 100644 index 000000000..09b95ecfb --- /dev/null +++ b/lib/state_notifier/common/download_file_notifier.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'download_file_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$downloadFileNotifierHash() => + r'1e16b1a213ec582509b1843d15b1987e27020a26'; + +/// See also [DownloadFileNotifier]. +@ProviderFor(DownloadFileNotifier) +final downloadFileNotifierProvider = + NotifierProvider.internal( + DownloadFileNotifier.new, + name: r'downloadFileNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$downloadFileNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$DownloadFileNotifier = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/state_notifier/common/misskey_notes/misskey_note_notifier.dart b/lib/state_notifier/common/misskey_notes/misskey_note_notifier.dart index e700169f2..6d99789d0 100644 --- a/lib/state_notifier/common/misskey_notes/misskey_note_notifier.dart +++ b/lib/state_notifier/common/misskey_notes/misskey_note_notifier.dart @@ -1,47 +1,60 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_select_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; - -class OpenLocalOnlyNoteFromRemoteException implements Exception {} - -class MisskeyNoteNotifier extends FamilyNotifier { +import "package:flutter/foundation.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +part "misskey_note_notifier.g.dart"; + +@Riverpod(dependencies: [accountContext, misskeyGetContext], keepAlive: true) +class MisskeyNoteNotifier extends _$MisskeyNoteNotifier { @override - void build(Account arg) { + void build() { return; } - Account get _account => arg; - /// 指定したアカウントから見たNoteを返す - Future lookupNote({ - required Account account, + Future lookupNote({ required Note note, + required AccountContext accountContext, }) async { - if (account.host == _account.host) { + if (note.user.host == null && + ref.read(accountContextProvider).getAccount.host == + accountContext.getAccount.host) { return note; } if (note.localOnly) { - throw OpenLocalOnlyNoteFromRemoteException(); + await ref.read(dialogStateNotifierProvider.notifier).showSimpleDialog( + message: (context) => + S.of(context).cannotOpenLocalOnlyNoteFromRemote, + ); + return null; } - final host = note.user.host ?? _account.host; + final host = note.url?.host ?? + note.user.host ?? + ref.read(accountContextProvider).getAccount.host; try { // まず、自分のサーバーの直近のノートに該当のノートが含まれているか見る - final user = await ref.read(misskeyProvider(account)).users.showByName( + final user = await ref + .read(misskeyProvider(accountContext.getAccount)) + .users + .showByName( UsersShowByUserNameRequest( userName: note.user.username, host: host, ), ); - final userNotes = await ref.read(misskeyProvider(account)).users.notes( + final userNotes = await ref + .read(misskeyProvider(accountContext.getAccount)) + .users + .notes( UsersNotesRequest( userId: user.id, untilDate: note.createdAt.add(const Duration(seconds: 1)), @@ -49,116 +62,147 @@ class MisskeyNoteNotifier extends FamilyNotifier { ); return userNotes - .firstWhere((e) => e.uri?.pathSegments.lastOrNull == note.id); - } catch (e) { + .firstWhere((e) => e.id == note.uri?.pathSegments.lastOrNull); + } catch (e, s) { + if (kDebugMode) { + print(e); + print(s); + } + // 最終手段として、連合で照会する - final response = await ref.read(misskeyProvider(account)).ap.show( - ApShowRequest( - uri: note.uri ?? - Uri( - scheme: "https", - host: host, - pathSegments: ["notes", note.id], - ), - ), - ); - return Note.fromJson(response.object); + final response = + await ref.read(dialogStateNotifierProvider.notifier).guard( + () async => await ref + .read(misskeyProvider(accountContext.getAccount)) + .ap + .show( + ApShowRequest( + uri: note.uri ?? + Uri( + scheme: "https", + host: host, + pathSegments: ["notes", note.id], + ), + ), + ), + ); + final result = response.valueOrNull?.object; + if (result == null) return null; + return Note.fromJson(result); } } /// 指定したアカウントから見たUserを返す - Future lookupUser({ - required Account account, + Future lookupUser({ required User user, + required AccountContext accountContext, }) async { - if (account.host == _account.host) { + final accountContextHost = ref.read(accountContextProvider).getAccount.host; + if (user.host == null && + accountContextHost == accountContext.getAccount.host) { return user; } - final host = user.host ?? _account.host; + final host = user.host ?? accountContext.getAccount.host; - try { - return ref.read(misskeyProvider(account)).users.showByName( - UsersShowByUserNameRequest( - userName: user.username, - host: host, - ), - ); - } catch (e) { - // 最終手段として、連合で照会する - // `users/show` で自動的に照会されるから必要ない - final response = await ref.read(misskeyProvider(account)).ap.show( - ApShowRequest( - uri: Uri( - scheme: "https", - host: user.host, - pathSegments: ["@$host"], + final response = await ref.read(dialogStateNotifierProvider.notifier).guard( + () async => ref + .read(misskeyProvider(accountContext.getAccount)) + .users + .showByName( + UsersShowByUserNameRequest(userName: user.username, host: host), ), - ), - ); - return UserDetailed.fromJson(response.object); - } + ); + return response.valueOrNull; } Future navigateToNoteDetailPage( - BuildContext context, - Note note, - Account? loginAs, - ) async { - final foundNote = loginAs == null - ? note - : await lookupNote( - note: note, - account: loginAs, - ); - if (!context.mounted) return; - context.pushRoute( - NoteDetailRoute( - note: foundNote, - account: loginAs ?? _account, - ), - ); + Note note, { + Account? account, + }) async { + final router = ref.read(appRouterProvider); + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + final accountContext = account != null + ? AccountContext( + getAccount: account, + postAccount: account.isDemoAccount + ? ref.read(accountContextProvider).postAccount + : account, + ) + : ref.read(accountContextProvider); + final foundNote = note.user.host == null && + note.uri?.host == accountContext.getAccount.host + ? note + : await lookupNote( + note: note, + accountContext: accountContext, + ); + if (foundNote == null) return; + await ref + .read(emojiRepositoryProvider(accountContext.getAccount)) + .loadFromSourceIfNeed(); + await router.push( + NoteDetailRoute(note: foundNote, accountContext: accountContext), + ); + }); } Future navigateToUserPage( - BuildContext context, - User user, - Account? loginAs, - ) async { - final foundUser = loginAs == null - ? user - : await lookupUser( - account: loginAs, - user: user, - ); - if (!context.mounted) return; - context.pushRoute( - UserRoute( - userId: foundUser.id, - account: loginAs ?? _account, - ), - ); + User user, { + Account? account, + }) async { + final router = ref.read(appRouterProvider); + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + final accountContext = account != null + ? AccountContext( + getAccount: account, + postAccount: account.isDemoAccount + ? ref.read(accountContextProvider).postAccount + : account, + ) + : ref.read(accountContextProvider); + final foundUser = user.host == null && + accountContext.getAccount == + ref.read(accountContextProvider).getAccount + ? user + : await lookupUser(user: user, accountContext: accountContext); + if (foundUser == null) return; + await ref + .read(emojiRepositoryProvider(accountContext.getAccount)) + .loadFromSourceIfNeed(); + await router.push( + UserRoute(userId: foundUser.id, accountContext: accountContext), + ); + }); } - Future openNoteInOtherAccount(BuildContext context, Note note) async { - final selectedAccount = await showDialog( - context: context, - builder: (context) => AccountSelectDialog( - host: note.localOnly ? _account.host : null, - ), - ); + Future openNoteInOtherAccount(Note note) async { + final accountContext = ref.read(accountContextProvider); + final selectedAccount = await ref.read(appRouterProvider).push( + AccountSelectRoute( + host: note.localOnly ? accountContext.getAccount.host : null, + remoteHost: note.user.host != accountContext.getAccount.host && + note.user.host != null + ? note.user.host + : null, + ), + ); if (selectedAccount == null) return; - if (!context.mounted) return; - await navigateToNoteDetailPage(context, note, selectedAccount); + await navigateToNoteDetailPage(note, account: selectedAccount); } - Future openUserInOtherAccount(BuildContext context, User user) async { - final selectedAccount = await showDialog( - context: context, - builder: (context) => const AccountSelectDialog(), - ); + Future openUserInOtherAccount(User user) async { + final accountContext = ref.read(accountContextProvider); + final selectedAccount = await ref.read(appRouterProvider).push( + AccountSelectRoute( + remoteHost: + user.host != accountContext.getAccount.host && user.host != null + ? user.host + : null, + ), + ); + if (selectedAccount == null) return; - if (!context.mounted) return; - await navigateToUserPage(context, user, selectedAccount); + await navigateToUserPage(user, account: selectedAccount); } } diff --git a/lib/state_notifier/common/misskey_notes/misskey_note_notifier.g.dart b/lib/state_notifier/common/misskey_notes/misskey_note_notifier.g.dart new file mode 100644 index 000000000..3939ee944 --- /dev/null +++ b/lib/state_notifier/common/misskey_notes/misskey_note_notifier.g.dart @@ -0,0 +1,35 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'misskey_note_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$misskeyNoteNotifierHash() => + r'5edd4690a767107f9c8a30e6b480a74de4e83d74'; + +/// See also [MisskeyNoteNotifier]. +@ProviderFor(MisskeyNoteNotifier) +final misskeyNoteNotifierProvider = + NotifierProvider.internal( + MisskeyNoteNotifier.new, + name: r'misskeyNoteNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$misskeyNoteNotifierHash, + dependencies: [ + accountContextProvider, + misskeyGetContextProvider + ], + allTransitiveDependencies: { + accountContextProvider, + ...?accountContextProvider.allTransitiveDependencies, + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies + }, +); + +typedef _$MisskeyNoteNotifier = Notifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/state_notifier/common/misskey_server_list_notifier.dart b/lib/state_notifier/common/misskey_server_list_notifier.dart index 9de5feeb0..a9f4ca2c2 100644 --- a/lib/state_notifier/common/misskey_server_list_notifier.dart +++ b/lib/state_notifier/common/misskey_server_list_notifier.dart @@ -1,6 +1,9 @@ -import 'package:collection/collection.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:collection/collection.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +part "misskey_server_list_notifier.g.dart"; final _queryProvider = StateProvider.autoDispose((ref) { return ""; @@ -27,8 +30,8 @@ class _InstanceInfos } } -class MisskeyServerListNotifier - extends AutoDisposeAsyncNotifier> { +@riverpod +class MisskeyServerListNotifier extends _$MisskeyServerListNotifier { @override Future> build() async { final query = ref.watch(_queryProvider); diff --git a/lib/state_notifier/common/misskey_server_list_notifier.g.dart b/lib/state_notifier/common/misskey_server_list_notifier.g.dart new file mode 100644 index 000000000..a5e3fd990 --- /dev/null +++ b/lib/state_notifier/common/misskey_server_list_notifier.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'misskey_server_list_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$misskeyServerListNotifierHash() => + r'ef2653b2551eaa7056ce2379306c4a52b2b2075f'; + +/// See also [MisskeyServerListNotifier]. +@ProviderFor(MisskeyServerListNotifier) +final misskeyServerListNotifierProvider = AutoDisposeAsyncNotifierProvider< + MisskeyServerListNotifier, List>.internal( + MisskeyServerListNotifier.new, + name: r'misskeyServerListNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$misskeyServerListNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$MisskeyServerListNotifier + = AutoDisposeAsyncNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/state_notifier/note_create_page/note_create_state_notifier.dart b/lib/state_notifier/note_create_page/note_create_state_notifier.dart index 9db5a75a8..32da9539d 100644 --- a/lib/state_notifier/note_create_page/note_create_state_notifier.dart +++ b/lib/state_notifier/note_create_page/note_create_state_notifier.dart @@ -1,29 +1,26 @@ -import 'dart:typed_data'; - -import 'package:dio/dio.dart'; -import 'package:file/file.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_image_compress/flutter_image_compress.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:mfm_parser/mfm_parser.dart'; -import 'package:miria/extensions/note_visibility_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/image_file.dart'; -import 'package:miria/repository/note_repository.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; -import 'package:miria/view/dialogs/simple_message_dialog.dart'; -import 'package:miria/view/note_create_page/drive_file_select_dialog.dart'; -import 'package:miria/view/note_create_page/drive_modal_sheet.dart'; -import 'package:miria/view/note_create_page/file_settings_dialog.dart'; -import 'package:miria/view/note_create_page/note_create_page.dart'; -import 'package:miria/view/user_select_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; - -part 'note_create_state_notifier.freezed.dart'; +import "dart:typed_data"; + +import "package:dio/dio.dart"; +import "package:file_picker/file_picker.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_image_compress/flutter_image_compress.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:mfm_parser/mfm_parser.dart"; +import "package:miria/extensions/note_visibility_extension.dart"; +import "package:miria/log.dart"; +import "package:miria/model/image_file.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:miria/view/note_create_page/drive_modal_sheet.dart"; +import "package:miria/view/note_create_page/file_settings_dialog.dart"; +import "package:miria/view/note_create_page/note_create_page.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +part "note_create_state_notifier.freezed.dart"; +part "note_create_state_notifier.g.dart"; enum NoteSendStatus { sending, finished, error } @@ -57,30 +54,17 @@ enum VoteExpireDurationType { } } -sealed class NoteCreateException implements Exception {} - -class EmptyNoteException implements NoteCreateException {} - -class TooFewVoteChoiceException implements NoteCreateException {} - -class EmptyVoteExpireDateException implements NoteCreateException {} - -class EmptyVoteExpireDurationException implements NoteCreateException {} - -class MentionToRemoteInLocalOnlyNoteException implements NoteCreateException {} - @freezed class NoteCreate with _$NoteCreate { const factory NoteCreate({ - required Account account, required NoteVisibility noteVisibility, required bool localOnly, + required ReactionAcceptance? reactionAcceptance, @Default([]) List replyTo, @Default([]) List files, NoteCreateChannel? channel, Note? reply, Note? renote, - required ReactionAcceptance? reactionAcceptance, @Default(false) bool isCw, @Default("") String cwText, @Default("") String text, @@ -108,21 +92,38 @@ class NoteCreateChannel with _$NoteCreateChannel { }) = _NoteCreateChannel; } -class NoteCreateNotifier extends StateNotifier { - FileSystem fileSystem; - Dio dio; - Misskey misskey; - NoteRepository noteRepository; - StateNotifier<(Object? error, BuildContext? context)> errorNotifier; - - NoteCreateNotifier( - super.state, - this.fileSystem, - this.dio, - this.misskey, - this.errorNotifier, - this.noteRepository, - ); +@Riverpod( + dependencies: [ + misskeyPostContext, + notesWith, + accountContext, + ], +) +class NoteCreateNotifier extends _$NoteCreateNotifier { + late final _fileSystem = ref.read(fileSystemProvider); + late final _dio = ref.read(dioProvider); + late final _misskey = ref.read(misskeyPostContextProvider); + late final _noteRepository = ref.read(notesWithProvider); + late final _dialogNotifier = ref.read(dialogStateNotifierProvider.notifier); + + @override + NoteCreate build() { + final account = ref.read(accountContextProvider).postAccount; + return NoteCreate( + noteVisibility: ref + .read(accountSettingsRepositoryProvider) + .fromAccount(account) + .defaultNoteVisibility, + localOnly: ref + .read(accountSettingsRepositoryProvider) + .fromAccount(account) + .defaultIsLocalOnly, + reactionAcceptance: ref + .read(accountSettingsRepositoryProvider) + .fromAccount(account) + .defaultReactionAcceptance, + ); + } /// 初期化する Future initialize( @@ -152,11 +153,11 @@ class NoteCreateNotifier extends StateNotifier { if (initialText != null) { resultState = resultState.copyWith(text: initialText); } - if (initialMediaFiles != null && initialMediaFiles.isNotEmpty == true) { + if (initialMediaFiles != null && initialMediaFiles.isNotEmpty) { resultState = resultState.copyWith( files: await Future.wait( initialMediaFiles.map((media) async { - final file = fileSystem.file(media); + final file = _fileSystem.file(media); final contents = await file.readAsBytes(); final fileName = file.basename; final extension = fileName.split(".").last.toLowerCase(); @@ -181,8 +182,10 @@ class NoteCreateNotifier extends StateNotifier { final files = []; for (final file in note.files) { if (file.type.startsWith("image")) { - final response = await dio.get(file.url, - options: Options(responseType: ResponseType.bytes)); + final response = await _dio.get( + file.url, + options: Options(responseType: ResponseType.bytes), + ); files.add( ImageFileAlreadyPostedFile( fileName: file.name, @@ -209,7 +212,7 @@ class NoteCreateNotifier extends StateNotifier { final replyTo = []; if (note.mentions.isNotEmpty) { replyTo.addAll( - await misskey.users + await _misskey.users .showByIds(UsersShowByIdsRequest(userIds: note.mentions)), ); } @@ -220,7 +223,9 @@ class NoteCreateNotifier extends StateNotifier { files: files, channel: deletedNoteChannel != null ? NoteCreateChannel( - id: deletedNoteChannel.id, name: deletedNoteChannel.name) + id: deletedNoteChannel.id, + name: deletedNoteChannel.name, + ) : null, cwText: note.cw ?? "", isCw: note.cw?.isNotEmpty == true, @@ -246,16 +251,19 @@ class NoteCreateNotifier extends StateNotifier { if (renote != null) { resultState = resultState.copyWith( - renote: renote, - noteVisibility: NoteVisibility.min( - resultState.noteVisibility, renote.visibility)); + renote: renote, + noteVisibility: NoteVisibility.min( + resultState.noteVisibility, + renote.visibility, + ), + ); } if (reply != null) { final replyTo = []; if (reply.mentions.isNotEmpty) { replyTo.addAll( - await misskey.users + await _misskey.users .showByIds(UsersShowByIdsRequest(userIds: reply.mentions)), ); } @@ -269,7 +277,10 @@ class NoteCreateNotifier extends StateNotifier { replyTo: [ reply.user, ...replyTo, - ]..removeWhere((element) => element.id == state.account.i.id), + ]..removeWhere( + (element) => + element.id == ref.read(accountContextProvider).postAccount.i.id, + ), ); } @@ -282,126 +293,139 @@ class NoteCreateNotifier extends StateNotifier { } // サイレンスの場合、ホーム以下に強制 - final isSilenced = state.account.i.isSilenced; - if (isSilenced == true) { + final isSilenced = + ref.read(accountContextProvider).postAccount.i.isSilenced; + if (isSilenced) { resultState = resultState.copyWith( - noteVisibility: NoteVisibility.min( - resultState.noteVisibility, NoteVisibility.home)); + noteVisibility: NoteVisibility.min( + resultState.noteVisibility, + NoteVisibility.home, + ), + ); } state = resultState; } /// ノートを投稿する - Future note(BuildContext context) async { + Future note() async { if (state.text.isEmpty && state.files.isEmpty && !state.isVote) { - throw EmptyNoteException(); + await _dialogNotifier.showSimpleDialog( + message: (context) => S.of(context).pleaseInputSomething, + ); + return; } if (state.isVote && state.voteContent.where((e) => e.isNotEmpty).length < 2) { - throw TooFewVoteChoiceException(); + await _dialogNotifier.showSimpleDialog( + message: (context) => S.of(context).pleaseAddVoteChoice, + ); + return; } if (state.isVote && state.voteExpireType == VoteExpireType.date && state.voteDate == null) { - throw EmptyVoteExpireDateException(); + await _dialogNotifier.showSimpleDialog( + message: (context) => S.of(context).pleaseSpecifyExpirationDate, + ); + return; } if (state.isVote && state.voteExpireType == VoteExpireType.duration && state.voteDuration == null) { - throw EmptyVoteExpireDurationException(); + await _dialogNotifier.showSimpleDialog( + message: (context) => S.of(context).pleaseSpecifyExpirationDuration, + ); + return; } - - try { - state = state.copyWith(isNoteSending: NoteSendStatus.sending); - - final fileIds = []; - - for (final file in state.files) { - DriveFile? response; - - switch (file) { - case ImageFile(): - final fileName = file.fileName.toLowerCase(); - var imageData = file.data; - try { - if (fileName.endsWith("jpg") || - fileName.endsWith("jpeg") || - fileName.endsWith("tiff") || - fileName.endsWith("tif")) { - imageData = - await FlutterImageCompress.compressWithList(file.data); + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + try { + state = state.copyWith(isNoteSending: NoteSendStatus.sending); + + final fileIds = []; + + for (final file in state.files) { + DriveFile? response; + + switch (file) { + case ImageFile(): + final fileName = file.fileName.toLowerCase(); + var imageData = file.data; + try { + if (fileName.endsWith("jpg") || + fileName.endsWith("jpeg") || + fileName.endsWith("tiff") || + fileName.endsWith("tif")) { + imageData = + await FlutterImageCompress.compressWithList(file.data); + } + } catch (e) { + logger.shout("failed to compress file"); } - } catch (e) { - print("failed to compress file"); - } - response = await misskey.drive.files.createAsBinary( - DriveFilesCreateRequest( - force: true, - name: file.fileName, - isSensitive: file.isNsfw, - comment: file.caption, - ), - imageData, - ); - fileIds.add(response.id); - - break; - case UnknownFile(): - response = await misskey.drive.files.createAsBinary( - DriveFilesCreateRequest( - force: true, - name: file.fileName, - isSensitive: file.isNsfw, - comment: file.caption, - ), - file.data, - ); - fileIds.add(response.id); - - break; - case UnknownAlreadyPostedFile(): - if (file.isEdited) { - await misskey.drive.files.update(DriveFilesUpdateRequest( - fileId: file.id, - name: file.fileName, - isSensitive: file.isNsfw, - comment: file.caption, - )); - } - fileIds.add(file.id); - break; - case ImageFileAlreadyPostedFile(): - if (file.isEdited) { - response = - await misskey.drive.files.update(DriveFilesUpdateRequest( - fileId: file.id, - name: file.fileName, - isSensitive: file.isNsfw, - comment: file.caption, - )); - } + response = await _misskey.drive.files.createAsBinary( + DriveFilesCreateRequest( + force: true, + name: file.fileName, + isSensitive: file.isNsfw, + comment: file.caption, + ), + imageData, + ); + fileIds.add(response.id); + + case UnknownFile(): + response = await _misskey.drive.files.createAsBinary( + DriveFilesCreateRequest( + force: true, + name: file.fileName, + isSensitive: file.isNsfw, + comment: file.caption, + ), + file.data, + ); + fileIds.add(response.id); + + case UnknownAlreadyPostedFile(): + if (file.isEdited) { + await _misskey.drive.files.update( + DriveFilesUpdateRequest( + fileId: file.id, + name: file.fileName, + isSensitive: file.isNsfw, + comment: file.caption, + ), + ); + } + fileIds.add(file.id); + case ImageFileAlreadyPostedFile(): + if (file.isEdited) { + response = await _misskey.drive.files.update( + DriveFilesUpdateRequest( + fileId: file.id, + name: file.fileName, + isSensitive: file.isNsfw, + comment: file.caption, + ), + ); + } - fileIds.add(file.id); - break; - } + fileIds.add(file.id); + } - if (response?.isSensitive == true && - !file.isNsfw && - !state.account.i.alwaysMarkNsfw) { - if (context.mounted) { - final confirmResult = await SimpleConfirmDialog.show( - context: context, - message: S.of(context).unexpectedSensitive, - primary: S.of(context).staySensitive, - secondary: S.of(context).unsetSensitive, + if (response?.isSensitive == true && + !file.isNsfw && + !ref.read(accountContextProvider).postAccount.i.alwaysMarkNsfw) { + final result = await _dialogNotifier.showDialog( + message: (context) => S.of(context).unexpectedSensitive, + actions: (context) => + [S.of(context).staySensitive, S.of(context).unsetSensitive], ); - if (confirmResult == false) { - await misskey.drive.files.update( + if (result == 1) { + await _misskey.drive.files.update( DriveFilesUpdateRequest( fileId: fileIds.last, isSensitive: false, @@ -410,61 +434,67 @@ class NoteCreateNotifier extends StateNotifier { } } } - } - - if (!mounted) return; - final nodes = const MfmParser().parse(state.text); - final userList = []; + final nodes = const MfmParser().parse(state.text); + final userList = []; - void findMfmMentions(List nodes) { - for (final node in nodes) { - if (node is MfmMention) { - userList.add(node); + void findMfmMentions(List nodes) { + for (final node in nodes) { + if (node is MfmMention) { + userList.add(node); + } + findMfmMentions(node.children ?? []); } - findMfmMentions(node.children ?? []); } - } - - findMfmMentions(nodes); - // 連合オフなのに他のサーバーの人がメンションに含まれている - if (state.localOnly && - userList.any((element) => - element.host != null && - element.host != misskey.apiService.host)) { - throw MentionToRemoteInLocalOnlyNoteException(); - } + findMfmMentions(nodes); + + // 連合オフなのに他のサーバーの人がメンションに含まれている + if (state.localOnly && + userList.any( + (element) => + element.host != null && element.host != _misskey.host, + )) { + await _dialogNotifier.showSimpleDialog( + message: (context) => + S.of(context).cannotMentionToRemoteInLocalOnlyNote, + ); + return; + } - final mentionTargetUsers = [ - for (final user in userList) - await misskey.users.showByName(UsersShowByUserNameRequest( - userName: user.username, host: user.host)) - ]; - final visibleUserIds = state.replyTo.map((e) => e.id).toList(); - visibleUserIds.addAll(mentionTargetUsers.map((e) => e.id)); - - final baseText = - "${state.replyTo.map((e) => "@${e.username}${e.host == null ? " " : "@${e.host} "}").join("")}${state.text}"; - final postText = baseText.isNotEmpty ? baseText : null; - - final durationType = state.voteDurationType; - final voteDuration = Duration( - days: durationType == VoteExpireDurationType.day - ? state.voteDuration ?? 0 - : 0, - hours: durationType == VoteExpireDurationType.hours - ? state.voteDuration ?? 0 - : 0, - minutes: durationType == VoteExpireDurationType.minutes - ? state.voteDuration ?? 0 - : 0, - seconds: durationType == VoteExpireDurationType.seconds - ? state.voteDuration ?? 0 - : 0, - ); + final mentionTargetUsers = [ + for (final user in userList) + await _misskey.users.showByName( + UsersShowByUserNameRequest( + userName: user.username, + host: user.host, + ), + ), + ]; + final visibleUserIds = state.replyTo.map((e) => e.id).toList() + ..addAll(mentionTargetUsers.map((e) => e.id)); + + final baseText = + "${state.replyTo.map((e) => "@${e.username}${e.host == null ? " " : "@${e.host} "}").join("")}${state.text}"; + final postText = baseText.isNotEmpty ? baseText : null; + + final durationType = state.voteDurationType; + final voteDuration = Duration( + days: durationType == VoteExpireDurationType.day + ? state.voteDuration ?? 0 + : 0, + hours: durationType == VoteExpireDurationType.hours + ? state.voteDuration ?? 0 + : 0, + minutes: durationType == VoteExpireDurationType.minutes + ? state.voteDuration ?? 0 + : 0, + seconds: durationType == VoteExpireDurationType.seconds + ? state.voteDuration ?? 0 + : 0, + ); - final poll = NotesCreatePollRequest( + final poll = NotesCreatePollRequest( choices: state.voteContent, multiple: state.isVoteMultiple, expiresAt: state.voteExpireType == VoteExpireType.date @@ -472,59 +502,66 @@ class NoteCreateNotifier extends StateNotifier { : null, expiredAfter: state.voteExpireType == VoteExpireType.duration ? voteDuration - : null); - - if (state.noteCreationMode == NoteCreationMode.update) { - await misskey.notes.update(NotesUpdateRequest( - noteId: state.noteId!, - text: postText ?? "", - cw: state.isCw ? state.cwText : null, - )); - noteRepository.registerNote(noteRepository.notes[state.noteId!]! - .copyWith( - text: postText ?? "", cw: state.isCw ? state.cwText : null)); - } else { - await misskey.notes.create(NotesCreateRequest( - visibility: state.noteVisibility, - text: postText, - cw: state.isCw ? state.cwText : null, - localOnly: state.localOnly, - replyId: state.reply?.id, - renoteId: state.renote?.id, - channelId: state.channel?.id, - fileIds: fileIds.isEmpty ? null : fileIds, - visibleUserIds: visibleUserIds.toSet().toList(), //distinct list - reactionAcceptance: state.reactionAcceptance, - poll: state.isVote ? poll : null, - )); + : null, + ); + + if (state.noteCreationMode == NoteCreationMode.update) { + await _misskey.notes.update( + NotesUpdateRequest( + noteId: state.noteId!, + text: postText ?? "", + cw: state.isCw ? state.cwText : null, + ), + ); + _noteRepository.registerNote( + _noteRepository.notes[state.noteId!]!.copyWith( + text: postText ?? "", + cw: state.isCw ? state.cwText : null, + ), + ); + } else { + await _misskey.notes.create( + NotesCreateRequest( + visibility: state.noteVisibility, + text: postText, + cw: state.isCw ? state.cwText : null, + localOnly: state.localOnly, + replyId: state.reply?.id, + renoteId: state.renote?.id, + channelId: state.channel?.id, + fileIds: fileIds.isEmpty ? null : fileIds, + visibleUserIds: visibleUserIds.toSet().toList(), //distinct list + reactionAcceptance: state.reactionAcceptance, + poll: state.isVote ? poll : null, + ), + ); + } + state = state.copyWith(isNoteSending: NoteSendStatus.finished); + } catch (e) { + state = state.copyWith(isNoteSending: NoteSendStatus.error); + rethrow; } - if (!mounted) return; - state = state.copyWith(isNoteSending: NoteSendStatus.finished); - } catch (e) { - state = state.copyWith(isNoteSending: NoteSendStatus.error); - rethrow; - } + }); } /// メディアを選択する - Future chooseFile(BuildContext context) async { - final result = await showModalBottomSheet( - context: context, builder: (context) => const DriveModalSheet()); + Future chooseFile() async { + final result = await ref + .read(appRouterProvider) + .push(const DriveModalRoute()); if (result == DriveModalSheetReturnValue.drive) { - if (!mounted) return; - final result = await showDialog?>( - context: context, - builder: (context) => DriveFileSelectDialog( - account: state.account, - allowMultiple: true, - ), - ); + final result = await ref.read(appRouterProvider).push>( + DriveFileSelectRoute( + account: ref.read(accountContextProvider).postAccount, + allowMultiple: true, + ), + ); if (result == null) return; final files = await Future.wait( result.map((file) async { if (file.type.startsWith("image")) { - final fileContentResponse = await dio.get( + final fileContentResponse = await _dio.get( file.url, options: Options(responseType: ResponseType.bytes), ); @@ -545,7 +582,6 @@ class NoteCreateNotifier extends StateNotifier { ); }), ); - if (!mounted) return; state = state.copyWith( files: [ ...state.files, @@ -556,13 +592,15 @@ class NoteCreateNotifier extends StateNotifier { final result = await FilePicker.platform.pickFiles( type: FileType.image, allowMultiple: true, + allowCompression: false, + compressionQuality: 0, ); if (result == null || result.files.isEmpty) return; final fsFiles = result.files.map((file) { final path = file.path; if (path != null) { - return fileSystem.file(path); + return _fileSystem.file(path); } return null; }).nonNulls; @@ -575,13 +613,7 @@ class NoteCreateNotifier extends StateNotifier { ), ); - if (!mounted) return; - state = state.copyWith( - files: [ - ...state.files, - ...files, - ], - ); + state = state.copyWith(files: [...state.files, ...files]); } } @@ -593,32 +625,32 @@ class NoteCreateNotifier extends StateNotifier { switch (file) { case ImageFile(): files[files.indexOf(file)] = ImageFile( - data: content, - fileName: file.fileName, - caption: file.caption, - isNsfw: file.isNsfw); - break; + data: content, + fileName: file.fileName, + caption: file.caption, + isNsfw: file.isNsfw, + ); case ImageFileAlreadyPostedFile(): files[files.indexOf(file)] = ImageFile( - data: content, - fileName: file.fileName, - caption: file.caption, - isNsfw: file.isNsfw); - break; + data: content, + fileName: file.fileName, + caption: file.caption, + isNsfw: file.isNsfw, + ); case UnknownFile(): files[files.indexOf(file)] = ImageFile( - data: content, - fileName: file.fileName, - caption: file.caption, - isNsfw: file.isNsfw); - break; + data: content, + fileName: file.fileName, + caption: file.caption, + isNsfw: file.isNsfw, + ); case UnknownAlreadyPostedFile(): files[files.indexOf(file)] = ImageFile( - data: content, - fileName: file.fileName, - caption: file.caption, - isNsfw: file.isNsfw); - break; + data: content, + fileName: file.fileName, + caption: file.caption, + isNsfw: file.isNsfw, + ); } state = state.copyWith(files: files); @@ -637,7 +669,6 @@ class NoteCreateNotifier extends StateNotifier { caption: result.caption, isNsfw: result.isNsfw, ); - break; case ImageFileAlreadyPostedFile(): files[index] = ImageFileAlreadyPostedFile( data: file.data, @@ -647,14 +678,13 @@ class NoteCreateNotifier extends StateNotifier { caption: result.caption, isEdited: true, ); - break; case UnknownFile(): files[index] = UnknownFile( - data: file.data, - fileName: result.fileName, - isNsfw: result.isNsfw, - caption: result.caption); - break; + data: file.data, + fileName: result.fileName, + isNsfw: result.isNsfw, + caption: result.caption, + ); case UnknownAlreadyPostedFile(): files[index] = UnknownAlreadyPostedFile( url: file.url, @@ -664,7 +694,6 @@ class NoteCreateNotifier extends StateNotifier { caption: result.caption, isEdited: true, ); - break; } state = state.copyWith(files: files); @@ -672,23 +701,20 @@ class NoteCreateNotifier extends StateNotifier { /// メディアを削除する void deleteFile(int index) { - final list = state.files.toList(); - list.removeAt(index); - - state = state.copyWith(files: list); + state = state.copyWith(files: state.files.toList()..removeAt(index)); } /// リプライ先ユーザーを追加する - Future addReplyUser(BuildContext context) async { - final user = await showDialog( - context: context, - builder: (context) => UserSelectDialog(account: state.account)); + Future addReplyUser() async { + final user = await ref.read(appRouterProvider).push( + UserSelectRoute(accountContext: ref.read(accountContextProvider)), + ); if (user != null) { state = state.copyWith(replyTo: [...state.replyTo, user]); } } - void deleteReplyUser(User user) async { + Future deleteReplyUser(User user) async { final list = state.replyTo.toList(); state = state.copyWith(replyTo: list..remove(user)); } @@ -698,23 +724,24 @@ class NoteCreateNotifier extends StateNotifier { state = state.copyWith(isCw: !state.isCw); } - bool validateNoteVisibility(NoteVisibility visibility, BuildContext context) { + Future validateNoteVisibility( + NoteVisibility visibility, + ) async { final replyVisibility = state.reply?.visibility; if (replyVisibility == NoteVisibility.specified || replyVisibility == NoteVisibility.followers || replyVisibility == NoteVisibility.home) { - SimpleMessageDialog.show( - context, - S.of(context).cannotPublicReplyToPrivateNote( + await _dialogNotifier.showSimpleDialog( + message: (context) => S.of(context).cannotPublicReplyToPrivateNote( replyVisibility!.displayName(context), ), ); + return false; } - if (state.account.i.isSilenced == true) { - SimpleMessageDialog.show( - context, - S.of(context).cannotPublicNoteBySilencedUser, + if (ref.read(accountContextProvider).postAccount.i.isSilenced) { + await _dialogNotifier.showSimpleDialog( + message: (context) => S.of(context).cannotPublicNoteBySilencedUser, ); return false; } @@ -727,27 +754,26 @@ class NoteCreateNotifier extends StateNotifier { } /// ノートの連合オン・オフを設定する - void toggleLocalOnly(BuildContext context) { + Future toggleLocalOnly() async { // チャンネルのノートは強制ローカルから変えられない if (state.channel != null) { - errorNotifier.state = ( - SpecifiedException(S.of(context).cannotFederateNoteToChannel), - context - ); + await ref.read(dialogStateNotifierProvider.notifier).showSimpleDialog( + message: (context) => S.of(context).cannotFederateNoteToChannel, + ); return; } if (state.reply?.localOnly == true) { - errorNotifier.state = ( - SpecifiedException(S.of(context).cannotFederateReplyToLocalOnlyNote), - context - ); + await ref.read(dialogStateNotifierProvider.notifier).showSimpleDialog( + message: (context) => + S.of(context).cannotFederateReplyToLocalOnlyNote, + ); return; } if (state.renote?.localOnly == true) { - errorNotifier.state = ( - SpecifiedException(S.of(context).cannotFederateRenoteToLocalOnlyNote), - context - ); + await ref.read(dialogStateNotifierProvider.notifier).showSimpleDialog( + message: (context) => + S.of(context).cannotFederateRenoteToLocalOnlyNote, + ); return; } state = state.copyWith(localOnly: !state.localOnly); @@ -785,19 +811,20 @@ class NoteCreateNotifier extends StateNotifier { /// 投票を追加する void addVoteContent() { if (state.voteContentCount == 10) return; - final list = state.voteContent.toList(); - list.add(""); + final list = [...state.voteContent, ""]; state = state.copyWith( - voteContentCount: state.voteContentCount + 1, voteContent: list); + voteContentCount: state.voteContentCount + 1, + voteContent: list, + ); } /// 投票の行を削除する void deleteVoteContent(int index) { if (state.voteContentCount == 2) return; - final list = state.voteContent.toList(); - list.removeAt(index); state = state.copyWith( - voteContentCount: state.voteContentCount - 1, voteContent: list); + voteContentCount: state.voteContentCount - 1, + voteContent: [...state.voteContent]..removeAt(index), + ); } /// 投票の内容を設定する diff --git a/lib/state_notifier/note_create_page/note_create_state_notifier.freezed.dart b/lib/state_notifier/note_create_page/note_create_state_notifier.freezed.dart index 232c70b42..9109e2b8d 100644 --- a/lib/state_notifier/note_create_page/note_create_state_notifier.freezed.dart +++ b/lib/state_notifier/note_create_page/note_create_state_notifier.freezed.dart @@ -12,20 +12,19 @@ part of 'note_create_state_notifier.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); /// @nodoc mixin _$NoteCreate { - Account get account => throw _privateConstructorUsedError; NoteVisibility get noteVisibility => throw _privateConstructorUsedError; bool get localOnly => throw _privateConstructorUsedError; + ReactionAcceptance? get reactionAcceptance => + throw _privateConstructorUsedError; List get replyTo => throw _privateConstructorUsedError; List get files => throw _privateConstructorUsedError; NoteCreateChannel? get channel => throw _privateConstructorUsedError; Note? get reply => throw _privateConstructorUsedError; Note? get renote => throw _privateConstructorUsedError; - ReactionAcceptance? get reactionAcceptance => - throw _privateConstructorUsedError; bool get isCw => throw _privateConstructorUsedError; String get cwText => throw _privateConstructorUsedError; String get text => throw _privateConstructorUsedError; @@ -43,7 +42,9 @@ mixin _$NoteCreate { NoteCreationMode? get noteCreationMode => throw _privateConstructorUsedError; String? get noteId => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of NoteCreate + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $NoteCreateCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -55,15 +56,14 @@ abstract class $NoteCreateCopyWith<$Res> { _$NoteCreateCopyWithImpl<$Res, NoteCreate>; @useResult $Res call( - {Account account, - NoteVisibility noteVisibility, + {NoteVisibility noteVisibility, bool localOnly, + ReactionAcceptance? reactionAcceptance, List replyTo, List files, NoteCreateChannel? channel, Note? reply, Note? renote, - ReactionAcceptance? reactionAcceptance, bool isCw, String cwText, String text, @@ -80,7 +80,6 @@ abstract class $NoteCreateCopyWith<$Res> { NoteCreationMode? noteCreationMode, String? noteId}); - $AccountCopyWith<$Res> get account; $NoteCreateChannelCopyWith<$Res>? get channel; $NoteCopyWith<$Res>? get reply; $NoteCopyWith<$Res>? get renote; @@ -96,18 +95,19 @@ class _$NoteCreateCopyWithImpl<$Res, $Val extends NoteCreate> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of NoteCreate + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ - Object? account = null, Object? noteVisibility = null, Object? localOnly = null, + Object? reactionAcceptance = freezed, Object? replyTo = null, Object? files = null, Object? channel = freezed, Object? reply = freezed, Object? renote = freezed, - Object? reactionAcceptance = freezed, Object? isCw = null, Object? cwText = null, Object? text = null, @@ -125,10 +125,6 @@ class _$NoteCreateCopyWithImpl<$Res, $Val extends NoteCreate> Object? noteId = freezed, }) { return _then(_value.copyWith( - account: null == account - ? _value.account - : account // ignore: cast_nullable_to_non_nullable - as Account, noteVisibility: null == noteVisibility ? _value.noteVisibility : noteVisibility // ignore: cast_nullable_to_non_nullable @@ -137,6 +133,10 @@ class _$NoteCreateCopyWithImpl<$Res, $Val extends NoteCreate> ? _value.localOnly : localOnly // ignore: cast_nullable_to_non_nullable as bool, + reactionAcceptance: freezed == reactionAcceptance + ? _value.reactionAcceptance + : reactionAcceptance // ignore: cast_nullable_to_non_nullable + as ReactionAcceptance?, replyTo: null == replyTo ? _value.replyTo : replyTo // ignore: cast_nullable_to_non_nullable @@ -157,10 +157,6 @@ class _$NoteCreateCopyWithImpl<$Res, $Val extends NoteCreate> ? _value.renote : renote // ignore: cast_nullable_to_non_nullable as Note?, - reactionAcceptance: freezed == reactionAcceptance - ? _value.reactionAcceptance - : reactionAcceptance // ignore: cast_nullable_to_non_nullable - as ReactionAcceptance?, isCw: null == isCw ? _value.isCw : isCw // ignore: cast_nullable_to_non_nullable @@ -224,14 +220,8 @@ class _$NoteCreateCopyWithImpl<$Res, $Val extends NoteCreate> ) as $Val); } - @override - @pragma('vm:prefer-inline') - $AccountCopyWith<$Res> get account { - return $AccountCopyWith<$Res>(_value.account, (value) { - return _then(_value.copyWith(account: value) as $Val); - }); - } - + /// Create a copy of NoteCreate + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $NoteCreateChannelCopyWith<$Res>? get channel { @@ -244,6 +234,8 @@ class _$NoteCreateCopyWithImpl<$Res, $Val extends NoteCreate> }); } + /// Create a copy of NoteCreate + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $NoteCopyWith<$Res>? get reply { @@ -256,6 +248,8 @@ class _$NoteCreateCopyWithImpl<$Res, $Val extends NoteCreate> }); } + /// Create a copy of NoteCreate + /// with the given fields replaced by the non-null parameter values. @override @pragma('vm:prefer-inline') $NoteCopyWith<$Res>? get renote { @@ -278,15 +272,14 @@ abstract class _$$NoteCreateImplCopyWith<$Res> @override @useResult $Res call( - {Account account, - NoteVisibility noteVisibility, + {NoteVisibility noteVisibility, bool localOnly, + ReactionAcceptance? reactionAcceptance, List replyTo, List files, NoteCreateChannel? channel, Note? reply, Note? renote, - ReactionAcceptance? reactionAcceptance, bool isCw, String cwText, String text, @@ -303,8 +296,6 @@ abstract class _$$NoteCreateImplCopyWith<$Res> NoteCreationMode? noteCreationMode, String? noteId}); - @override - $AccountCopyWith<$Res> get account; @override $NoteCreateChannelCopyWith<$Res>? get channel; @override @@ -321,18 +312,19 @@ class __$$NoteCreateImplCopyWithImpl<$Res> _$NoteCreateImpl _value, $Res Function(_$NoteCreateImpl) _then) : super(_value, _then); + /// Create a copy of NoteCreate + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ - Object? account = null, Object? noteVisibility = null, Object? localOnly = null, + Object? reactionAcceptance = freezed, Object? replyTo = null, Object? files = null, Object? channel = freezed, Object? reply = freezed, Object? renote = freezed, - Object? reactionAcceptance = freezed, Object? isCw = null, Object? cwText = null, Object? text = null, @@ -350,10 +342,6 @@ class __$$NoteCreateImplCopyWithImpl<$Res> Object? noteId = freezed, }) { return _then(_$NoteCreateImpl( - account: null == account - ? _value.account - : account // ignore: cast_nullable_to_non_nullable - as Account, noteVisibility: null == noteVisibility ? _value.noteVisibility : noteVisibility // ignore: cast_nullable_to_non_nullable @@ -362,6 +350,10 @@ class __$$NoteCreateImplCopyWithImpl<$Res> ? _value.localOnly : localOnly // ignore: cast_nullable_to_non_nullable as bool, + reactionAcceptance: freezed == reactionAcceptance + ? _value.reactionAcceptance + : reactionAcceptance // ignore: cast_nullable_to_non_nullable + as ReactionAcceptance?, replyTo: null == replyTo ? _value._replyTo : replyTo // ignore: cast_nullable_to_non_nullable @@ -382,10 +374,6 @@ class __$$NoteCreateImplCopyWithImpl<$Res> ? _value.renote : renote // ignore: cast_nullable_to_non_nullable as Note?, - reactionAcceptance: freezed == reactionAcceptance - ? _value.reactionAcceptance - : reactionAcceptance // ignore: cast_nullable_to_non_nullable - as ReactionAcceptance?, isCw: null == isCw ? _value.isCw : isCw // ignore: cast_nullable_to_non_nullable @@ -454,15 +442,14 @@ class __$$NoteCreateImplCopyWithImpl<$Res> class _$NoteCreateImpl implements _NoteCreate { const _$NoteCreateImpl( - {required this.account, - required this.noteVisibility, + {required this.noteVisibility, required this.localOnly, + required this.reactionAcceptance, final List replyTo = const [], final List files = const [], this.channel, this.reply, this.renote, - required this.reactionAcceptance, this.isCw = false, this.cwText = "", this.text = "", @@ -482,12 +469,12 @@ class _$NoteCreateImpl implements _NoteCreate { _files = files, _voteContent = voteContent; - @override - final Account account; @override final NoteVisibility noteVisibility; @override final bool localOnly; + @override + final ReactionAcceptance? reactionAcceptance; final List _replyTo; @override @JsonKey() @@ -513,8 +500,6 @@ class _$NoteCreateImpl implements _NoteCreate { @override final Note? renote; @override - final ReactionAcceptance? reactionAcceptance; - @override @JsonKey() final bool isCw; @override @@ -563,7 +548,7 @@ class _$NoteCreateImpl implements _NoteCreate { @override String toString() { - return 'NoteCreate(account: $account, noteVisibility: $noteVisibility, localOnly: $localOnly, replyTo: $replyTo, files: $files, channel: $channel, reply: $reply, renote: $renote, reactionAcceptance: $reactionAcceptance, isCw: $isCw, cwText: $cwText, text: $text, isTextFocused: $isTextFocused, isNoteSending: $isNoteSending, isVote: $isVote, voteContent: $voteContent, voteContentCount: $voteContentCount, voteExpireType: $voteExpireType, isVoteMultiple: $isVoteMultiple, voteDate: $voteDate, voteDuration: $voteDuration, voteDurationType: $voteDurationType, noteCreationMode: $noteCreationMode, noteId: $noteId)'; + return 'NoteCreate(noteVisibility: $noteVisibility, localOnly: $localOnly, reactionAcceptance: $reactionAcceptance, replyTo: $replyTo, files: $files, channel: $channel, reply: $reply, renote: $renote, isCw: $isCw, cwText: $cwText, text: $text, isTextFocused: $isTextFocused, isNoteSending: $isNoteSending, isVote: $isVote, voteContent: $voteContent, voteContentCount: $voteContentCount, voteExpireType: $voteExpireType, isVoteMultiple: $isVoteMultiple, voteDate: $voteDate, voteDuration: $voteDuration, voteDurationType: $voteDurationType, noteCreationMode: $noteCreationMode, noteId: $noteId)'; } @override @@ -571,18 +556,17 @@ class _$NoteCreateImpl implements _NoteCreate { return identical(this, other) || (other.runtimeType == runtimeType && other is _$NoteCreateImpl && - (identical(other.account, account) || other.account == account) && (identical(other.noteVisibility, noteVisibility) || other.noteVisibility == noteVisibility) && (identical(other.localOnly, localOnly) || other.localOnly == localOnly) && + (identical(other.reactionAcceptance, reactionAcceptance) || + other.reactionAcceptance == reactionAcceptance) && const DeepCollectionEquality().equals(other._replyTo, _replyTo) && const DeepCollectionEquality().equals(other._files, _files) && (identical(other.channel, channel) || other.channel == channel) && (identical(other.reply, reply) || other.reply == reply) && (identical(other.renote, renote) || other.renote == renote) && - (identical(other.reactionAcceptance, reactionAcceptance) || - other.reactionAcceptance == reactionAcceptance) && (identical(other.isCw, isCw) || other.isCw == isCw) && (identical(other.cwText, cwText) || other.cwText == cwText) && (identical(other.text, text) || other.text == text) && @@ -613,15 +597,14 @@ class _$NoteCreateImpl implements _NoteCreate { @override int get hashCode => Object.hashAll([ runtimeType, - account, noteVisibility, localOnly, + reactionAcceptance, const DeepCollectionEquality().hash(_replyTo), const DeepCollectionEquality().hash(_files), channel, reply, renote, - reactionAcceptance, isCw, cwText, text, @@ -639,7 +622,9 @@ class _$NoteCreateImpl implements _NoteCreate { noteId ]); - @JsonKey(ignore: true) + /// Create a copy of NoteCreate + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$NoteCreateImplCopyWith<_$NoteCreateImpl> get copyWith => @@ -648,15 +633,14 @@ class _$NoteCreateImpl implements _NoteCreate { abstract class _NoteCreate implements NoteCreate { const factory _NoteCreate( - {required final Account account, - required final NoteVisibility noteVisibility, + {required final NoteVisibility noteVisibility, required final bool localOnly, + required final ReactionAcceptance? reactionAcceptance, final List replyTo, final List files, final NoteCreateChannel? channel, final Note? reply, final Note? renote, - required final ReactionAcceptance? reactionAcceptance, final bool isCw, final String cwText, final String text, @@ -673,13 +657,13 @@ abstract class _NoteCreate implements NoteCreate { final NoteCreationMode? noteCreationMode, final String? noteId}) = _$NoteCreateImpl; - @override - Account get account; @override NoteVisibility get noteVisibility; @override bool get localOnly; @override + ReactionAcceptance? get reactionAcceptance; + @override List get replyTo; @override List get files; @@ -690,8 +674,6 @@ abstract class _NoteCreate implements NoteCreate { @override Note? get renote; @override - ReactionAcceptance? get reactionAcceptance; - @override bool get isCw; @override String get cwText; @@ -721,8 +703,11 @@ abstract class _NoteCreate implements NoteCreate { NoteCreationMode? get noteCreationMode; @override String? get noteId; + + /// Create a copy of NoteCreate + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$NoteCreateImplCopyWith<_$NoteCreateImpl> get copyWith => throw _privateConstructorUsedError; } @@ -732,7 +717,9 @@ mixin _$NoteCreateChannel { String get id => throw _privateConstructorUsedError; String get name => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of NoteCreateChannel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $NoteCreateChannelCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -756,6 +743,8 @@ class _$NoteCreateChannelCopyWithImpl<$Res, $Val extends NoteCreateChannel> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of NoteCreateChannel + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -794,6 +783,8 @@ class __$$NoteCreateChannelImplCopyWithImpl<$Res> $Res Function(_$NoteCreateChannelImpl) _then) : super(_value, _then); + /// Create a copy of NoteCreateChannel + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -840,7 +831,9 @@ class _$NoteCreateChannelImpl implements _NoteCreateChannel { @override int get hashCode => Object.hash(runtimeType, id, name); - @JsonKey(ignore: true) + /// Create a copy of NoteCreateChannel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$NoteCreateChannelImplCopyWith<_$NoteCreateChannelImpl> get copyWith => @@ -857,8 +850,11 @@ abstract class _NoteCreateChannel implements NoteCreateChannel { String get id; @override String get name; + + /// Create a copy of NoteCreateChannel + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$NoteCreateChannelImplCopyWith<_$NoteCreateChannelImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/state_notifier/note_create_page/note_create_state_notifier.g.dart b/lib/state_notifier/note_create_page/note_create_state_notifier.g.dart new file mode 100644 index 000000000..a4167f385 --- /dev/null +++ b/lib/state_notifier/note_create_page/note_create_state_notifier.g.dart @@ -0,0 +1,38 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'note_create_state_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$noteCreateNotifierHash() => + r'a6c19cac73b572cf30473220f793e1826dbeb286'; + +/// See also [NoteCreateNotifier]. +@ProviderFor(NoteCreateNotifier) +final noteCreateNotifierProvider = + AutoDisposeNotifierProvider.internal( + NoteCreateNotifier.new, + name: r'noteCreateNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$noteCreateNotifierHash, + dependencies: [ + misskeyPostContextProvider, + notesWithProvider, + accountContextProvider + ], + allTransitiveDependencies: { + misskeyPostContextProvider, + ...?misskeyPostContextProvider.allTransitiveDependencies, + notesWithProvider, + ...?notesWithProvider.allTransitiveDependencies, + accountContextProvider, + ...?accountContextProvider.allTransitiveDependencies + }, +); + +typedef _$NoteCreateNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/state_notifier/note_file_dialog/image_viewer_info_notifier.dart b/lib/state_notifier/note_file_dialog/image_viewer_info_notifier.dart new file mode 100644 index 000000000..037d23a8b --- /dev/null +++ b/lib/state_notifier/note_file_dialog/image_viewer_info_notifier.dart @@ -0,0 +1,54 @@ +import "dart:math"; + +import "package:flutter/material.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; +part "image_viewer_info_notifier.freezed.dart"; +part "image_viewer_info_notifier.g.dart"; + +@riverpod +class ImageViewerInfoNotifier extends _$ImageViewerInfoNotifier { + @override + ImageViewerInfo build() { + return const ImageViewerInfo(); + } + + void reset() { + state = const ImageViewerInfo(); + } + + void update(ImageViewerInfo imageScale) { + state = imageScale; + } + + void updateScale(double scale) { + state = state.copyWith( + scale: scale, + ); + } + + void addPointer() { + state = state.copyWith( + pointersCount: state.pointersCount + 1, + ); + } + + void removePointer() { + final v = max(0, state.pointersCount - 1); + state = state.copyWith( + pointersCount: v, + isDoubleTap: false, + ); + } +} + +@freezed +class ImageViewerInfo with _$ImageViewerInfo { + const factory ImageViewerInfo({ + @Default(1.0) double scale, + @Default(1.0) double lastScale, + @Default(0) int pointersCount, + @Default(false) bool isDoubleTap, + @Default(null) Offset? lastTapLocalPosition, + }) = _ImageViewerInfo; +} diff --git a/lib/state_notifier/note_file_dialog/image_viewer_info_notifier.freezed.dart b/lib/state_notifier/note_file_dialog/image_viewer_info_notifier.freezed.dart new file mode 100644 index 000000000..6b902e5c3 --- /dev/null +++ b/lib/state_notifier/note_file_dialog/image_viewer_info_notifier.freezed.dart @@ -0,0 +1,238 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'image_viewer_info_notifier.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ImageViewerInfo { + double get scale => throw _privateConstructorUsedError; + double get lastScale => throw _privateConstructorUsedError; + int get pointersCount => throw _privateConstructorUsedError; + bool get isDoubleTap => throw _privateConstructorUsedError; + Offset? get lastTapLocalPosition => throw _privateConstructorUsedError; + + /// Create a copy of ImageViewerInfo + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ImageViewerInfoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ImageViewerInfoCopyWith<$Res> { + factory $ImageViewerInfoCopyWith( + ImageViewerInfo value, $Res Function(ImageViewerInfo) then) = + _$ImageViewerInfoCopyWithImpl<$Res, ImageViewerInfo>; + @useResult + $Res call( + {double scale, + double lastScale, + int pointersCount, + bool isDoubleTap, + Offset? lastTapLocalPosition}); +} + +/// @nodoc +class _$ImageViewerInfoCopyWithImpl<$Res, $Val extends ImageViewerInfo> + implements $ImageViewerInfoCopyWith<$Res> { + _$ImageViewerInfoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ImageViewerInfo + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? scale = null, + Object? lastScale = null, + Object? pointersCount = null, + Object? isDoubleTap = null, + Object? lastTapLocalPosition = freezed, + }) { + return _then(_value.copyWith( + scale: null == scale + ? _value.scale + : scale // ignore: cast_nullable_to_non_nullable + as double, + lastScale: null == lastScale + ? _value.lastScale + : lastScale // ignore: cast_nullable_to_non_nullable + as double, + pointersCount: null == pointersCount + ? _value.pointersCount + : pointersCount // ignore: cast_nullable_to_non_nullable + as int, + isDoubleTap: null == isDoubleTap + ? _value.isDoubleTap + : isDoubleTap // ignore: cast_nullable_to_non_nullable + as bool, + lastTapLocalPosition: freezed == lastTapLocalPosition + ? _value.lastTapLocalPosition + : lastTapLocalPosition // ignore: cast_nullable_to_non_nullable + as Offset?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$ImageViewerInfoImplCopyWith<$Res> + implements $ImageViewerInfoCopyWith<$Res> { + factory _$$ImageViewerInfoImplCopyWith(_$ImageViewerInfoImpl value, + $Res Function(_$ImageViewerInfoImpl) then) = + __$$ImageViewerInfoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {double scale, + double lastScale, + int pointersCount, + bool isDoubleTap, + Offset? lastTapLocalPosition}); +} + +/// @nodoc +class __$$ImageViewerInfoImplCopyWithImpl<$Res> + extends _$ImageViewerInfoCopyWithImpl<$Res, _$ImageViewerInfoImpl> + implements _$$ImageViewerInfoImplCopyWith<$Res> { + __$$ImageViewerInfoImplCopyWithImpl( + _$ImageViewerInfoImpl _value, $Res Function(_$ImageViewerInfoImpl) _then) + : super(_value, _then); + + /// Create a copy of ImageViewerInfo + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? scale = null, + Object? lastScale = null, + Object? pointersCount = null, + Object? isDoubleTap = null, + Object? lastTapLocalPosition = freezed, + }) { + return _then(_$ImageViewerInfoImpl( + scale: null == scale + ? _value.scale + : scale // ignore: cast_nullable_to_non_nullable + as double, + lastScale: null == lastScale + ? _value.lastScale + : lastScale // ignore: cast_nullable_to_non_nullable + as double, + pointersCount: null == pointersCount + ? _value.pointersCount + : pointersCount // ignore: cast_nullable_to_non_nullable + as int, + isDoubleTap: null == isDoubleTap + ? _value.isDoubleTap + : isDoubleTap // ignore: cast_nullable_to_non_nullable + as bool, + lastTapLocalPosition: freezed == lastTapLocalPosition + ? _value.lastTapLocalPosition + : lastTapLocalPosition // ignore: cast_nullable_to_non_nullable + as Offset?, + )); + } +} + +/// @nodoc + +class _$ImageViewerInfoImpl implements _ImageViewerInfo { + const _$ImageViewerInfoImpl( + {this.scale = 1.0, + this.lastScale = 1.0, + this.pointersCount = 0, + this.isDoubleTap = false, + this.lastTapLocalPosition = null}); + + @override + @JsonKey() + final double scale; + @override + @JsonKey() + final double lastScale; + @override + @JsonKey() + final int pointersCount; + @override + @JsonKey() + final bool isDoubleTap; + @override + @JsonKey() + final Offset? lastTapLocalPosition; + + @override + String toString() { + return 'ImageViewerInfo(scale: $scale, lastScale: $lastScale, pointersCount: $pointersCount, isDoubleTap: $isDoubleTap, lastTapLocalPosition: $lastTapLocalPosition)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ImageViewerInfoImpl && + (identical(other.scale, scale) || other.scale == scale) && + (identical(other.lastScale, lastScale) || + other.lastScale == lastScale) && + (identical(other.pointersCount, pointersCount) || + other.pointersCount == pointersCount) && + (identical(other.isDoubleTap, isDoubleTap) || + other.isDoubleTap == isDoubleTap) && + (identical(other.lastTapLocalPosition, lastTapLocalPosition) || + other.lastTapLocalPosition == lastTapLocalPosition)); + } + + @override + int get hashCode => Object.hash(runtimeType, scale, lastScale, pointersCount, + isDoubleTap, lastTapLocalPosition); + + /// Create a copy of ImageViewerInfo + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ImageViewerInfoImplCopyWith<_$ImageViewerInfoImpl> get copyWith => + __$$ImageViewerInfoImplCopyWithImpl<_$ImageViewerInfoImpl>( + this, _$identity); +} + +abstract class _ImageViewerInfo implements ImageViewerInfo { + const factory _ImageViewerInfo( + {final double scale, + final double lastScale, + final int pointersCount, + final bool isDoubleTap, + final Offset? lastTapLocalPosition}) = _$ImageViewerInfoImpl; + + @override + double get scale; + @override + double get lastScale; + @override + int get pointersCount; + @override + bool get isDoubleTap; + @override + Offset? get lastTapLocalPosition; + + /// Create a copy of ImageViewerInfo + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ImageViewerInfoImplCopyWith<_$ImageViewerInfoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/state_notifier/note_file_dialog/image_viewer_info_notifier.g.dart b/lib/state_notifier/note_file_dialog/image_viewer_info_notifier.g.dart new file mode 100644 index 000000000..3eab88d07 --- /dev/null +++ b/lib/state_notifier/note_file_dialog/image_viewer_info_notifier.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'image_viewer_info_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$imageViewerInfoNotifierHash() => + r'72fb1a4dc91a618f416a5b506e48944861631ebe'; + +/// See also [ImageViewerInfoNotifier]. +@ProviderFor(ImageViewerInfoNotifier) +final imageViewerInfoNotifierProvider = AutoDisposeNotifierProvider< + ImageViewerInfoNotifier, ImageViewerInfo>.internal( + ImageViewerInfoNotifier.new, + name: r'imageViewerInfoNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$imageViewerInfoNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$ImageViewerInfoNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/state_notifier/photo_edit_page/color_filter_preset.dart b/lib/state_notifier/photo_edit_page/color_filter_preset.dart index 253b9cddc..6360ff005 100644 --- a/lib/state_notifier/photo_edit_page/color_filter_preset.dart +++ b/lib/state_notifier/photo_edit_page/color_filter_preset.dart @@ -1,6 +1,6 @@ -import 'dart:math'; +import "dart:math"; -import 'package:image_editor/image_editor.dart'; +import "package:image_editor/image_editor.dart"; class ColorFilterPresets { final List presets; @@ -8,131 +8,205 @@ class ColorFilterPresets { ColorFilterPresets() : presets = [ ColorFilterPreset( - name: "clarendon", - option: [_brightness(.1), _contrast(.1), _saturation(.15)]), - ColorFilterPreset( - name: "addictiveRed", option: [_addictiveColor(50, 0, 0)]), - ColorFilterPreset( - name: "addictiveGreen", option: [_addictiveColor(0, 50, 0)]), - ColorFilterPreset( - name: "addictiveBlue", option: [_addictiveColor(0, 0, 50)]), - ColorFilterPreset( - name: "gingham", option: [_sepia(.04), _contrast(-.15)]), - ColorFilterPreset( - name: "moon", - option: [_grayscale(), _contrast(-.04), _brightness(0.1)]), - ColorFilterPreset(name: "lark", option: [ - _brightness(0.08), - _grayscale(), - _contrast(-.04), - ]), - ColorFilterPreset( - name: "Reyes", - option: [_sepia(0.4), _brightness(0.13), _contrast(-.06)]), - ColorFilterPreset( - name: "Juno", - option: [_rgbScale(1.01, 1.04, 1), _saturation(0.3)]), - ColorFilterPreset( - name: "Slumber", option: [_brightness(.1), _saturation(-0.5)]), - ColorFilterPreset( - name: "Crema", - option: [_rgbScale(1.04, 1, 1.02), _saturation(-0.05)]), - ColorFilterPreset( - name: "Ludwig", option: [_brightness(.05), _saturation(-0.03)]), - ColorFilterPreset( - name: "Aden", - option: [_colorOverlay(228, 130, 225, 0.13), _saturation(-0.2)]), - ColorFilterPreset( - name: "Perpetua", option: [_rgbScale(1.05, 1.1, 1)]), - ColorFilterPreset(name: "Amaro", option: [ - _saturation(0.3), - _brightness(0.15), - ]), - ColorFilterPreset( - name: "Mayfair", - option: [_colorOverlay(230, 115, 108, 0.05), _saturation(0.15)]), - ColorFilterPreset(name: "Rise", option: [ - _colorOverlay(255, 170, 0, 0.1), - _brightness(0.09), - _saturation(0.1) - ]), - ColorFilterPreset(name: "Hudson", option: [ - _rgbScale(1, 1, 1.25), - _contrast(0.1), - _brightness(0.15) - ]), - ColorFilterPreset(name: "Valencia", option: [ - _colorOverlay(255, 255, 80, 0.8), - _saturation(0.1), - _contrast(0.05), - ]), - ColorFilterPreset(name: "X-Pro II", option: [ - _colorOverlay(255, 255, 0, 0.7), - _saturation(0.2), - _contrast(0.15) - ]), - ColorFilterPreset( - name: "Sierra", option: [_contrast(-0.15), _saturation(0.1)]), - ColorFilterPreset( - name: "Lo-Fi", option: [_contrast(0.15), _saturation(0.2)]), + name: "clarendon", + option: [_brightness(.1), _contrast(.1), _saturation(.15)], + ), + ColorFilterPreset( + name: "addictiveRed", + option: [_addictiveColor(50, 0, 0)], + ), + ColorFilterPreset( + name: "addictiveGreen", + option: [_addictiveColor(0, 50, 0)], + ), + ColorFilterPreset( + name: "addictiveBlue", + option: [_addictiveColor(0, 0, 50)], + ), + ColorFilterPreset( + name: "gingham", + option: [_sepia(.04), _contrast(-.15)], + ), + ColorFilterPreset( + name: "moon", + option: [_grayscale(), _contrast(-.04), _brightness(0.1)], + ), + ColorFilterPreset( + name: "lark", + option: [ + _brightness(0.08), + _grayscale(), + _contrast(-.04), + ], + ), + ColorFilterPreset( + name: "Reyes", + option: [_sepia(0.4), _brightness(0.13), _contrast(-.06)], + ), + ColorFilterPreset( + name: "Juno", + option: [_rgbScale(1.01, 1.04, 1), _saturation(0.3)], + ), + ColorFilterPreset( + name: "Slumber", + option: [_brightness(.1), _saturation(-0.5)], + ), + ColorFilterPreset( + name: "Crema", + option: [_rgbScale(1.04, 1, 1.02), _saturation(-0.05)], + ), + ColorFilterPreset( + name: "Ludwig", + option: [_brightness(.05), _saturation(-0.03)], + ), + ColorFilterPreset( + name: "Aden", + option: [_colorOverlay(228, 130, 225, 0.13), _saturation(-0.2)], + ), + ColorFilterPreset( + name: "Perpetua", + option: [_rgbScale(1.05, 1.1, 1)], + ), + ColorFilterPreset( + name: "Amaro", + option: [ + _saturation(0.3), + _brightness(0.15), + ], + ), + ColorFilterPreset( + name: "Mayfair", + option: [_colorOverlay(230, 115, 108, 0.05), _saturation(0.15)], + ), + ColorFilterPreset( + name: "Rise", + option: [ + _colorOverlay(255, 170, 0, 0.1), + _brightness(0.09), + _saturation(0.1), + ], + ), + ColorFilterPreset( + name: "Hudson", + option: [ + _rgbScale(1, 1, 1.25), + _contrast(0.1), + _brightness(0.15), + ], + ), + ColorFilterPreset( + name: "Valencia", + option: [ + _colorOverlay(255, 255, 80, 0.8), + _saturation(0.1), + _contrast(0.05), + ], + ), + ColorFilterPreset( + name: "X-Pro II", + option: [ + _colorOverlay(255, 255, 0, 0.7), + _saturation(0.2), + _contrast(0.15), + ], + ), + ColorFilterPreset( + name: "Sierra", + option: [_contrast(-0.15), _saturation(0.1)], + ), + ColorFilterPreset( + name: "Lo-Fi", + option: [_contrast(0.15), _saturation(0.2)], + ), ColorFilterPreset(name: "InkWell", option: [_grayscale()]), ColorFilterPreset( - name: "Hefe", option: [_contrast(0.1), _saturation(0.15)]), - ColorFilterPreset( - name: "Nashville", - option: [_colorOverlay(220, 115, 188, 0.12), _contrast(-0.05)]), - ColorFilterPreset( - name: "Stinson", option: [_brightness(0.1), _sepia(0.3)]), - ColorFilterPreset(name: "Vesper", option: [ - _colorOverlay(225, 225, 0, 0.5), - _brightness(0.06), - _contrast(0.06) - ]), - ColorFilterPreset( - name: "Earlybird", - option: [_colorOverlay(255, 165, 40, 0.2), _saturation(0.15)]), - ColorFilterPreset( - name: "Brannan", - option: [_contrast(0.2), _colorOverlay(140, 10, 185, 0.1)]), - ColorFilterPreset( - name: "Sutro", option: [_brightness(-0.1), _saturation(-0.1)]), - ColorFilterPreset( - name: "Toaster", - option: [_sepia(0.1), _colorOverlay(255, 145, 0, 0.2)]), - ColorFilterPreset( - name: "Walden", - option: [_brightness(0.1), _colorOverlay(255, 255, 0, 0.2)]), - ColorFilterPreset( - name: "1997", - option: [_colorOverlay(255, 25, 0, 0.15), _brightness(0.1)]), - ColorFilterPreset(name: "Kelvin", option: [ - _colorOverlay(255, 140, 0, 0.1), - _rgbScale(1.15, 1.05, 1), - _saturation(0.35) - ]), - ColorFilterPreset(name: "Maven", option: [ - _colorOverlay(225, 240, 0, 0.1), - _saturation(0.25), - _contrast(0.05) - ]), - ColorFilterPreset( - name: "Ginza", option: [_sepia(0.06), _brightness(0.1)]), - ColorFilterPreset( - name: "Skyline", option: [_saturation(0.35), _brightness(0.1)]), - ColorFilterPreset( - name: "Dogpatch", option: [_contrast(0.15), _brightness(0.1)]), - ColorFilterPreset( - name: "Brooklyn", - option: [_colorOverlay(25, 240, 252, 0.05), _sepia(0.3)]), - ColorFilterPreset( - name: "Helena", - option: [_colorOverlay(208, 208, 86, 0.2), _contrast(0.15)]), - ColorFilterPreset( - name: "Ashby", - option: [_colorOverlay(255, 160, 25, 0.1), _brightness(0.1)]), - ColorFilterPreset( - name: "Charmes", - option: [_colorOverlay(255, 50, 80, 0.12), _contrast(0.05)]) + name: "Hefe", + option: [_contrast(0.1), _saturation(0.15)], + ), + ColorFilterPreset( + name: "Nashville", + option: [_colorOverlay(220, 115, 188, 0.12), _contrast(-0.05)], + ), + ColorFilterPreset( + name: "Stinson", + option: [_brightness(0.1), _sepia(0.3)], + ), + ColorFilterPreset( + name: "Vesper", + option: [ + _colorOverlay(225, 225, 0, 0.5), + _brightness(0.06), + _contrast(0.06), + ], + ), + ColorFilterPreset( + name: "Earlybird", + option: [_colorOverlay(255, 165, 40, 0.2), _saturation(0.15)], + ), + ColorFilterPreset( + name: "Brannan", + option: [_contrast(0.2), _colorOverlay(140, 10, 185, 0.1)], + ), + ColorFilterPreset( + name: "Sutro", + option: [_brightness(-0.1), _saturation(-0.1)], + ), + ColorFilterPreset( + name: "Toaster", + option: [_sepia(0.1), _colorOverlay(255, 145, 0, 0.2)], + ), + ColorFilterPreset( + name: "Walden", + option: [_brightness(0.1), _colorOverlay(255, 255, 0, 0.2)], + ), + ColorFilterPreset( + name: "1997", + option: [_colorOverlay(255, 25, 0, 0.15), _brightness(0.1)], + ), + ColorFilterPreset( + name: "Kelvin", + option: [ + _colorOverlay(255, 140, 0, 0.1), + _rgbScale(1.15, 1.05, 1), + _saturation(0.35), + ], + ), + ColorFilterPreset( + name: "Maven", + option: [ + _colorOverlay(225, 240, 0, 0.1), + _saturation(0.25), + _contrast(0.05), + ], + ), + ColorFilterPreset( + name: "Ginza", + option: [_sepia(0.06), _brightness(0.1)], + ), + ColorFilterPreset( + name: "Skyline", + option: [_saturation(0.35), _brightness(0.1)], + ), + ColorFilterPreset( + name: "Dogpatch", + option: [_contrast(0.15), _brightness(0.1)], + ), + ColorFilterPreset( + name: "Brooklyn", + option: [_colorOverlay(25, 240, 252, 0.05), _sepia(0.3)], + ), + ColorFilterPreset( + name: "Helena", + option: [_colorOverlay(208, 208, 86, 0.2), _contrast(0.15)], + ), + ColorFilterPreset( + name: "Ashby", + option: [_colorOverlay(255, 160, 25, 0.1), _brightness(0.1)], + ), + ColorFilterPreset( + name: "Charmes", + option: [_colorOverlay(255, 50, 80, 0.12), _contrast(0.05)], + ), ]; } @@ -144,161 +218,129 @@ class ColorFilterPreset { } ColorOption _colorOverlay(double red, double green, double blue, double scale) { - return ColorOption(matrix: [ - (1 - scale), - 0, - 0, - 0, - -1 * red * scale, - 0, - (1 - scale), - 0, - 0, - -1 * green * scale, - 0, - 0, - (1 - scale), - 0, - -1 * blue * scale, - 0, - 0, - 0, - 1, - 0 - ]); + return ColorOption( + matrix: [ + (1 - scale), + 0, + 0, + 0, + -1 * red * scale, + 0, + (1 - scale), + 0, + 0, + -1 * green * scale, + 0, + 0, + (1 - scale), + 0, + -1 * blue * scale, + 0, + 0, + 0, + 1, + 0, + ], + ); } ColorOption _rgbScale(double r, double g, double b) { - return ColorOption(matrix: [ - r, - 0, - 0, - 0, - 0, - 0, - g, - 0, - 0, - 0, - 0, - 0, - b, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - ]); + return ColorOption( + matrix: [ + r, + 0, + 0, + 0, + 0, + 0, + g, + 0, + 0, + 0, + 0, + 0, + b, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + ], + ); } ColorOption _addictiveColor(double r, double g, double b) { - return ColorOption(matrix: [ - 1, - 0, - 0, - 0, - r, - 0, - 1, - 0, - 0, - g, - 0, - 0, - 1, - 0, - b, - 0, - 0, - 0, - 1, - 0, - ]); + return ColorOption( + matrix: [ + 1, + 0, + 0, + 0, + r, + 0, + 1, + 0, + 0, + g, + 0, + 0, + 1, + 0, + b, + 0, + 0, + 0, + 1, + 0, + ], + ); } ColorOption _grayscale() { - return ColorOption(matrix: [ - 0.2126, - 0.7152, - 0.0722, - 0, - 0, - 0.2126, - 0.7152, - 0.0722, - 0, - 0, - 0.2126, - 0.7152, - 0.0722, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - ]); + return ColorOption( + matrix: [ + 0.2126, + 0.7152, + 0.0722, + 0, + 0, + 0.2126, + 0.7152, + 0.0722, + 0, + 0, + 0.2126, + 0.7152, + 0.0722, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + ], + ); } ColorOption _sepia(double value) { - return ColorOption(matrix: [ - (1 - (0.607 * value)), - 0.769 * value, - 0.189 * value, - 0, - 0, - 0.349 * value, - (1 - (0.314 * value)), - 0.168 * value, - 0, - 0, - 0.272 * value, - 0.534 * value, - (1 - (0.869 * value)), - 0, - 0, - 0, - 0, - 0, - 1, - 0, - ]); -} - -ColorOption _invert() { - return ColorOption(matrix: [ - -1, - 0, - 0, - 0, - 255, - 0, - -1, - 0, - 0, - 255, - 0, - 0, - -1, - 0, - 255, - 0, - 0, - 0, - 1, - 0, - ]); -} - -ColorOption _hue(double value) { - value = value * pi; - - if (value == 0) { - return ColorOption(matrix: [ - 1, + return ColorOption( + matrix: [ + (1 - (0.607 * value)), + 0.769 * value, + 0.189 * value, + 0, + 0, + 0.349 * value, + (1 - (0.314 * value)), + 0.168 * value, + 0, + 0, + 0.272 * value, + 0.534 * value, + (1 - (0.869 * value)), 0, 0, 0, @@ -306,50 +348,97 @@ ColorOption _hue(double value) { 0, 1, 0, + ], + ); +} + +ColorOption _invert() { + return ColorOption( + matrix: [ + -1, + 0, 0, 0, + 255, 0, + -1, 0, - 1, 0, + 255, 0, 0, + -1, + 0, + 255, + 0, 0, 0, 1, 0, - ]); + ], + ); +} + +ColorOption _hue(double value) { + value = value * pi; + + if (value == 0) { + return ColorOption( + matrix: [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + ], + ); } - var cosVal = cos(value); - var sinVal = sin(value); - var lumR = 0.213; - var lumG = 0.715; - var lumB = 0.072; + final cosVal = cos(value); + final sinVal = sin(value); + const lumR = 0.213; + const lumG = 0.715; + const lumB = 0.072; return ColorOption( - matrix: List.from([ - (lumR + (cosVal * (1 - lumR))) + (sinVal * (-lumR)), - (lumG + (cosVal * (-lumG))) + (sinVal * (-lumG)), - (lumB + (cosVal * (-lumB))) + (sinVal * (1 - lumB)), - 0, - 0, - (lumR + (cosVal * (-lumR))) + (sinVal * 0.143), - (lumG + (cosVal * (1 - lumG))) + (sinVal * 0.14), - (lumB + (cosVal * (-lumB))) + (sinVal * (-0.283)), - 0, - 0, - (lumR + (cosVal * (-lumR))) + (sinVal * (-(1 - lumR))), - (lumG + (cosVal * (-lumG))) + (sinVal * lumG), - (lumB + (cosVal * (1 - lumB))) + (sinVal * lumB), - 0, - 0, - 0, - 0, - 0, - 1, - 0, - ]).map((i) => i.toDouble()).toList()); + matrix: List.from([ + (lumR + (cosVal * (1 - lumR))) + (sinVal * (-lumR)), + (lumG + (cosVal * (-lumG))) + (sinVal * (-lumG)), + (lumB + (cosVal * (-lumB))) + (sinVal * (1 - lumB)), + 0, + 0, + (lumR + (cosVal * (-lumR))) + (sinVal * 0.143), + (lumG + (cosVal * (1 - lumG))) + (sinVal * 0.14), + (lumB + (cosVal * (-lumB))) + (sinVal * (-0.283)), + 0, + 0, + (lumR + (cosVal * (-lumR))) + (sinVal * (-(1 - lumR))), + (lumG + (cosVal * (-lumG))) + (sinVal * lumG), + (lumB + (cosVal * (1 - lumB))) + (sinVal * lumB), + 0, + 0, + 0, + 0, + 0, + 1, + 0, + ]).map((i) => i.toDouble()).toList(), + ); } ColorOption _brightness(double value) { @@ -360,53 +449,56 @@ ColorOption _brightness(double value) { } if (value == 0) { - return ColorOption(matrix: [ + return ColorOption( + matrix: [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + ], + ); + } + + return ColorOption( + matrix: List.from([ 1, 0, 0, 0, - 0, + value, 0, 1, 0, 0, - 0, + value, 0, 0, 1, 0, - 0, + value, 0, 0, 0, 1, 0, - ]); - } - - return ColorOption( - matrix: List.from([ - 1, - 0, - 0, - 0, - value, - 0, - 1, - 0, - 0, - value, - 0, - 0, - 1, - 0, - value, - 0, - 0, - 0, - 1, - 0 - ]).map((i) => i.toDouble()).toList()); + ]).map((i) => i.toDouble()).toList(), + ); } ColorOption _contrast(double value) { @@ -420,88 +512,93 @@ ColorOption _contrast(double value) { // alpha: color.alpha, // ); // } - double adj = value * 255; - double factor = (259 * (adj + 255)) / (255 * (259 - adj)); - - return ColorOption(matrix: [ - factor, - 0, - 0, - 0, - 128 * (1 - factor), - 0, - factor, - 0, - 0, - 128 * (1 - factor), - 0, - 0, - factor, - 0, - 128 * (1 - factor), - 0, - 0, - 0, - 1, - 0, - ]); -} + final adj = value * 255; + final factor = (259 * (adj + 255)) / (255 * (259 - adj)); -ColorOption _saturation(double value) { - value = value * 100; - - if (value == 0) { - return ColorOption(matrix: [ - 1, + return ColorOption( + matrix: [ + factor, 0, 0, 0, + 128 * (1 - factor), 0, + factor, 0, - 1, 0, + 128 * (1 - factor), 0, 0, + factor, 0, - 0, - 1, - 0, - 0, + 128 * (1 - factor), 0, 0, 0, 1, 0, - ]); + ], + ); +} + +ColorOption _saturation(double value) { + value = value * 100; + + if (value == 0) { + return ColorOption( + matrix: [ + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + ], + ); } - var x = - ((1 + ((value > 0) ? ((3 * value) / 100) : (value / 100)))).toDouble(); - var lumR = 0.3086; - var lumG = 0.6094; - var lumB = 0.082; + final x = + (1 + ((value > 0) ? ((3 * value) / 100) : (value / 100))).toDouble(); + const lumR = 0.3086; + const lumG = 0.6094; + const lumB = 0.082; return ColorOption( - matrix: List.from([ - (lumR * (1 - x)) + x, - lumG * (1 - x), - lumB * (1 - x), - 0, - 0, - lumR * (1 - x), - (lumG * (1 - x)) + x, - lumB * (1 - x), - 0, - 0, - lumR * (1 - x), - lumG * (1 - x), - (lumB * (1 - x)) + x, - 0, - 0, - 0, - 0, - 0, - 1, - 0, - ]).map((i) => i.toDouble()).toList()); + matrix: List.from([ + (lumR * (1 - x)) + x, + lumG * (1 - x), + lumB * (1 - x), + 0, + 0, + lumR * (1 - x), + (lumG * (1 - x)) + x, + lumB * (1 - x), + 0, + 0, + lumR * (1 - x), + lumG * (1 - x), + (lumB * (1 - x)) + x, + 0, + 0, + 0, + 0, + 0, + 1, + 0, + ]).map((i) => i.toDouble()).toList(), + ); } diff --git a/lib/state_notifier/photo_edit_page/image_meta_dialog.dart b/lib/state_notifier/photo_edit_page/image_meta_dialog.dart index 798acc00c..89b1baaa0 100644 --- a/lib/state_notifier/photo_edit_page/image_meta_dialog.dart +++ b/lib/state_notifier/photo_edit_page/image_meta_dialog.dart @@ -1,9 +1,10 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; -part 'image_meta_dialog.freezed.dart'; +part "image_meta_dialog.freezed.dart"; @freezed class ImageMeta with _$ImageMeta { @@ -14,60 +15,46 @@ class ImageMeta with _$ImageMeta { }) = _ImageMeta; } -class ImageMetaDialog extends ConsumerStatefulWidget { - const ImageMetaDialog({super.key, required this.initialMeta}); +class ImageMetaDialog extends HookConsumerWidget { + const ImageMetaDialog({required this.initialMeta, super.key}); final ImageMeta initialMeta; - @override - ConsumerState createState() => ImageMetaDialogState(); -} + Widget build(BuildContext context, WidgetRef ref) { + final fileNameController = + useTextEditingController(text: initialMeta.fileName); + final isNsfw = useState(initialMeta.isNsfw); + final captionController = + useTextEditingController(text: initialMeta.caption); -class ImageMetaDialogState extends ConsumerState { - late final TextEditingController fileNameController = TextEditingController() - ..text = widget.initialMeta.fileName; - late bool isNsfw = widget.initialMeta.isNsfw; - late final TextEditingController captionController = TextEditingController() - ..text = widget.initialMeta.caption; - - @override - void dispose() { - fileNameController.dispose(); - captionController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { return AlertDialog( content: SizedBox( - width: MediaQuery.of(context).size.width * 0.8, - height: MediaQuery.of(context).size.height * 0.8, - child: Column( - children: [ - Text(S.of(context).fileName), - TextField( - controller: fileNameController, - decoration: - const InputDecoration(prefixIcon: Icon(Icons.file_present)), - ), - CheckboxListTile( - value: isNsfw, - onChanged: (value) => setState(() { - isNsfw = !isNsfw; - }), - title: Text(S.of(context).markAsSensitive), - ), - Text(S.of(context).caption), - TextField( - controller: fileNameController, - decoration: - const InputDecoration(prefixIcon: Icon(Icons.file_present)), - minLines: 5, - maxLines: null, - ), - ], - )), + width: MediaQuery.of(context).size.width * 0.8, + height: MediaQuery.of(context).size.height * 0.8, + child: Column( + children: [ + Text(S.of(context).fileName), + TextField( + controller: fileNameController, + decoration: + const InputDecoration(prefixIcon: Icon(Icons.file_present)), + ), + CheckboxListTile( + value: isNsfw.value, + onChanged: (value) => isNsfw.value = !isNsfw.value, + title: Text(S.of(context).markAsSensitive), + ), + Text(S.of(context).caption), + TextField( + controller: captionController, + decoration: + const InputDecoration(prefixIcon: Icon(Icons.file_present)), + minLines: 5, + maxLines: null, + ), + ], + ), + ), ); } } diff --git a/lib/state_notifier/photo_edit_page/image_meta_dialog.freezed.dart b/lib/state_notifier/photo_edit_page/image_meta_dialog.freezed.dart index 21bdcb197..575f2dac0 100644 --- a/lib/state_notifier/photo_edit_page/image_meta_dialog.freezed.dart +++ b/lib/state_notifier/photo_edit_page/image_meta_dialog.freezed.dart @@ -12,7 +12,7 @@ part of 'image_meta_dialog.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); /// @nodoc mixin _$ImageMeta { @@ -20,7 +20,9 @@ mixin _$ImageMeta { bool get isNsfw => throw _privateConstructorUsedError; String get caption => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of ImageMeta + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $ImageMetaCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -43,6 +45,8 @@ class _$ImageMetaCopyWithImpl<$Res, $Val extends ImageMeta> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of ImageMeta + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -86,6 +90,8 @@ class __$$ImageMetaImplCopyWithImpl<$Res> _$ImageMetaImpl _value, $Res Function(_$ImageMetaImpl) _then) : super(_value, _then); + /// Create a copy of ImageMeta + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -142,7 +148,9 @@ class _$ImageMetaImpl implements _ImageMeta { @override int get hashCode => Object.hash(runtimeType, fileName, isNsfw, caption); - @JsonKey(ignore: true) + /// Create a copy of ImageMeta + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ImageMetaImplCopyWith<_$ImageMetaImpl> get copyWith => @@ -161,8 +169,11 @@ abstract class _ImageMeta implements ImageMeta { bool get isNsfw; @override String get caption; + + /// Create a copy of ImageMeta + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$ImageMetaImplCopyWith<_$ImageMetaImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/state_notifier/photo_edit_page/photo_edit_state_notifier.dart b/lib/state_notifier/photo_edit_page/photo_edit_state_notifier.dart index 989144cb5..9592823e4 100644 --- a/lib/state_notifier/photo_edit_page/photo_edit_state_notifier.dart +++ b/lib/state_notifier/photo_edit_page/photo_edit_state_notifier.dart @@ -1,21 +1,21 @@ -import 'dart:math'; -import 'dart:typed_data'; -import 'dart:ui'; - -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:image_editor/image_editor.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/image_file.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/state_notifier/photo_edit_page/color_filter_preset.dart'; -import 'package:miria/view/photo_edit_page/license_confirm_dialog.dart'; -import 'package:miria/view/reaction_picker_dialog/reaction_picker_dialog.dart'; - -part 'photo_edit_state_notifier.freezed.dart'; +import "dart:math"; +import "dart:typed_data"; +import "dart:ui"; + +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter/rendering.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:image_editor/image_editor.dart"; +import "package:miria/model/image_file.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/state_notifier/photo_edit_page/color_filter_preset.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +part "photo_edit_state_notifier.freezed.dart"; +part "photo_edit_state_notifier.g.dart"; @freezed class PhotoEdit with _$PhotoEdit { @@ -53,10 +53,14 @@ class EditedEmojiData with _$EditedEmojiData { }) = _EditedEmojiData; } -class PhotoEditStateNotifier extends StateNotifier { +@Riverpod(dependencies: [accountContext]) +class PhotoEditStateNotifier extends _$PhotoEditStateNotifier { static final List _acceptReactions = []; - PhotoEditStateNotifier(super.state); + PhotoEditStateNotifier(); + + @override + PhotoEdit build() => const PhotoEdit(); /// 状態を初期化する Future initialize(MisskeyPostFile file) async { @@ -65,10 +69,8 @@ class PhotoEditStateNotifier extends StateNotifier { switch (file) { case ImageFile(): initialImage = file.data; - break; case ImageFileAlreadyPostedFile(): initialImage = file.data; - break; default: throw UnsupportedError("$file is unsupported."); } @@ -167,9 +169,9 @@ class PhotoEditStateNotifier extends StateNotifier { } /// 画像を回転する - void rotate() { + Future rotate() async { final angle = ((state.angle - 90) % 360).abs(); - draw( + await draw( state.copyWith( angle: angle, defaultSize: Size(state.defaultSize.height, state.defaultSize.width), @@ -196,8 +198,8 @@ class PhotoEditStateNotifier extends StateNotifier { } /// 画像を切り取る - void crop() { - draw( + Future crop() async { + await draw( state.copyWith( clipMode: !state.clipMode, colorFilterMode: false, @@ -296,7 +298,7 @@ class PhotoEditStateNotifier extends StateNotifier { colorFilterMode: !state.colorFilterMode, ); if (!state.colorFilterMode) return; - createPreviewImage(); + await createPreviewImage(); } Future createPreviewImage() async { @@ -325,8 +327,8 @@ class PhotoEditStateNotifier extends StateNotifier { /// 画像の色調補正を設定する Future selectColorFilter(String name) async { if (state.adaptivePresets.any((element) => element == name)) { - final list = state.adaptivePresets.toList(); - list.removeWhere((element) => element == name); + final list = state.adaptivePresets.toList() + ..removeWhere((element) => element == name); await draw(state.copyWith(adaptivePresets: list)); } else { await draw( @@ -337,31 +339,29 @@ class PhotoEditStateNotifier extends StateNotifier { } /// リアクションを追加する - Future addReaction(Account account, BuildContext context) async { - final reaction = await showDialog( - context: context, - builder: (context) => - ReactionPickerDialog(account: account, isAcceptSensitive: true), - ); + Future addReaction() async { + final reaction = await ref.read(appRouterProvider).push( + ReactionPickerRoute( + account: ref.read(accountContextProvider).postAccount, + isAcceptSensitive: true, + ), + ); if (reaction == null) return; switch (reaction) { case CustomEmojiData(): // カスタム絵文字の場合、ライセンスを確認する if (_acceptReactions.none((e) => e == reaction.baseName)) { - if (!mounted) return; - final dialogResult = await showDialog( - context: context, - builder: (context) => LicenseConfirmDialog( - emoji: reaction.baseName, - account: account, - ), - ); + final dialogResult = await ref.read(appRouterProvider).push( + LicenseConfirmRoute( + emoji: reaction.baseName, + account: ref.read(accountContextProvider).postAccount, + ), + ); if (dialogResult != true) return; _acceptReactions.add(reaction.baseName); } - break; case UnicodeEmojiData(): break; default: @@ -434,8 +434,8 @@ class PhotoEditStateNotifier extends StateNotifier { ); } - void clearSelectMode() { - draw( + Future clearSelectMode() async { + await draw( state.copyWith( selectedEmojiIndex: null, colorFilterMode: false, diff --git a/lib/state_notifier/photo_edit_page/photo_edit_state_notifier.freezed.dart b/lib/state_notifier/photo_edit_page/photo_edit_state_notifier.freezed.dart index 309c4a1af..6bb5673ba 100644 --- a/lib/state_notifier/photo_edit_page/photo_edit_state_notifier.freezed.dart +++ b/lib/state_notifier/photo_edit_page/photo_edit_state_notifier.freezed.dart @@ -12,7 +12,7 @@ part of 'photo_edit_state_notifier.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); /// @nodoc mixin _$PhotoEdit { @@ -32,7 +32,9 @@ mixin _$PhotoEdit { List get emojis => throw _privateConstructorUsedError; int? get selectedEmojiIndex => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of PhotoEdit + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $PhotoEditCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -69,6 +71,8 @@ class _$PhotoEditCopyWithImpl<$Res, $Val extends PhotoEdit> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of PhotoEdit + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -181,6 +185,8 @@ class __$$PhotoEditImplCopyWithImpl<$Res> _$PhotoEditImpl _value, $Res Function(_$PhotoEditImpl) _then) : super(_value, _then); + /// Create a copy of PhotoEdit + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -397,7 +403,9 @@ class _$PhotoEditImpl implements _PhotoEdit { const DeepCollectionEquality().hash(_emojis), selectedEmojiIndex); - @JsonKey(ignore: true) + /// Create a copy of PhotoEdit + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$PhotoEditImplCopyWith<_$PhotoEditImpl> get copyWith => @@ -449,8 +457,11 @@ abstract class _PhotoEdit implements PhotoEdit { List get emojis; @override int? get selectedEmojiIndex; + + /// Create a copy of PhotoEdit + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$PhotoEditImplCopyWith<_$PhotoEditImpl> get copyWith => throw _privateConstructorUsedError; } @@ -460,7 +471,9 @@ mixin _$ColorFilterPreview { String get name => throw _privateConstructorUsedError; Uint8List? get image => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of ColorFilterPreview + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $ColorFilterPreviewCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -484,6 +497,8 @@ class _$ColorFilterPreviewCopyWithImpl<$Res, $Val extends ColorFilterPreview> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of ColorFilterPreview + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -522,6 +537,8 @@ class __$$ColorFilterPreviewImplCopyWithImpl<$Res> $Res Function(_$ColorFilterPreviewImpl) _then) : super(_value, _then); + /// Create a copy of ColorFilterPreview + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -569,7 +586,9 @@ class _$ColorFilterPreviewImpl implements _ColorFilterPreview { int get hashCode => Object.hash( runtimeType, name, const DeepCollectionEquality().hash(image)); - @JsonKey(ignore: true) + /// Create a copy of ColorFilterPreview + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ColorFilterPreviewImplCopyWith<_$ColorFilterPreviewImpl> get copyWith => @@ -586,8 +605,11 @@ abstract class _ColorFilterPreview implements ColorFilterPreview { String get name; @override Uint8List? get image; + + /// Create a copy of ColorFilterPreview + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$ColorFilterPreviewImplCopyWith<_$ColorFilterPreviewImpl> get copyWith => throw _privateConstructorUsedError; } @@ -599,7 +621,9 @@ mixin _$EditedEmojiData { Offset get position => throw _privateConstructorUsedError; double get angle => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + /// Create a copy of EditedEmojiData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $EditedEmojiDataCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -624,6 +648,8 @@ class _$EditedEmojiDataCopyWithImpl<$Res, $Val extends EditedEmojiData> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of EditedEmojiData + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -673,6 +699,8 @@ class __$$EditedEmojiDataImplCopyWithImpl<$Res> _$EditedEmojiDataImpl _value, $Res Function(_$EditedEmojiDataImpl) _then) : super(_value, _then); + /// Create a copy of EditedEmojiData + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -740,7 +768,9 @@ class _$EditedEmojiDataImpl implements _EditedEmojiData { @override int get hashCode => Object.hash(runtimeType, emoji, scale, position, angle); - @JsonKey(ignore: true) + /// Create a copy of EditedEmojiData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$EditedEmojiDataImplCopyWith<_$EditedEmojiDataImpl> get copyWith => @@ -763,8 +793,11 @@ abstract class _EditedEmojiData implements EditedEmojiData { Offset get position; @override double get angle; + + /// Create a copy of EditedEmojiData + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$EditedEmojiDataImplCopyWith<_$EditedEmojiDataImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/state_notifier/photo_edit_page/photo_edit_state_notifier.g.dart b/lib/state_notifier/photo_edit_page/photo_edit_state_notifier.g.dart new file mode 100644 index 000000000..f43360d87 --- /dev/null +++ b/lib/state_notifier/photo_edit_page/photo_edit_state_notifier.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'photo_edit_state_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$photoEditStateNotifierHash() => + r'8775951cc945fb8d595f0c3ac490e2e3d8820039'; + +/// See also [PhotoEditStateNotifier]. +@ProviderFor(PhotoEditStateNotifier) +final photoEditStateNotifierProvider = + AutoDisposeNotifierProvider.internal( + PhotoEditStateNotifier.new, + name: r'photoEditStateNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$photoEditStateNotifierHash, + dependencies: [accountContextProvider], + allTransitiveDependencies: { + accountContextProvider, + ...?accountContextProvider.allTransitiveDependencies + }, +); + +typedef _$PhotoEditStateNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/state_notifier/user_list_page/users_lists_notifier.dart b/lib/state_notifier/user_list_page/users_lists_notifier.dart index c32e3e8c8..792fb5e5d 100644 --- a/lib/state_notifier/user_list_page/users_lists_notifier.dart +++ b/lib/state_notifier/user_list_page/users_lists_notifier.dart @@ -1,62 +1,79 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/users_list_settings.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:miria/model/users_list_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +part "users_lists_notifier.g.dart"; + +@Riverpod(dependencies: [misskeyPostContext]) +class UsersListsNotifier extends _$UsersListsNotifier { + late final _misskey = ref.read(misskeyPostContextProvider); -class UsersListsNotifier - extends AutoDisposeFamilyAsyncNotifier, Misskey> { @override - Future> build(Misskey arg) async { + Future> build() async { final response = await _misskey.users.list.list(); return response.toList(); } - Misskey get _misskey => arg; - Future create(UsersListSettings settings) async { - final list = await _misskey.users.list.create( - UsersListsCreateRequest( - name: settings.name, - ), - ); - if (settings.isPublic) { - await _misskey.users.list.update( - UsersListsUpdateRequest( - listId: list.id, - isPublic: settings.isPublic, - ), + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + final list = await _misskey.users.list.create( + UsersListsCreateRequest(name: settings.name), ); - } - state = AsyncValue.data([...?state.valueOrNull, list]); + if (settings.isPublic) { + await _misskey.users.list.update( + UsersListsUpdateRequest( + listId: list.id, + isPublic: settings.isPublic, + ), + ); + } + state = AsyncValue.data([...?state.valueOrNull, list]); + }); } Future delete(String listId) async { - await _misskey.users.list.delete(UsersListsDeleteRequest(listId: listId)); - state = AsyncValue.data( - state.valueOrNull?.where((e) => e.id != listId).toList() ?? [], - ); + final result = + await ref.read(dialogStateNotifierProvider.notifier).showDialog( + message: (context) => S.of(context).confirmDeleteList, + actions: (context) => [ + S.of(context).doDeleting, + S.of(context).cancel, + ], + ); + if (result != 0) return; + + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + await _misskey.users.list.delete(UsersListsDeleteRequest(listId: listId)); + state = AsyncValue.data( + [...?state.valueOrNull?.where((e) => e.id != listId)], + ); + }); } Future push( String listId, User user, ) async { - await _misskey.users.list.push( - UsersListsPushRequest( - listId: listId, - userId: user.id, - ), - ); - state = AsyncValue.data( - state.valueOrNull - ?.map( - (list) => (list.id == listId) - ? list.copyWith( - userIds: [...list.userIds, user.id], - ) - : list, - ) - .toList() ?? - [], + await ref.read(dialogStateNotifierProvider.notifier).guard( + () async { + await _misskey.users.list.push( + UsersListsPushRequest( + listId: listId, + userId: user.id, + ), + ); + state = AsyncValue.data( + [ + for (final list in [...?state.valueOrNull]) + list.id == listId + ? list.copyWith(userIds: [...list.userIds, user.id]) + : list, + ], + ); + }, ); } @@ -64,25 +81,23 @@ class UsersListsNotifier String listId, User user, ) async { - await _misskey.users.list.pull( - UsersListsPullRequest( - listId: listId, - userId: user.id, - ), - ); - state = AsyncValue.data( - state.valueOrNull - ?.map( - (list) => (list.id == listId) - ? list.copyWith( - userIds: list.userIds - .where((userId) => userId != user.id) - .toList(), - ) - : list, - ) - .toList() ?? - [], - ); + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + await _misskey.users.list.pull( + UsersListsPullRequest( + listId: listId, + userId: user.id, + ), + ); + state = AsyncValue.data([ + for (final list in [...?state.valueOrNull]) + list.id == listId + ? list.copyWith( + userIds: [ + ...list.userIds.where((userId) => userId != user.id), + ], + ) + : list, + ]); + }); } } diff --git a/lib/state_notifier/user_list_page/users_lists_notifier.g.dart b/lib/state_notifier/user_list_page/users_lists_notifier.g.dart new file mode 100644 index 000000000..49c090ca9 --- /dev/null +++ b/lib/state_notifier/user_list_page/users_lists_notifier.g.dart @@ -0,0 +1,30 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'users_lists_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$usersListsNotifierHash() => + r'9ed5c5424d4e68f5f7468227498b1b5a6bcf46a1'; + +/// See also [UsersListsNotifier]. +@ProviderFor(UsersListsNotifier) +final usersListsNotifierProvider = AutoDisposeAsyncNotifierProvider< + UsersListsNotifier, List>.internal( + UsersListsNotifier.new, + name: r'usersListsNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$usersListsNotifierHash, + dependencies: [misskeyPostContextProvider], + allTransitiveDependencies: { + misskeyPostContextProvider, + ...?misskeyPostContextProvider.allTransitiveDependencies + }, +); + +typedef _$UsersListsNotifier = AutoDisposeAsyncNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/util/punycode.dart b/lib/util/punycode.dart index 69d5b17fd..84177b1f9 100644 --- a/lib/util/punycode.dart +++ b/lib/util/punycode.dart @@ -1,12 +1,12 @@ -import 'package:punycode/punycode.dart'; +import "package:punycode/punycode.dart"; String toAscii(String host) { return host.splitMapJoin( - '.', + ".", onNonMatch: (n) { - if (RegExp(r'[^\x00-\x7F]').hasMatch(n)) { + if (RegExp(r"[^\x00-\x7F]").hasMatch(n)) { try { - return 'xn--${punycodeEncode(n)}'; + return "xn--${punycodeEncode(n)}"; } catch (_) {} } return n; diff --git a/lib/view/abuse_dialog/abuse_dialog.dart b/lib/view/abuse_dialog/abuse_dialog.dart new file mode 100644 index 000000000..39be3c346 --- /dev/null +++ b/lib/view/abuse_dialog/abuse_dialog.dart @@ -0,0 +1,84 @@ +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/hooks/use_async.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:miria/view/common/sending_elevated_button.dart"; +import "package:misskey_dart/misskey_dart.dart"; + +@RoutePage() +class AbuseDialog extends HookConsumerWidget implements AutoRouteWrapper { + final Account account; + final User targetUser; + final String? defaultText; + + const AbuseDialog({ + required this.account, + required this.targetUser, + super.key, + this.defaultText, + }); + + @override + Widget wrappedRoute(BuildContext context) => AccountContextScope.as( + account: account, + child: this, + ); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final controller = useTextEditingController(text: defaultText); + + final abuse = useHandledFuture(() async { + await ref.read(misskeyPostContextProvider).users.reportAbuse( + UsersReportAbuseRequest( + userId: targetUser.id, + comment: controller.text, + ), + ); + await ref.read(dialogStateNotifierProvider.notifier).showSimpleDialog( + message: (context) => S.of(context).thanksForReport, + ); + await ref.read(appRouterProvider).maybePop(); + }); + + return AlertDialog( + title: SimpleMfmText( + S.of(context).reportAbuseOf(targetUser.name ?? targetUser.username), + ), + content: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(S.of(context).detail), + TextField( + controller: controller, + maxLines: null, + minLines: 5, + autofocus: true, + ), + Text( + S.of(context).pleaseInputReasonWhyAbuse, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + actions: [ + switch (abuse.value) { + AsyncLoading() => const SendingElevatedButton(), + _ => ElevatedButton( + onPressed: () async => abuse.execute(), + child: Text(S.of(context).reportAbuse), + ), + }, + ], + ); + } +} diff --git a/lib/view/announcements_page/announcements_page.dart b/lib/view/announcements_page/announcements_page.dart index 97476dc16..de204fed3 100644 --- a/lib/view/announcements_page/announcements_page.dart +++ b/lib/view/announcements_page/announcements_page.dart @@ -1,15 +1,19 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/federation_page/federation_announcements.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/federation_page/federation_announcements.dart"; @RoutePage() -class AnnouncementPage extends StatelessWidget { - final Account account; +class AnnouncementPage extends StatelessWidget implements AutoRouteWrapper { + final AccountContext accountContext; - const AnnouncementPage({super.key, required this.account}); + const AnnouncementPage({required this.accountContext, super.key}); + + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); @override Widget build(BuildContext context) { @@ -17,10 +21,7 @@ class AnnouncementPage extends StatelessWidget { appBar: AppBar(title: Text(S.of(context).announcement)), body: Padding( padding: const EdgeInsets.only(left: 10, right: 10), - child: AccountScope( - account: account, - child: FederationAnnouncements(host: account.host), - ), + child: FederationAnnouncements(host: accountContext.getAccount.host), ), ); } diff --git a/lib/view/antenna_page/antenna_list.dart b/lib/view/antenna_page/antenna_list.dart index 6a8f90a81..9f51e5bcc 100644 --- a/lib/view/antenna_page/antenna_list.dart +++ b/lib/view/antenna_page/antenna_list.dart @@ -1,26 +1,22 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/error_detail.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/antenna_page/antennas_notifier.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:miria/view/dialogs/simple_confirm_dialog.dart"; class AntennaList extends ConsumerWidget { const AntennaList({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final account = AccountScope.of(context); - final misskey = ref.watch(misskeyProvider(account)); - final antennas = ref.watch(antennasNotifierProvider(misskey)); + final antennas = ref.watch(antennasNotifierProvider); - return antennas.when( - data: (antennas) { - return ListView.builder( + return switch (antennas) { + AsyncData(value: final antennas) => ListView.builder( itemCount: antennas.length, itemBuilder: (context, index) { final antenna = antennas[index]; @@ -38,26 +34,25 @@ class AntennaList extends ConsumerWidget { if (!context.mounted) return; if (result ?? false) { await ref - .read( - antennasNotifierProvider(misskey).notifier, - ) - .delete(antenna.id) - .expectFailure(context); + .read(antennasNotifierProvider.notifier) + .delete(antenna.id); } }, ), - onTap: () => context.pushRoute( + onTap: () async => context.pushRoute( AntennaNotesRoute( antenna: antenna, - account: account, + accountContext: ref.read(accountContextProvider), ), ), ); }, - ); - }, - error: (e, st) => Center(child: ErrorDetail(error: e, stackTrace: st)), - loading: () => const Center(child: CircularProgressIndicator()), - ); + ), + AsyncError(error: final e, stackTrace: final st) => + Center(child: ErrorDetail(error: e, stackTrace: st)), + AsyncLoading() => const Center( + child: CircularProgressIndicator.adaptive(), + ), + }; } } diff --git a/lib/view/antenna_page/antenna_notes.dart b/lib/view/antenna_page/antenna_notes.dart index ea3393bd6..eaa8fff2d 100644 --- a/lib/view/antenna_page/antenna_notes.dart +++ b/lib/view/antenna_page/antenna_notes.dart @@ -1,37 +1,38 @@ -import 'package:flutter/material.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; class AntennaNotes extends ConsumerWidget { final String antennaId; - const AntennaNotes({super.key, required this.antennaId}); + const AntennaNotes({required this.antennaId, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final account = AccountScope.of(context); return PushableListView( - initializeFuture: () async { - final response = await ref - .read(misskeyProvider(AccountScope.of(context))) - .antennas - .notes(AntennasNotesRequest(antennaId: antennaId)); - ref.read(notesProvider(account)).registerAll(response); - return response.toList(); - }, - nextFuture: (lastItem, _) async { - final response = await ref - .read(misskeyProvider(AccountScope.of(context))) - .antennas - .notes(AntennasNotesRequest( - antennaId: antennaId, untilId: lastItem.id)); - ref.read(notesProvider(account)).registerAll(response); - return response.toList(); - }, - itemBuilder: (context, item) => MisskeyNote(note: item)); + initializeFuture: () async { + final response = await ref + .read(misskeyGetContextProvider) + .antennas + .notes(AntennasNotesRequest(antennaId: antennaId)); + ref.read(notesWithProvider).registerAll(response); + return response.toList(); + }, + nextFuture: (lastItem, _) async { + final response = + await ref.read(misskeyGetContextProvider).antennas.notes( + AntennasNotesRequest( + antennaId: antennaId, + untilId: lastItem.id, + ), + ); + ref.read(notesWithProvider).registerAll(response); + return response.toList(); + }, + itemBuilder: (context, item) => MisskeyNote(note: item), + ); } } diff --git a/lib/view/antenna_page/antenna_notes_page.dart b/lib/view/antenna_page/antenna_notes_page.dart index ab3b67547..32ca42dd4 100644 --- a/lib/view/antenna_page/antenna_notes_page.dart +++ b/lib/view/antenna_page/antenna_notes_page.dart @@ -1,70 +1,67 @@ -import 'package:auto_route/annotations.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/antenna_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/antenna_page/antenna_notes.dart'; -import 'package:miria/view/antenna_page/antenna_settings_dialog.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/antenna_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/antenna_page/antenna_notes.dart"; +import "package:miria/view/antenna_page/antennas_notifier.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:misskey_dart/misskey_dart.dart"; @RoutePage() -class AntennaNotesPage extends ConsumerWidget { +class AntennaNotesPage extends ConsumerWidget implements AutoRouteWrapper { final Antenna antenna; - final Account account; + final AccountContext accountContext; - const AntennaNotesPage( - {super.key, required this.antenna, required this.account}); + const AntennaNotesPage({ + required this.antenna, + required this.accountContext, + super.key, + }); + + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); @override Widget build(BuildContext context, WidgetRef ref) { - final misskey = ref.watch(misskeyProvider(account)); final antenna = ref.watch( - antennasNotifierProvider(misskey).select( + antennasNotifierProvider.select( (antennas) => antennas.valueOrNull ?.firstWhereOrNull((e) => e.id == this.antenna.id), ), ) ?? this.antenna; - return AccountScope( - account: account, - child: Scaffold( - appBar: AppBar( - title: Text(antenna.name), - actions: [ - IconButton( - icon: const Icon(Icons.settings), - onPressed: () async { - final settings = await showDialog( - context: context, - builder: (context) => AntennaSettingsDialog( - title: Text(S.of(context).edit), - initialSettings: AntennaSettings.fromAntenna(antenna), - account: account, - ), - ); - if (!context.mounted) return; - if (settings != null) { - ref - .read(antennasNotifierProvider(misskey).notifier) - .updateAntenna(antenna.id, settings) - .expectFailure(context); - } - }, - ), - ], - ), - body: Padding( - padding: const EdgeInsets.only(right: 10), - child: AntennaNotes( - antennaId: antenna.id, + return Scaffold( + appBar: AppBar( + title: Text(antenna.name), + actions: [ + IconButton( + icon: const Icon(Icons.settings), + onPressed: () async { + final settings = await context.pushRoute( + AntennaSettingsRoute( + title: Text(S.of(context).edit), + initialSettings: AntennaSettings.fromAntenna(antenna), + account: accountContext.postAccount, + ), + ); + if (!context.mounted) return; + if (settings == null) return; + await ref + .read(antennasNotifierProvider.notifier) + .updateAntenna(antenna.id, settings); + }, ), - ), + ], + ), + body: Padding( + padding: const EdgeInsets.only(right: 10), + child: AntennaNotes(antennaId: antenna.id), ), ); } diff --git a/lib/view/antenna_page/antenna_page.dart b/lib/view/antenna_page/antenna_page.dart index 3ba90de04..d8b335dfa 100644 --- a/lib/view/antenna_page/antenna_page.dart +++ b/lib/view/antenna_page/antenna_page.dart @@ -1,56 +1,51 @@ -import 'package:auto_route/annotations.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/antenna_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/antenna_page/antenna_list.dart'; -import 'package:miria/view/antenna_page/antenna_settings_dialog.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/antenna_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/antenna_page/antenna_list.dart"; +import "package:miria/view/antenna_page/antennas_notifier.dart"; +import "package:miria/view/common/account_scope.dart"; @RoutePage() -class AntennaPage extends ConsumerWidget { - final Account account; +class AntennaPage extends ConsumerWidget implements AutoRouteWrapper { + final AccountContext accountContext; - const AntennaPage({required this.account, super.key}); + const AntennaPage({required this.accountContext, super.key}); @override - Widget build(BuildContext context, WidgetRef ref) { - final misskey = ref.watch(misskeyProvider(account)); + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); - return AccountScope( - account: account, - child: Scaffold( - appBar: AppBar( - title: Text(S.of(context).antenna), - actions: [ - IconButton( - icon: const Icon(Icons.add), - onPressed: () async { - final settings = await showDialog( - context: context, - builder: (context) => AntennaSettingsDialog( - title: Text(S.of(context).create), - account: account, - ), - ); - if (!context.mounted) return; - if (settings != null) { - await ref - .read(antennasNotifierProvider(misskey).notifier) - .create(settings) - .expectFailure(context); - } - }, - ), - ], - ), - body: const Padding( - padding: EdgeInsets.only(left: 10, right: 10), - child: AntennaList(), - ), + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + appBar: AppBar( + title: Text(S.of(context).antenna), + actions: [ + IconButton( + icon: const Icon(Icons.add), + onPressed: () async { + final settings = await context.pushRoute( + AntennaSettingsRoute( + title: Text(S.of(context).create), + account: accountContext.postAccount, + ), + ); + if (!context.mounted) return; + if (settings == null) return; + await ref + .read(antennasNotifierProvider.notifier) + .create(settings); + }, + ), + ], + ), + body: const Padding( + padding: EdgeInsets.only(left: 10, right: 10), + child: AntennaList(), ), ); } diff --git a/lib/view/antenna_page/antenna_settings_dialog.dart b/lib/view/antenna_page/antenna_settings_dialog.dart index 8cb7779be..612ae5d24 100644 --- a/lib/view/antenna_page/antenna_settings_dialog.dart +++ b/lib/view/antenna_page/antenna_settings_dialog.dart @@ -1,38 +1,26 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/extensions/user_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/antenna_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/user_select_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/user_extension.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/antenna_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; -final _formKeyProvider = Provider.autoDispose((ref) => GlobalKey()); +part "antenna_settings_dialog.g.dart"; -final _initialSettingsProvider = Provider.autoDispose( - (ref) => throw UnimplementedError(), -); +@Riverpod(dependencies: []) +AntennaSettings _initialSettings(_InitialSettingsRef ref) => + throw UnimplementedError(); -final _textControllerProvider = ChangeNotifierProvider.autoDispose( - (ref) => TextEditingController( - text: ref.watch( - _initialSettingsProvider.select( - (settings) => settings.users.join("\n"), - ), - ), - ), - dependencies: [_initialSettingsProvider], -); - -final _antennaSettingsNotifierProvider = - NotifierProvider.autoDispose<_AntennaSettingsNotifier, AntennaSettings>( - _AntennaSettingsNotifier.new, - dependencies: [_initialSettingsProvider], -); - -class _AntennaSettingsNotifier extends AutoDisposeNotifier { +@Riverpod(dependencies: [_initialSettings]) +class _AntennaSettingsNotifier extends _$AntennaSettingsNotifier { @override AntennaSettings build() { return ref.watch(_initialSettingsProvider); @@ -107,19 +95,18 @@ class _AntennaSettingsNotifier extends AutoDisposeNotifier { } } -final _usersListListProvider = FutureProvider.family, Misskey>( - (ref, misskey) async { - final response = await misskey.users.list.list(); - return response.toList(); - }, -); +@Riverpod(dependencies: [misskeyGetContext]) +Future> _usersListList(_UsersListListRef ref) async => + [...await ref.read(misskeyGetContextProvider).users.list.list()]; -class AntennaSettingsDialog extends StatelessWidget { +@RoutePage() +class AntennaSettingsDialog extends StatelessWidget + implements AutoRouteWrapper { const AntennaSettingsDialog({ + required this.account, super.key, this.title, this.initialSettings = const AntennaSettings(), - required this.account, }); final Widget? title; @@ -144,27 +131,34 @@ class AntennaSettingsDialog extends StatelessWidget { ), ); } + + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: account, child: this); } -class AntennaSettingsForm extends ConsumerWidget { +class AntennaSettingsForm extends HookConsumerWidget { const AntennaSettingsForm({ - super.key, required this.account, + super.key, }); final Account account; @override Widget build(BuildContext context, WidgetRef ref) { - final formKey = ref.watch(_formKeyProvider); + final formKey = useState(GlobalKey()); final initialSettings = ref.watch(_initialSettingsProvider); final settings = ref.watch(_antennaSettingsNotifierProvider); - final misskey = ref.watch(misskeyProvider(account)); - final list = ref.watch(_usersListListProvider(misskey)); - final controller = ref.watch(_textControllerProvider); + final list = ref.watch(_usersListListProvider); + final controller = useTextEditingController(); + ref.listen( + _initialSettingsProvider.select((settings) => settings.users.join("\n")), + (_, next) => controller.text = next, + ); return Form( - key: formKey, + key: formKey.value, child: Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, @@ -251,13 +245,13 @@ class AntennaSettingsForm extends ConsumerWidget { ), TextButton( onPressed: () async { - final user = await showDialog( - context: context, - builder: (context) => UserSelectDialog(account: account), + final user = await context.pushRoute( + UserSelectRoute( + accountContext: AccountContext.as(account), + ), ); - if (user == null) { - return; - } + if (user == null) return; + if (!context.mounted) return; if (!controller.text.endsWith("\n") && controller.text.isNotEmpty) { @@ -339,8 +333,8 @@ class AntennaSettingsForm extends ConsumerWidget { child: ElevatedButton( child: Text(S.of(context).done), onPressed: () { - if (formKey.currentState!.validate()) { - formKey.currentState!.save(); + if (formKey.value.currentState!.validate()) { + formKey.value.currentState!.save(); final settings = ref.read(_antennaSettingsNotifierProvider); if (settings == initialSettings) { Navigator.of(context).pop(); diff --git a/lib/view/antenna_page/antenna_settings_dialog.g.dart b/lib/view/antenna_page/antenna_settings_dialog.g.dart new file mode 100644 index 000000000..341a73818 --- /dev/null +++ b/lib/view/antenna_page/antenna_settings_dialog.g.dart @@ -0,0 +1,64 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'antenna_settings_dialog.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$initialSettingsHash() => r'3b39e5af696c3cf1f2c31b32096d8cff96734d61'; + +/// See also [_initialSettings]. +@ProviderFor(_initialSettings) +final _initialSettingsProvider = AutoDisposeProvider.internal( + _initialSettings, + name: r'_initialSettingsProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$initialSettingsHash, + dependencies: const [], + allTransitiveDependencies: const {}, +); + +typedef _InitialSettingsRef = AutoDisposeProviderRef; +String _$usersListListHash() => r'348c98aa5cbb2c466be69801978f189e9f5a91fc'; + +/// See also [_usersListList]. +@ProviderFor(_usersListList) +final _usersListListProvider = + AutoDisposeFutureProvider>.internal( + _usersListList, + name: r'_usersListListProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$usersListListHash, + dependencies: [misskeyGetContextProvider], + allTransitiveDependencies: { + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies + }, +); + +typedef _UsersListListRef = AutoDisposeFutureProviderRef>; +String _$antennaSettingsNotifierHash() => + r'a33758bbcc3f54c6eb50d2176d6ed1ce7b54f9b5'; + +/// See also [_AntennaSettingsNotifier]. +@ProviderFor(_AntennaSettingsNotifier) +final _antennaSettingsNotifierProvider = AutoDisposeNotifierProvider< + _AntennaSettingsNotifier, AntennaSettings>.internal( + _AntennaSettingsNotifier.new, + name: r'_antennaSettingsNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$antennaSettingsNotifierHash, + dependencies: [_initialSettingsProvider], + allTransitiveDependencies: { + _initialSettingsProvider, + ...?_initialSettingsProvider.allTransitiveDependencies + }, +); + +typedef _$AntennaSettingsNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/antenna_page/antennas_notifier.dart b/lib/view/antenna_page/antennas_notifier.dart new file mode 100644 index 000000000..41d5bdd10 --- /dev/null +++ b/lib/view/antenna_page/antennas_notifier.dart @@ -0,0 +1,89 @@ +import "package:miria/model/antenna_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +part "antennas_notifier.g.dart"; + +@Riverpod(dependencies: [misskeyPostContext]) +class AntennasNotifier extends _$AntennasNotifier { + late final _misskey = ref.read(misskeyPostContextProvider); + + @override + Future> build() async { + final response = await _misskey.antennas.list(); + return response.toList(); + } + + Future create(AntennaSettings settings) async { + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + final antenna = await _misskey.antennas.create( + AntennasCreateRequest( + name: settings.name, + src: settings.src, + keywords: settings.keywords, + excludeKeywords: settings.excludeKeywords, + users: settings.users, + caseSensitive: settings.caseSensitive, + withReplies: settings.withReplies, + withFile: settings.withFile, + notify: settings.notify, + localOnly: settings.localOnly, + ), + ); + state = AsyncValue.data([...?state.valueOrNull, antenna]); + }); + } + + Future delete(String antennaId) async { + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + await _misskey.antennas + .delete(AntennasDeleteRequest(antennaId: antennaId)); + state = AsyncValue.data( + state.valueOrNull?.where((e) => e.id != antennaId).toList() ?? [], + ); + }); + } + + Future updateAntenna( + String antennaId, + AntennaSettings settings, + ) async { + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + await _misskey.antennas.update( + AntennasUpdateRequest( + antennaId: antennaId, + name: settings.name, + src: settings.src, + keywords: settings.keywords, + excludeKeywords: settings.excludeKeywords, + users: settings.users, + caseSensitive: settings.caseSensitive, + withReplies: settings.withReplies, + withFile: settings.withFile, + notify: settings.notify, + localOnly: settings.localOnly, + ), + ); + }); + + state = AsyncValue.data([ + for (final antenna in [...?state.valueOrNull]) + antenna.id == antennaId + ? antenna.copyWith( + name: settings.name, + src: settings.src, + keywords: settings.keywords, + excludeKeywords: settings.excludeKeywords, + users: settings.users, + caseSensitive: settings.caseSensitive, + withReplies: settings.withReplies, + withFile: settings.withFile, + notify: settings.notify, + localOnly: settings.localOnly, + ) + : antenna, + ]); + } +} diff --git a/lib/view/antenna_page/antennas_notifier.g.dart b/lib/view/antenna_page/antennas_notifier.g.dart new file mode 100644 index 000000000..e13fa66c4 --- /dev/null +++ b/lib/view/antenna_page/antennas_notifier.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'antennas_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$antennasNotifierHash() => r'0cecb08c54c64bfafc6cc235c2e1e86f3ec237d2'; + +/// See also [AntennasNotifier]. +@ProviderFor(AntennasNotifier) +final antennasNotifierProvider = + AutoDisposeAsyncNotifierProvider>.internal( + AntennasNotifier.new, + name: r'antennasNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$antennasNotifierHash, + dependencies: [misskeyPostContextProvider], + allTransitiveDependencies: { + misskeyPostContextProvider, + ...?misskeyPostContextProvider.allTransitiveDependencies + }, +); + +typedef _$AntennasNotifier = AutoDisposeAsyncNotifier>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/channel_description_dialog.dart b/lib/view/channel_description_dialog.dart new file mode 100644 index 000000000..22a6f1da4 --- /dev/null +++ b/lib/view/channel_description_dialog.dart @@ -0,0 +1,45 @@ +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/view/channels_page/channel_detail_info.dart"; +import "package:miria/view/common/account_scope.dart"; + +@RoutePage() +class ChannelDescriptionDialog extends ConsumerWidget + implements AutoRouteWrapper { + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: account, child: this); + + final String channelId; + final Account account; + const ChannelDescriptionDialog({ + required this.channelId, + required this.account, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return AlertDialog( + titlePadding: EdgeInsets.zero, + title: Container( + padding: const EdgeInsets.all(10), + decoration: BoxDecoration(color: Theme.of(context).primaryColorDark), + child: Text( + S.of(context).channelInformation, + style: const TextStyle(color: Colors.white), + ), + ), + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: MediaQuery.of(context).size.height * 0.8, + child: SingleChildScrollView( + child: ChannelDetailInfo(channelId: channelId), + ), + ), + ); + } +} diff --git a/lib/view/channel_dialog.dart b/lib/view/channel_dialog.dart deleted file mode 100644 index 094108f95..000000000 --- a/lib/view/channel_dialog.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/view/channels_page/channel_detail_info.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class ChannelDialog extends ConsumerWidget { - final String channelId; - final Account account; - const ChannelDialog( - {super.key, required this.channelId, required this.account}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return AccountScope( - account: account, - child: AlertDialog( - titlePadding: EdgeInsets.zero, - title: Container( - padding: const EdgeInsets.all(10), - decoration: - BoxDecoration(color: Theme.of(context).primaryColorDark), - child: Text( - S.of(context).channelInformation, - style: const TextStyle(color: Colors.white), - )), - content: SizedBox( - width: MediaQuery.of(context).size.width * 0.8, - height: MediaQuery.of(context).size.height * 0.8, - child: SingleChildScrollView( - child: ChannelDetailInfo( - channelId: channelId, - ), - )))); - } -} diff --git a/lib/view/channels_page/channel_detail_info.dart b/lib/view/channels_page/channel_detail_info.dart index 366b1cc4f..b1b21a744 100644 --- a/lib/view/channels_page/channel_detail_info.dart +++ b/lib/view/channels_page/channel_detail_info.dart @@ -1,142 +1,171 @@ -import 'package:flutter/material.dart'; -import 'package:miria/extensions/date_time_extension.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/error_detail.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class ChannelDetailInfo extends ConsumerStatefulWidget { - final String channelId; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/date_time_extension.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/dialogs/simple_message_dialog.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; - const ChannelDetailInfo({super.key, required this.channelId}); +part "channel_detail_info.freezed.dart"; +part "channel_detail_info.g.dart"; - @override - ConsumerState createState() => - ChannelDetailInfoState(); +@freezed +class ChannelDetailState with _$ChannelDetailState { + factory ChannelDetailState({ + required CommunityChannel channel, + AsyncValue? follow, + AsyncValue? favorite, + }) = _ChannelDetailState; } -class ChannelDetailInfoState extends ConsumerState { - CommunityChannel? data; - (Object?, StackTrace)? error; +@Riverpod(dependencies: [misskeyGetContext, misskeyPostContext, notesWith]) +class ChannelDetail extends _$ChannelDetail { + @override + Future build( + String channelId, + ) async { + final result = await ref + .read(misskeyGetContextProvider) + .channels + .show(ChannelsShowRequest(channelId: channelId)); + + ref.read(notesWithProvider).registerAll(result.pinnedNotes ?? []); + + return ChannelDetailState(channel: result); + } Future follow() async { - await ref - .read(misskeyProvider(AccountScope.of(context))) - .channels - .follow(ChannelsFollowRequest(channelId: widget.channelId)); - setState(() { - data = data?.copyWith(isFollowing: true); - }); + final before = await future; + + state = AsyncData( + before.copyWith( + channel: before.channel.copyWith(isFollowing: true), + follow: await AsyncValue.guard( + () async => ref + .read(misskeyPostContextProvider) + .channels + .follow(ChannelsFollowRequest(channelId: channelId)), + ), + ), + ); } Future unfollow() async { - await ref - .read(misskeyProvider(AccountScope.of(context))) - .channels - .unfollow(ChannelsUnfollowRequest(channelId: widget.channelId)); - setState(() { - data = data?.copyWith(isFollowing: false); - }); + final before = await future; + + state = AsyncData( + before.copyWith( + channel: before.channel.copyWith(isFollowing: false), + follow: await AsyncValue.guard( + () async => ref + .read(misskeyPostContextProvider) + .channels + .unfollow(ChannelsUnfollowRequest(channelId: channelId)), + ), + ), + ); } Future favorite() async { - await ref - .read(misskeyProvider(AccountScope.of(context))) - .channels - .favorite(ChannelsFavoriteRequest(channelId: widget.channelId)); - setState(() { - data = data?.copyWith(isFavorited: true); - }); + final before = await future; + + state = AsyncData( + before.copyWith( + channel: before.channel.copyWith(isFavorited: true), + favorite: await AsyncValue.guard( + () async => ref + .read(misskeyPostContextProvider) + .channels + .favorite(ChannelsFavoriteRequest(channelId: channelId)), + ), + ), + ); } Future unfavorite() async { - await ref - .read(misskeyProvider(AccountScope.of(context))) - .channels - .unfavorite(ChannelsUnfavoriteRequest(channelId: widget.channelId)); - setState(() { - data = data?.copyWith(isFavorited: false); - }); + final before = await future; + state = AsyncData( + before.copyWith( + channel: before.channel.copyWith(isFavorited: false), + favorite: await AsyncValue.guard( + () async => ref + .read(misskeyPostContextProvider) + .channels + .unfavorite(ChannelsUnfavoriteRequest(channelId: channelId)), + ), + ), + ); } +} + +class ChannelDetailInfo extends ConsumerWidget { + final String channelId; + + const ChannelDetailInfo({required this.channelId, super.key}); @override - void didChangeDependencies() { - super.didChangeDependencies(); - Future(() async { - try { - final account = AccountScope.of(context); - final result = await ref - .read(misskeyProvider(AccountScope.of(context))) - .channels - .show(ChannelsShowRequest(channelId: widget.channelId)); - - ref.read(notesProvider(account)).registerAll(result.pinnedNotes ?? []); - if (!mounted) return; - setState(() { - data = result; - }); - } catch (e, s) { - if (!mounted) return; - setState(() { - error = (e, s); - }); - } - }); + Widget build(BuildContext context, WidgetRef ref) { + final data = ref.watch(channelDetailProvider(channelId)); + + return switch (data) { + AsyncLoading() => const Center( + child: CircularProgressIndicator.adaptive(), + ), + AsyncError(:final error, :final stackTrace) => + ErrorDetail(error: error, stackTrace: stackTrace), + AsyncData(:final value) => ChannelDetailArea(channel: value.channel), + }; } +} - @override - Widget build(BuildContext context) { - final data = this.data; - final isFavorited = data?.isFavorited; - final isFollowing = data?.isFollowing; - - if (data == null) { - if (error == null) { - return const Center(child: CircularProgressIndicator()); - } else { - return ErrorDetail(error: error?.$1, stackTrace: error?.$2); - } - } +class ChannelDetailArea extends ConsumerWidget { + final CommunityChannel channel; + const ChannelDetailArea({required this.channel, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { return Column( children: [ - if (data.bannerUrl != null) Image.network(data.bannerUrl!.toString()), + if (channel.bannerUrl != null) + Image.network(channel.bannerUrl!.toString()), const Padding(padding: EdgeInsets.only(top: 10)), Align( alignment: Alignment.centerRight, child: Container( decoration: BoxDecoration( - border: Border.all(color: Theme.of(context).primaryColor)), + border: Border.all(color: Theme.of(context).primaryColor), + ), padding: const EdgeInsets.all(10), child: Column( mainAxisAlignment: MainAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.end, children: [ Text( - S.of(context).channelJoinningCounts(data.usersCount), + S.of(context).channelJoinningCounts(channel.usersCount), style: Theme.of(context).textTheme.bodySmall, ), Text( - S.of(context).channelNotes(data.notesCount), + S.of(context).channelNotes(channel.notesCount), style: Theme.of(context).textTheme.bodySmall, ), - if (data.lastNotedAt != null) + if (channel.lastNotedAt != null) Text( S.of(context).channelLastNotedAt( - data.lastNotedAt!.differenceNow(context), + channel.lastNotedAt!.differenceNow(context), ), style: Theme.of(context).textTheme.bodySmall, - ) + ), ], ), ), ), - if (data.isSensitive) + if (channel.isSensitive) Align( alignment: Alignment.centerRight, child: Padding( @@ -153,34 +182,109 @@ class ChannelDetailInfoState extends ConsumerState { ), const Padding(padding: EdgeInsets.only(top: 10)), Align( - alignment: Alignment.centerRight, - child: Wrap( - children: [ - if (isFavorited != null) - isFavorited - ? ElevatedButton.icon( - onPressed: unfavorite.expectFailure(context), - icon: const Icon(Icons.favorite_border), - label: Text(S.of(context).favorite)) - : OutlinedButton( - onPressed: favorite.expectFailure(context), - child: Text(S.of(context).willFavorite)), - const Padding(padding: EdgeInsets.only(left: 10)), - if (isFollowing != null) - isFollowing - ? ElevatedButton.icon( - onPressed: unfollow.expectFailure(context), - icon: const Icon(Icons.check), - label: Text(S.of(context).following)) - : OutlinedButton( - onPressed: follow.expectFailure(context), - child: Text(S.of(context).willFollow)) - ], - )), - MfmText(mfmText: data.description ?? ""), - for (final pinnedNote in data.pinnedNotes ?? []) - MisskeyNote(note: pinnedNote) + alignment: Alignment.centerRight, + child: Wrap( + children: [ + ChannelFavoriteButton( + channelId: channel.id, + isFavorite: channel.isFavorited, + ), + const Padding(padding: EdgeInsets.only(left: 10)), + ChannelFollowingButton( + isFollowing: channel.isFollowing, + channelId: channel.id, + ), + ], + ), + ), + MfmText(mfmText: channel.description ?? ""), + for (final pinnedNote in channel.pinnedNotes ?? []) + MisskeyNote(note: pinnedNote), ], ); } } + +class ChannelFavoriteButton extends ConsumerWidget { + final bool? isFavorite; + final String channelId; + + const ChannelFavoriteButton({ + required this.isFavorite, + required this.channelId, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final provider = channelDetailProvider(channelId); + final followingState = ref.watch( + provider.select((value) => value.valueOrNull?.favorite), + ); + + ref.listen(provider.select((value) => value.valueOrNull?.favorite?.error), + (_, next) async { + if (next == null) return; + await SimpleMessageDialog.show(context, next.toString()); + }); + + return switch (isFavorite) { + null => const SizedBox.shrink(), + true => ElevatedButton.icon( + onPressed: followingState is AsyncLoading + ? null + : ref.read(provider.notifier).unfavorite, + icon: const Icon(Icons.check), + label: Text(S.of(context).favorited), + ), + false => OutlinedButton( + onPressed: followingState is AsyncLoading + ? null + : ref.read(provider.notifier).favorite, + child: Text(S.of(context).willFavorite), + ) + }; + } +} + +class ChannelFollowingButton extends ConsumerWidget { + final bool? isFollowing; + final String channelId; + + const ChannelFollowingButton({ + required this.isFollowing, + required this.channelId, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final provider = channelDetailProvider(channelId); + final followState = ref.watch( + provider.select((value) => value.valueOrNull?.follow), + ); + + ref.listen(provider.select((value) => value.valueOrNull?.follow?.error), + (_, next) async { + if (next == null) return; + await SimpleMessageDialog.show(context, next.toString()); + }); + + return switch (isFollowing) { + null => const SizedBox.shrink(), + true => ElevatedButton.icon( + onPressed: followState is AsyncLoading + ? null + : ref.read(provider.notifier).unfollow, + icon: const Icon(Icons.favorite_border), + label: Text(S.of(context).following), + ), + false => OutlinedButton( + onPressed: followState is AsyncLoading + ? null + : ref.read(provider.notifier).follow, + child: Text(S.of(context).willFollow), + ) + }; + } +} diff --git a/lib/view/channels_page/channel_detail_info.freezed.dart b/lib/view/channels_page/channel_detail_info.freezed.dart new file mode 100644 index 000000000..cf37ee3bc --- /dev/null +++ b/lib/view/channels_page/channel_detail_info.freezed.dart @@ -0,0 +1,201 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'channel_detail_info.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$ChannelDetailState { + CommunityChannel get channel => throw _privateConstructorUsedError; + AsyncValue? get follow => throw _privateConstructorUsedError; + AsyncValue? get favorite => throw _privateConstructorUsedError; + + /// Create a copy of ChannelDetailState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $ChannelDetailStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $ChannelDetailStateCopyWith<$Res> { + factory $ChannelDetailStateCopyWith( + ChannelDetailState value, $Res Function(ChannelDetailState) then) = + _$ChannelDetailStateCopyWithImpl<$Res, ChannelDetailState>; + @useResult + $Res call( + {CommunityChannel channel, + AsyncValue? follow, + AsyncValue? favorite}); + + $CommunityChannelCopyWith<$Res> get channel; +} + +/// @nodoc +class _$ChannelDetailStateCopyWithImpl<$Res, $Val extends ChannelDetailState> + implements $ChannelDetailStateCopyWith<$Res> { + _$ChannelDetailStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of ChannelDetailState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? channel = null, + Object? follow = freezed, + Object? favorite = freezed, + }) { + return _then(_value.copyWith( + channel: null == channel + ? _value.channel + : channel // ignore: cast_nullable_to_non_nullable + as CommunityChannel, + follow: freezed == follow + ? _value.follow + : follow // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + favorite: freezed == favorite + ? _value.favorite + : favorite // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + ) as $Val); + } + + /// Create a copy of ChannelDetailState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $CommunityChannelCopyWith<$Res> get channel { + return $CommunityChannelCopyWith<$Res>(_value.channel, (value) { + return _then(_value.copyWith(channel: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$ChannelDetailStateImplCopyWith<$Res> + implements $ChannelDetailStateCopyWith<$Res> { + factory _$$ChannelDetailStateImplCopyWith(_$ChannelDetailStateImpl value, + $Res Function(_$ChannelDetailStateImpl) then) = + __$$ChannelDetailStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {CommunityChannel channel, + AsyncValue? follow, + AsyncValue? favorite}); + + @override + $CommunityChannelCopyWith<$Res> get channel; +} + +/// @nodoc +class __$$ChannelDetailStateImplCopyWithImpl<$Res> + extends _$ChannelDetailStateCopyWithImpl<$Res, _$ChannelDetailStateImpl> + implements _$$ChannelDetailStateImplCopyWith<$Res> { + __$$ChannelDetailStateImplCopyWithImpl(_$ChannelDetailStateImpl _value, + $Res Function(_$ChannelDetailStateImpl) _then) + : super(_value, _then); + + /// Create a copy of ChannelDetailState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? channel = null, + Object? follow = freezed, + Object? favorite = freezed, + }) { + return _then(_$ChannelDetailStateImpl( + channel: null == channel + ? _value.channel + : channel // ignore: cast_nullable_to_non_nullable + as CommunityChannel, + follow: freezed == follow + ? _value.follow + : follow // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + favorite: freezed == favorite + ? _value.favorite + : favorite // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + )); + } +} + +/// @nodoc + +class _$ChannelDetailStateImpl implements _ChannelDetailState { + _$ChannelDetailStateImpl({required this.channel, this.follow, this.favorite}); + + @override + final CommunityChannel channel; + @override + final AsyncValue? follow; + @override + final AsyncValue? favorite; + + @override + String toString() { + return 'ChannelDetailState(channel: $channel, follow: $follow, favorite: $favorite)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$ChannelDetailStateImpl && + (identical(other.channel, channel) || other.channel == channel) && + (identical(other.follow, follow) || other.follow == follow) && + (identical(other.favorite, favorite) || + other.favorite == favorite)); + } + + @override + int get hashCode => Object.hash(runtimeType, channel, follow, favorite); + + /// Create a copy of ChannelDetailState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$ChannelDetailStateImplCopyWith<_$ChannelDetailStateImpl> get copyWith => + __$$ChannelDetailStateImplCopyWithImpl<_$ChannelDetailStateImpl>( + this, _$identity); +} + +abstract class _ChannelDetailState implements ChannelDetailState { + factory _ChannelDetailState( + {required final CommunityChannel channel, + final AsyncValue? follow, + final AsyncValue? favorite}) = _$ChannelDetailStateImpl; + + @override + CommunityChannel get channel; + @override + AsyncValue? get follow; + @override + AsyncValue? get favorite; + + /// Create a copy of ChannelDetailState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$ChannelDetailStateImplCopyWith<_$ChannelDetailStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/view/channels_page/channel_detail_info.g.dart b/lib/view/channels_page/channel_detail_info.g.dart new file mode 100644 index 000000000..5fb373575 --- /dev/null +++ b/lib/view/channels_page/channel_detail_info.g.dart @@ -0,0 +1,228 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'channel_detail_info.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$channelDetailHash() => r'1856423bc5f37b9238872a785f42fa214c0bb2c5'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$ChannelDetail + extends BuildlessAutoDisposeAsyncNotifier { + late final String channelId; + + FutureOr build( + String channelId, + ); +} + +/// See also [ChannelDetail]. +@ProviderFor(ChannelDetail) +const channelDetailProvider = ChannelDetailFamily(); + +/// See also [ChannelDetail]. +class ChannelDetailFamily extends Family { + /// See also [ChannelDetail]. + const ChannelDetailFamily(); + + static final Iterable _dependencies = [ + misskeyGetContextProvider, + misskeyPostContextProvider, + notesWithProvider + ]; + + static final Iterable _allTransitiveDependencies = + { + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies, + misskeyPostContextProvider, + ...?misskeyPostContextProvider.allTransitiveDependencies, + notesWithProvider, + ...?notesWithProvider.allTransitiveDependencies + }; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'channelDetailProvider'; + + /// See also [ChannelDetail]. + ChannelDetailProvider call( + String channelId, + ) { + return ChannelDetailProvider( + channelId, + ); + } + + @visibleForOverriding + @override + ChannelDetailProvider getProviderOverride( + covariant ChannelDetailProvider provider, + ) { + return call( + provider.channelId, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(ChannelDetail Function() create) { + return _$ChannelDetailFamilyOverride(this, create); + } +} + +class _$ChannelDetailFamilyOverride implements FamilyOverride { + _$ChannelDetailFamilyOverride(this.overriddenFamily, this.create); + + final ChannelDetail Function() create; + + @override + final ChannelDetailFamily overriddenFamily; + + @override + ChannelDetailProvider getProviderOverride( + covariant ChannelDetailProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [ChannelDetail]. +class ChannelDetailProvider extends AutoDisposeAsyncNotifierProviderImpl< + ChannelDetail, ChannelDetailState> { + /// See also [ChannelDetail]. + ChannelDetailProvider( + String channelId, + ) : this._internal( + () => ChannelDetail()..channelId = channelId, + from: channelDetailProvider, + name: r'channelDetailProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$channelDetailHash, + dependencies: ChannelDetailFamily._dependencies, + allTransitiveDependencies: + ChannelDetailFamily._allTransitiveDependencies, + channelId: channelId, + ); + + ChannelDetailProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.channelId, + }) : super.internal(); + + final String channelId; + + @override + FutureOr runNotifierBuild( + covariant ChannelDetail notifier, + ) { + return notifier.build( + channelId, + ); + } + + @override + Override overrideWith(ChannelDetail Function() create) { + return ProviderOverride( + origin: this, + override: ChannelDetailProvider._internal( + () => create()..channelId = channelId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + channelId: channelId, + ), + ); + } + + @override + (String,) get argument { + return (channelId,); + } + + @override + AutoDisposeAsyncNotifierProviderElement + createElement() { + return _ChannelDetailProviderElement(this); + } + + ChannelDetailProvider _copyWith( + ChannelDetail Function() create, + ) { + return ChannelDetailProvider._internal( + () => create()..channelId = channelId, + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + channelId: channelId, + ); + } + + @override + bool operator ==(Object other) { + return other is ChannelDetailProvider && other.channelId == channelId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, channelId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin ChannelDetailRef + on AutoDisposeAsyncNotifierProviderRef { + /// The parameter `channelId` of this provider. + String get channelId; +} + +class _ChannelDetailProviderElement + extends AutoDisposeAsyncNotifierProviderElement with ChannelDetailRef { + _ChannelDetailProviderElement(super.provider); + + @override + String get channelId => (origin as ChannelDetailProvider).channelId; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/channels_page/channel_detail_page.dart b/lib/view/channels_page/channel_detail_page.dart index 13a2cff66..b5b660a56 100644 --- a/lib/view/channels_page/channel_detail_page.dart +++ b/lib/view/channels_page/channel_detail_page.dart @@ -1,66 +1,88 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/channels_page/channel_detail_info.dart'; -import 'package:miria/view/channels_page/channel_timeline.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/channels_page/channel_detail_info.dart"; +import "package:miria/view/channels_page/channel_timeline.dart"; +import "package:miria/view/common/account_scope.dart"; @RoutePage() -class ChannelDetailPage extends ConsumerWidget { - final Account account; +class ChannelDetailPage extends ConsumerWidget implements AutoRouteWrapper { + final AccountContext accountContext; final String channelId; const ChannelDetailPage({ - super.key, - required this.account, + required this.accountContext, required this.channelId, + super.key, }); + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); + @override Widget build(BuildContext context, WidgetRef ref) { return DefaultTabController( length: 2, - child: AccountScope( - account: account, - child: Scaffold( - appBar: AppBar( - title: Text(S.of(context).channel), - bottom: TabBar(tabs: [ + child: Scaffold( + appBar: AppBar( + title: Text(S.of(context).channel), + bottom: TabBar( + tabs: [ Tab(child: Text(S.of(context).channelInformation)), - Tab(child: Text(S.of(context).timeline)) - ]), - ), - body: TabBarView( - children: [ - SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: ChannelDetailInfo(channelId: channelId))), - Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: ChannelTimeline(channelId: channelId)), + Tab(child: Text(S.of(context).timeline)), ], ), - floatingActionButton: FloatingActionButton( - child: const Icon(Icons.edit), - onPressed: () async { - final communityChannel = await ref - .read(misskeyProvider(account)) - .channels - .show(ChannelsShowRequest(channelId: channelId)); - context.pushRoute(NoteCreateRoute( - initialAccount: account, - channel: communityChannel, - )); - }, - ), ), + body: TabBarView( + children: [ + SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: ChannelDetailInfo(channelId: channelId), + ), + ), + Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: ChannelTimeline(channelId: channelId), + ), + ], + ), + floatingActionButton: ref.read(accountContextProvider).isSame + ? ChannelDetailFloatingActionButton( + channelId: channelId, + ) + : null, ), ); } } + +class ChannelDetailFloatingActionButton extends ConsumerWidget { + final String channelId; + + const ChannelDetailFloatingActionButton({required this.channelId, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final channelDetail = ref.watch(channelDetailProvider(channelId)); + return switch (channelDetail) { + AsyncData(:final value) => FloatingActionButton( + child: const Icon(Icons.edit), + onPressed: () async { + if (!context.mounted) return; + await context.pushRoute( + NoteCreateRoute( + initialAccount: ref.read(accountContextProvider).postAccount, + channel: value.channel, + ), + ); + }, + ), + _ => const SizedBox.shrink(), + }; + } +} diff --git a/lib/view/channels_page/channel_favorited.dart b/lib/view/channels_page/channel_favorited.dart index 0bd9a0029..31420c0ed 100644 --- a/lib/view/channels_page/channel_favorited.dart +++ b/lib/view/channels_page/channel_favorited.dart @@ -1,10 +1,9 @@ -import 'package:flutter/material.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/channels_page/community_channel_view.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/futable_list_builder.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/channels_page/community_channel_view.dart"; +import "package:miria/view/common/futable_list_builder.dart"; +import "package:misskey_dart/misskey_dart.dart"; class ChannelFavorited extends ConsumerWidget { const ChannelFavorited({super.key, this.onChannelSelected}); @@ -13,10 +12,9 @@ class ChannelFavorited extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final account = AccountScope.of(context); return FutureListView( future: ref - .read(misskeyProvider(account)) + .read(misskeyPostContextProvider) .channels .myFavorite(const ChannelsMyFavoriteRequest()), builder: (context, item) => CommunityChannelView( diff --git a/lib/view/channels_page/channel_followed.dart b/lib/view/channels_page/channel_followed.dart index 537bf2e21..79dcc240e 100644 --- a/lib/view/channels_page/channel_followed.dart +++ b/lib/view/channels_page/channel_followed.dart @@ -1,10 +1,9 @@ -import 'package:flutter/material.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/channels_page/community_channel_view.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/channels_page/community_channel_view.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; class ChannelFollowed extends ConsumerWidget { const ChannelFollowed({super.key, this.onChannelSelected}); @@ -13,18 +12,17 @@ class ChannelFollowed extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final account = AccountScope.of(context); return PushableListView( initializeFuture: () async { final response = await ref - .read(misskeyProvider(account)) + .read(misskeyPostContextProvider) .channels .followed(const ChannelsFollowedRequest()); return response.toList(); }, nextFuture: (lastItem, _) async { final response = await ref - .read(misskeyProvider(account)) + .read(misskeyPostContextProvider) .channels .followed(ChannelsFollowedRequest(untilId: lastItem.id)); return response.toList(); diff --git a/lib/view/channels_page/channel_search.dart b/lib/view/channels_page/channel_search.dart index e2886f1da..bb6837451 100644 --- a/lib/view/channels_page/channel_search.dart +++ b/lib/view/channels_page/channel_search.dart @@ -1,25 +1,19 @@ -import 'package:flutter/material.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/channels_page/community_channel_view.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/channels_page/community_channel_view.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; final channelSearchProvider = StateProvider.autoDispose((ref) => ""); -class ChannelSearch extends ConsumerStatefulWidget { +class ChannelSearch extends ConsumerWidget { const ChannelSearch({super.key, this.onChannelSelected}); final void Function(CommunityChannel channel)? onChannelSelected; @override - ConsumerState createState() => ChannelSearchState(); -} - -class ChannelSearchState extends ConsumerState { - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Column( children: [ const Padding(padding: EdgeInsets.only(top: 5)), @@ -33,9 +27,7 @@ class ChannelSearchState extends ConsumerState { Expanded( child: Padding( padding: const EdgeInsets.only(left: 10, right: 10), - child: ChannelSearchList( - onChannelSelected: widget.onChannelSelected, - ), + child: ChannelSearchList(onChannelSelected: onChannelSelected), ), ), ], @@ -60,17 +52,19 @@ class ChannelSearchList extends ConsumerWidget { listKey: searchValue, initializeFuture: () async { final channels = await ref - .read(misskeyProvider(AccountScope.of(context))) + .read(misskeyGetContextProvider) .channels .search(ChannelsSearchRequest(query: searchValue)); return channels.toList(); }, nextFuture: (lastItem, _) async { - final channels = await ref - .read(misskeyProvider(AccountScope.of(context))) - .channels - .search(ChannelsSearchRequest( - query: searchValue, untilId: lastItem.id)); + final channels = + await ref.read(misskeyGetContextProvider).channels.search( + ChannelsSearchRequest( + query: searchValue, + untilId: lastItem.id, + ), + ); return channels.toList(); }, itemBuilder: (context, item) { diff --git a/lib/view/channels_page/channel_timeline.dart b/lib/view/channels_page/channel_timeline.dart index 239bf6cae..05d346596 100644 --- a/lib/view/channels_page/channel_timeline.dart +++ b/lib/view/channels_page/channel_timeline.dart @@ -1,40 +1,41 @@ -import 'package:flutter/material.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; class ChannelTimeline extends ConsumerWidget { final String channelId; const ChannelTimeline({ - super.key, required this.channelId, + super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { - final account = AccountScope.of(context); return PushableListView( - initializeFuture: () async { - final response = await ref - .read(misskeyProvider(account)) - .channels - .timeline( - ChannelsTimelineRequest(channelId: channelId, limit: 30)); - ref.read(notesProvider(account)).registerAll(response); - return response.toList(); - }, - nextFuture: (lastItem, _) async { - final response = await ref - .read(misskeyProvider(account)) - .channels - .timeline(ChannelsTimelineRequest( - channelId: channelId, untilId: lastItem.id, limit: 30)); - ref.read(notesProvider(account)).registerAll(response); - return response.toList(); - }, - itemBuilder: (context, item) => MisskeyNote(note: item)); + initializeFuture: () async { + final response = + await ref.read(misskeyGetContextProvider).channels.timeline( + ChannelsTimelineRequest(channelId: channelId, limit: 30), + ); + ref.read(notesWithProvider).registerAll(response); + return response.toList(); + }, + nextFuture: (lastItem, _) async { + final response = + await ref.read(misskeyGetContextProvider).channels.timeline( + ChannelsTimelineRequest( + channelId: channelId, + untilId: lastItem.id, + limit: 30, + ), + ); + ref.read(notesWithProvider).registerAll(response); + return response.toList(); + }, + itemBuilder: (context, item) => MisskeyNote(note: item), + ); } } diff --git a/lib/view/channels_page/channel_trend.dart b/lib/view/channels_page/channel_trend.dart index 401127c8f..870bae7af 100644 --- a/lib/view/channels_page/channel_trend.dart +++ b/lib/view/channels_page/channel_trend.dart @@ -1,11 +1,10 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/channels_page/community_channel_view.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/futable_list_builder.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/cupertino.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/channels_page/community_channel_view.dart"; +import "package:miria/view/common/futable_list_builder.dart"; +import "package:misskey_dart/misskey_dart.dart"; class ChannelTrend extends ConsumerWidget { const ChannelTrend({super.key, this.onChannelSelected}); @@ -15,10 +14,7 @@ class ChannelTrend extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return FutureListView( - future: ref - .read(misskeyProvider(AccountScope.of(context))) - .channels - .featured(), + future: ref.read(misskeyGetContextProvider).channels.featured(), builder: (context, item) => CommunityChannelView( channel: item, onTap: onChannelSelected != null diff --git a/lib/view/channels_page/channels_page.dart b/lib/view/channels_page/channels_page.dart index 6a8a7873f..e9a184d16 100644 --- a/lib/view/channels_page/channels_page.dart +++ b/lib/view/channels_page/channels_page.dart @@ -1,18 +1,21 @@ -import 'package:auto_route/annotations.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/view/channels_page/channel_favorited.dart'; -import 'package:miria/view/channels_page/channel_followed.dart'; -import 'package:miria/view/channels_page/channel_search.dart'; -import 'package:miria/view/channels_page/channel_trend.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/channels_page/channel_favorited.dart"; +import "package:miria/view/channels_page/channel_followed.dart"; +import "package:miria/view/channels_page/channel_search.dart"; +import "package:miria/view/channels_page/channel_trend.dart"; +import "package:miria/view/common/account_scope.dart"; @RoutePage() -class ChannelsPage extends StatelessWidget { - final Account account; +class ChannelsPage extends StatelessWidget implements AutoRouteWrapper { + final AccountContext accountContext; - const ChannelsPage({super.key, required this.account}); + const ChannelsPage({required this.accountContext, super.key}); + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); @override Widget build(BuildContext context) { @@ -28,30 +31,33 @@ class ChannelsPage extends StatelessWidget { Tab(text: S.of(context).trend), Tab(text: S.of(context).favorite), Tab(text: S.of(context).following), - Tab(text: S.of(context).managing) + Tab(text: S.of(context).managing), ], isScrollable: true, tabAlignment: TabAlignment.center, ), ), - body: AccountScope( - account: account, - child: TabBarView(children: [ - const Padding( - padding: EdgeInsets.only(left: 10, right: 10), - child: ChannelSearch()), - const Padding( - padding: EdgeInsets.only(left: 10, right: 10), - child: ChannelTrend(), - ), - const Padding( - padding: EdgeInsets.only(left: 10, right: 10), - child: ChannelFavorited()), - const Padding( - padding: EdgeInsets.only(left: 10, right: 10), - child: ChannelFollowed()), - Text(S.of(context).notImplemented), - ])), + body: TabBarView( + children: [ + const Padding( + padding: EdgeInsets.only(left: 10, right: 10), + child: ChannelSearch(), + ), + const Padding( + padding: EdgeInsets.only(left: 10, right: 10), + child: ChannelTrend(), + ), + const Padding( + padding: EdgeInsets.only(left: 10, right: 10), + child: ChannelFavorited(), + ), + const Padding( + padding: EdgeInsets.only(left: 10, right: 10), + child: ChannelFollowed(), + ), + Text(S.of(context).notImplemented), + ], + ), ), ); } diff --git a/lib/view/channels_page/community_channel_view.dart b/lib/view/channels_page/community_channel_view.dart index eb0eac71d..84440e173 100644 --- a/lib/view/channels_page/community_channel_view.dart +++ b/lib/view/channels_page/community_channel_view.dart @@ -1,89 +1,98 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/extensions/date_time_extension.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/date_time_extension.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class CommunityChannelView extends StatelessWidget { +class CommunityChannelView extends ConsumerWidget { final CommunityChannel channel; final void Function()? onTap; const CommunityChannelView({ - super.key, required this.channel, + super.key, this.onTap, }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Padding( - padding: const EdgeInsets.all(10), - child: GestureDetector( - onTap: onTap ?? - () => context.pushRoute(ChannelDetailRoute( - account: AccountScope.of(context), channelId: channel.id)), - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Theme.of(context).dividerColor)), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (channel.bannerUrl != null) - SizedBox( - height: 150, - width: double.infinity, - child: Image.network( - channel.bannerUrl!.toString(), - fit: BoxFit.fitWidth, - errorBuilder: (_, __, ___) => const SizedBox.shrink(), - ), + padding: const EdgeInsets.all(10), + child: GestureDetector( + onTap: onTap ?? + () async => context.pushRoute( + ChannelDetailRoute( + accountContext: ref.read(accountContextProvider), + channelId: channel.id, ), - Padding( - padding: const EdgeInsets.all(10), - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Text( - channel.name, - style: Theme.of(context).textTheme.titleLarge, - ), - Text( - channel.description ?? "", - style: Theme.of(context).textTheme.bodySmall, - maxLines: 2, + ), + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).dividerColor), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (channel.bannerUrl != null) + SizedBox( + height: 150, + width: double.infinity, + child: Image.network( + channel.bannerUrl!.toString(), + fit: BoxFit.fitWidth, + errorBuilder: (_, __, ___) => const SizedBox.shrink(), + ), + ), + Padding( + padding: const EdgeInsets.all(10), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + channel.name, + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + channel.description ?? "", + style: Theme.of(context).textTheme.bodySmall, + maxLines: 2, + ), + Padding( + padding: const EdgeInsets.only(top: 5, bottom: 5), + child: Container( + decoration: BoxDecoration( + border: Border.all( + color: Theme.of(context).dividerColor, + ), ), - Padding( - padding: const EdgeInsets.only(top: 5, bottom: 5), - child: Container( - decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).dividerColor)), - child: Padding( - padding: const EdgeInsets.all(5), - child: Text( - S.of(context).channelStatistics( - channel.notesCount, - channel.usersCount, - channel.lastNotedAt - ?.differenceNow(context) ?? - channel.createdAt - .differenceNow(context), - ), - style: - Theme.of(context).textTheme.bodySmall, - ))), + child: Padding( + padding: const EdgeInsets.all(5), + child: Text( + S.of(context).channelStatistics( + channel.notesCount, + channel.usersCount, + channel.lastNotedAt?.differenceNow(context) ?? + channel.createdAt.differenceNow(context), + ), + style: Theme.of(context).textTheme.bodySmall, + ), ), - ]), + ), + ), + ], ), - ], - ), + ), + ], ), - )); + ), + ), + ); } } diff --git a/lib/view/clip_list_page/clip_detail_note_list.dart b/lib/view/clip_list_page/clip_detail_note_list.dart index ec82f554b..b871d17d8 100644 --- a/lib/view/clip_list_page/clip_detail_note_list.dart +++ b/lib/view/clip_list_page/clip_detail_note_list.dart @@ -1,35 +1,32 @@ -import 'package:flutter/cupertino.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/cupertino.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; class ClipDetailNoteList extends ConsumerWidget { final String id; - const ClipDetailNoteList({super.key, required this.id}); + const ClipDetailNoteList({required this.id, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return PushableListView( initializeFuture: () async { - final account = AccountScope.of(context); final response = await ref - .read(misskeyProvider(account)) + .read(misskeyGetContextProvider) .clips .notes(ClipsNotesRequest(clipId: id)); - ref.read(notesProvider(account)).registerAll(response); + ref.read(notesWithProvider).registerAll(response); return response.toList(); }, nextFuture: (latestItem, _) async { - final account = AccountScope.of(context); final response = await ref - .read(misskeyProvider(account)) + .read(misskeyGetContextProvider) .clips .notes(ClipsNotesRequest(clipId: id, untilId: latestItem.id)); - ref.read(notesProvider(account)).registerAll(response); + ref.read(notesWithProvider).registerAll(response); return response.toList(); }, itemBuilder: (context, item) { diff --git a/lib/view/clip_list_page/clip_detail_page.dart b/lib/view/clip_list_page/clip_detail_page.dart index 841763374..f0e387d15 100644 --- a/lib/view/clip_list_page/clip_detail_page.dart +++ b/lib/view/clip_list_page/clip_detail_page.dart @@ -1,64 +1,66 @@ -import 'package:auto_route/annotations.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/clip_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/clip_list_page/clip_detail_note_list.dart'; -import 'package:miria/view/clip_list_page/clip_settings_dialog.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/hooks/use_async.dart"; +import "package:miria/model/clip_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/state_notifier/clip_list_page/clips_notifier.dart"; +import "package:miria/view/clip_list_page/clip_detail_note_list.dart"; +import "package:miria/view/common/account_scope.dart"; @RoutePage() -class ClipDetailPage extends ConsumerWidget { - final Account account; +class ClipDetailPage extends HookConsumerWidget implements AutoRouteWrapper { + final AccountContext accountContext; final String id; - const ClipDetailPage({super.key, required this.account, required this.id}); + const ClipDetailPage({ + required this.accountContext, + required this.id, + super.key, + }); + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); @override Widget build(BuildContext context, WidgetRef ref) { - final misskey = ref.watch(misskeyProvider(account)); final clip = ref.watch( - clipsNotifierProvider(misskey).select( + clipsNotifierProvider.select( (clips) => clips.valueOrNull?.firstWhereOrNull((e) => e.id == id), ), ); - - return AccountScope( - account: account, - child: Scaffold( - appBar: AppBar( - title: Text(clip?.name ?? ""), - actions: [ - if (clip != null) - IconButton( - icon: const Icon(Icons.settings), - onPressed: () async { - final settings = await showDialog( - context: context, - builder: (context) => ClipSettingsDialog( - title: Text(S.of(context).edit), - initialSettings: ClipSettings.fromClip(clip), - ), - ); - if (!context.mounted) return; - if (settings != null) { - ref - .read(clipsNotifierProvider(misskey).notifier) - .updateClip(clip.id, settings) - .expectFailure(context); - } - }, - ), - ], - ), - body: Padding( - padding: const EdgeInsets.only(right: 10), - child: ClipDetailNoteList(id: id), + final updateClip = useAsync(() async { + final target = clip; + if (target == null) return; + final settings = await context.pushRoute( + ClipSettingsRoute( + title: Text(S.of(context).edit), + initialSettings: ClipSettings.fromClip(target), ), + ); + if (settings == null) return; + await ref + .read(clipsNotifierProvider.notifier) + .updateClip(target.id, settings); + }); + + return Scaffold( + appBar: AppBar( + title: Text(clip?.name ?? ""), + actions: [ + if (clip != null) + IconButton( + icon: const Icon(Icons.settings), + onPressed: updateClip.executeOrNull, + ), + ], + ), + body: Padding( + padding: const EdgeInsets.only(right: 10), + child: ClipDetailNoteList(id: id), ), ); } diff --git a/lib/view/clip_list_page/clip_list_page.dart b/lib/view/clip_list_page/clip_list_page.dart index f08119762..ca5508966 100644 --- a/lib/view/clip_list_page/clip_list_page.dart +++ b/lib/view/clip_list_page/clip_list_page.dart @@ -1,88 +1,78 @@ -import 'package:auto_route/annotations.dart'; -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart' hide Clip; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/clip_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/clip_list_page/clip_settings_dialog.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/clip_item.dart'; -import 'package:miria/view/common/error_detail.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart" hide Clip; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/hooks/use_async.dart"; +import "package:miria/model/clip_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/state_notifier/clip_list_page/clips_notifier.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/clip_item.dart"; +import "package:miria/view/common/error_detail.dart"; @RoutePage() -class ClipListPage extends ConsumerWidget { - const ClipListPage({super.key, required this.account}); - final Account account; +class ClipListPage extends ConsumerWidget implements AutoRouteWrapper { + final AccountContext accountContext; + + const ClipListPage({required this.accountContext, super.key}); + + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); @override Widget build(BuildContext context, WidgetRef ref) { - final misskey = ref.watch(misskeyProvider(account)); - final clips = ref.watch(clipsNotifierProvider(misskey)); + final clips = ref.watch(clipsNotifierProvider); return Scaffold( appBar: AppBar( - title: Text(S.of(context).clip), + title: Text(S.of(context).clip), actions: [ IconButton( icon: const Icon(Icons.add), onPressed: () async { - final settings = await showDialog( - context: context, - builder: (context) => ClipSettingsDialog( - title: Text(S.of(context).create), - ), + final settings = await context.pushRoute( + ClipSettingsRoute(title: Text(S.of(context).create)), ); if (!context.mounted) return; - if (settings != null) { - ref - .read(clipsNotifierProvider(misskey).notifier) - .create(settings) - .expectFailure(context); - } + if (settings == null) return; + await ref.read(clipsNotifierProvider.notifier).create(settings); }, ), ], ), - body: clips.when( - data: (clips) => AccountScope( - account: account, - child: ListView.builder( - itemCount: clips.length, + body: switch (clips) { + AsyncLoading() => const Center( + child: CircularProgressIndicator.adaptive(), + ), + AsyncError(:final error, :final stackTrace) => + Center(child: ErrorDetail(error: error, stackTrace: stackTrace)), + AsyncData(:final value) => ListView.builder( + itemCount: value.length, itemBuilder: (context, index) { - final clip = clips[index]; - return ClipItem( - clip: clips[index], - trailing: IconButton( - icon: const Icon(Icons.delete), - onPressed: () async { - final result = await SimpleConfirmDialog.show( - context: context, - message: S.of(context).confirmDeleteClip, - primary: S.of(context).willDelete, - secondary: S.of(context).cancel, - ); - if (!context.mounted) return; - if (result ?? false) { - await ref - .read( - clipsNotifierProvider(misskey).notifier, - ) - .delete(clip.id) - .expectFailure(context); - } - }, - ), - ); + final clip = value[index]; + return ClipItem(clip: clip, trailing: _RemoveButton(id: clip.id)); }, - ), - ), - error: (e, st) => Center(child: ErrorDetail(error: e, stackTrace: st)), - loading: () => const Center(child: CircularProgressIndicator()), - ), + ) + }, + ); + } +} + +class _RemoveButton extends HookConsumerWidget { + final String id; + + const _RemoveButton({required this.id}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final delete = useAsync( + () async => ref.read(clipsNotifierProvider.notifier).delete(id), + ); + return IconButton( + icon: const Icon(Icons.delete), + onPressed: delete.executeOrNull, ); } } diff --git a/lib/view/clip_list_page/clip_settings_dialog.dart b/lib/view/clip_list_page/clip_settings_dialog.dart index 4ea614b3f..87c728184 100644 --- a/lib/view/clip_list_page/clip_settings_dialog.dart +++ b/lib/view/clip_list_page/clip_settings_dialog.dart @@ -1,7 +1,8 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/clip_settings.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/clip_settings.dart"; final _formKeyProvider = Provider.autoDispose((ref) => GlobalKey()); @@ -42,6 +43,7 @@ class _ClipSettingsNotifier extends AutoDisposeNotifier { } } +@RoutePage() class ClipSettingsDialog extends StatelessWidget { const ClipSettingsDialog({ super.key, diff --git a/lib/view/clip_modal_sheet/clip_modal_sheet.dart b/lib/view/clip_modal_sheet/clip_modal_sheet.dart new file mode 100644 index 000000000..d86b08a04 --- /dev/null +++ b/lib/view/clip_modal_sheet/clip_modal_sheet.dart @@ -0,0 +1,170 @@ +import "package:auto_route/auto_route.dart"; +import "package:dio/dio.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/hooks/use_async.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/clip_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/state_notifier/clip_list_page/clips_notifier.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +part "clip_modal_sheet.g.dart"; + +@Riverpod(keepAlive: false, dependencies: [misskeyPostContext]) +class _NotesClipsNotifier extends _$NotesClipsNotifier { + @override + Future> build(String noteId) async { + final response = await ref.read(misskeyPostContextProvider).notes.clips( + NotesClipsRequest(noteId: noteId), + ); + return response.toList(); + } + + void addClip(Clip clip) { + state = AsyncValue.data([...state.valueOrNull ?? [], clip]); + } + + void removeClip(String clipId) { + state = AsyncValue.data( + (state.valueOrNull ?? []).where((clip) => clip.id != clipId).toList(), + ); + } +} + +@Riverpod( + keepAlive: false, + dependencies: [ + ClipsNotifier, + _NotesClipsNotifier, + misskeyPostContext, + ], +) +class _ClipModalSheetNotifier extends _$ClipModalSheetNotifier { + @override + Future> build(String noteId) async { + final (userClips, noteClips) = await ( + ref.watch(clipsNotifierProvider.future), + ref.watch(_notesClipsNotifierProvider(noteId).future), + ).wait; + + return [ + for (final userClip in userClips) + (userClip, noteClips.any((noteClip) => noteClip.id == userClip.id)), + ]; + } + + Future addToClip(Clip clip) async { + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + try { + await ref.read(misskeyPostContextProvider).clips.addNote( + ClipsAddNoteRequest(clipId: clip.id, noteId: noteId), + ); + ref.read(_notesClipsNotifierProvider(noteId).notifier).addClip(clip); + } on DioException catch (e) { + if (e.response != null) { + // すでにクリップに追加されている場合、削除するかどうかを確認する + if (((e.response?.data as Map?)?["error"] as Map?)?["code"] == + "ALREADY_CLIPPED") { + final confirm = + await ref.read(dialogStateNotifierProvider.notifier).showDialog( + message: (context) => S.of(context).alreadyAddedClip, + actions: (context) => + [S.of(context).deleteClip, S.of(context).noneAction], + ); + if (confirm == 0) { + await removeFromClip(clip); + } + return; + } + } + rethrow; + } + }); + } + + Future removeFromClip(Clip clip) async { + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + await ref.read(misskeyPostContextProvider).clips.removeNote( + ClipsRemoveNoteRequest( + clipId: clip.id, + noteId: noteId, + ), + ); + ref + .read(_notesClipsNotifierProvider(noteId).notifier) + .removeClip(clip.id); + }); + } +} + +@RoutePage() +class ClipModalSheet extends HookConsumerWidget implements AutoRouteWrapper { + final Account account; + final String noteId; + + const ClipModalSheet({ + required this.account, + required this.noteId, + super.key, + }); + + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: account, child: this); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(_clipModalSheetNotifierProvider(noteId)); + final notifier = _clipModalSheetNotifierProvider(noteId).notifier; + + final create = useAsync(() async { + final settings = await context.pushRoute( + ClipSettingsRoute(title: Text(S.of(context).create)), + ); + if (settings == null) return; + await ref.read(clipsNotifierProvider.notifier).create(settings); + }); + + return switch (state) { + AsyncData(:final value) => ListView.builder( + itemCount: value.length + 1, + itemBuilder: (context, index) { + if (index < value.length) { + final (clip, isClipped) = value[index]; + return ListTile( + leading: isClipped + ? const Icon(Icons.check) + : SizedBox(width: Theme.of(context).iconTheme.size), + onTap: () async { + if (isClipped) { + await ref.read(notifier).removeFromClip(clip); + } else { + await ref.read(notifier).addToClip(clip); + } + }, + title: Text(clip.name ?? ""), + subtitle: Text(clip.description ?? ""), + ); + } else { + return ListTile( + leading: const Icon(Icons.add), + title: Text(S.of(context).createClip), + onTap: create.executeOrNull, + ); + } + }, + ), + AsyncLoading() => + const Center(child: CircularProgressIndicator.adaptive()), + AsyncError(:final error, :final stackTrace) => + Center(child: ErrorDetail(error: error, stackTrace: stackTrace)) + }; + } +} diff --git a/lib/view/clip_modal_sheet/clip_modal_sheet.g.dart b/lib/view/clip_modal_sheet/clip_modal_sheet.g.dart new file mode 100644 index 000000000..ed0e57d53 --- /dev/null +++ b/lib/view/clip_modal_sheet/clip_modal_sheet.g.dart @@ -0,0 +1,423 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'clip_modal_sheet.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$notesClipsNotifierHash() => + r'7962974959d64b1ca68c7365025f910dc39e75e8'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$NotesClipsNotifier + extends BuildlessAutoDisposeAsyncNotifier> { + late final String noteId; + + FutureOr> build( + String noteId, + ); +} + +/// See also [_NotesClipsNotifier]. +@ProviderFor(_NotesClipsNotifier) +const _notesClipsNotifierProvider = _NotesClipsNotifierFamily(); + +/// See also [_NotesClipsNotifier]. +class _NotesClipsNotifierFamily extends Family { + /// See also [_NotesClipsNotifier]. + const _NotesClipsNotifierFamily(); + + static final Iterable _dependencies = [ + misskeyPostContextProvider + ]; + + static final Iterable _allTransitiveDependencies = + { + misskeyPostContextProvider, + ...?misskeyPostContextProvider.allTransitiveDependencies + }; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'_notesClipsNotifierProvider'; + + /// See also [_NotesClipsNotifier]. + _NotesClipsNotifierProvider call( + String noteId, + ) { + return _NotesClipsNotifierProvider( + noteId, + ); + } + + @visibleForOverriding + @override + _NotesClipsNotifierProvider getProviderOverride( + covariant _NotesClipsNotifierProvider provider, + ) { + return call( + provider.noteId, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(_NotesClipsNotifier Function() create) { + return _$NotesClipsNotifierFamilyOverride(this, create); + } +} + +class _$NotesClipsNotifierFamilyOverride implements FamilyOverride { + _$NotesClipsNotifierFamilyOverride(this.overriddenFamily, this.create); + + final _NotesClipsNotifier Function() create; + + @override + final _NotesClipsNotifierFamily overriddenFamily; + + @override + _NotesClipsNotifierProvider getProviderOverride( + covariant _NotesClipsNotifierProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [_NotesClipsNotifier]. +class _NotesClipsNotifierProvider extends AutoDisposeAsyncNotifierProviderImpl< + _NotesClipsNotifier, List> { + /// See also [_NotesClipsNotifier]. + _NotesClipsNotifierProvider( + String noteId, + ) : this._internal( + () => _NotesClipsNotifier()..noteId = noteId, + from: _notesClipsNotifierProvider, + name: r'_notesClipsNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$notesClipsNotifierHash, + dependencies: _NotesClipsNotifierFamily._dependencies, + allTransitiveDependencies: + _NotesClipsNotifierFamily._allTransitiveDependencies, + noteId: noteId, + ); + + _NotesClipsNotifierProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.noteId, + }) : super.internal(); + + final String noteId; + + @override + FutureOr> runNotifierBuild( + covariant _NotesClipsNotifier notifier, + ) { + return notifier.build( + noteId, + ); + } + + @override + Override overrideWith(_NotesClipsNotifier Function() create) { + return ProviderOverride( + origin: this, + override: _NotesClipsNotifierProvider._internal( + () => create()..noteId = noteId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + noteId: noteId, + ), + ); + } + + @override + (String,) get argument { + return (noteId,); + } + + @override + AutoDisposeAsyncNotifierProviderElement<_NotesClipsNotifier, List> + createElement() { + return _NotesClipsNotifierProviderElement(this); + } + + _NotesClipsNotifierProvider _copyWith( + _NotesClipsNotifier Function() create, + ) { + return _NotesClipsNotifierProvider._internal( + () => create()..noteId = noteId, + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + noteId: noteId, + ); + } + + @override + bool operator ==(Object other) { + return other is _NotesClipsNotifierProvider && other.noteId == noteId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, noteId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin _NotesClipsNotifierRef + on AutoDisposeAsyncNotifierProviderRef> { + /// The parameter `noteId` of this provider. + String get noteId; +} + +class _NotesClipsNotifierProviderElement + extends AutoDisposeAsyncNotifierProviderElement<_NotesClipsNotifier, + List> with _NotesClipsNotifierRef { + _NotesClipsNotifierProviderElement(super.provider); + + @override + String get noteId => (origin as _NotesClipsNotifierProvider).noteId; +} + +String _$clipModalSheetNotifierHash() => + r'978ab378797102eac12dd3714bf742a671d769ad'; + +abstract class _$ClipModalSheetNotifier + extends BuildlessAutoDisposeAsyncNotifier> { + late final String noteId; + + FutureOr> build( + String noteId, + ); +} + +/// See also [_ClipModalSheetNotifier]. +@ProviderFor(_ClipModalSheetNotifier) +const _clipModalSheetNotifierProvider = _ClipModalSheetNotifierFamily(); + +/// See also [_ClipModalSheetNotifier]. +class _ClipModalSheetNotifierFamily extends Family { + /// See also [_ClipModalSheetNotifier]. + const _ClipModalSheetNotifierFamily(); + + static final Iterable _dependencies = [ + clipsNotifierProvider, + _notesClipsNotifierProvider, + misskeyPostContextProvider + ]; + + static final Iterable _allTransitiveDependencies = + { + clipsNotifierProvider, + ...?clipsNotifierProvider.allTransitiveDependencies, + _notesClipsNotifierProvider, + ...?_notesClipsNotifierProvider.allTransitiveDependencies, + misskeyPostContextProvider, + ...?misskeyPostContextProvider.allTransitiveDependencies + }; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'_clipModalSheetNotifierProvider'; + + /// See also [_ClipModalSheetNotifier]. + _ClipModalSheetNotifierProvider call( + String noteId, + ) { + return _ClipModalSheetNotifierProvider( + noteId, + ); + } + + @visibleForOverriding + @override + _ClipModalSheetNotifierProvider getProviderOverride( + covariant _ClipModalSheetNotifierProvider provider, + ) { + return call( + provider.noteId, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(_ClipModalSheetNotifier Function() create) { + return _$ClipModalSheetNotifierFamilyOverride(this, create); + } +} + +class _$ClipModalSheetNotifierFamilyOverride implements FamilyOverride { + _$ClipModalSheetNotifierFamilyOverride(this.overriddenFamily, this.create); + + final _ClipModalSheetNotifier Function() create; + + @override + final _ClipModalSheetNotifierFamily overriddenFamily; + + @override + _ClipModalSheetNotifierProvider getProviderOverride( + covariant _ClipModalSheetNotifierProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [_ClipModalSheetNotifier]. +class _ClipModalSheetNotifierProvider + extends AutoDisposeAsyncNotifierProviderImpl<_ClipModalSheetNotifier, + List<(Clip, bool)>> { + /// See also [_ClipModalSheetNotifier]. + _ClipModalSheetNotifierProvider( + String noteId, + ) : this._internal( + () => _ClipModalSheetNotifier()..noteId = noteId, + from: _clipModalSheetNotifierProvider, + name: r'_clipModalSheetNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$clipModalSheetNotifierHash, + dependencies: _ClipModalSheetNotifierFamily._dependencies, + allTransitiveDependencies: + _ClipModalSheetNotifierFamily._allTransitiveDependencies, + noteId: noteId, + ); + + _ClipModalSheetNotifierProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.noteId, + }) : super.internal(); + + final String noteId; + + @override + FutureOr> runNotifierBuild( + covariant _ClipModalSheetNotifier notifier, + ) { + return notifier.build( + noteId, + ); + } + + @override + Override overrideWith(_ClipModalSheetNotifier Function() create) { + return ProviderOverride( + origin: this, + override: _ClipModalSheetNotifierProvider._internal( + () => create()..noteId = noteId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + noteId: noteId, + ), + ); + } + + @override + (String,) get argument { + return (noteId,); + } + + @override + AutoDisposeAsyncNotifierProviderElement<_ClipModalSheetNotifier, + List<(Clip, bool)>> createElement() { + return _ClipModalSheetNotifierProviderElement(this); + } + + _ClipModalSheetNotifierProvider _copyWith( + _ClipModalSheetNotifier Function() create, + ) { + return _ClipModalSheetNotifierProvider._internal( + () => create()..noteId = noteId, + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + noteId: noteId, + ); + } + + @override + bool operator ==(Object other) { + return other is _ClipModalSheetNotifierProvider && other.noteId == noteId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, noteId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin _ClipModalSheetNotifierRef + on AutoDisposeAsyncNotifierProviderRef> { + /// The parameter `noteId` of this provider. + String get noteId; +} + +class _ClipModalSheetNotifierProviderElement + extends AutoDisposeAsyncNotifierProviderElement<_ClipModalSheetNotifier, + List<(Clip, bool)>> with _ClipModalSheetNotifierRef { + _ClipModalSheetNotifierProviderElement(super.provider); + + @override + String get noteId => (origin as _ClipModalSheetNotifierProvider).noteId; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/common/account_scope.dart b/lib/view/common/account_scope.dart index 570d0c55a..92dbccceb 100644 --- a/lib/view/common/account_scope.dart +++ b/lib/view/common/account_scope.dart @@ -1,25 +1,31 @@ -import 'package:flutter/widgets.dart'; -import 'package:miria/model/account.dart'; +import "package:flutter/widgets.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; -class AccountScope extends InheritedWidget { - final Account account; +class AccountContextScope extends ConsumerWidget { + final AccountContext context; + final Widget child; - const AccountScope({ + const AccountContextScope({ + required this.context, + required this.child, super.key, - required this.account, - required super.child, }); - static Account of(BuildContext context) { - final account = context.dependOnInheritedWidgetOfExactType(); - if (account == null) { - throw Exception("has not ancestor"); - } - - return account.account; - } + factory AccountContextScope.as({ + required Account account, + required Widget child, + }) => + AccountContextScope(context: AccountContext.as(account), child: child); @override - bool updateShouldNotify(covariant AccountScope oldWidget) => - account != oldWidget.account; + Widget build(BuildContext context, WidgetRef ref) { + return ProviderScope( + overrides: [ + accountContextProvider.overrideWithValue(this.context), + ], + child: child, + ); + } } diff --git a/lib/view/common/account_select_dialog.dart b/lib/view/common/account_select_dialog.dart index 3b02ad9d7..35bf2f9eb 100644 --- a/lib/view/common/account_select_dialog.dart +++ b/lib/view/common/account_select_dialog.dart @@ -1,48 +1,77 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/avatar_icon.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/hooks/use_async.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/avatar_icon.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; -class AccountSelectDialog extends ConsumerWidget { - const AccountSelectDialog({super.key, this.host}); +@RoutePage() +class AccountSelectDialog extends HookConsumerWidget { + const AccountSelectDialog({super.key, this.host, this.remoteHost}); /// nullではないとき, 指定されたサーバーのアカウントのみ表示する final String? host; + /// 相手先のホスト + final String? remoteHost; + @override Widget build(BuildContext context, WidgetRef ref) { final accounts = ref.watch(accountsProvider); + + final navigateAsRemote = useHandledFuture(() async { + final remoteHost = this.remoteHost; + if (remoteHost == null) return; + final meta = + await ref.read(misskeyWithoutAccountProvider(remoteHost)).meta(); + + await ref + .read(appRouterProvider) + .maybePop(Account.demoAccount(remoteHost, meta)); + }); + return AlertDialog( title: Text(S.of(context).openAsOtherAccount), content: SizedBox( width: MediaQuery.of(context).size.width * 0.8, height: MediaQuery.of(context).size.height * 0.8, child: ListView( - children: accounts - .where((account) => host == null || account.host == host) - .map( - (account) => AccountScope( - account: account, - child: ListTile( - leading: AvatarIcon(user: account.i), - title: SimpleMfmText( - account.i.name ?? account.i.username, - style: Theme.of(context).textTheme.titleMedium, - ), - subtitle: Text( - account.acct.toString(), - style: Theme.of(context).textTheme.bodySmall, - ), - onTap: () { - Navigator.of(context).pop(account); - }, + children: [ + if (remoteHost != null) + switch (navigateAsRemote.value) { + AsyncLoading() => const Center( + child: CircularProgressIndicator.adaptive(), + ), + _ => ListTile( + leading: const Icon(Icons.language), + title: const Text("相手先のサーバー(ログインなし)"), + onTap: navigateAsRemote.executeOrNull, + ), + }, + for (final account in accounts + .where((account) => host == null || account.host == host)) + AccountContextScope.as( + account: account, + child: ListTile( + leading: AvatarIcon(user: account.i), + title: SimpleMfmText( + account.i.name ?? account.i.username, + style: Theme.of(context).textTheme.titleMedium, + ), + subtitle: Text( + account.acct.toString(), + style: Theme.of(context).textTheme.bodySmall, ), + onTap: () { + Navigator.of(context).pop(account); + }, ), - ) - .toList(), + ), + ], ), ), ); diff --git a/lib/view/common/avatar_icon.dart b/lib/view/common/avatar_icon.dart index 78d7f50cb..3bfdbd84d 100644 --- a/lib/view/common/avatar_icon.dart +++ b/lib/view/common/avatar_icon.dart @@ -1,34 +1,29 @@ -import 'dart:math'; +import "dart:math"; -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/network_image.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class AvatarIcon extends StatefulWidget { +class AvatarIcon extends HookConsumerWidget { final User user; final double height; final VoidCallback? onTap; const AvatarIcon({ - super.key, required this.user, + super.key, this.height = 48, this.onTap, }); - @override - State createState() => AvatarIconState(); -} - -class AvatarIconState extends State { - Color? catEarColor; - Color? averageColor() { // https://github.com/woltapp/blurhash/blob/master/Algorithm.md - final blurhash = widget.user.avatarBlurhash; + final blurhash = user.avatarBlurhash; if (blurhash == null) { return null; } @@ -36,7 +31,7 @@ class AvatarIconState extends State { .substring(2, 6) .split("") .map( - r'0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~' + r"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz#$%*+,-.:;=?@[]^_{|}~" .indexOf, ) .fold(0, (acc, i) => acc * 83 + i); @@ -44,24 +39,18 @@ class AvatarIconState extends State { } @override - void initState() { - super.initState(); - - catEarColor = (widget.user.isCat ? averageColor() : null); - } - - @override - Widget build(BuildContext context) { - final baseHeight = MediaQuery.textScalerOf(context).scale(widget.height); + Widget build(BuildContext context, WidgetRef ref) { + final baseHeight = MediaQuery.textScalerOf(context).scale(height); + final catEarColor = useMemoized(() => user.isCat ? averageColor() : null); return GestureDetector( - onTap: widget.onTap ?? - () { - context.pushRoute( - UserRoute( - userId: widget.user.id, account: AccountScope.of(context)), - ); - }, + onTap: onTap ?? + () async => context.pushRoute( + UserRoute( + userId: user.id, + accountContext: ref.read(accountContextProvider), + ), + ), child: Padding( padding: EdgeInsets.only( top: 3, @@ -70,7 +59,7 @@ class AvatarIconState extends State { ), child: Stack( children: [ - if (widget.user.isCat) + if (user.isCat) Positioned( left: 0, top: 0, @@ -91,7 +80,7 @@ class AvatarIconState extends State { ), ), ), - if (widget.user.isCat) + if (user.isCat) Positioned( left: 0, top: 0, @@ -119,12 +108,12 @@ class AvatarIconState extends State { height: baseHeight, child: NetworkImageView( fit: BoxFit.cover, - url: widget.user.avatarUrl.toString(), + url: user.avatarUrl.toString(), type: ImageType.avatarIcon, ), ), ), - for (final decoration in widget.user.avatarDecorations) + for (final decoration in user.avatarDecorations) Transform.scale( scaleX: 2, scaleY: 2, @@ -142,14 +131,18 @@ class AvatarIconState extends State { child: SizedBox( width: baseHeight, child: NetworkImageView( - url: decoration.url, type: ImageType.other), + url: decoration.url, + type: ImageType.other, + ), ), ) : SizedBox( width: baseHeight, child: NetworkImageView( - url: decoration.url, - type: ImageType.avatarDecoration)), + url: decoration.url, + type: ImageType.avatarDecoration, + ), + ), ), ), ), diff --git a/lib/view/common/clip_item.dart b/lib/view/common/clip_item.dart index c4677402a..484e2b5f2 100644 --- a/lib/view/common/clip_item.dart +++ b/lib/view/common/clip_item.dart @@ -1,25 +1,30 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class ClipItem extends StatelessWidget { +class ClipItem extends ConsumerWidget { final Clip clip; final Widget? trailing; const ClipItem({ - super.key, required this.clip, + super.key, this.trailing, }); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return ListTile( - onTap: () => context.pushRoute( - ClipDetailRoute(account: AccountScope.of(context), id: clip.id)), + onTap: () async => context.pushRoute( + ClipDetailRoute( + accountContext: ref.read(accountContextProvider), + id: clip.id, + ), + ), title: Text(clip.name ?? ""), subtitle: SimpleMfmText(clip.description ?? ""), trailing: trailing, diff --git a/lib/view/common/color_picker_dialog.dart b/lib/view/common/color_picker_dialog.dart index 69451b105..2e2cb4ef4 100644 --- a/lib/view/common/color_picker_dialog.dart +++ b/lib/view/common/color_picker_dialog.dart @@ -1,31 +1,28 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_colorpicker/flutter_colorpicker.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_colorpicker/flutter_colorpicker.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; -class ColorPickerDialog extends StatefulWidget { +@RoutePage() +class ColorPickerDialog extends HookWidget { const ColorPickerDialog({super.key}); - @override - State createState() => ColorPickerDialogState(); -} - -class ColorPickerDialogState extends State { - Color pickedColor = const Color.fromRGBO(134, 179, 0, 1.0); - @override Widget build(BuildContext context) { + final pickedColor = useState(const Color.fromRGBO(134, 179, 0, 1.0)); return AlertDialog( - title: Text(S.of(context).pickColor), - content: ColorPicker( - pickerColor: pickedColor, - onColorChanged: (color) => setState(() => pickedColor = color), + title: Text(S.of(context).pickColor), + content: ColorPicker( + pickerColor: pickedColor.value, + onColorChanged: (color) => pickedColor.value = color, + ), + actions: [ + TextButton( + onPressed: () async => context.maybePop(pickedColor), + child: Text(S.of(context).decideColor), ), - actions: [ - TextButton( - onPressed: () { - Navigator.of(context).pop(pickedColor); - }, - child: Text(S.of(context).decideColor)) - ]); + ], + ); } } diff --git a/lib/view/common/common_drawer.dart b/lib/view/common/common_drawer.dart index 78816912c..cfbdd9c4c 100644 --- a/lib/view/common/common_drawer.dart +++ b/lib/view/common/common_drawer.dart @@ -1,18 +1,30 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/model/acct.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/avatar_icon.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/acct.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/avatar_icon.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; class CommonDrawer extends ConsumerWidget { final Acct initialOpenAcct; + final bool allOpen; - const CommonDrawer({super.key, required this.initialOpenAcct}); + const CommonDrawer({ + required this.initialOpenAcct, + this.allOpen = false, + super.key, + }); + + void closeDrawer(BuildContext context) { + final state = Scaffold.of(context); + if (state.isDrawerOpen) { + state.closeDrawer(); + } + } @override Widget build(BuildContext context, WidgetRef ref) { @@ -24,11 +36,11 @@ class CommonDrawer extends ConsumerWidget { child: ListView( children: [ for (final account in accounts) ...[ - AccountScope( + AccountContextScope.as( account: account, child: ExpansionTile( leading: AvatarIcon(user: account.i), - initiallyExpanded: account.acct == initialOpenAcct, + initiallyExpanded: account.acct == initialOpenAcct || allOpen, title: SimpleMfmText( account.i.name ?? account.i.username, style: Theme.of(context).textTheme.titleMedium, @@ -41,82 +53,121 @@ class CommonDrawer extends ConsumerWidget { ListTile( leading: const Icon(Icons.notifications), title: Text(S.of(context).notification), - onTap: () { - Navigator.of(context).pop(); - context.pushRoute(NotificationRoute(account: account)); + onTap: () async { + closeDrawer(context); + await context.pushRoute( + NotificationRoute( + accountContext: AccountContext.as(account), + ), + ); }, ), ListTile( leading: const Icon(Icons.star), title: Text(S.of(context).favorite), - onTap: () { - Navigator.of(context).pop(); - context.pushRoute(FavoritedNoteRoute(account: account)); + onTap: () async { + closeDrawer(context); + await context.pushRoute( + FavoritedNoteRoute( + accountContext: AccountContext.as(account), + ), + ); }, ), ListTile( leading: const Icon(Icons.list), title: Text(S.of(context).list), - onTap: () { - Navigator.of(context).pop(); - context.pushRoute(UsersListRoute(account: account)); + onTap: () async { + closeDrawer(context); + await context.pushRoute( + UsersListRoute( + accountContext: AccountContext.as(account), + ), + ); }, ), ListTile( leading: const Icon(Icons.settings_input_antenna), title: Text(S.of(context).antenna), - onTap: () { - Navigator.of(context).pop(); - context.pushRoute(AntennaRoute(account: account)); + onTap: () async { + closeDrawer(context); + await context.pushRoute( + AntennaRoute( + accountContext: AccountContext.as(account), + ), + ); }, ), ListTile( leading: const Icon(Icons.attach_file), title: Text(S.of(context).clip), - onTap: () { - Navigator.of(context).pop(); - context.pushRoute(ClipListRoute(account: account)); + onTap: () async { + closeDrawer(context); + await context.pushRoute( + ClipListRoute( + accountContext: AccountContext.as(account), + ), + ); }, ), ListTile( leading: const Icon(Icons.tv), title: Text(S.of(context).channel), - onTap: () { - Navigator.of(context).pop(); - context.pushRoute(ChannelsRoute(account: account)); + onTap: () async { + closeDrawer(context); + await context.pushRoute( + ChannelsRoute( + accountContext: AccountContext.as(account), + ), + ); }, ), ListTile( leading: const Icon(Icons.search), title: Text(S.of(context).search), - onTap: () { - Navigator.of(context).pop(); - context.pushRoute(SearchRoute(account: account)); + onTap: () async { + closeDrawer(context); + await context.pushRoute( + SearchRoute( + accountContext: AccountContext.as(account), + ), + ); }, ), ListTile( leading: const Icon(Icons.tag), title: Text(S.of(context).explore), - onTap: () { - Navigator.of(context).pop(); - context.pushRoute(ExploreRoute(account: account)); + onTap: () async { + closeDrawer(context); + await context.pushRoute( + ExploreRoute( + accountContext: AccountContext.as(account), + ), + ); }, ), ListTile( leading: const Icon(Icons.gamepad), title: Text(S.of(context).misskeyGames), - onTap: () { - Navigator.of(context).pop(); - context.pushRoute(MisskeyGamesRoute(account: account)); + onTap: () async { + closeDrawer(context); + await context.pushRoute( + MisskeyGamesRoute( + accountContext: AccountContext.as(account), + ), + ); }, ), ListTile( leading: const Icon(Icons.settings), - title: Text(S.of(context).accountSetting( - account.i.name ?? account.i.username)), - onTap: () { - Navigator.of(context).pop(); - context.pushRoute( + title: Text( + S.of(context).accountSetting( + account.i.name ?? account.i.username, + ), + ), + onTap: () async { + closeDrawer(context); + await context.pushRoute( SeveralAccountSettingsRoute(account: account), ); }, @@ -128,9 +179,9 @@ class CommonDrawer extends ConsumerWidget { ListTile( leading: const Icon(Icons.settings), title: Text(S.of(context).settings), - onTap: () { - Navigator.of(context).pop(); - context.pushRoute(const SettingsRoute()); + onTap: () async { + closeDrawer(context); + await context.pushRoute(const SettingsRoute()); }, ), ], diff --git a/lib/view/common/constants.dart b/lib/view/common/constants.dart index 8051a71e0..407ed5037 100644 --- a/lib/view/common/constants.dart +++ b/lib/view/common/constants.dart @@ -1,7 +1,7 @@ -import 'package:intl/intl.dart'; +import "package:intl/intl.dart"; Pattern get availableServerVersion => RegExp(r"^(1[3-9]\.|20[2-9][0-9])"); -RegExp get htmlTagRemove => RegExp(r"""<("[^"]*"|'[^']*'|[^'">])*>"""); +RegExp get htmlTagRemove => RegExp("""<("[^"]*"|'[^']*'|[^'">])*>"""); final intFormatter = NumberFormat("#,###"); diff --git a/lib/view/common/date_time_picker.dart b/lib/view/common/date_time_picker.dart index 402ff91ac..7b3557bf6 100644 --- a/lib/view/common/date_time_picker.dart +++ b/lib/view/common/date_time_picker.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import "package:flutter/material.dart"; Future showDateTimePicker({ required BuildContext context, diff --git a/lib/view/common/dialog/dialog_scope.dart b/lib/view/common/dialog/dialog_scope.dart new file mode 100644 index 000000000..852e6b8ae --- /dev/null +++ b/lib/view/common/dialog/dialog_scope.dart @@ -0,0 +1,59 @@ +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:mfm/mfm.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; + +class DialogScope extends ConsumerWidget { + final Widget child; + + const DialogScope({ + required this.child, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final dialogs = ref.watch( + dialogStateNotifierProvider.select((value) => value.dialogs), + ); + + return Stack( + children: [ + child, + if (dialogs.isNotEmpty) + const Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration(color: Color.fromRGBO(0, 0, 0, .3)), + child: SizedBox.shrink(), + ), + ), + for (final dialog in dialogs) + PopScope( + onPopInvoked: (didPop) async => ref + .read(dialogStateNotifierProvider.notifier) + .completeDialog(dialog, null), + child: AlertDialog.adaptive( + content: dialog.isMFM + ? AccountContextScope( + context: dialog.accountContext!, + child: Mfm( + mfmText: dialog.message(context), + ), + ) + : Text(dialog.message(context)), + actions: [ + for (final action in dialog.actions(context).indexed) + TextButton( + onPressed: () => ref + .read(dialogStateNotifierProvider.notifier) + .completeDialog(dialog, action.$1), + child: Text(action.$2), + ), + ], + ), + ), + ], + ); + } +} diff --git a/lib/view/common/dialog/dialog_state.dart b/lib/view/common/dialog/dialog_state.dart new file mode 100644 index 000000000..2203657c0 --- /dev/null +++ b/lib/view/common/dialog/dialog_state.dart @@ -0,0 +1,110 @@ +import "dart:async"; + +import "package:dio/dio.dart"; +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/error_dialog_handler.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +part "dialog_state.freezed.dart"; +part "dialog_state.g.dart"; + +@freezed +class DialogsState with _$DialogsState { + factory DialogsState({ + @Default([]) List dialogs, + }) = _DialogsState; +} + +@freezed +class DialogData with _$DialogData { + factory DialogData({ + required String Function(BuildContext context) message, + required List Function(BuildContext context) actions, + required Completer completer, + @Assert( + "!isMFM || isMFM && accountContext != null", + "account context must not be null when isMFM is true", + ) + AccountContext? accountContext, + @Default(false) bool isMFM, + }) = _DialogData; +} + +@riverpod +class DialogStateNotifier extends _$DialogStateNotifier { + @override + DialogsState build() => DialogsState(); + + Future showDialog({ + required String Function(BuildContext context) message, + required List Function(BuildContext context) actions, + bool isMFM = false, + AccountContext? accountContext, + }) { + final completer = Completer(); + state = state.copyWith( + dialogs: [ + ...state.dialogs, + DialogData( + message: message, + actions: actions, + completer: completer, + isMFM: isMFM, + accountContext: accountContext, + ), + ], + ); + return completer.future; + } + + Future showSimpleDialog({ + required String Function(BuildContext context) message, + }) => + showDialog(message: message, actions: (context) => [S.of(context).done]); + + void completeDialog(DialogData dialog, int? button) { + dialog.completer.complete(button); + state = state.copyWith( + dialogs: state.dialogs.where((element) => element != dialog).toList(), + ); + } + + Future> guard(Future Function() future) async { + final result = await AsyncValue.guard(future); + if (result is AsyncError) { + if (kDebugMode) { + print(result.error); + print(result.stackTrace); + } + await showSimpleDialog( + message: _handleError(result.error, result.stackTrace), + ); + } + return result; + } +} + +String Function(BuildContext context) _handleError( + Object? error, + StackTrace? trace, +) { + if (error is Exception) { + if (error is DioException) { + return (context) => + "${S.of(context).thrownError}\n${error.type} [${error.response?.statusCode ?? "---"}] ${error.response?.data ?? ""}"; + } else if (error is SpecifiedException) { + return (context) => error.message; + } + return (context) => "${S.of(context).thrownError}\n$error"; + } + + if (error is Error) { + return (context) => "${S.of(context).thrownError}\n$error"; + } + + return (context) => "${S.of(context).thrownError}\n$error"; +} diff --git a/lib/view/common/dialog/dialog_state.freezed.dart b/lib/view/common/dialog/dialog_state.freezed.dart new file mode 100644 index 000000000..b2fa37ec5 --- /dev/null +++ b/lib/view/common/dialog/dialog_state.freezed.dart @@ -0,0 +1,422 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'dialog_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$DialogsState { + List get dialogs => throw _privateConstructorUsedError; + + /// Create a copy of DialogsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $DialogsStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DialogsStateCopyWith<$Res> { + factory $DialogsStateCopyWith( + DialogsState value, $Res Function(DialogsState) then) = + _$DialogsStateCopyWithImpl<$Res, DialogsState>; + @useResult + $Res call({List dialogs}); +} + +/// @nodoc +class _$DialogsStateCopyWithImpl<$Res, $Val extends DialogsState> + implements $DialogsStateCopyWith<$Res> { + _$DialogsStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of DialogsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? dialogs = null, + }) { + return _then(_value.copyWith( + dialogs: null == dialogs + ? _value.dialogs + : dialogs // ignore: cast_nullable_to_non_nullable + as List, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$DialogsStateImplCopyWith<$Res> + implements $DialogsStateCopyWith<$Res> { + factory _$$DialogsStateImplCopyWith( + _$DialogsStateImpl value, $Res Function(_$DialogsStateImpl) then) = + __$$DialogsStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({List dialogs}); +} + +/// @nodoc +class __$$DialogsStateImplCopyWithImpl<$Res> + extends _$DialogsStateCopyWithImpl<$Res, _$DialogsStateImpl> + implements _$$DialogsStateImplCopyWith<$Res> { + __$$DialogsStateImplCopyWithImpl( + _$DialogsStateImpl _value, $Res Function(_$DialogsStateImpl) _then) + : super(_value, _then); + + /// Create a copy of DialogsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? dialogs = null, + }) { + return _then(_$DialogsStateImpl( + dialogs: null == dialogs + ? _value._dialogs + : dialogs // ignore: cast_nullable_to_non_nullable + as List, + )); + } +} + +/// @nodoc + +class _$DialogsStateImpl with DiagnosticableTreeMixin implements _DialogsState { + _$DialogsStateImpl({final List dialogs = const []}) + : _dialogs = dialogs; + + final List _dialogs; + @override + @JsonKey() + List get dialogs { + if (_dialogs is EqualUnmodifiableListView) return _dialogs; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_dialogs); + } + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'DialogsState(dialogs: $dialogs)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'DialogsState')) + ..add(DiagnosticsProperty('dialogs', dialogs)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$DialogsStateImpl && + const DeepCollectionEquality().equals(other._dialogs, _dialogs)); + } + + @override + int get hashCode => + Object.hash(runtimeType, const DeepCollectionEquality().hash(_dialogs)); + + /// Create a copy of DialogsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$DialogsStateImplCopyWith<_$DialogsStateImpl> get copyWith => + __$$DialogsStateImplCopyWithImpl<_$DialogsStateImpl>(this, _$identity); +} + +abstract class _DialogsState implements DialogsState { + factory _DialogsState({final List dialogs}) = _$DialogsStateImpl; + + @override + List get dialogs; + + /// Create a copy of DialogsState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$DialogsStateImplCopyWith<_$DialogsStateImpl> get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +mixin _$DialogData { + String Function(BuildContext) get message => + throw _privateConstructorUsedError; + List Function(BuildContext) get actions => + throw _privateConstructorUsedError; + Completer get completer => throw _privateConstructorUsedError; + @Assert("!isMFM || isMFM && accountContext != null", + "account context must not be null when isMFM is true") + AccountContext? get accountContext => throw _privateConstructorUsedError; + bool get isMFM => throw _privateConstructorUsedError; + + /// Create a copy of DialogData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $DialogDataCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $DialogDataCopyWith<$Res> { + factory $DialogDataCopyWith( + DialogData value, $Res Function(DialogData) then) = + _$DialogDataCopyWithImpl<$Res, DialogData>; + @useResult + $Res call( + {String Function(BuildContext) message, + List Function(BuildContext) actions, + Completer completer, + @Assert("!isMFM || isMFM && accountContext != null", + "account context must not be null when isMFM is true") + AccountContext? accountContext, + bool isMFM}); + + $AccountContextCopyWith<$Res>? get accountContext; +} + +/// @nodoc +class _$DialogDataCopyWithImpl<$Res, $Val extends DialogData> + implements $DialogDataCopyWith<$Res> { + _$DialogDataCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of DialogData + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? message = null, + Object? actions = null, + Object? completer = null, + Object? accountContext = freezed, + Object? isMFM = null, + }) { + return _then(_value.copyWith( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String Function(BuildContext), + actions: null == actions + ? _value.actions + : actions // ignore: cast_nullable_to_non_nullable + as List Function(BuildContext), + completer: null == completer + ? _value.completer + : completer // ignore: cast_nullable_to_non_nullable + as Completer, + accountContext: freezed == accountContext + ? _value.accountContext + : accountContext // ignore: cast_nullable_to_non_nullable + as AccountContext?, + isMFM: null == isMFM + ? _value.isMFM + : isMFM // ignore: cast_nullable_to_non_nullable + as bool, + ) as $Val); + } + + /// Create a copy of DialogData + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $AccountContextCopyWith<$Res>? get accountContext { + if (_value.accountContext == null) { + return null; + } + + return $AccountContextCopyWith<$Res>(_value.accountContext!, (value) { + return _then(_value.copyWith(accountContext: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$DialogDataImplCopyWith<$Res> + implements $DialogDataCopyWith<$Res> { + factory _$$DialogDataImplCopyWith( + _$DialogDataImpl value, $Res Function(_$DialogDataImpl) then) = + __$$DialogDataImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String Function(BuildContext) message, + List Function(BuildContext) actions, + Completer completer, + @Assert("!isMFM || isMFM && accountContext != null", + "account context must not be null when isMFM is true") + AccountContext? accountContext, + bool isMFM}); + + @override + $AccountContextCopyWith<$Res>? get accountContext; +} + +/// @nodoc +class __$$DialogDataImplCopyWithImpl<$Res> + extends _$DialogDataCopyWithImpl<$Res, _$DialogDataImpl> + implements _$$DialogDataImplCopyWith<$Res> { + __$$DialogDataImplCopyWithImpl( + _$DialogDataImpl _value, $Res Function(_$DialogDataImpl) _then) + : super(_value, _then); + + /// Create a copy of DialogData + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? message = null, + Object? actions = null, + Object? completer = null, + Object? accountContext = freezed, + Object? isMFM = null, + }) { + return _then(_$DialogDataImpl( + message: null == message + ? _value.message + : message // ignore: cast_nullable_to_non_nullable + as String Function(BuildContext), + actions: null == actions + ? _value.actions + : actions // ignore: cast_nullable_to_non_nullable + as List Function(BuildContext), + completer: null == completer + ? _value.completer + : completer // ignore: cast_nullable_to_non_nullable + as Completer, + accountContext: freezed == accountContext + ? _value.accountContext + : accountContext // ignore: cast_nullable_to_non_nullable + as AccountContext?, + isMFM: null == isMFM + ? _value.isMFM + : isMFM // ignore: cast_nullable_to_non_nullable + as bool, + )); + } +} + +/// @nodoc + +class _$DialogDataImpl with DiagnosticableTreeMixin implements _DialogData { + _$DialogDataImpl( + {required this.message, + required this.actions, + required this.completer, + @Assert("!isMFM || isMFM && accountContext != null", + "account context must not be null when isMFM is true") + this.accountContext, + this.isMFM = false}); + + @override + final String Function(BuildContext) message; + @override + final List Function(BuildContext) actions; + @override + final Completer completer; + @override + @Assert("!isMFM || isMFM && accountContext != null", + "account context must not be null when isMFM is true") + final AccountContext? accountContext; + @override + @JsonKey() + final bool isMFM; + + @override + String toString({DiagnosticLevel minLevel = DiagnosticLevel.info}) { + return 'DialogData(message: $message, actions: $actions, completer: $completer, accountContext: $accountContext, isMFM: $isMFM)'; + } + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DiagnosticsProperty('type', 'DialogData')) + ..add(DiagnosticsProperty('message', message)) + ..add(DiagnosticsProperty('actions', actions)) + ..add(DiagnosticsProperty('completer', completer)) + ..add(DiagnosticsProperty('accountContext', accountContext)) + ..add(DiagnosticsProperty('isMFM', isMFM)); + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$DialogDataImpl && + (identical(other.message, message) || other.message == message) && + (identical(other.actions, actions) || other.actions == actions) && + (identical(other.completer, completer) || + other.completer == completer) && + (identical(other.accountContext, accountContext) || + other.accountContext == accountContext) && + (identical(other.isMFM, isMFM) || other.isMFM == isMFM)); + } + + @override + int get hashCode => Object.hash( + runtimeType, message, actions, completer, accountContext, isMFM); + + /// Create a copy of DialogData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$DialogDataImplCopyWith<_$DialogDataImpl> get copyWith => + __$$DialogDataImplCopyWithImpl<_$DialogDataImpl>(this, _$identity); +} + +abstract class _DialogData implements DialogData { + factory _DialogData( + {required final String Function(BuildContext) message, + required final List Function(BuildContext) actions, + required final Completer completer, + @Assert("!isMFM || isMFM && accountContext != null", + "account context must not be null when isMFM is true") + final AccountContext? accountContext, + final bool isMFM}) = _$DialogDataImpl; + + @override + String Function(BuildContext) get message; + @override + List Function(BuildContext) get actions; + @override + Completer get completer; + @override + @Assert("!isMFM || isMFM && accountContext != null", + "account context must not be null when isMFM is true") + AccountContext? get accountContext; + @override + bool get isMFM; + + /// Create a copy of DialogData + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$DialogDataImplCopyWith<_$DialogDataImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/view/common/dialog/dialog_state.g.dart b/lib/view/common/dialog/dialog_state.g.dart new file mode 100644 index 000000000..c269b8893 --- /dev/null +++ b/lib/view/common/dialog/dialog_state.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'dialog_state.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$dialogStateNotifierHash() => + r'343762a93c3daf2931bda3d287046cc566cda24e'; + +/// See also [DialogStateNotifier]. +@ProviderFor(DialogStateNotifier) +final dialogStateNotifierProvider = + AutoDisposeNotifierProvider.internal( + DialogStateNotifier.new, + name: r'dialogStateNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$dialogStateNotifierHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$DialogStateNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/common/error_detail.dart b/lib/view/common/error_detail.dart index 1ef407178..d613759d6 100644 --- a/lib/view/common/error_detail.dart +++ b/lib/view/common/error_detail.dart @@ -1,30 +1,30 @@ -import 'dart:async'; -import 'dart:io'; +import "dart:async"; +import "dart:io"; -import 'package:dio/dio.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:dio/dio.dart"; +import "package:flutter/cupertino.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; class ErrorDetail extends StatelessWidget { final Object? error; final StackTrace? stackTrace; const ErrorDetail({ - super.key, required this.error, required this.stackTrace, + super.key, }); @override Widget build(BuildContext context) { final e = error; - if (e is DioError) { + if (e is DioException) { final response = e.response; - if (e.type == DioErrorType.connectionError) { + if (e.type == DioExceptionType.connectionError) { return Text(S.of(context).thrownConnectionError); - } else if (e.type == DioErrorType.connectionTimeout || - e.type == DioErrorType.receiveTimeout || - e.type == DioErrorType.sendTimeout) { + } else if (e.type == DioExceptionType.connectionTimeout || + e.type == DioExceptionType.receiveTimeout || + e.type == DioExceptionType.sendTimeout) { return Text(S.of(context).thrownConnectionTimeout); } else if (response != null) { return Text("[${response.statusCode}] ${response.data}"); diff --git a/lib/view/common/error_dialog_handler.dart b/lib/view/common/error_dialog_handler.dart index 48393dd0b..7c3d21143 100644 --- a/lib/view/common/error_dialog_handler.dart +++ b/lib/view/common/error_dialog_handler.dart @@ -1,6 +1,6 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; +import "package:flutter/cupertino.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; //TODO: 微妙な方法 class SpecifiedException implements Exception { @@ -9,6 +9,7 @@ class SpecifiedException implements Exception { } extension FutureExtension on Future { + @Deprecated("use `dialogStateNotifier`") Future expectFailure(BuildContext context) { return catchError((e) { final widgetRef = ProviderScope.containerOf(context, listen: false); @@ -19,6 +20,7 @@ extension FutureExtension on Future { } extension FutureFunctionExtension on Future Function() { + @Deprecated("use `dialogStateNotifier`") Future Function() expectFailure(BuildContext context) { return () => this.call().catchError((e) { final widgetRef = ProviderScope.containerOf(context, listen: false); diff --git a/lib/view/common/error_dialog_listener.dart b/lib/view/common/error_dialog_listener.dart index 76204e8fe..b4a0a27ed 100644 --- a/lib/view/common/error_dialog_listener.dart +++ b/lib/view/common/error_dialog_listener.dart @@ -1,30 +1,30 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/repository/account_repository.dart'; -import 'package:miria/state_notifier/common/misskey_notes/misskey_note_notifier.dart'; -import 'package:miria/state_notifier/note_create_page/note_create_state_notifier.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/dialogs/simple_message_dialog.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:dio/dio.dart"; +import "package:flutter/cupertino.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/account_repository.dart"; +import "package:miria/view/common/error_dialog_handler.dart"; +import "package:miria/view/dialogs/simple_message_dialog.dart"; class ErrorDialogListener extends ConsumerWidget { final Widget child; - const ErrorDialogListener({super.key, required this.child}); + const ErrorDialogListener({required this.child, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - ref.listen(errorEventProvider, (_, next) { + ref.listen(errorEventProvider, (_, next) async { final error = next.$1; if (error == null) return; if (error is Exception) { - if (error is DioError) { - SimpleMessageDialog.show(next.$2!, - "${S.of(context).thrownError}\n${error.type} [${error.response?.statusCode ?? "---"}] ${error.response?.data ?? ""}"); + if (error is DioException) { + await SimpleMessageDialog.show( + next.$2!, + "${S.of(context).thrownError}\n${error.type} [${error.response?.statusCode ?? "---"}] ${error.response?.data ?? ""}", + ); } else if (error is SpecifiedException) { - SimpleMessageDialog.show(next.$2!, error.message); + await SimpleMessageDialog.show(next.$2!, error.message); } else if (error is ValidateMisskeyException) { final message = switch (error) { InvalidServerException(:final server) => @@ -38,29 +38,18 @@ class ErrorDialogListener extends ConsumerWidget { AlreadyLoggedInException(:final acct) => S.of(context).alreadyLoggedIn(acct), }; - SimpleMessageDialog.show(next.$2!, message); - } else if (error is OpenLocalOnlyNoteFromRemoteException) { - SimpleMessageDialog.show( + await SimpleMessageDialog.show(next.$2!, message); + } else { + await SimpleMessageDialog.show( next.$2!, - S.of(context).cannotOpenLocalOnlyNoteFromRemote, + "${S.of(context).thrownError}\n$next", ); - } else if (error is NoteCreateException) { - final message = switch (error) { - EmptyNoteException() => S.of(context).pleaseInputSomething, - TooFewVoteChoiceException() => S.of(context).pleaseAddVoteChoice, - EmptyVoteExpireDateException() => - S.of(context).pleaseSpecifyExpirationDate, - EmptyVoteExpireDurationException() => - S.of(context).pleaseSpecifyExpirationDuration, - MentionToRemoteInLocalOnlyNoteException() => - S.of(context).cannotMentionToRemoteInLocalOnlyNote, - }; - SimpleMessageDialog.show(next.$2!, message); - } else { - SimpleMessageDialog.show(next.$2!, "${S.of(context).thrownError}\n$next"); } } else if (error is Error) { - SimpleMessageDialog.show(next.$2!, "${S.of(context).thrownError}\n$next"); + await SimpleMessageDialog.show( + next.$2!, + "${S.of(context).thrownError}\n$next", + ); } }); diff --git a/lib/view/common/error_notification.dart b/lib/view/common/error_notification.dart index 924c5a550..1a53babd4 100644 --- a/lib/view/common/error_notification.dart +++ b/lib/view/common/error_notification.dart @@ -1,15 +1,15 @@ -import 'package:flutter/material.dart'; -import 'package:miria/view/common/error_detail.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:miria/view/common/error_detail.dart"; class ErrorNotification extends StatelessWidget { final Object? error; final StackTrace? stackTrace; const ErrorNotification({ - super.key, required this.error, required this.stackTrace, + super.key, }); @override @@ -20,7 +20,8 @@ class ErrorNotification extends StatelessWidget { child: Container( padding: const EdgeInsets.all(5), decoration: BoxDecoration( - border: Border.all(color: Theme.of(context).dividerColor)), + border: Border.all(color: Theme.of(context).dividerColor), + ), child: Column( mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.start, @@ -33,7 +34,7 @@ class ErrorNotification extends StatelessWidget { ErrorDetail( error: error, stackTrace: stackTrace, - ) + ), ], ), ), diff --git a/lib/view/common/futable_list_builder.dart b/lib/view/common/futable_list_builder.dart index d8f35cb34..1b79b7e8f 100644 --- a/lib/view/common/futable_list_builder.dart +++ b/lib/view/common/futable_list_builder.dart @@ -1,32 +1,27 @@ -import 'dart:async'; +import "dart:async"; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; -class FutureListView extends StatefulWidget { +class FutureListView extends StatelessWidget { final Future> future; final Widget Function(BuildContext, T) builder; final bool shrinkWrap; final ScrollPhysics? physics; const FutureListView({ - super.key, required this.future, required this.builder, + super.key, this.shrinkWrap = false, this.physics, }); - @override - State createState() => FutureListViewState(); -} - -class FutureListViewState extends State> { @override Widget build(BuildContext context) { return FutureBuilder>( - future: widget.future, + future: future, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.done) { final data = snapshot.data; @@ -40,11 +35,11 @@ class FutureListViewState extends State> { final list = data.toList(); return ListView.builder( - shrinkWrap: widget.shrinkWrap, - physics: widget.physics, - itemCount: data.length, - itemBuilder: (context, index) => - widget.builder(context, list[index])); + shrinkWrap: shrinkWrap, + physics: physics, + itemCount: data.length, + itemBuilder: (context, index) => builder(context, list[index]), + ); } else if (snapshot.hasError) { if (kDebugMode) { print(snapshot.error); @@ -52,7 +47,7 @@ class FutureListViewState extends State> { } return Text("${S.of(context).thrownError}: ${snapshot.error}"); } else { - return const Center(child: CircularProgressIndicator()); + return const Center(child: CircularProgressIndicator.adaptive()); } }, ); diff --git a/lib/view/common/futurable.dart b/lib/view/common/futurable.dart index 89e172628..0c7340f05 100644 --- a/lib/view/common/futurable.dart +++ b/lib/view/common/futurable.dart @@ -1,41 +1,45 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/view/common/error_notification.dart'; +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:miria/view/common/error_notification.dart"; class CommonFuture extends StatelessWidget { final Future future; final Function(T)? futureFinished; final Widget Function(BuildContext, T) complete; - const CommonFuture( - {super.key, - required this.future, - required this.complete, - this.futureFinished}); + const CommonFuture({ + required this.future, + required this.complete, + super.key, + this.futureFinished, + }); @override Widget build(BuildContext context) { - return FutureBuilder(future: () async { - final result = await future; - futureFinished?.call(result); - return result; - }(), builder: (context, snapshot) { - if (snapshot.hasError) { - if (kDebugMode) { - print(snapshot.error); - print(snapshot.stackTrace); + return FutureBuilder( + future: () async { + final result = await future; + futureFinished?.call(result); + return result; + }(), + builder: (context, snapshot) { + if (snapshot.hasError) { + if (kDebugMode) { + print(snapshot.error); + print(snapshot.stackTrace); + } + return ErrorNotification( + error: snapshot.error, + stackTrace: snapshot.stackTrace, + ); } - return ErrorNotification( - error: snapshot.error, - stackTrace: snapshot.stackTrace, - ); - } - if (snapshot.connectionState == ConnectionState.done) { - return complete(context, snapshot.data as T); - } + if (snapshot.connectionState == ConnectionState.done) { + return complete(context, snapshot.data as T); + } - return const Center(child: CircularProgressIndicator()); - }); + return const Center(child: CircularProgressIndicator.adaptive()); + }, + ); } } diff --git a/lib/view/common/image_dialog.dart b/lib/view/common/image_dialog.dart deleted file mode 100644 index f14e7b19c..000000000 --- a/lib/view/common/image_dialog.dart +++ /dev/null @@ -1,255 +0,0 @@ -import 'dart:math'; - -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:image_gallery_saver/image_gallery_saver.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/misskey_notes/network_image.dart'; -import 'package:permission_handler/permission_handler.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class ImageDialog extends ConsumerStatefulWidget { - final List imageUrlList; - final int initialPage; - - const ImageDialog({ - super.key, - required this.imageUrlList, - required this.initialPage, - }); - - @override - ConsumerState createState() => ImageDialogState(); -} - -class ImageDialogState extends ConsumerState { - var scale = 1.0; - late final pageController = PageController(initialPage: widget.initialPage); - var verticalDragX = 0.0; - var verticalDragY = 0.0; - int? listeningId; - final TransformationController _transformationController = - TransformationController(); - - @override - void initState() { - super.initState(); - pageController.addListener(() { - setState(() { - verticalDragX = 0; - verticalDragY = 0; - listeningId = null; - }); - }); - } - - @override - Widget build(BuildContext context) { - return AlertDialog( - backgroundColor: Colors.transparent, - titlePadding: EdgeInsets.zero, - contentPadding: EdgeInsets.zero, - actionsPadding: EdgeInsets.zero, - insetPadding: EdgeInsets.zero, - content: SizedBox( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - child: Stack( - children: [ - Positioned.fill( - child: Listener( - onPointerDown: (event) { - if (listeningId != null) { - setState(() { - verticalDragX = 0; - verticalDragY = 0; - }); - listeningId = null; - return; - } - if (scale != 1.0) return; - listeningId = event.pointer; - }, - onPointerMove: (event) { - if (listeningId != null) { - setState(() { - verticalDragX += event.delta.dx; - verticalDragY += event.delta.dy; - }); - } - }, - onPointerUp: (event) { - final angle = - (atan2(verticalDragY, verticalDragX).abs() / - pi * - 180); - if (listeningId != null && - verticalDragY.abs() > 10 && - (angle > 60 && angle < 120)) { - Navigator.of(context).pop(); - } else { - listeningId = null; - } - }, - child: GestureDetector( - onDoubleTapDown: (details) { - listeningId = null; - if (scale != 1.0) { - _transformationController.value = - Matrix4.identity(); - scale = 1.0; - } else { - final position = details.localPosition; - _transformationController - .value = Matrix4.identity() - ..translate(-position.dx * 2, -position.dy * 2) - ..scale(3.0); - scale = 3.0; - } - }, - child: Transform.translate( - offset: Offset(verticalDragX, verticalDragY), - child: PageView( - controller: pageController, - physics: scale == 1.0 - ? const ScrollPhysics() - : const NeverScrollableScrollPhysics(), - children: [ - for (final url in widget.imageUrlList) - ScaleNotifierInteractiveViewer( - imageUrl: url, - controller: _transformationController, - onScaleChanged: (scaleUpdated) => - setState(() { - scale = scaleUpdated; - }), - ), - ], - ), - )))), - Positioned( - left: 10, - top: 10, - child: RawMaterialButton( - onPressed: () { - Navigator.of(context).pop(); - }, - constraints: - const BoxConstraints(minWidth: 0, minHeight: 0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - padding: EdgeInsets.zero, - fillColor: Theme.of(context) - .scaffoldBackgroundColor - .withAlpha(200), - shape: const CircleBorder(), - child: Padding( - padding: const EdgeInsets.all(5), - child: Icon(Icons.close, - color: Theme.of(context) - .textTheme - .bodyMedium - ?.color - ?.withAlpha(200)))), - ), - if (defaultTargetPlatform == TargetPlatform.android || - defaultTargetPlatform == TargetPlatform.iOS) - Positioned( - right: 10, - top: 10, - child: RawMaterialButton( - onPressed: () async { - final page = pageController.page?.toInt(); - if (page == null) return; - final response = await ref.read(dioProvider).get( - widget.imageUrlList[page], - options: - Options(responseType: ResponseType.bytes)); - - if (defaultTargetPlatform == TargetPlatform.android) { - final androidInfo = - await DeviceInfoPlugin().androidInfo; - if (androidInfo.version.sdkInt <= 32) { - final permissionStatus = - await Permission.storage.status; - if (permissionStatus.isDenied) { - await Permission.storage.request(); - } - } else { - final permissionStatus = - await Permission.photos.status; - if (permissionStatus.isDenied) { - await Permission.photos.request(); - } - } - } - - await ImageGallerySaver.saveImage(response.data); - if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar(SnackBar( - content: Text(S.of(context).savedImage), - duration: const Duration(seconds: 1))); - }, - constraints: - const BoxConstraints(minWidth: 0, minHeight: 0), - materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, - padding: EdgeInsets.zero, - fillColor: Theme.of(context) - .scaffoldBackgroundColor - .withAlpha(200), - shape: const CircleBorder(), - child: Padding( - padding: const EdgeInsets.all(5), - child: Icon(Icons.save, - color: Theme.of(context) - .textTheme - .bodyMedium - ?.color - ?.withAlpha(200))))), - ], - ), - )); - } -} - -class ScaleNotifierInteractiveViewer extends StatefulWidget { - final String imageUrl; - final TransformationController controller; - final void Function(double) onScaleChanged; - - const ScaleNotifierInteractiveViewer({ - super.key, - required this.imageUrl, - required this.controller, - required this.onScaleChanged, - }); - - @override - State createState() => ScaleNotifierInteractiveViewerState(); -} - -class ScaleNotifierInteractiveViewerState - extends State { - var scale = 1.0; - - @override - Widget build(BuildContext context) { - return SizedBox( - width: MediaQuery.of(context).size.width * 0.95, - height: MediaQuery.of(context).size.height * 0.95, - child: InteractiveViewer( - // ピンチイン・ピンチアウト終了後の処理 - transformationController: widget.controller, - onInteractionEnd: (details) { - scale = widget.controller.value.getMaxScaleOnAxis(); - widget.onScaleChanged(scale); - }, - child: NetworkImageView( - url: widget.imageUrl, - type: ImageType.image, - ), - )); - } -} diff --git a/lib/view/common/interactive_viewer.dart b/lib/view/common/interactive_viewer.dart new file mode 100644 index 000000000..aa80682d6 --- /dev/null +++ b/lib/view/common/interactive_viewer.dart @@ -0,0 +1,1493 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import "dart:io"; +import "dart:math" as math; + +import "package:flutter/foundation.dart" show clampDouble; +import "package:flutter/gestures.dart"; +import "package:flutter/material.dart"; +import "package:flutter/physics.dart"; +import "package:vector_math/vector_math_64.dart" show Matrix4, Quad, Vector3; + +// Examples can assume: +// late BuildContext context; +// late Offset? _childWasTappedAt; +// late TransformationController _transformationController; +// Widget child = const Placeholder(); + +/// A signature for widget builders that take a [Quad] of the current viewport. +/// +/// See also: +/// +/// * [InteractiveViewer.builder], whose builder is of this type. +/// * [WidgetBuilder], which is similar, but takes no viewport. +typedef InteractiveViewerWidgetBuilder = Widget Function( + BuildContext context, + Quad viewport, +); + +/// A widget that enables pan and zoom interactions with its child. +/// +/// {@youtube 560 315 https://www.youtube.com/watch?v=zrn7V3bMJvg} +/// +/// The user can transform the child by dragging to pan or pinching to zoom. +/// +/// By default, InteractiveViewer clips its child using [Clip.hardEdge]. +/// To prevent this behavior, consider setting [clipBehavior] to [Clip.none]. +/// When [clipBehavior] is [Clip.none], InteractiveViewer may draw outside of +/// its original area of the screen, such as when a child is zoomed in and +/// increases in size. However, it will not receive gestures outside of its original area. +/// To prevent dead areas where InteractiveViewer does not receive gestures, +/// don't set [clipBehavior] or be sure that the InteractiveViewer widget is the +/// size of the area that should be interactive. +/// +/// See also: +/// * The [Flutter Gallery's transformations demo](https://github.com/flutter/gallery/blob/master/lib/demos/reference/transformations_demo.dart), +/// which includes the use of InteractiveViewer. +/// * The [flutter-go demo](https://github.com/justinmc/flutter-go), which includes robust positioning of an InteractiveViewer child +/// that works for all screen sizes and child sizes. +/// * The [Lazy Flutter Performance Session](https://www.youtube.com/watch?v=qax_nOpgz7E), which includes the use of an InteractiveViewer to +/// performantly view subsets of a large set of widgets using the builder constructor. +/// +/// {@tool dartpad} +/// This example shows a simple Container that can be panned and zoomed. +/// +/// ** See code in examples/api/lib/widgets/interactive_viewer/interactive_viewer.0.dart ** +/// {@end-tool} +@immutable +class InteractiveViewer extends StatefulWidget { + /// Create an InteractiveViewer. + InteractiveViewer({ + required Widget this.child, + super.key, + this.clipBehavior = Clip.hardEdge, + @Deprecated( + "Use panAxis instead. " + "This feature was deprecated after v3.3.0-0.5.pre.", + ) + this.alignPanAxis = false, + this.panAxis = PanAxis.free, + this.boundaryMargin = EdgeInsets.zero, + this.constrained = true, + // These default scale values were eyeballed as reasonable limits for common + // use cases. + this.maxScale = 2.5, + this.minScale = 0.8, + this.interactionEndFrictionCoefficient = _kDrag, + this.onInteractionEnd, + this.onInteractionStart, + this.onInteractionUpdate, + this.panEnabled = true, + this.scaleEnabled = true, + this.scaleFactor = kDefaultMouseScrollToScaleFactor, + this.transformationController, + this.alignment, + this.trackpadScrollCausesScale = false, + this.canChangeScale = true, + }) : assert(minScale > 0), + assert(interactionEndFrictionCoefficient > 0), + assert(minScale.isFinite), + assert(maxScale > 0), + assert(!maxScale.isNaN), + assert(maxScale >= minScale), + // boundaryMargin must be either fully infinite or fully finite, but not + // a mix of both. + assert( + (boundaryMargin.horizontal.isInfinite && + boundaryMargin.vertical.isInfinite) || + (boundaryMargin.top.isFinite && + boundaryMargin.right.isFinite && + boundaryMargin.bottom.isFinite && + boundaryMargin.left.isFinite), + ), + builder = null; + + /// Creates an InteractiveViewer for a child that is created on demand. + /// + /// Can be used to render a child that changes in response to the current + /// transformation. + /// + /// See the [builder] attribute docs for an example of using it to optimize a + /// large child. + InteractiveViewer.builder({ + required InteractiveViewerWidgetBuilder this.builder, + super.key, + this.clipBehavior = Clip.hardEdge, + @Deprecated( + "Use panAxis instead. " + "This feature was deprecated after v3.3.0-0.5.pre.", + ) + this.alignPanAxis = false, + this.panAxis = PanAxis.free, + this.boundaryMargin = EdgeInsets.zero, + // These default scale values were eyeballed as reasonable limits for common + // use cases. + this.maxScale = 2.5, + this.minScale = 0.8, + this.interactionEndFrictionCoefficient = _kDrag, + this.onInteractionEnd, + this.onInteractionStart, + this.onInteractionUpdate, + this.panEnabled = true, + this.scaleEnabled = true, + this.scaleFactor = 200.0, + this.transformationController, + this.alignment, + this.trackpadScrollCausesScale = false, + this.canChangeScale = true, + }) : assert(minScale > 0), + assert(interactionEndFrictionCoefficient > 0), + assert(minScale.isFinite), + assert(maxScale > 0), + assert(!maxScale.isNaN), + assert(maxScale >= minScale), + // boundaryMargin must be either fully infinite or fully finite, but not + // a mix of both. + assert( + (boundaryMargin.horizontal.isInfinite && + boundaryMargin.vertical.isInfinite) || + (boundaryMargin.top.isFinite && + boundaryMargin.right.isFinite && + boundaryMargin.bottom.isFinite && + boundaryMargin.left.isFinite), + ), + constrained = false, + child = null; + + /// The alignment of the child's origin, relative to the size of the box. + final Alignment? alignment; + + /// If set to [Clip.none], the child may extend beyond the size of the InteractiveViewer, + /// but it will not receive gestures in these areas. + /// Be sure that the InteractiveViewer is the desired size when using [Clip.none]. + /// + /// Defaults to [Clip.hardEdge]. + final Clip clipBehavior; + + /// This property is deprecated, please use [panAxis] instead. + /// + /// If true, panning is only allowed in the direction of the horizontal axis + /// or the vertical axis. + /// + /// In other words, when this is true, diagonal panning is not allowed. A + /// single gesture begun along one axis cannot also cause panning along the + /// other axis without stopping and beginning a new gesture. This is a common + /// pattern in tables where data is displayed in columns and rows. + /// + /// See also: + /// * [constrained], which has an example of creating a table that uses + /// alignPanAxis. + @Deprecated( + "Use panAxis instead. " + "This feature was deprecated after v3.3.0-0.5.pre.", + ) + final bool alignPanAxis; + + /// When set to [PanAxis.aligned], panning is only allowed in the horizontal + /// axis or the vertical axis, diagonal panning is not allowed. + /// + /// When set to [PanAxis.vertical] or [PanAxis.horizontal] panning is only + /// allowed in the specified axis. For example, if set to [PanAxis.vertical], + /// panning will only be allowed in the vertical axis. And if set to [PanAxis.horizontal], + /// panning will only be allowed in the horizontal axis. + /// + /// When set to [PanAxis.free] panning is allowed in all directions. + /// + /// Defaults to [PanAxis.free]. + final PanAxis panAxis; + + /// A margin for the visible boundaries of the child. + /// + /// Any transformation that results in the viewport being able to view outside + /// of the boundaries will be stopped at the boundary. The boundaries do not + /// rotate with the rest of the scene, so they are always aligned with the + /// viewport. + /// + /// To produce no boundaries at all, pass infinite [EdgeInsets], such as + /// `EdgeInsets.all(double.infinity)`. + /// + /// No edge can be NaN. + /// + /// Defaults to [EdgeInsets.zero], which results in boundaries that are the + /// exact same size and position as the [child]. + final EdgeInsets boundaryMargin; + + /// Builds the child of this widget. + /// + /// Passed with the [InteractiveViewer.builder] constructor. Otherwise, the + /// [child] parameter must be passed directly, and this is null. + /// + /// {@tool dartpad} + /// This example shows how to use builder to create a [Table] whose cell + /// contents are only built when they are visible. Built and remove cells are + /// logged in the console for illustration. + /// + /// ** See code in examples/api/lib/widgets/interactive_viewer/interactive_viewer.builder.0.dart ** + /// {@end-tool} + /// + /// See also: + /// + /// * [ListView.builder], which follows a similar pattern. + final InteractiveViewerWidgetBuilder? builder; + + /// The child [Widget] that is transformed by InteractiveViewer. + /// + /// If the [InteractiveViewer.builder] constructor is used, then this will be + /// null, otherwise it is required. + final Widget? child; + + /// Whether the normal size constraints at this point in the widget tree are + /// applied to the child. + /// + /// If set to false, then the child will be given infinite constraints. This + /// is often useful when a child should be bigger than the InteractiveViewer. + /// + /// For example, for a child which is bigger than the viewport but can be + /// panned to reveal parts that were initially offscreen, [constrained] must + /// be set to false to allow it to size itself properly. If [constrained] is + /// true and the child can only size itself to the viewport, then areas + /// initially outside of the viewport will not be able to receive user + /// interaction events. If experiencing regions of the child that are not + /// receptive to user gestures, make sure [constrained] is false and the child + /// is sized properly. + /// + /// Defaults to true. + /// + /// {@tool dartpad} + /// This example shows how to create a pannable table. Because the table is + /// larger than the entire screen, setting [constrained] to false is necessary + /// to allow it to be drawn to its full size. The parts of the table that + /// exceed the screen size can then be panned into view. + /// + /// ** See code in examples/api/lib/widgets/interactive_viewer/interactive_viewer.constrained.0.dart ** + /// {@end-tool} + final bool constrained; + + /// If false, the user will be prevented from panning. + /// + /// Defaults to true. + /// + /// See also: + /// + /// * [scaleEnabled], which is similar but for scale. + final bool panEnabled; + + /// If false, the user will be prevented from scaling. + /// + /// Defaults to true. + /// + /// See also: + /// + /// * [panEnabled], which is similar but for panning. + final bool scaleEnabled; + + /// {@macro flutter.gestures.scale.trackpadScrollCausesScale} + final bool trackpadScrollCausesScale; + + /// Determines the amount of scale to be performed per pointer scroll. + /// + /// Defaults to [kDefaultMouseScrollToScaleFactor]. + /// + /// Increasing this value above the default causes scaling to feel slower, + /// while decreasing it causes scaling to feel faster. + /// + /// The amount of scale is calculated as the exponential function of the + /// [PointerScrollEvent.scrollDelta] to [scaleFactor] ratio. In the Flutter + /// engine, the mousewheel [PointerScrollEvent.scrollDelta] is hardcoded to 20 + /// per scroll, while a trackpad scroll can be any amount. + /// + /// Affects only pointer device scrolling, not pinch to zoom. + final double scaleFactor; + + /// The maximum allowed scale. + /// + /// The scale will be clamped between this and [minScale] inclusively. + /// + /// Defaults to 2.5. + /// + /// Must be greater than zero and greater than [minScale]. + final double maxScale; + + /// The minimum allowed scale. + /// + /// The scale will be clamped between this and [maxScale] inclusively. + /// + /// Scale is also affected by [boundaryMargin]. If the scale would result in + /// viewing beyond the boundary, then it will not be allowed. By default, + /// boundaryMargin is EdgeInsets.zero, so scaling below 1.0 will not be + /// allowed in most cases without first increasing the boundaryMargin. + /// + /// Defaults to 0.8. + /// + /// Must be a finite number greater than zero and less than [maxScale]. + final double minScale; + + /// Changes the deceleration behavior after a gesture. + /// + /// Defaults to 0.0000135. + /// + /// Must be a finite number greater than zero. + final double interactionEndFrictionCoefficient; + + /// Called when the user ends a pan or scale gesture on the widget. + /// + /// At the time this is called, the [TransformationController] will have + /// already been updated to reflect the change caused by the interaction, + /// though a pan may cause an inertia animation after this is called as well. + /// + /// {@template flutter.widgets.InteractiveViewer.onInteractionEnd} + /// Will be called even if the interaction is disabled with [panEnabled] or + /// [scaleEnabled] for both touch gestures and mouse interactions. + /// + /// A [GestureDetector] wrapping the InteractiveViewer will not respond to + /// [GestureDetector.onScaleStart], [GestureDetector.onScaleUpdate], and + /// [GestureDetector.onScaleEnd]. Use [onInteractionStart], + /// [onInteractionUpdate], and [onInteractionEnd] to respond to those + /// gestures. + /// {@endtemplate} + /// + /// See also: + /// + /// * [onInteractionStart], which handles the start of the same interaction. + /// * [onInteractionUpdate], which handles an update to the same interaction. + final GestureScaleEndCallback? onInteractionEnd; + + /// Called when the user begins a pan or scale gesture on the widget. + /// + /// At the time this is called, the [TransformationController] will not have + /// changed due to this interaction. + /// + /// {@macro flutter.widgets.InteractiveViewer.onInteractionEnd} + /// + /// The coordinates provided in the details' `focalPoint` and + /// `localFocalPoint` are normal Flutter event coordinates, not + /// InteractiveViewer scene coordinates. See + /// [TransformationController.toScene] for how to convert these coordinates to + /// scene coordinates relative to the child. + /// + /// See also: + /// + /// * [onInteractionUpdate], which handles an update to the same interaction. + /// * [onInteractionEnd], which handles the end of the same interaction. + final GestureScaleStartCallback? onInteractionStart; + + /// Called when the user updates a pan or scale gesture on the widget. + /// + /// At the time this is called, the [TransformationController] will have + /// already been updated to reflect the change caused by the interaction, if + /// the interaction caused the matrix to change. + /// + /// {@macro flutter.widgets.InteractiveViewer.onInteractionEnd} + /// + /// The coordinates provided in the details' `focalPoint` and + /// `localFocalPoint` are normal Flutter event coordinates, not + /// InteractiveViewer scene coordinates. See + /// [TransformationController.toScene] for how to convert these coordinates to + /// scene coordinates relative to the child. + /// + /// See also: + /// + /// * [onInteractionStart], which handles the start of the same interaction. + /// * [onInteractionEnd], which handles the end of the same interaction. + final GestureScaleUpdateCallback? onInteractionUpdate; + + /// A [TransformationController] for the transformation performed on the + /// child. + /// + /// Whenever the child is transformed, the [Matrix4] value is updated and all + /// listeners are notified. If the value is set, InteractiveViewer will update + /// to respect the new value. + /// + /// {@tool dartpad} + /// This example shows how transformationController can be used to animate the + /// transformation back to its starting position. + /// + /// ** See code in examples/api/lib/widgets/interactive_viewer/interactive_viewer.transformation_controller.0.dart ** + /// {@end-tool} + /// + /// See also: + /// + /// * [ValueNotifier], the parent class of TransformationController. + /// * [TextEditingController] for an example of another similar pattern. + final TransformationController? transformationController; + + final bool canChangeScale; + + // Used as the coefficient of friction in the inertial translation animation. + // This value was eyeballed to give a feel similar to Google Photos. + static const double _kDrag = 0.0000135; + + /// Returns the closest point to the given point on the given line segment. + @visibleForTesting + static Vector3 getNearestPointOnLine(Vector3 point, Vector3 l1, Vector3 l2) { + final lengthSquared = math.pow(l2.x - l1.x, 2.0).toDouble() + + math.pow(l2.y - l1.y, 2.0).toDouble(); + + // In this case, l1 == l2. + if (lengthSquared == 0) { + return l1; + } + + // Calculate how far down the line segment the closest point is and return + // the point. + final l1P = point - l1; + final l1L2 = l2 - l1; + final fraction = clampDouble(l1P.dot(l1L2) / lengthSquared, 0.0, 1.0); + return l1 + l1L2 * fraction; + } + + /// Given a quad, return its axis aligned bounding box. + @visibleForTesting + static Quad getAxisAlignedBoundingBox(Quad quad) { + final double minX = math.min( + quad.point0.x, + math.min( + quad.point1.x, + math.min( + quad.point2.x, + quad.point3.x, + ), + ), + ); + final double minY = math.min( + quad.point0.y, + math.min( + quad.point1.y, + math.min( + quad.point2.y, + quad.point3.y, + ), + ), + ); + final double maxX = math.max( + quad.point0.x, + math.max( + quad.point1.x, + math.max( + quad.point2.x, + quad.point3.x, + ), + ), + ); + final double maxY = math.max( + quad.point0.y, + math.max( + quad.point1.y, + math.max( + quad.point2.y, + quad.point3.y, + ), + ), + ); + return Quad.points( + Vector3(minX, minY, 0), + Vector3(maxX, minY, 0), + Vector3(maxX, maxY, 0), + Vector3(minX, maxY, 0), + ); + } + + /// Returns true iff the point is inside the rectangle given by the Quad, + /// inclusively. + /// Algorithm from https://math.stackexchange.com/a/190373. + @visibleForTesting + static bool pointIsInside(Vector3 point, Quad quad) { + final aM = point - quad.point0; + final aB = quad.point1 - quad.point0; + final aD = quad.point3 - quad.point0; + + final aMAB = aM.dot(aB); + final aBAB = aB.dot(aB); + final aMAD = aM.dot(aD); + final aDAD = aD.dot(aD); + + return 0 <= aMAB && aMAB <= aBAB && 0 <= aMAD && aMAD <= aDAD; + } + + /// Get the point inside (inclusively) the given Quad that is nearest to the + /// given Vector3. + @visibleForTesting + static Vector3 getNearestPointInside(Vector3 point, Quad quad) { + // If the point is inside the axis aligned bounding box, then it's ok where + // it is. + if (pointIsInside(point, quad)) { + return point; + } + + // Otherwise, return the nearest point on the quad. + final closestPoints = [ + InteractiveViewer.getNearestPointOnLine(point, quad.point0, quad.point1), + InteractiveViewer.getNearestPointOnLine(point, quad.point1, quad.point2), + InteractiveViewer.getNearestPointOnLine(point, quad.point2, quad.point3), + InteractiveViewer.getNearestPointOnLine(point, quad.point3, quad.point0), + ]; + var minDistance = double.infinity; + late Vector3 closestOverall; + for (final closePoint in closestPoints) { + final distance = math.sqrt( + math.pow(point.x - closePoint.x, 2) + + math.pow(point.y - closePoint.y, 2), + ); + if (distance < minDistance) { + minDistance = distance; + closestOverall = closePoint; + } + } + return closestOverall; + } + + @override + State createState() => _InteractiveViewerState(); +} + +class _InteractiveViewerState extends State + with TickerProviderStateMixin { + TransformationController? _transformationController; + + final GlobalKey _childKey = GlobalKey(); + final GlobalKey _parentKey = GlobalKey(); + Animation? _animation; + Animation? _scaleAnimation; + late Offset _scaleAnimationFocalPoint; + late AnimationController _controller; + late AnimationController _scaleController; + Axis? _currentAxis; // Used with panAxis. + Offset? _referenceFocalPoint; // Point where the current gesture began. + double? _scaleStart; // Scale value at start of scaling gesture. + double? _rotationStart = 0.0; // Rotation at start of rotation gesture. + double _currentRotation = 0.0; // Rotation of _transformationController.value. + _GestureType? _gestureType; + + // TODO(justinmc): Add rotateEnabled parameter to the widget and remove this + // hardcoded value when the rotation feature is implemented. + // https://github.com/flutter/flutter/issues/57698 + final bool _rotateEnabled = false; + + // The _boundaryRect is calculated by adding the boundaryMargin to the size of + // the child. + Rect get _boundaryRect { + assert(_childKey.currentContext != null); + assert(!widget.boundaryMargin.left.isNaN); + assert(!widget.boundaryMargin.right.isNaN); + assert(!widget.boundaryMargin.top.isNaN); + assert(!widget.boundaryMargin.bottom.isNaN); + + final childRenderBox = + _childKey.currentContext!.findRenderObject()! as RenderBox; + final childSize = childRenderBox.size; + final boundaryRect = + widget.boundaryMargin.inflateRect(Offset.zero & childSize); + assert( + !boundaryRect.isEmpty, + "InteractiveViewer's child must have nonzero dimensions.", + ); + // Boundaries that are partially infinite are not allowed because Matrix4's + // rotation and translation methods don't handle infinites well. + assert( + boundaryRect.isFinite || + (boundaryRect.left.isInfinite && + boundaryRect.top.isInfinite && + boundaryRect.right.isInfinite && + boundaryRect.bottom.isInfinite), + "boundaryRect must either be infinite in all directions or finite in all directions.", + ); + return boundaryRect; + } + + // The Rect representing the child's parent. + Rect get _viewport { + assert(_parentKey.currentContext != null); + final parentRenderBox = + _parentKey.currentContext!.findRenderObject()! as RenderBox; + return Offset.zero & parentRenderBox.size; + } + + // Return a new matrix representing the given matrix after applying the given + // translation. + Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation) { + if (translation == Offset.zero) { + return matrix.clone(); + } + + late final Offset alignedTranslation; + + if (_currentAxis != null) { + switch (widget.panAxis) { + case PanAxis.horizontal: + alignedTranslation = _alignAxis(translation, Axis.horizontal); + case PanAxis.vertical: + alignedTranslation = _alignAxis(translation, Axis.vertical); + case PanAxis.aligned: + alignedTranslation = _alignAxis(translation, _currentAxis!); + case PanAxis.free: + alignedTranslation = translation; + } + } else { + alignedTranslation = translation; + } + + final nextMatrix = matrix.clone() + ..translate( + alignedTranslation.dx, + alignedTranslation.dy, + ); + + // Transform the viewport to determine where its four corners will be after + // the child has been transformed. + final nextViewport = _transformViewport(nextMatrix, _viewport); + + // If the boundaries are infinite, then no need to check if the translation + // fits within them. + if (_boundaryRect.isInfinite) { + return nextMatrix; + } + + // Expand the boundaries with rotation. This prevents the problem where a + // mismatch in orientation between the viewport and boundaries effectively + // limits translation. With this approach, all points that are visible with + // no rotation are visible after rotation. + final boundariesAabbQuad = _getAxisAlignedBoundingBoxWithRotation( + _boundaryRect, + _currentRotation, + ); + + // If the given translation fits completely within the boundaries, allow it. + final offendingDistance = _exceedsBy(boundariesAabbQuad, nextViewport); + if (offendingDistance == Offset.zero) { + return nextMatrix; + } + + // Desired translation goes out of bounds, so translate to the nearest + // in-bounds point instead. + final nextTotalTranslation = _getMatrixTranslation(nextMatrix); + final currentScale = matrix.getMaxScaleOnAxis(); + final correctedTotalTranslation = Offset( + nextTotalTranslation.dx - offendingDistance.dx * currentScale, + nextTotalTranslation.dy - offendingDistance.dy * currentScale, + ); + // TODO(justinmc): This needs some work to handle rotation properly. The + // idea is that the boundaries are axis aligned (boundariesAabbQuad), but + // calculating the translation to put the viewport inside that Quad is more + // complicated than this when rotated. + // https://github.com/flutter/flutter/issues/57698 + final correctedMatrix = matrix.clone() + ..setTranslation( + Vector3( + correctedTotalTranslation.dx, + correctedTotalTranslation.dy, + 0.0, + ), + ); + + // Double check that the corrected translation fits. + final correctedViewport = _transformViewport(correctedMatrix, _viewport); + final offendingCorrectedDistance = + _exceedsBy(boundariesAabbQuad, correctedViewport); + if (offendingCorrectedDistance == Offset.zero) { + return correctedMatrix; + } + + // If the corrected translation doesn't fit in either direction, don't allow + // any translation at all. This happens when the viewport is larger than the + // entire boundary. + if (offendingCorrectedDistance.dx != 0.0 && + offendingCorrectedDistance.dy != 0.0) { + return matrix.clone(); + } + + // Otherwise, allow translation in only the direction that fits. This + // happens when the viewport is larger than the boundary in one direction. + final unidirectionalCorrectedTotalTranslation = Offset( + offendingCorrectedDistance.dx == 0.0 ? correctedTotalTranslation.dx : 0.0, + offendingCorrectedDistance.dy == 0.0 ? correctedTotalTranslation.dy : 0.0, + ); + return matrix.clone() + ..setTranslation( + Vector3( + unidirectionalCorrectedTotalTranslation.dx, + unidirectionalCorrectedTotalTranslation.dy, + 0.0, + ), + ); + } + + // Return a new matrix representing the given matrix after applying the given + // scale. + Matrix4 _matrixScale(Matrix4 matrix, double scale) { + if (scale == 1.0) { + return matrix.clone(); + } + assert(scale != 0.0); + + // Don't allow a scale that results in an overall scale beyond min/max + // scale. + final currentScale = _transformationController!.value.getMaxScaleOnAxis(); + final double totalScale = math.max( + currentScale * scale, + // Ensure that the scale cannot make the child so big that it can't fit + // inside the boundaries (in either direction). + math.max( + _viewport.width / _boundaryRect.width, + _viewport.height / _boundaryRect.height, + ), + ); + final clampedTotalScale = clampDouble( + totalScale, + widget.minScale, + widget.maxScale, + ); + final clampedScale = clampedTotalScale / currentScale; + return matrix.clone()..scale(clampedScale); + } + + // Return a new matrix representing the given matrix after applying the given + // rotation. + Matrix4 _matrixRotate(Matrix4 matrix, double rotation, Offset focalPoint) { + if (rotation == 0) { + return matrix.clone(); + } + final focalPointScene = _transformationController!.toScene( + focalPoint, + ); + return matrix.clone() + ..translate(focalPointScene.dx, focalPointScene.dy) + ..rotateZ(-rotation) + ..translate(-focalPointScene.dx, -focalPointScene.dy); + } + + // Returns true iff the given _GestureType is enabled. + bool _gestureIsSupported(_GestureType? gestureType) { + switch (gestureType) { + case _GestureType.rotate: + return _rotateEnabled; + + case _GestureType.scale: + return widget.scaleEnabled; + + case _GestureType.pan: + case null: + return widget.panEnabled; + } + } + + // Decide which type of gesture this is by comparing the amount of scale + // and rotation in the gesture, if any. Scale starts at 1 and rotation + // starts at 0. Pan will have no scale and no rotation because it uses only one + // finger. + _GestureType _getGestureType(ScaleUpdateDetails details) { + final scale = !widget.scaleEnabled ? 1.0 : details.scale; + final rotation = !_rotateEnabled ? 0.0 : details.rotation; + if ((scale - 1).abs() > rotation.abs()) { + return _GestureType.scale; + } else if (rotation != 0.0) { + return _GestureType.rotate; + } else { + return _GestureType.pan; + } + } + + // Handle the start of a gesture. All of pan, scale, and rotate are handled + // with GestureDetector's scale gesture. + void _onScaleStart(ScaleStartDetails details) { + widget.onInteractionStart?.call(details); + + if (_controller.isAnimating) { + _controller.stop(); + _controller.reset(); + _animation?.removeListener(_onAnimate); + _animation = null; + } + if (_scaleController.isAnimating) { + _scaleController.stop(); + _scaleController.reset(); + _scaleAnimation?.removeListener(_onScaleAnimate); + _scaleAnimation = null; + } + + _gestureType = null; + _currentAxis = null; + _scaleStart = _transformationController!.value.getMaxScaleOnAxis(); + _referenceFocalPoint = _transformationController!.toScene( + details.localFocalPoint, + ); + _rotationStart = _currentRotation; + } + + // Handle an update to an ongoing gesture. All of pan, scale, and rotate are + // handled with GestureDetector's scale gesture. + void _onScaleUpdate(ScaleUpdateDetails details) { + final scale = _transformationController!.value.getMaxScaleOnAxis(); + _scaleAnimationFocalPoint = details.localFocalPoint; + final focalPointScene = _transformationController!.toScene( + details.localFocalPoint, + ); + + if (_gestureType == _GestureType.pan) { + // When a gesture first starts, it sometimes has no change in scale and + // rotation despite being a two-finger gesture. Here the gesture is + // allowed to be reinterpreted as its correct type after originally + // being marked as a pan. + _gestureType = _getGestureType(details); + } else { + _gestureType ??= _getGestureType(details); + } + if (!_gestureIsSupported(_gestureType)) { + widget.onInteractionUpdate?.call(details); + return; + } + + switch (_gestureType!) { + case _GestureType.scale: + assert(_scaleStart != null); + // details.scale gives us the amount to change the scale as of the + // start of this gesture, so calculate the amount to scale as of the + // previous call to _onScaleUpdate. + final desiredScale = _scaleStart! * details.scale; + final scaleChange = desiredScale / scale; + _transformationController!.value = _matrixScale( + _transformationController!.value, + scaleChange, + ); + + // While scaling, translate such that the user's two fingers stay on + // the same places in the scene. That means that the focal point of + // the scale should be on the same place in the scene before and after + // the scale. + final focalPointSceneScaled = _transformationController!.toScene( + details.localFocalPoint, + ); + _transformationController!.value = _matrixTranslate( + _transformationController!.value, + focalPointSceneScaled - _referenceFocalPoint!, + ); + + // details.localFocalPoint should now be at the same location as the + // original _referenceFocalPoint point. If it's not, that's because + // the translate came in contact with a boundary. In that case, update + // _referenceFocalPoint so subsequent updates happen in relation to + // the new effective focal point. + final focalPointSceneCheck = _transformationController!.toScene( + details.localFocalPoint, + ); + if (_round(_referenceFocalPoint!) != _round(focalPointSceneCheck)) { + _referenceFocalPoint = focalPointSceneCheck; + } + + case _GestureType.rotate: + if (details.rotation == 0.0) { + widget.onInteractionUpdate?.call(details); + return; + } + final desiredRotation = _rotationStart! + details.rotation; + _transformationController!.value = _matrixRotate( + _transformationController!.value, + _currentRotation - desiredRotation, + details.localFocalPoint, + ); + _currentRotation = desiredRotation; + + case _GestureType.pan: + assert(_referenceFocalPoint != null); + // details may have a change in scale here when scaleEnabled is false. + // In an effort to keep the behavior similar whether or not scaleEnabled + // is true, these gestures are thrown away. + if (details.scale != 1.0) { + widget.onInteractionUpdate?.call(details); + return; + } + _currentAxis ??= _getPanAxis(_referenceFocalPoint!, focalPointScene); + // Translate so that the same point in the scene is underneath the + // focal point before and after the movement. + final translationChange = focalPointScene - _referenceFocalPoint!; + _transformationController!.value = _matrixTranslate( + _transformationController!.value, + translationChange, + ); + _referenceFocalPoint = _transformationController!.toScene( + details.localFocalPoint, + ); + } + widget.onInteractionUpdate?.call(details); + } + + // Handle the end of a gesture of _GestureType. All of pan, scale, and rotate + // are handled with GestureDetector's scale gesture. + void _onScaleEnd(ScaleEndDetails details) { + widget.onInteractionEnd?.call(details); + _scaleStart = null; + _rotationStart = null; + _referenceFocalPoint = null; + + _animation?.removeListener(_onAnimate); + _scaleAnimation?.removeListener(_onScaleAnimate); + _controller.reset(); + _scaleController.reset(); + + if (!_gestureIsSupported(_gestureType)) { + _currentAxis = null; + return; + } + + if (_gestureType == _GestureType.pan) { + if (details.velocity.pixelsPerSecond.distance < kMinFlingVelocity) { + _currentAxis = null; + return; + } + final translationVector = + _transformationController!.value.getTranslation(); + final translation = Offset(translationVector.x, translationVector.y); + final frictionSimulationX = FrictionSimulation( + widget.interactionEndFrictionCoefficient, + translation.dx, + details.velocity.pixelsPerSecond.dx, + ); + final frictionSimulationY = FrictionSimulation( + widget.interactionEndFrictionCoefficient, + translation.dy, + details.velocity.pixelsPerSecond.dy, + ); + final tFinal = _getFinalTime( + details.velocity.pixelsPerSecond.distance, + widget.interactionEndFrictionCoefficient, + ); + _animation = Tween( + begin: translation, + end: Offset(frictionSimulationX.finalX, frictionSimulationY.finalX), + ).animate( + CurvedAnimation( + parent: _controller, + curve: Curves.decelerate, + ), + ); + _controller.duration = Duration(milliseconds: (tFinal * 1000).round()); + _animation!.addListener(_onAnimate); + _controller.forward(); + } else if (_gestureType == _GestureType.scale) { + if (details.scaleVelocity.abs() < 0.1) { + _currentAxis = null; + return; + } + final scale = _transformationController!.value.getMaxScaleOnAxis(); + final frictionSimulation = FrictionSimulation( + widget.interactionEndFrictionCoefficient * widget.scaleFactor, + scale, + details.scaleVelocity / 10, + ); + final tFinal = _getFinalTime( + details.scaleVelocity.abs(), + widget.interactionEndFrictionCoefficient, + effectivelyMotionless: 0.1, + ); + _scaleAnimation = Tween( + begin: scale, + end: frictionSimulation.x(tFinal), + ).animate( + CurvedAnimation( + parent: _scaleController, + curve: Curves.decelerate, + ), + ); + _scaleController.duration = + Duration(milliseconds: (tFinal * 1000).round()); + _scaleAnimation!.addListener(_onScaleAnimate); + _scaleController.forward(); + } + } + + // Handle mousewheel and web trackpad scroll events. + void _receivedPointerSignal(PointerSignalEvent event) { + final double scaleChange; + if (event is PointerScrollEvent) { + if (event.kind == PointerDeviceKind.trackpad && + !widget.trackpadScrollCausesScale) { + // Trackpad scroll, so treat it as a pan. + widget.onInteractionStart?.call( + ScaleStartDetails( + focalPoint: event.position, + localFocalPoint: event.localPosition, + ), + ); + + final localDelta = PointerEvent.transformDeltaViaPositions( + untransformedEndPosition: event.position + event.scrollDelta, + untransformedDelta: event.scrollDelta, + transform: event.transform, + ); + + if (!_gestureIsSupported(_GestureType.pan)) { + widget.onInteractionUpdate?.call( + ScaleUpdateDetails( + focalPoint: event.position - event.scrollDelta, + localFocalPoint: event.localPosition - event.scrollDelta, + focalPointDelta: -localDelta, + ), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + return; + } + + final focalPointScene = _transformationController!.toScene( + event.localPosition, + ); + + final newFocalPointScene = _transformationController!.toScene( + event.localPosition - localDelta, + ); + + _transformationController!.value = _matrixTranslate( + _transformationController!.value, + newFocalPointScene - focalPointScene, + ); + + widget.onInteractionUpdate?.call( + ScaleUpdateDetails( + focalPoint: event.position - event.scrollDelta, + localFocalPoint: event.localPosition - localDelta, + focalPointDelta: -localDelta, + ), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + return; + } + // Ignore left and right mouse wheel scroll. + if (event.scrollDelta.dy == 0.0) { + return; + } + scaleChange = math.exp(-event.scrollDelta.dy / widget.scaleFactor); + } else if (event is PointerScaleEvent) { + scaleChange = event.scale; + } else { + return; + } + widget.onInteractionStart?.call( + ScaleStartDetails( + focalPoint: event.position, + localFocalPoint: event.localPosition, + ), + ); + + if (!_gestureIsSupported(_GestureType.scale)) { + widget.onInteractionUpdate?.call( + ScaleUpdateDetails( + focalPoint: event.position, + localFocalPoint: event.localPosition, + scale: scaleChange, + ), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + return; + } + + final focalPointScene = _transformationController!.toScene( + event.localPosition, + ); + + _transformationController!.value = _matrixScale( + _transformationController!.value, + scaleChange, + ); + + // After scaling, translate such that the event's position is at the + // same scene point before and after the scale. + final focalPointSceneScaled = _transformationController!.toScene( + event.localPosition, + ); + _transformationController!.value = _matrixTranslate( + _transformationController!.value, + focalPointSceneScaled - focalPointScene, + ); + + widget.onInteractionUpdate?.call( + ScaleUpdateDetails( + focalPoint: event.position, + localFocalPoint: event.localPosition, + scale: scaleChange, + ), + ); + widget.onInteractionEnd?.call(ScaleEndDetails()); + } + + // Handle inertia drag animation. + void _onAnimate() { + if (!_controller.isAnimating) { + _currentAxis = null; + _animation?.removeListener(_onAnimate); + _animation = null; + _controller.reset(); + return; + } + // Translate such that the resulting translation is _animation.value. + final translationVector = _transformationController!.value.getTranslation(); + final translation = Offset(translationVector.x, translationVector.y); + final translationScene = _transformationController!.toScene( + translation, + ); + final animationScene = _transformationController!.toScene( + _animation!.value, + ); + final translationChangeScene = animationScene - translationScene; + _transformationController!.value = _matrixTranslate( + _transformationController!.value, + translationChangeScene, + ); + } + + // Handle inertia scale animation. + void _onScaleAnimate() { + if (!_scaleController.isAnimating) { + _currentAxis = null; + _scaleAnimation?.removeListener(_onScaleAnimate); + _scaleAnimation = null; + _scaleController.reset(); + return; + } + final desiredScale = _scaleAnimation!.value; + final scaleChange = + desiredScale / _transformationController!.value.getMaxScaleOnAxis(); + final referenceFocalPoint = _transformationController!.toScene( + _scaleAnimationFocalPoint, + ); + _transformationController!.value = _matrixScale( + _transformationController!.value, + scaleChange, + ); + + // While scaling, translate such that the user's two fingers stay on + // the same places in the scene. That means that the focal point of + // the scale should be on the same place in the scene before and after + // the scale. + final focalPointSceneScaled = _transformationController!.toScene( + _scaleAnimationFocalPoint, + ); + _transformationController!.value = _matrixTranslate( + _transformationController!.value, + focalPointSceneScaled - referenceFocalPoint, + ); + } + + void _onTransformationControllerChange() { + // A change to the TransformationController's value is a change to the + // state. + setState(() {}); + } + + @override + void initState() { + super.initState(); + + _transformationController = + widget.transformationController ?? TransformationController(); + _transformationController!.addListener(_onTransformationControllerChange); + _controller = AnimationController( + vsync: this, + ); + _scaleController = AnimationController( + vsync: this, + ); + } + + @override + void didUpdateWidget(InteractiveViewer oldWidget) { + super.didUpdateWidget(oldWidget); + // Handle all cases of needing to dispose and initialize + // transformationControllers. + if (oldWidget.transformationController == null) { + if (widget.transformationController != null) { + _transformationController! + .removeListener(_onTransformationControllerChange); + _transformationController!.dispose(); + _transformationController = widget.transformationController; + _transformationController! + .addListener(_onTransformationControllerChange); + } + } else { + if (widget.transformationController == null) { + _transformationController! + .removeListener(_onTransformationControllerChange); + _transformationController = TransformationController(); + _transformationController! + .addListener(_onTransformationControllerChange); + } else if (widget.transformationController != + oldWidget.transformationController) { + _transformationController! + .removeListener(_onTransformationControllerChange); + _transformationController = widget.transformationController; + _transformationController! + .addListener(_onTransformationControllerChange); + } + } + } + + @override + void dispose() { + _controller.dispose(); + _scaleController.dispose(); + _transformationController! + .removeListener(_onTransformationControllerChange); + if (widget.transformationController == null) { + _transformationController!.dispose(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + Widget child; + if (widget.child != null) { + child = _InteractiveViewerBuilt( + childKey: _childKey, + clipBehavior: widget.clipBehavior, + constrained: widget.constrained, + matrix: _transformationController!.value, + alignment: widget.alignment, + child: widget.child!, + ); + } else { + // When using InteractiveViewer.builder, then constrained is false and the + // viewport is the size of the constraints. + assert(widget.builder != null); + assert(!widget.constrained); + child = LayoutBuilder( + builder: (context, constraints) { + final matrix = _transformationController!.value; + return _InteractiveViewerBuilt( + childKey: _childKey, + clipBehavior: widget.clipBehavior, + constrained: widget.constrained, + alignment: widget.alignment, + matrix: matrix, + child: widget.builder!( + context, + _transformViewport(matrix, Offset.zero & constraints.biggest), + ), + ); + }, + ); + } + + final scale = _transformationController!.value.getMaxScaleOnAxis(); + + final isDesktop = !(Platform.isAndroid || Platform.isIOS); + final canChangeScale = + widget.canChangeScale && !(isDesktop && scale == 1.0); + + return Listener( + key: _parentKey, + onPointerSignal: _receivedPointerSignal, + child: GestureDetector( + behavior: HitTestBehavior.opaque, // Necessary when panning off screen. + onScaleEnd: canChangeScale ? _onScaleEnd : null, + onScaleStart: canChangeScale ? _onScaleStart : null, + onScaleUpdate: canChangeScale ? _onScaleUpdate : null, + trackpadScrollCausesScale: widget.trackpadScrollCausesScale, + trackpadScrollToScaleFactor: Offset(0, -1 / widget.scaleFactor), + child: child, + ), + ); + } +} + +// This widget allows us to easily swap in and out the LayoutBuilder in +// InteractiveViewer's depending on if it's using a builder or a child. +class _InteractiveViewerBuilt extends StatelessWidget { + const _InteractiveViewerBuilt({ + required this.child, + required this.childKey, + required this.clipBehavior, + required this.constrained, + required this.matrix, + required this.alignment, + }); + + final Widget child; + final GlobalKey childKey; + final Clip clipBehavior; + final bool constrained; + final Matrix4 matrix; + final Alignment? alignment; + + @override + Widget build(BuildContext context) { + Widget child = Transform( + transform: matrix, + alignment: alignment, + child: KeyedSubtree( + key: childKey, + child: this.child, + ), + ); + + if (!constrained) { + child = OverflowBox( + alignment: Alignment.topLeft, + minWidth: 0.0, + minHeight: 0.0, + maxWidth: double.infinity, + maxHeight: double.infinity, + child: child, + ); + } + + return ClipRect( + clipBehavior: clipBehavior, + child: child, + ); + } +} + +// A classification of relevant user gestures. Each contiguous user gesture is +// represented by exactly one _GestureType. +enum _GestureType { + pan, + scale, + rotate, +} + +// Given a velocity and drag, calculate the time at which motion will come to +// a stop, within the margin of effectivelyMotionless. +double _getFinalTime( + double velocity, + double drag, { + double effectivelyMotionless = 10, +}) { + return math.log(effectivelyMotionless / velocity) / math.log(drag / 100); +} + +// Return the translation from the given Matrix4 as an Offset. +Offset _getMatrixTranslation(Matrix4 matrix) { + final nextTranslation = matrix.getTranslation(); + return Offset(nextTranslation.x, nextTranslation.y); +} + +// Transform the four corners of the viewport by the inverse of the given +// matrix. This gives the viewport after the child has been transformed by the +// given matrix. The viewport transforms as the inverse of the child (i.e. +// moving the child left is equivalent to moving the viewport right). +Quad _transformViewport(Matrix4 matrix, Rect viewport) { + final inverseMatrix = matrix.clone()..invert(); + return Quad.points( + inverseMatrix.transform3( + Vector3( + viewport.topLeft.dx, + viewport.topLeft.dy, + 0.0, + ), + ), + inverseMatrix.transform3( + Vector3( + viewport.topRight.dx, + viewport.topRight.dy, + 0.0, + ), + ), + inverseMatrix.transform3( + Vector3( + viewport.bottomRight.dx, + viewport.bottomRight.dy, + 0.0, + ), + ), + inverseMatrix.transform3( + Vector3( + viewport.bottomLeft.dx, + viewport.bottomLeft.dy, + 0.0, + ), + ), + ); +} + +// Find the axis aligned bounding box for the rect rotated about its center by +// the given amount. +Quad _getAxisAlignedBoundingBoxWithRotation(Rect rect, double rotation) { + final rotationMatrix = Matrix4.identity() + ..translate(rect.size.width / 2, rect.size.height / 2) + ..rotateZ(rotation) + ..translate(-rect.size.width / 2, -rect.size.height / 2); + final boundariesRotated = Quad.points( + rotationMatrix.transform3(Vector3(rect.left, rect.top, 0.0)), + rotationMatrix.transform3(Vector3(rect.right, rect.top, 0.0)), + rotationMatrix.transform3(Vector3(rect.right, rect.bottom, 0.0)), + rotationMatrix.transform3(Vector3(rect.left, rect.bottom, 0.0)), + ); + return InteractiveViewer.getAxisAlignedBoundingBox(boundariesRotated); +} + +// Return the amount that viewport lies outside of boundary. If the viewport +// is completely contained within the boundary (inclusively), then returns +// Offset.zero. +Offset _exceedsBy(Quad boundary, Quad viewport) { + final viewportPoints = [ + viewport.point0, + viewport.point1, + viewport.point2, + viewport.point3, + ]; + var largestExcess = Offset.zero; + for (final point in viewportPoints) { + final pointInside = + InteractiveViewer.getNearestPointInside(point, boundary); + final excess = Offset( + pointInside.x - point.x, + pointInside.y - point.y, + ); + if (excess.dx.abs() > largestExcess.dx.abs()) { + largestExcess = Offset(excess.dx, largestExcess.dy); + } + if (excess.dy.abs() > largestExcess.dy.abs()) { + largestExcess = Offset(largestExcess.dx, excess.dy); + } + } + + return _round(largestExcess); +} + +// Round the output values. This works around a precision problem where +// values that should have been zero were given as within 10^-10 of zero. +Offset _round(Offset offset) { + return Offset( + double.parse(offset.dx.toStringAsFixed(9)), + double.parse(offset.dy.toStringAsFixed(9)), + ); +} + +// Align the given offset to the given axis by allowing movement only in the +// axis direction. +Offset _alignAxis(Offset offset, Axis axis) { + switch (axis) { + case Axis.horizontal: + return Offset(offset.dx, 0.0); + case Axis.vertical: + return Offset(0.0, offset.dy); + } +} + +// Given two points, return the axis where the distance between the points is +// greatest. If they are equal, return null. +Axis? _getPanAxis(Offset point1, Offset point2) { + if (point1 == point2) { + return null; + } + final x = point2.dx - point1.dx; + final y = point2.dy - point1.dy; + return x.abs() > y.abs() ? Axis.horizontal : Axis.vertical; +} + +/// This enum is used to specify the behavior of the [InteractiveViewer] when +/// the user drags the viewport. +enum PanAxis { + /// The user can only pan the viewport along the horizontal axis. + horizontal, + + /// The user can only pan the viewport along the vertical axis. + vertical, + + /// The user can pan the viewport along the horizontal and vertical axes + /// but not diagonally. + aligned, + + /// The user can pan the viewport freely in any direction. + free, +} diff --git a/lib/view/common/misskey_ad.dart b/lib/view/common/misskey_ad.dart index 1e53b7c25..24f072d6e 100644 --- a/lib/view/common/misskey_ad.dart +++ b/lib/view/common/misskey_ad.dart @@ -1,63 +1,51 @@ -import 'dart:math'; +import "dart:math"; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/network_image.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:url_launcher/url_launcher.dart'; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; +import "package:url_launcher/url_launcher.dart"; -class MisskeyAd extends ConsumerStatefulWidget { +class MisskeyAd extends HookConsumerWidget { const MisskeyAd({super.key}); @override - ConsumerState createState() => MisskeyAdState(); -} - -class MisskeyAdState extends ConsumerState { - MetaAd? choosen; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); + Widget build(BuildContext context, WidgetRef ref) { + final targetAd = useMemoized(() { + if (ref.read(accountContextProvider).getAccount.i.policies.canHideAds && + !ref + .read(accountSettingsRepositoryProvider) + .fromAccount(ref.read(accountContextProvider).getAccount) + .forceShowAd) { + return null; + } - if (AccountScope.of(context).i.policies.canHideAds && - !ref - .read(accountSettingsRepositoryProvider) - .fromAccount(AccountScope.of(context)) - .forceShowAd) { - return; - } + final ads = ref.read(accountContextProvider).getAccount.meta?.ads ?? []; + if (ads.isEmpty) return null; - final ads = AccountScope.of(context).meta?.ads ?? []; - final totalRatio = ads.map((e) => e.ratio).sum; - final choosenRatio = Random().nextDouble() * totalRatio; + final totalRatio = ads.map((e) => e.ratio).sum; + final choosenRatio = Random().nextDouble() * totalRatio; - var calculatingRatio = 0.0; - for (final ad in ads) { - if (calculatingRatio + ad.ratio > choosenRatio) { - choosen = ad; - break; + var calculatingRatio = 0.0; + for (final ad in ads) { + if (calculatingRatio + ad.ratio > choosenRatio) { + return ad; + } + calculatingRatio += ad.ratio; } - calculatingRatio += ad.ratio; - } - if (choosen == null && ads.isNotEmpty) { - choosen = ads.last; - } - } - @override - Widget build(BuildContext context) { - final targetAd = choosen; + return ads.last; + }); + if (targetAd == null) { return const SizedBox.shrink(); } return Center( child: GestureDetector( - onTap: () => + onTap: () async => launchUrl(targetAd.url, mode: LaunchMode.externalApplication), child: Padding( padding: const EdgeInsets.all(8.0), diff --git a/lib/view/common/misskey_notes/abuse_dialog.dart b/lib/view/common/misskey_notes/abuse_dialog.dart deleted file mode 100644 index d60326adb..000000000 --- a/lib/view/common/misskey_notes/abuse_dialog.dart +++ /dev/null @@ -1,89 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:miria/view/dialogs/simple_message_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class AbuseDialog extends ConsumerStatefulWidget { - final Account account; - final User targetUser; - final String? defaultText; - - const AbuseDialog({ - super.key, - required this.account, - required this.targetUser, - this.defaultText, - }); - - @override - ConsumerState createState() => AbuseDialogState(); -} - -class AbuseDialogState extends ConsumerState { - final controller = TextEditingController(); - - @override - void initState() { - super.initState(); - controller.text = widget.defaultText ?? ""; - } - - Future abuse() async { - await ref - .read(misskeyProvider(widget.account)) - .users - .reportAbuse(UsersReportAbuseRequest( - userId: widget.targetUser.id, - comment: controller.text, - )); - if (!mounted) return; - Navigator.of(context).pop(); - showDialog( - context: context, - builder: (context) => - SimpleMessageDialog(message: S.of(context).thanksForReport)); - } - - @override - Widget build(BuildContext context) { - return AccountScope( - account: widget.account, - child: AlertDialog( - title: SimpleMfmText( - S.of(context).reportAbuseOf( - widget.targetUser.name ?? widget.targetUser.username, - ), - ), - content: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(S.of(context).detail), - TextField( - controller: controller, - maxLines: null, - minLines: 5, - autofocus: true, - ), - Text( - S.of(context).pleaseInputReasonWhyAbuse, - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ), - ), - actions: [ - ElevatedButton( - onPressed: abuse.expectFailure(context), - child: Text(S.of(context).reportAbuse)) - ], - ), - ); - } -} diff --git a/lib/view/common/misskey_notes/clip_modal_sheet.dart b/lib/view/common/misskey_notes/clip_modal_sheet.dart deleted file mode 100644 index 50e4f7c88..000000000 --- a/lib/view/common/misskey_notes/clip_modal_sheet.dart +++ /dev/null @@ -1,187 +0,0 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/clip_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/clip_list_page/clip_settings_dialog.dart'; -import 'package:miria/view/common/error_detail.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -final _notesClipsNotifierProvider = AsyncNotifierProvider.autoDispose - .family<_NotesClipsNotifier, List, (Misskey, String)>( - _NotesClipsNotifier.new, -); - -class _NotesClipsNotifier - extends AutoDisposeFamilyAsyncNotifier, (Misskey, String)> { - @override - Future> build((Misskey, String) arg) async { - final response = await arg.$1.notes.clips( - NotesClipsRequest(noteId: arg.$2), - ); - return response.toList(); - } - - void addClip(Clip clip) { - state = AsyncValue.data([...state.valueOrNull ?? [], clip]); - } - - void removeClip(String clipId) { - state = AsyncValue.data( - (state.valueOrNull ?? []).where((clip) => clip.id != clipId).toList(), - ); - } -} - -final _clipModalSheetNotifierProvider = AsyncNotifierProvider.autoDispose - .family<_ClipModalSheetNotifier, List<(Clip, bool)>, (Misskey, String)>( - _ClipModalSheetNotifier.new, -); - -class _ClipModalSheetNotifier extends AutoDisposeFamilyAsyncNotifier< - List<(Clip, bool)>, (Misskey, String)> { - @override - Future> build((Misskey, String) arg) async { - final [userClips, noteClips] = await Future.wait([ - ref.watch(clipsNotifierProvider(_misskey).future), - ref.watch(_notesClipsNotifierProvider(arg).future), - ]); - return userClips - .map( - (userClip) => ( - userClip, - noteClips.any((noteClip) => noteClip.id == userClip.id) - ), - ) - .toList(); - } - - Misskey get _misskey => arg.$1; - - String get _noteId => arg.$2; - - Future addToClip(Clip clip) async { - await _misskey.clips.addNote( - ClipsAddNoteRequest( - clipId: clip.id, - noteId: _noteId, - ), - ); - ref.read(_notesClipsNotifierProvider(arg).notifier).addClip(clip); - } - - Future removeFromClip(Clip clip) async { - await _misskey.clips.removeNote( - ClipsRemoveNoteRequest( - clipId: clip.id, - noteId: _noteId, - ), - ); - ref.read(_notesClipsNotifierProvider(arg).notifier).removeClip(clip.id); - } -} - -class ClipModalSheet extends ConsumerWidget { - const ClipModalSheet({ - super.key, - required this.account, - required this.noteId, - }); - - final Account account; - final String noteId; - - @override - Widget build(BuildContext context, WidgetRef ref) { - final misskey = ref.watch(misskeyProvider(account)); - final arg = (misskey, noteId); - final state = ref.watch(_clipModalSheetNotifierProvider(arg)); - - Future add(Clip clip) async { - final context = ref.context; - try { - await ref - .read(_clipModalSheetNotifierProvider(arg).notifier) - .addToClip(clip); - } catch (e) { - // TODO: あとでなおす - if (e is DioError && e.response?.data != null) { - if ((e.response?.data as Map?)?["error"]?["code"] == - "ALREADY_CLIPPED") { - if (!context.mounted) return; - final result = await SimpleConfirmDialog.show( - context: context, - message: S.of(context).alreadyAddedClip, - primary: S.of(context).deleteClip, - secondary: S.of(context).noneAction, - ); - if (result == true) { - await ref - .read(_clipModalSheetNotifierProvider(arg).notifier) - .removeFromClip(clip); - } - return; - } - } - - rethrow; - } - } - - return state.when( - data: (data) { - return ListView.builder( - itemCount: data.length + 1, - itemBuilder: (context, index) { - if (index < data.length) { - final (clip, isClipped) = data[index]; - return ListTile( - leading: isClipped - ? const Icon(Icons.check) - : SizedBox(width: Theme.of(context).iconTheme.size), - onTap: () async { - if (isClipped) { - await ref - .read(_clipModalSheetNotifierProvider(arg).notifier) - .removeFromClip(clip) - .expectFailure(context); - } else { - await add(clip).expectFailure(context); - } - }, - title: Text(clip.name ?? ""), - subtitle: Text(clip.description ?? ""), - ); - } else { - return ListTile( - leading: const Icon(Icons.add), - title: Text(S.of(context).createClip), - onTap: () async { - final settings = await showDialog( - context: context, - builder: (context) => ClipSettingsDialog( - title: Text(S.of(context).create), - ), - ); - if (!context.mounted) return; - if (settings != null) { - await ref - .read(clipsNotifierProvider(misskey).notifier) - .create(settings) - .expectFailure(context); - } - }, - ); - } - }, - ); - }, - error: (e, st) => Center(child: ErrorDetail(error: e, stackTrace: st)), - loading: () => const Center(child: CircularProgressIndicator()), - ); - } -} diff --git a/lib/view/common/misskey_notes/custom_emoji.dart b/lib/view/common/misskey_notes/custom_emoji.dart index b051156a9..5154f3975 100644 --- a/lib/view/common/misskey_notes/custom_emoji.dart +++ b/lib/view/common/misskey_notes/custom_emoji.dart @@ -1,12 +1,11 @@ -import 'package:flutter/material.dart'; -import 'package:miria/model/general_settings.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/network_image.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:twemoji_v2/twemoji_v2.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/general_settings.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:twemoji_v2/twemoji_v2.dart"; class CustomEmoji extends ConsumerStatefulWidget { final MisskeyEmojiData emojiData; @@ -17,8 +16,8 @@ class CustomEmoji extends ConsumerStatefulWidget { final bool forceSquare; const CustomEmoji({ - super.key, required this.emojiData, + super.key, this.fontSizeRatio = 1, this.isAttachTooltip = true, this.size, @@ -48,14 +47,14 @@ class CustomEmojiState extends ConsumerState { return Uri( scheme: "https", host: emojiData.isCurrentServer - ? AccountScope.of(context).host + ? ref.read(accountContextProvider).getAccount.host : emojiData.hostedName - .replaceAll(RegExp(r'^\:(.+?)@'), "") + .replaceAll(RegExp(r"^\:(.+?)@"), "") .replaceAll(":", ""), pathSegments: ["proxy", "image.webp"], queryParameters: { "url": Uri.encodeFull(emojiData.url.toString()), - "emoji": "1" + "emoji": "1", }, ); } @@ -101,7 +100,6 @@ class CustomEmojiState extends ConsumerState { height: scopedFontSize, ), ); - break; case UnicodeEmojiData(): switch ( ref.read(generalSettingsRepositoryProvider).settings.emojiType) { @@ -111,27 +109,24 @@ class CustomEmojiState extends ConsumerState { child: Text( emojiData.char, strutStyle: StrutStyle( - height: 1.0, - forceStrutHeight: true, - fontSize: scopedFontSize), + height: 1.0, + forceStrutHeight: true, + fontSize: scopedFontSize, + ), style: style.merge(AppTheme.of(context).unicodeEmojiStyle), ), ); - break; case EmojiType.twemoji: cachedImage = Twemoji( height: scopedFontSize, emoji: emojiData.char, ); - break; } - break; case NotEmojiData(): cachedImage = Text( emojiData.name, style: style, ); - break; } return cachedImage!; } @@ -143,10 +138,10 @@ class ConditionalTooltip extends StatelessWidget { final Widget child; const ConditionalTooltip({ - super.key, required this.isAttachTooltip, required this.message, required this.child, + super.key, }); @override diff --git a/lib/view/common/misskey_notes/in_note_button.dart b/lib/view/common/misskey_notes/in_note_button.dart index b7d347be8..971dce864 100644 --- a/lib/view/common/misskey_notes/in_note_button.dart +++ b/lib/view/common/misskey_notes/in_note_button.dart @@ -1,11 +1,11 @@ -import 'package:flutter/material.dart'; -import 'package:miria/view/themes/app_theme.dart'; +import "package:flutter/material.dart"; +import "package:miria/view/themes/app_theme.dart"; class InNoteButton extends StatelessWidget { final void Function() onPressed; final Widget child; - const InNoteButton({super.key, required this.onPressed, required this.child}); + const InNoteButton({required this.onPressed, required this.child, super.key}); @override Widget build(BuildContext context) { @@ -17,7 +17,8 @@ class InNoteButton extends StatelessWidget { foregroundColor: Theme.of(context).textTheme.bodyMedium?.color, padding: const EdgeInsets.all(5), textStyle: TextStyle( - fontSize: Theme.of(context).textTheme.bodyMedium?.fontSize), + fontSize: Theme.of(context).textTheme.bodyMedium?.fontSize, + ), minimumSize: const Size(double.infinity, 0), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), diff --git a/lib/view/common/misskey_notes/link_navigator.dart b/lib/view/common/misskey_notes/link_navigator.dart index 689c4cf5f..95760cc3b 100644 --- a/lib/view/common/misskey_notes/link_navigator.dart +++ b/lib/view/common/misskey_notes/link_navigator.dart @@ -1,31 +1,37 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:url_launcher/url_launcher.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:url_launcher/url_launcher.dart"; class LinkNavigator { const LinkNavigator(); Future onTapLink( - BuildContext context, WidgetRef ref, String url, String? host) async { + BuildContext context, + WidgetRef ref, + String url, + String? host, + ) async { final uri = Uri.tryParse(url); if (uri == null) { return; //TODO: なおす } - var account = AccountScope.of(context); + var accountContext = ref.read(accountContextProvider); // 他サーバーや外部サイトは別アプリで起動する - if (uri.host != AccountScope.of(context).host) { + if (uri.host != accountContext.getAccount.host) { try { - await ref.read(dioProvider).getUri(Uri( - scheme: "https", - host: uri.host, - pathSegments: [".well-known", "nodeinfo"])); + await ref.read(dioProvider).getUri( + Uri( + scheme: "https", + host: uri.host, + pathSegments: [".well-known", "nodeinfo"], + ), + ); final meta = await ref.read(misskeyWithoutAccountProvider(uri.host)).meta(); final endpoints = @@ -33,13 +39,15 @@ class LinkNavigator { if (!endpoints.contains("emojis")) { throw Exception("Is not misskey"); } - - account = Account.demoAccount(uri.host, meta); + final account = Account.demoAccount(uri.host, meta); + accountContext = accountContext.copyWith(getAccount: account); await ref.read(emojiRepositoryProvider(account)).loadFromSourceIfNeed(); } catch (e) { if (await canLaunchUrl(uri)) { - if (!await launchUrl(uri, - mode: LaunchMode.externalNonBrowserApplication)) { + if (!await launchUrl( + uri, + mode: LaunchMode.externalNonBrowserApplication, + )) { await launchUrl(uri, mode: LaunchMode.externalApplication); } return; @@ -47,30 +55,59 @@ class LinkNavigator { } } - if (uri.pathSegments.length == 2 && uri.pathSegments.first == "clips") { + if (!context.mounted) return; + if (uri.pathSegments.isEmpty) { + await context.pushRoute( + FederationRoute(accountContext: accountContext, host: uri.host), + ); + } else if (uri.pathSegments.length == 2 && + uri.pathSegments.first == "clips") { // クリップはクリップの画面で開く - context.pushRoute( - ClipDetailRoute(account: account, id: uri.pathSegments[1])); + await context.pushRoute( + ClipDetailRoute( + accountContext: accountContext, + id: uri.pathSegments[1], + ), + ); } else if (uri.pathSegments.length == 2 && uri.pathSegments.first == "channels") { - context.pushRoute( - ChannelDetailRoute(account: account, channelId: uri.pathSegments[1])); + await context.pushRoute( + ChannelDetailRoute( + accountContext: accountContext, + channelId: uri.pathSegments[1], + ), + ); } else if (uri.pathSegments.length == 2 && uri.pathSegments.first == "notes") { final note = await ref - .read(misskeyProvider(account)) + .read(misskeyProvider(accountContext.getAccount)) .notes .show(NotesShowRequest(noteId: uri.pathSegments[1])); - context.pushRoute(NoteDetailRoute(account: account, note: note)); + if (!context.mounted) return; + await context.pushRoute( + NoteDetailRoute(accountContext: accountContext, note: note), + ); + } else if (uri.pathSegments.length == 2 && + uri.pathSegments.first == "announcements") { + //TODO: とりあえずはこれでゆるして + await context.pushRoute( + FederationRoute(accountContext: accountContext, host: uri.host), + ); } else if (uri.pathSegments.length == 3 && uri.pathSegments[1] == "pages") { - final page = await ref.read(misskeyProvider(account)).pages.show( - PagesShowRequest( - name: uri.pathSegments[2], - username: uri.pathSegments[0].substring(1))); - context.pushRoute(MisskeyRouteRoute(account: account, page: page)); + final page = + await ref.read(misskeyProvider(accountContext.getAccount)).pages.show( + PagesShowRequest( + name: uri.pathSegments[2], + username: uri.pathSegments[0].substring(1), + ), + ); + if (!context.mounted) return; + await context.pushRoute( + MisskeyRouteRoute(accountContext: accountContext, page: page), + ); } else if (uri.pathSegments.length == 1 && uri.pathSegments.first.startsWith("@")) { - await onMentionTap(context, ref, account, uri.pathSegments.first, host); + await onMentionTap(context, ref, uri.pathSegments.first, host); } else { if (await canLaunchUrl(uri)) { await launchUrl(uri, mode: LaunchMode.externalApplication); @@ -78,14 +115,18 @@ class LinkNavigator { } } - Future onMentionTap(BuildContext context, WidgetRef ref, - Account account, String userName, String? host) async { + Future onMentionTap( + BuildContext context, + WidgetRef ref, + String userName, + String? host, + ) async { // 自分のインスタンスの誰か // 本当は向こうで呼べばいいのでいらないのだけど - final regResult = RegExp(r'^@?(.+?)(@(.+?))?$').firstMatch(userName); - - final contextHost = account.host; - final noteHost = host ?? account.host; + final regResult = RegExp(r"^@?(.+?)(@(.+?))?$").firstMatch(userName); + final accountContext = ref.read(accountContextProvider); + final contextHost = accountContext.getAccount.host; + final noteHost = host ?? accountContext.getAccount.host; final regResultHost = regResult?.group(3); final String? finalHost; @@ -101,10 +142,19 @@ class LinkNavigator { finalHost = noteHost; } - final response = await ref.read(misskeyProvider(account)).users.showByName( - UsersShowByUserNameRequest( - userName: regResult?.group(1) ?? "", host: finalHost)); + final response = await ref + .read(misskeyProvider(accountContext.getAccount)) + .users + .showByName( + UsersShowByUserNameRequest( + userName: regResult?.group(1) ?? "", + host: finalHost, + ), + ); - context.pushRoute(UserRoute(userId: response.id, account: account)); + if (!context.mounted) return; + await context.pushRoute( + UserRoute(userId: response.id, accountContext: accountContext), + ); } } diff --git a/lib/view/common/misskey_notes/link_preview.dart b/lib/view/common/misskey_notes/link_preview.dart index c7bfc2cf4..9c3733d66 100644 --- a/lib/view/common/misskey_notes/link_preview.dart +++ b/lib/view/common/misskey_notes/link_preview.dart @@ -1,20 +1,20 @@ -import 'dart:math'; +import "dart:math"; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/summaly_result.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/common/misskey_notes/link_navigator.dart'; -import 'package:miria/view/common/misskey_notes/player_embed.dart'; -import 'package:miria/view/common/misskey_notes/twitter_embed.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:webview_flutter/webview_flutter.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:cached_network_image/cached_network_image.dart"; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/summaly_result.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/error_dialog_handler.dart"; +import "package:miria/view/common/misskey_notes/link_navigator.dart"; +import "package:miria/view/common/misskey_notes/player_embed.dart"; +import "package:miria/view/common/misskey_notes/twitter_embed.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:webview_flutter/webview_flutter.dart"; final _summalyProvider = AsyncNotifierProvider.family<_Summaly, SummalyResult, (String, String)>( @@ -25,7 +25,7 @@ class _Summaly extends FamilyAsyncNotifier { @override Future build((String, String) arg) async { final (host, link) = arg; - final dio = ref.watch(dioProvider); + final dio = ref.read(dioProvider); final url = Uri.parse(link); // https://github.com/misskey-dev/misskey/blob/2023.9.3/packages/frontend/src/components/MkUrlPreview.vue#L141-L145 final replacedUrl = url @@ -53,10 +53,10 @@ class _Summaly extends FamilyAsyncNotifier { class LinkPreview extends ConsumerWidget { const LinkPreview({ - super.key, required this.account, required this.link, required this.host, + super.key, }); final Account account; @@ -79,10 +79,10 @@ class LinkPreview extends ConsumerWidget { class LinkPreviewItem extends StatefulWidget { const LinkPreviewItem({ - super.key, required this.link, required this.summalyResult, required this.host, + super.key, }); final String link; @@ -156,11 +156,13 @@ class _LinkPreviewItemState extends State { isPlayerOpen = false; }), icon: const Icon(Icons.close), - label: Text(playerUrl != null - ? S.of(context).closePlayer - : S.of(context).closeTweet), + label: Text( + playerUrl != null + ? S.of(context).closePlayer + : S.of(context).closeTweet, + ), ), - ] + ], ], ); } @@ -168,9 +170,9 @@ class _LinkPreviewItemState extends State { class LinkPreviewTile extends ConsumerWidget { const LinkPreviewTile({ - super.key, required this.link, required this.host, + super.key, this.summalyResult = const SummalyResult(player: Player()), }); @@ -192,10 +194,14 @@ class LinkPreviewTile extends ConsumerWidget { onTap: () async => await const LinkNavigator() .onTapLink(context, ref, link, host) .expectFailure(context), - onLongPress: () { - Clipboard.setData(ClipboardData(text: link)); + onLongPress: () async { + await Clipboard.setData(ClipboardData(text: link)); + if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(S.of(context).doneCopy),duration: const Duration(seconds: 1)), + SnackBar( + content: Text(S.of(context).doneCopy), + duration: const Duration(seconds: 1), + ), ); }, child: DecoratedBox( diff --git a/lib/view/common/misskey_notes/local_only_icon.dart b/lib/view/common/misskey_notes/local_only_icon.dart index 662c45253..9785a18eb 100644 --- a/lib/view/common/misskey_notes/local_only_icon.dart +++ b/lib/view/common/misskey_notes/local_only_icon.dart @@ -1,6 +1,5 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; +import "package:flutter/material.dart"; +import "package:flutter_svg/flutter_svg.dart"; class LocalOnlyIcon extends StatelessWidget { final double? size; @@ -10,32 +9,14 @@ class LocalOnlyIcon extends StatelessWidget { @override Widget build(BuildContext context) { - return Stack( - children: [ - Icon( - Icons.rocket, - size: size, - color: color, - ), - Transform.translate( - offset: Offset(3, (size ?? 22) / 2 - 1), - child: Transform.rotate( - angle: 45 * pi / 180, - child: Container( - width: size, - height: size, - decoration: BoxDecoration( - border: Border( - left: BorderSide( - color: color ?? Colors.grey, - width: 2, - ), - ), - ), - ), - ), - ) - ], + return SvgPicture.asset( + "assets/images/unfederate.svg", + colorFilter: ColorFilter.mode( + color ?? const Color(0xff5f6368), + BlendMode.srcIn, + ), + height: size, + width: size, ); } } diff --git a/lib/view/common/misskey_notes/mfm_text.dart b/lib/view/common/misskey_notes/mfm_text.dart index bf5a8272d..5f127083a 100644 --- a/lib/view/common/misskey_notes/mfm_text.dart +++ b/lib/view/common/misskey_notes/mfm_text.dart @@ -1,38 +1,42 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_highlighting/flutter_highlighting.dart'; -import 'package:flutter_highlighting/themes/github-dark.dart'; -import 'package:flutter_highlighting/themes/github.dart'; -import 'package:highlighting/languages/all.dart'; -import 'package:mfm_parser/mfm_parser.dart'; -import 'package:miria/model/general_settings.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/common/misskey_notes/link_navigator.dart'; -import 'package:miria/view/common/misskey_notes/network_image.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:miria/view/common/misskey_notes/custom_emoji.dart'; -import 'package:miria/extensions/date_time_extension.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mfm/mfm.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:twemoji_v2/twemoji_v2.dart'; -import 'package:url_launcher/url_launcher.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/gestures.dart"; +import "package:flutter/material.dart"; +import "package:flutter_highlighting/flutter_highlighting.dart"; +import "package:flutter_highlighting/themes/github-dark.dart"; +import "package:flutter_highlighting/themes/github.dart"; +import "package:highlighting/languages/all.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:mfm/mfm.dart"; +import "package:mfm_parser/mfm_parser.dart"; +import "package:miria/extensions/date_time_extension.dart"; +import "package:miria/model/general_settings.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/misskey_notes/custom_emoji.dart"; +import "package:miria/view/common/misskey_notes/link_navigator.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:twemoji_v2/twemoji_v2.dart"; +import "package:url_launcher/url_launcher.dart"; -InlineSpan _unicodeEmojiBuilder(BuildContext builderContext, String emoji, - TextStyle? style, WidgetRef ref, void Function() onTap) { +InlineSpan _unicodeEmojiBuilder( + BuildContext builderContext, + String emoji, + TextStyle? style, + WidgetRef ref, + void Function() onTap, +) { if (ref.read(generalSettingsRepositoryProvider).settings.emojiType == EmojiType.system) { return TextSpan( - text: emoji, - style: style, - recognizer: MfmBlurScope.of(builderContext) - ? null - : (TapGestureRecognizer()..onTap = () => onTap)); + text: emoji, + style: style, + recognizer: MfmBlurScope.of(builderContext) + ? null + : (TapGestureRecognizer()..onTap = () => onTap), + ); } else { return WidgetSpan( child: GestureDetector( @@ -50,7 +54,7 @@ InlineSpan _unicodeEmojiBuilder(BuildContext builderContext, String emoji, } } -class MfmText extends ConsumerStatefulWidget { +class MfmText extends ConsumerWidget { final String? mfmText; final List? mfmNode; final String? host; @@ -78,42 +82,37 @@ class MfmText extends ConsumerStatefulWidget { this.maxLines, }) : assert(mfmText != null || mfmNode != null); - @override - ConsumerState createState() => MfmTextState(); -} - -class MfmTextState extends ConsumerState { Future onSearch(String query) async { final uri = Uri( - scheme: "https", - host: "google.com", - pathSegments: ["search"], - queryParameters: {"q": query}); - launchUrl(uri); - } - - void onHashtagTap(String hashtag) { - context.pushRoute( - HashtagRoute(account: AccountScope.of(context), hashtag: hashtag)); + scheme: "https", + host: "google.com", + pathSegments: ["search"], + queryParameters: {"q": query}, + ); + await launchUrl(uri); } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Mfm( - mfmText: widget.mfmText, - mfmNode: widget.mfmNode, + mfmText: mfmText, + mfmNode: mfmNode, emojiBuilder: (builderContext, emojiName, style) { final emojiData = MisskeyEmojiData.fromEmojiName( - emojiName: ":$emojiName:", - repository: ref - .read(emojiRepositoryProvider(AccountScope.of(builderContext))), - emojiInfo: widget.emoji); + emojiName: ":$emojiName:", + repository: ref.read( + emojiRepositoryProvider( + ref.read(accountContextProvider).getAccount, + ), + ), + emojiInfo: emoji, + ); return DefaultTextStyle( style: style ?? DefaultTextStyle.of(builderContext).style, child: GestureDetector( onTap: MfmBlurScope.of(builderContext) ? null - : () => widget.onEmojiTap?.call(emojiData), + : () => onEmojiTap?.call(emojiData), child: EmojiInk( child: CustomEmoji( emojiData: emojiData, @@ -129,7 +128,7 @@ class MfmTextState extends ConsumerState { emoji, style, ref, - () => widget.onEmojiTap?.call(UnicodeEmojiData(char: emoji)), + () => onEmojiTap?.call(UnicodeEmojiData(char: emoji)), ), codeBlockBuilder: (context, code, lang) => CodeBlock( code: code, @@ -140,8 +139,9 @@ class MfmTextState extends ConsumerState { alignment: PlaceholderAlignment.middle, child: Container( decoration: BoxDecoration( - border: Border.all(color: Theme.of(context).dividerColor), - borderRadius: BorderRadius.circular(10)), + border: Border.all(color: Theme.of(context).dividerColor), + borderRadius: BorderRadius.circular(10), + ), padding: const EdgeInsets.only(left: 5, right: 5), margin: const EdgeInsets.only(left: 5, right: 5), child: Text.rich( @@ -159,24 +159,31 @@ class MfmTextState extends ConsumerState { monospaceStyle: AppTheme.of(context).monospaceStyle, cursiveStyle: AppTheme.of(context).cursiveStyle, fantasyStyle: AppTheme.of(context).fantasyStyle, - linkTap: (src) => const LinkNavigator() - .onTapLink(context, ref, src, widget.host) - .expectFailure(context), + linkTap: (src) async => + const LinkNavigator().onTapLink(context, ref, src, host), linkStyle: AppTheme.of(context).linkStyle, hashtagStyle: AppTheme.of(context).hashtagStyle, - mentionTap: (userName, host, acct) => const LinkNavigator() - .onMentionTap( - context, ref, AccountScope.of(context), acct, widget.host) - .expectFailure(context), - hashtagTap: onHashtagTap, + mentionTap: (userName, host, acct) async => + const LinkNavigator().onMentionTap( + context, + ref, + acct, + host, + ), + hashtagTap: (hashtag) async => await context.pushRoute( + HashtagRoute( + accountContext: ref.read(accountContextProvider), + hashtag: hashtag, + ), + ), searchTap: onSearch, - style: widget.style, - isNyaize: widget.isNyaize, - suffixSpan: widget.suffixSpan, - prefixSpan: widget.prefixSpan, - isUseAnimation: widget.isEnableAnimatedMFM, + style: style, + isNyaize: isNyaize, + suffixSpan: suffixSpan, + prefixSpan: prefixSpan, + isUseAnimation: isEnableAnimatedMFM, defaultBorderColor: Theme.of(context).primaryColor, - maxLines: widget.maxLines, + maxLines: maxLines, ); } } @@ -186,9 +193,9 @@ class CodeBlock extends StatelessWidget { final String code; const CodeBlock({ + required this.code, super.key, this.language, - required this.code, }); String resolveLanguage(String language) { @@ -206,15 +213,16 @@ class CodeBlock extends StatelessWidget { return SizedBox( width: double.infinity, child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: HighlightView( - code, - languageId: resolvedLanguage, - theme: - AppTheme.of(context).isDarkMode ? githubDarkTheme : githubTheme, - padding: const EdgeInsets.all(10), - textStyle: AppTheme.of(context).monospaceStyle, - )), + scrollDirection: Axis.horizontal, + child: HighlightView( + code, + languageId: resolvedLanguage, + theme: + AppTheme.of(context).isDarkMode ? githubDarkTheme : githubTheme, + padding: const EdgeInsets.all(10), + textStyle: AppTheme.of(context).monospaceStyle, + ), + ), ); } } @@ -222,12 +230,14 @@ class CodeBlock extends StatelessWidget { class EmojiInk extends ConsumerWidget { final Widget child; - const EmojiInk({super.key, required this.child}); + const EmojiInk({required this.child, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final isEnabled = ref.watch(generalSettingsRepositoryProvider - .select((value) => value.settings.enableDirectReaction)); + final isEnabled = ref.watch( + generalSettingsRepositoryProvider + .select((value) => value.settings.enableDirectReaction), + ); if (isEnabled) { return InkWell(child: child); } else { @@ -263,8 +273,11 @@ class SimpleMfmText extends ConsumerWidget { child: CustomEmoji( emojiData: MisskeyEmojiData.fromEmojiName( emojiName: ":$emojiName:", - repository: - ref.read(emojiRepositoryProvider(AccountScope.of(context))), + repository: ref.read( + emojiRepositoryProvider( + ref.read(accountContextProvider).getAccount, + ), + ), emojiInfo: emojis, ), fontSizeRatio: 1, @@ -286,38 +299,33 @@ class SimpleMfmText extends ConsumerWidget { } } -class UserInformation extends ConsumerStatefulWidget { +class UserInformation extends ConsumerWidget { final User user; const UserInformation({ - super.key, required this.user, + super.key, }); - @override - ConsumerState createState() => UserInformationState(); -} - -class UserInformationState extends ConsumerState { - String resolveIconUrl(Uri uri) { + String resolveIconUrl(Uri uri, WidgetRef ref) { final baseUrl = uri.toString(); if (baseUrl.startsWith("/")) { - return "https://${widget.user.host ?? AccountScope.of(context).host}$baseUrl"; + return "https://${user.host ?? ref.read(accountContextProvider).getAccount.host}$baseUrl"; } else { return baseUrl; } } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return SimpleMfmText( - widget.user.name ?? widget.user.username, + user.name ?? user.username, style: Theme.of(context) .textTheme .bodyMedium ?.copyWith(fontWeight: FontWeight.bold), - emojis: widget.user.emojis, + emojis: user.emojis, suffixSpan: [ - for (final badge in widget.user.badgeRoles) + for (final badge in user.badgeRoles) if (badge.iconUrl != null) WidgetSpan( alignment: PlaceholderAlignment.middle, @@ -325,19 +333,19 @@ class UserInformationState extends ConsumerState { message: badge.name, child: NetworkImageView( type: ImageType.role, - url: resolveIconUrl(badge.iconUrl!), - height: (DefaultTextStyle.of(context).style.fontSize ?? 22), + url: resolveIconUrl(badge.iconUrl!, ref), + height: DefaultTextStyle.of(context).style.fontSize ?? 22, loadingBuilder: (context, widget, event) => SizedBox( - width: DefaultTextStyle.of(context).style.fontSize ?? 22, - height: - DefaultTextStyle.of(context).style.fontSize ?? 22), + width: DefaultTextStyle.of(context).style.fontSize ?? 22, + height: DefaultTextStyle.of(context).style.fontSize ?? 22, + ), errorBuilder: (context, e, s) => SizedBox( - width: DefaultTextStyle.of(context).style.fontSize ?? 22, - height: - DefaultTextStyle.of(context).style.fontSize ?? 22), + width: DefaultTextStyle.of(context).style.fontSize ?? 22, + height: DefaultTextStyle.of(context).style.fontSize ?? 22, + ), ), ), - ) + ), ], ); } diff --git a/lib/view/common/misskey_notes/misskey_file_view.dart b/lib/view/common/misskey_notes/misskey_file_view.dart index 2fa01106d..263706420 100644 --- a/lib/view/common/misskey_notes/misskey_file_view.dart +++ b/lib/view/common/misskey_notes/misskey_file_view.dart @@ -1,37 +1,32 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/general_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/image_dialog.dart'; -import 'package:miria/view/common/misskey_notes/in_note_button.dart'; -import 'package:miria/view/common/misskey_notes/network_image.dart'; -import 'package:miria/view/common/misskey_notes/video_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/general_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/misskey_notes/in_note_button.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; +import "package:miria/view/common/note_file_dialog/note_file_dialog.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class MisskeyFileView extends ConsumerStatefulWidget { +class MisskeyFileView extends HookConsumerWidget { final List files; final double height; + final String? noteUrl; const MisskeyFileView({ - super.key, required this.files, + super.key, this.height = 200, + this.noteUrl, }); @override - ConsumerState createState() => MisskeyFileViewState(); -} - -class MisskeyFileViewState extends ConsumerState { - late bool isElipsed = widget.files.length >= 5; - - @override - Widget build(BuildContext context) { - final targetFiles = widget.files; + Widget build(BuildContext context, WidgetRef ref) { + final isElipsed = useState(files.length >= 5); + final targetFiles = files; if (targetFiles.isEmpty) { return Container(); @@ -39,16 +34,20 @@ class MisskeyFileViewState extends ConsumerState { final targetFile = targetFiles.first; return Center( child: ConstrainedBox( - constraints: BoxConstraints( - maxHeight: widget.height, maxWidth: double.infinity), - child: MisskeyImage( - isSensitive: targetFile.isSensitive, - thumbnailUrl: targetFile.thumbnailUrl, - targetFiles: [targetFile.url.toString()], - fileType: targetFile.type, - name: targetFile.name, - position: 0, - )), + constraints: BoxConstraints( + maxHeight: height, + maxWidth: double.infinity, + ), + child: MisskeyImage( + isSensitive: targetFile.isSensitive, + thumbnailUrl: targetFile.thumbnailUrl, + targetFiles: [targetFile], + fileType: targetFile.type, + name: targetFile.name, + position: 0, + noteUrl: noteUrl, + ), + ), ); } else { return Column( @@ -63,259 +62,246 @@ class MisskeyFileViewState extends ConsumerState { children: [ for (final targetFile in targetFiles .mapIndexed( - (index, element) => (element: element, index: index)) - .take(isElipsed ? 4 : targetFiles.length)) + (index, element) => (element: element, index: index), + ) + .take(isElipsed.value ? 4 : targetFiles.length)) SizedBox( - height: widget.height, - width: double.infinity, - child: MisskeyImage( - isSensitive: targetFile.element.isSensitive, - thumbnailUrl: targetFile.element.thumbnailUrl, - targetFiles: targetFiles.map((e) => e.url).toList(), - fileType: targetFile.element.type, - name: targetFile.element.name, - position: targetFile.index, - )) + height: height, + width: double.infinity, + child: MisskeyImage( + isSensitive: targetFile.element.isSensitive, + thumbnailUrl: targetFile.element.thumbnailUrl, + targetFiles: targetFiles, + fileType: targetFile.element.type, + name: targetFile.element.name, + position: targetFile.index, + noteUrl: noteUrl, + ), + ), ], ), - if (isElipsed) + if (isElipsed.value) InNoteButton( - onPressed: () => setState(() { - isElipsed = !isElipsed; - }), - child: Text(S.of(context).showMoreFiles)) + onPressed: () => isElipsed.value = !isElipsed.value, + child: Text(S.of(context).showMoreFiles), + ), ], ); } } } -class MisskeyImage extends ConsumerStatefulWidget { +class MisskeyImage extends HookConsumerWidget { final bool isSensitive; final String? thumbnailUrl; - final List targetFiles; + final List targetFiles; final int position; final String fileType; final String name; + final String? noteUrl; const MisskeyImage({ - super.key, required this.isSensitive, required this.thumbnailUrl, required this.targetFiles, required this.position, required this.fileType, required this.name, + this.noteUrl, + super.key, }); @override - ConsumerState createState() => MisskeyImageState(); -} - -class MisskeyImageState extends ConsumerState { - var nsfwAccepted = false; - Widget? cachedWidget; + Widget build(BuildContext context, WidgetRef ref) { + final initialNsfw = useMemoized(() { + final nsfwSetting = ref.read( + generalSettingsRepositoryProvider + .select((repository) => repository.settings.nsfwInherit), + ); + if (nsfwSetting == NSFWInherit.allHidden) { + // 強制的にNSFW表示 + return false; + } else if (nsfwSetting == NSFWInherit.ignore) { + // 設定を無視 + return true; + } else if (nsfwSetting == NSFWInherit.inherit && !isSensitive) { + // 閲覧注意ではなく、継承する + return true; + } else if (nsfwSetting == NSFWInherit.removeNsfw && !isSensitive) { + return true; + } else { + return false; + } + }); + final nsfwAccepted = useState(initialNsfw); - @override - void didUpdateWidget(covariant MisskeyImage oldWidget) { - super.didUpdateWidget(oldWidget); - if (!const ListEquality() - .equals(oldWidget.targetFiles, widget.targetFiles)) { - cachedWidget = null; - } - } + final nsfwSetting = ref.watch( + generalSettingsRepositoryProvider + .select((repository) => repository.settings.nsfwInherit), + ); - @override - void didChangeDependencies() { - super.didChangeDependencies(); + final delayed = useFuture( + // ignore: discarded_futures + useMemoized( + () => Future.delayed(const Duration(milliseconds: 100)), + ), + ); - final nsfwSetting = ref.read(generalSettingsRepositoryProvider - .select((repository) => repository.settings.nsfwInherit)); - if (nsfwSetting == NSFWInherit.allHidden) { - // 強制的にNSFW表示 - setState(() { - nsfwAccepted = false; - }); - } else if (nsfwSetting == NSFWInherit.ignore) { - // 設定を無視 - setState(() { - nsfwAccepted = true; - }); - } else if (nsfwSetting == NSFWInherit.inherit && !widget.isSensitive) { - // 閲覧注意ではなく、継承する - setState(() { - nsfwAccepted = true; - }); - } else if (nsfwSetting == NSFWInherit.removeNsfw && !widget.isSensitive) { - setState(() { - nsfwAccepted = true; - }); - } - } + if (delayed.connectionState != ConnectionState.done) return Container(); - @override - Widget build(BuildContext context) { - final nsfwSetting = ref.watch(generalSettingsRepositoryProvider - .select((repository) => repository.settings.nsfwInherit)); return Stack( children: [ Align( - child: GestureDetector(onTap: () { - if (!nsfwAccepted) { - setState(() { - nsfwAccepted = true; - }); - return; - } else { - if (widget.fileType.startsWith("image")) { - showDialog( - context: context, - builder: (context) => ImageDialog( - imageUrlList: widget.targetFiles, - initialPage: widget.position, - )); - } else if (widget.fileType.startsWith(RegExp("video|audio"))) { - showDialog( + child: GestureDetector( + onTap: () async { + if (!nsfwAccepted.value) { + nsfwAccepted.value = true; + return; + } else { + await showDialog( context: context, - builder: (context) => VideoDialog( - url: widget.targetFiles[widget.position], - fileType: widget.fileType, + builder: (context) => NoteFileDialog( + driveFiles: targetFiles, + initialPage: position, + noteUrl: noteUrl, ), ); - } else { - launchUrl(Uri.parse(widget.targetFiles[widget.position]), - mode: LaunchMode.externalApplication); } - } - }, child: Builder( - builder: (context) { - if (!nsfwAccepted) { - return Padding( - padding: const EdgeInsets.all(4.0), - child: Container( - decoration: const BoxDecoration(color: Colors.black54), + }, + child: Builder( + builder: (context) { + if (!nsfwAccepted.value) { + return Padding( + padding: const EdgeInsets.all(4.0), + child: Container( + decoration: const BoxDecoration(color: Colors.black54), + width: double.infinity, + child: Center( + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.warning_rounded, + color: Colors.white, + ), + const Padding(padding: EdgeInsets.only(left: 5)), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + S.of(context).sensitive, + style: const TextStyle(color: Colors.white), + ), + Text( + S.of(context).tapToShow, + style: TextStyle( + color: Colors.white, + fontSize: Theme.of(context) + .textTheme + .bodySmall + ?.fontSize, + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + if (fileType.startsWith("image")) { + return SizedBox( + height: 200, + child: NetworkImageView( + url: thumbnailUrl ?? targetFiles[position].url, + type: ImageType.imageThumbnail, + loadingBuilder: (context, widget, chunkEvent) => SizedBox( + width: double.infinity, + height: 200, + child: widget, + ), + ), + ); + } else if (fileType.startsWith(RegExp("video|audio"))) { + return Stack( + children: [ + Positioned.fill( + child: SizedBox( + height: 200, + child: thumbnailUrl != null + ? NetworkImageView( + url: thumbnailUrl!, + type: ImageType.imageThumbnail, + loadingBuilder: + (context, widget, chunkEvent) => SizedBox( + width: double.infinity, + height: 200, + child: widget, + ), + ) + : const SizedBox.shrink(), + ), + ), + const Positioned.fill( + child: Center( + child: Icon(Icons.play_circle, size: 60), + ), + ), + ], + ); + } else { + return Container( + decoration: const BoxDecoration(color: Colors.transparent), width: double.infinity, + height: 200, child: Center( child: Row( mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.warning_rounded, - color: Colors.white), + const Icon( + Icons.file_present, + ), const Padding(padding: EdgeInsets.only(left: 5)), Column( mainAxisSize: MainAxisSize.min, children: [ Text( - S.of(context).sensitive, - style: const TextStyle(color: Colors.white), + name, ), - Text( - S.of(context).tapToShow, - style: TextStyle( - color: Colors.white, - fontSize: Theme.of(context) - .textTheme - .bodySmall - ?.fontSize), - ) ], ), ], ), ), - ), - ); - } - - if (cachedWidget != null) { - return cachedWidget!; - } - - cachedWidget = FutureBuilder( - future: Future.delayed(const Duration(milliseconds: 100)), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - if (widget.fileType.startsWith("image")) { - cachedWidget = SizedBox( - height: 200, - child: NetworkImageView( - url: widget.thumbnailUrl ?? - widget.targetFiles[widget.position], - type: ImageType.imageThumbnail, - loadingBuilder: (context, widget, chunkEvent) => - SizedBox( - width: double.infinity, - height: 200, - child: widget, - ), - ), - ); - } else if (widget.fileType - .startsWith(RegExp("video|audio"))) { - cachedWidget = Stack( - children: [ - Positioned.fill( - child: SizedBox( - height: 200, - child: widget.thumbnailUrl != null - ? NetworkImageView( - url: widget.thumbnailUrl!, - type: ImageType.imageThumbnail, - loadingBuilder: - (context, widget, chunkEvent) => - SizedBox( - width: double.infinity, - height: 200, - child: widget, - ), - ) - : const SizedBox.shrink(), - ), - ), - const Positioned.fill( - child: Center( - child: Icon(Icons.play_circle, size: 60))) - ], - ); - } else { - cachedWidget = TextButton.icon( - onPressed: () { - launchUrl( - Uri.parse(widget.targetFiles[widget.position]), - mode: LaunchMode.externalApplication); - }, - icon: const Icon(Icons.file_download_outlined), - label: Text(widget.name)); - } - - return cachedWidget!; - } - return Container(); - }, - ); - return cachedWidget!; - }, - )), + ); + } + }, + ), + ), ), - if (widget.isSensitive && + if (isSensitive && (nsfwSetting == NSFWInherit.ignore || nsfwSetting == NSFWInherit.allHidden)) Positioned( - left: 5, - top: 5, - child: DecoratedBox( - decoration: BoxDecoration( - color: Theme.of(context).primaryColor.withAlpha(140), - border: Border.all(color: Colors.transparent), - borderRadius: BorderRadius.circular(3)), - child: Padding( - padding: const EdgeInsets.only(left: 2, right: 2), - child: Text( - S.of(context).sensitive, - style: TextStyle(color: Colors.white.withAlpha(170)), - ), + left: 5, + top: 5, + child: DecoratedBox( + decoration: BoxDecoration( + color: Theme.of(context).primaryColor.withAlpha(140), + border: Border.all(color: Colors.transparent), + borderRadius: BorderRadius.circular(3), + ), + child: Padding( + padding: const EdgeInsets.only(left: 2, right: 2), + child: Text( + S.of(context).sensitive, + style: TextStyle(color: Colors.white.withAlpha(170)), ), - )), + ), + ), + ), ], ); } diff --git a/lib/view/common/misskey_notes/misskey_note.dart b/lib/view/common/misskey_notes/misskey_note.dart index c8f3dded0..6de90fb40 100644 --- a/lib/view/common/misskey_notes/misskey_note.dart +++ b/lib/view/common/misskey_notes/misskey_note.dart @@ -1,82 +1,66 @@ -import 'dart:math'; - -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:dotted_border/dotted_border.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mfm_parser/mfm_parser.dart' as parser; -import 'package:miria/const.dart'; -import 'package:miria/extensions/date_time_extension.dart'; -import 'package:miria/extensions/list_mfm_node_extension.dart'; -import 'package:miria/extensions/note_extension.dart'; -import 'package:miria/extensions/note_visibility_extension.dart'; -import 'package:miria/extensions/user_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/avatar_icon.dart'; -import 'package:miria/view/common/constants.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/common/misskey_notes/in_note_button.dart'; -import 'package:miria/view/common/misskey_notes/link_preview.dart'; -import 'package:miria/view/common/misskey_notes/local_only_icon.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:miria/view/common/misskey_notes/misskey_file_view.dart'; -import 'package:miria/view/common/misskey_notes/note_modal_sheet.dart'; -import 'package:miria/view/common/misskey_notes/note_vote.dart'; -import 'package:miria/view/common/misskey_notes/reaction_button.dart'; -import 'package:miria/view/common/misskey_notes/renote_modal_sheet.dart'; -import 'package:miria/view/common/misskey_notes/renote_user_dialog.dart'; -import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; -import 'package:miria/view/reaction_picker_dialog/reaction_picker_dialog.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class MisskeyNote extends ConsumerStatefulWidget { +import "dart:math"; + +import "package:auto_route/auto_route.dart"; +import "package:collection/collection.dart"; +import "package:dotted_border/dotted_border.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:mfm_parser/mfm_parser.dart" as parser; +import "package:miria/const.dart"; +import "package:miria/extensions/date_time_extension.dart"; +import "package:miria/extensions/list_mfm_node_extension.dart"; +import "package:miria/extensions/note_extension.dart"; +import "package:miria/extensions/note_visibility_extension.dart"; +import "package:miria/extensions/user_extension.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/state_notifier/common/misskey_notes/misskey_note_notifier.dart"; +import "package:miria/view/common/avatar_icon.dart"; +import "package:miria/view/common/constants.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:miria/view/common/misskey_notes/in_note_button.dart"; +import "package:miria/view/common/misskey_notes/link_preview.dart"; +import "package:miria/view/common/misskey_notes/local_only_icon.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:miria/view/common/misskey_notes/misskey_file_view.dart"; +import "package:miria/view/common/misskey_notes/note_vote.dart"; +import "package:miria/view/common/misskey_notes/reaction_button.dart"; +import "package:miria/view/dialogs/simple_confirm_dialog.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:misskey_dart/misskey_dart.dart"; + +class MisskeyNote extends HookConsumerWidget { final Note note; final bool isDisplayBorder; final int recursive; - final Account? loginAs; final bool isForceUnvisibleReply; final bool isForceUnvisibleRenote; final bool isVisibleAllReactions; final bool isForceVisibleLong; const MisskeyNote({ - super.key, required this.note, + super.key, this.isDisplayBorder = true, this.recursive = 1, - this.loginAs, this.isForceUnvisibleReply = false, this.isForceUnvisibleRenote = false, this.isVisibleAllReactions = false, this.isForceVisibleLong = false, }); - @override - ConsumerState createState() => MisskeyNoteState(); -} - -class MisskeyNoteState extends ConsumerState { - final globalKey = GlobalKey(); - late var isAllReactionVisible = widget.isVisibleAllReactions; - bool isLongVisibleInitialized = false; - - List? displayTextNodes; - DateTime? latestUpdatedAt; - bool shouldCollaposed(List node) { final result = nodeMaxTextLength(node); return result.$1 >= 500 || result.$2 >= 6; } (int length, int newLinesCount) nodeMaxTextLength( - List nodes) { + List nodes, + ) { var thisNodeCount = 0; var newLinesCount = 0; for (final node in nodes) { @@ -104,656 +88,773 @@ class MisskeyNoteState extends ConsumerState { return (thisNodeCount, newLinesCount); } - @override - Widget build(BuildContext context) { - final account = AccountScope.of(context); + Future reactionControl( + WidgetRef ref, + BuildContext context, + Note displayNote, { + MisskeyEmojiData? requestEmoji, + }) async { + // 他のサーバーからログインしている場合は不可 + if (!ref.read(accountContextProvider).isSame) return; - final latestActualNote = ref.watch( - notesProvider(account).select((value) => value.notes[widget.note.id])); - final renoteId = widget.note.renote?.id; - final Note? renoteNote; + final account = ref.read(accountContextProvider).postAccount; + final isLikeOnly = + displayNote.reactionAcceptance == ReactionAcceptance.likeOnly || + (displayNote.reactionAcceptance == + ReactionAcceptance.likeOnlyForRemote && + displayNote.user.host != null); + // すでにリアクション済み + if (displayNote.myReaction != null && requestEmoji != null) { + return; + } - bool isEmptyRenote = latestActualNote?.isEmptyRenote == true; + // カスタム絵文字押下でのリアクション無効 + if (requestEmoji != null && + !ref + .read(generalSettingsRepositoryProvider) + .settings + .enableDirectReaction) { + return; + } - if (isEmptyRenote) { - renoteNote = ref.watch( - notesProvider(account).select((value) => value.notes[renoteId])); + // いいねのみでカスタム絵文字押下 + if (requestEmoji != null && isLikeOnly) { + return; + } + if (displayNote.myReaction != null && requestEmoji == null) { + if (await SimpleConfirmDialog.show( + context: context, + message: S.of(context).confirmDeleteReaction, + primary: S.of(context).cancelReaction, + secondary: S.of(context).cancel, + ) != + true) { + return; + } + + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + await ref + .read(misskeyPostContextProvider) + .notes + .reactions + .delete(NotesReactionsDeleteRequest(noteId: displayNote.id)); + if (account.host == "misskey.io" || account.host == "nijimiss.moe") { + await Future.delayed( + const Duration(milliseconds: misskeyHQReactionDelay), + ); + } + await ref.read(notesProvider(account)).refresh(displayNote.id); + return; + }); + } + final misskey = ref.read(misskeyPostContextProvider); + final note = ref.read(notesProvider(account)); + + final MisskeyEmojiData selectedEmoji; + if (isLikeOnly) { + selectedEmoji = const UnicodeEmojiData(char: "❤️"); + } else if (requestEmoji == null) { + final dialogResult = + await ref.read(appRouterProvider).push( + ReactionPickerRoute( + account: account, + isAcceptSensitive: displayNote.reactionAcceptance != + ReactionAcceptance.nonSensitiveOnly && + displayNote.reactionAcceptance != + ReactionAcceptance + .nonSensitiveOnlyForLocalLikeOnlyForRemote, + ), + ); + if (dialogResult == null) return; + selectedEmoji = dialogResult; } else { - renoteNote = null; + selectedEmoji = requestEmoji; } - final displayNote = renoteNote ?? latestActualNote; - if (displayNote == null || latestActualNote == null) { - // 削除された? - return Container(); + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + await misskey.notes.reactions.create( + NotesReactionsCreateRequest( + noteId: displayNote.id, + reaction: ":${selectedEmoji.baseName}:", + ), + ); + }); + if (account.host == "misskey.io") { + await Future.delayed( + const Duration(milliseconds: misskeyHQReactionDelay), + ); } + await note.refresh(displayNote.id); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isAllReactionVisible = useState(isVisibleAllReactions); + final globalKey = useState(GlobalKey()); - if (widget.recursive == 3) { + final account = ref.read(accountContextProvider).getAccount; + final isPostAccountContext = ref.read(accountContextProvider).isSame; + + final latestActualNote = ref.watch( + notesProvider(account).select((value) => value.notes[note.id]), + ); + final renoteId = note.renote?.id; + + final isEmptyRenote = latestActualNote?.isEmptyRenote == true; + final renoteNote = isEmptyRenote + ? ref.watch( + notesProvider(account).select((value) => value.notes[renoteId]), + ) + : null; + + final displayNote = renoteNote ?? latestActualNote; + + // 削除された? + if (displayNote == null || latestActualNote == null) { return Container(); } - if (latestUpdatedAt != displayNote.updatedAt) { - latestUpdatedAt = displayNote.updatedAt; - displayTextNodes = null; - } + if (recursive == 3) return Container(); - displayTextNodes ??= const parser.MfmParser().parse(displayNote.text ?? ""); + final displayTextNodes = useMemoized( + () => const parser.MfmParser().parse(displayNote.text ?? ""), + [displayNote.updatedAt, displayNote.text], + ); - final noteStatus = ref.watch(notesProvider(account) - .select((value) => value.noteStatuses[widget.note.id]))!; + final noteStatus = ref.watch( + notesProvider(account).select((value) => value.noteStatuses[note.id]), + )!; if (noteStatus.isIncludeMuteWord && !noteStatus.isMuteOpened) { return SizedBox( width: double.infinity, child: GestureDetector( - onTap: () => ref.read(notesProvider(account)).updateNoteStatus( - widget.note.id, - (status) => status.copyWith(isMuteOpened: true), - ), - child: Padding( - padding: const EdgeInsets.only(top: 5.0, bottom: 5.0, left: 10.0), - child: Text( - S.of(context).mutedNotePlaceholder( - displayNote.user.name ?? displayNote.user.username), - style: Theme.of(context).textTheme.bodySmall, + onTap: () => ref.read(notesProvider(account)).updateNoteStatus( + note.id, + (status) => status.copyWith(isMuteOpened: true), ), - )), + child: Padding( + padding: const EdgeInsets.only(top: 5.0, bottom: 5.0, left: 10.0), + child: Text( + S.of(context).mutedNotePlaceholder( + displayNote.user.name ?? displayNote.user.username, + ), + style: Theme.of(context).textTheme.bodySmall, + ), + ), + ), ); } - if (!noteStatus.isLongVisibleInitialized || - widget.isForceUnvisibleRenote || - widget.isForceUnvisibleReply || - widget.isForceVisibleLong) { - final isReactionedRenote = ref - .read(generalSettingsRepositoryProvider) - .settings - .enableFavoritedRenoteElipsed && - !widget.isForceVisibleLong && - !(displayNote.cw?.isNotEmpty == true) && - (renoteId != null && displayNote.myReaction != null); - - final isLongVisible = !(ref - .read(generalSettingsRepositoryProvider) - .settings - .enableLongTextElipsed && - !isReactionedRenote && - !widget.isForceVisibleLong && - !(displayNote.cw?.isNotEmpty == true) && - shouldCollaposed(displayTextNodes!)); + // 初期化処理 + useMemoized( + () { + if (!noteStatus.isLongVisibleInitialized || + isForceUnvisibleRenote || + isForceUnvisibleReply || + isForceVisibleLong) { + final isReactionedRenote = ref + .read(generalSettingsRepositoryProvider) + .settings + .enableFavoritedRenoteElipsed && + !isForceVisibleLong && + !(displayNote.cw?.isNotEmpty == true) && + (renoteId != null && displayNote.myReaction != null); - ref.read(notesProvider(account)).updateNoteStatus( - widget.note.id, - (status) => status.copyWith( - isLongVisible: isLongVisible, - isReactionedRenote: isReactionedRenote, - isLongVisibleInitialized: true, - ), - isNotify: false, - ); - } + final isLongVisible = !(ref + .read(generalSettingsRepositoryProvider) + .settings + .enableLongTextElipsed && + !isReactionedRenote && + !isForceVisibleLong && + !(displayNote.cw?.isNotEmpty == true) && + shouldCollaposed(displayTextNodes)); + + ref.read(notesProvider(account)).updateNoteStatus( + note.id, + (status) => status.copyWith( + isLongVisible: isLongVisible, + isReactionedRenote: isReactionedRenote, + isLongVisibleInitialized: true, + ), + isNotify: false, + ); + } + }, + [note], + ); final userId = "@${displayNote.user.username}${displayNote.user.host == null ? "" : "@${displayNote.user.host}"}"; - final isCwOpened = ref.watch(notesProvider(account) - .select((value) => value.noteStatuses[widget.note.id]!.isCwOpened)); - final isReactionedRenote = ref.watch(notesProvider(account).select( - (value) => value.noteStatuses[widget.note.id]!.isReactionedRenote)); - final isLongVisible = ref.watch(notesProvider(account) - .select((value) => value.noteStatuses[widget.note.id]!.isLongVisible)); + final isCwOpened = ref.watch( + notesProvider(account) + .select((value) => value.noteStatuses[note.id]!.isCwOpened), + ); + final isReactionedRenote = ref.watch( + notesProvider(account).select( + (value) => value.noteStatuses[note.id]!.isReactionedRenote, + ), + ); + final isLongVisible = ref.watch( + notesProvider(account) + .select((value) => value.noteStatuses[note.id]!.isLongVisible), + ); + + final links = + useMemoized(() => displayTextNodes.extractLinks(), displayTextNodes); - final links = displayTextNodes!.extractLinks(); + final displayReactions = useMemoized( + () { + return displayNote.reactions.entries + .mapIndexed((i, e) => (index: i, element: e)) + .sorted((a, b) { + final primary = b.element.value.compareTo(a.element.value); + if (primary != 0) return primary; + return a.index.compareTo(b.index); + }).take( + isAllReactionVisible.value ? displayNote.reactions.length : 16, + ); + }, + [displayNote.reactions, isAllReactionVisible.value], + ); - return MediaQuery( - data: MediaQuery.of(context).copyWith( - textScaler: widget.recursive > 1 - ? TextScaler.linear(MediaQuery.textScalerOf(context).scale(0.7)) - : null, - ), - child: RepaintBoundary( - key: globalKey, - child: Align( - alignment: Alignment.center, - child: Container( - constraints: const BoxConstraints(maxWidth: 800), - margin: EdgeInsets.only( - left: displayNote.channel?.color != null ? 5.0 : 0.0), - padding: EdgeInsets.only( - top: MediaQuery.textScalerOf(context).scale(5), - bottom: MediaQuery.textScalerOf(context).scale(5), - left: displayNote.channel?.color != null ? 4.0 : 0.0, + final reactionControl = + useCallback Function({MisskeyEmojiData? requestEmoji})>( + ({ + requestEmoji, + }) async { + // 他のサーバーからログインしている場合は不可 + if (!ref.read(accountContextProvider).isSame) return; + + final account = ref.read(accountContextProvider).postAccount; + final isLikeOnly = + displayNote.reactionAcceptance == ReactionAcceptance.likeOnly || + (displayNote.reactionAcceptance == + ReactionAcceptance.likeOnlyForRemote && + displayNote.user.host != null); + // すでにリアクション済み + if (displayNote.myReaction != null && requestEmoji != null) return; + + // すでにリアクション済みで、リアクション取り消し + if (displayNote.myReaction != null) { + final dialogValue = + await ref.read(dialogStateNotifierProvider.notifier).showDialog( + message: (context) => S.of(context).confirmDeleteReaction, + actions: (context) => [ + S.of(context).cancelReaction, + S.of(context).cancel, + ], + ); + if (dialogValue != 0) return; + + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + await ref + .read(misskeyPostContextProvider) + .notes + .reactions + .delete(NotesReactionsDeleteRequest(noteId: displayNote.id)); + if (account.host == "misskey.io" || + account.host == "nijimiss.moe") { + await Future.delayed( + const Duration(milliseconds: misskeyHQReactionDelay), + ); + } + + await ref.read(notesWithProvider).refresh(displayNote.id); + }); + + return; + } + + // カスタム絵文字押下でのリアクション無効 + if (requestEmoji != null && + !ref + .read(generalSettingsRepositoryProvider) + .settings + .enableDirectReaction) { + return; + } + // いいねのみでカスタム絵文字押下 + if (requestEmoji != null && isLikeOnly) return; + if (displayNote.myReaction != null && requestEmoji == null) { + if (await SimpleConfirmDialog.show( + context: context, + message: S.of(context).confirmDeleteReaction, + primary: S.of(context).cancelReaction, + secondary: S.of(context).cancel, + ) != + true) { + return; + } + + await ref + .read(misskeyPostContextProvider) + .notes + .reactions + .delete(NotesReactionsDeleteRequest(noteId: displayNote.id)); + if (account.host == "misskey.io") { + await Future.delayed( + const Duration(milliseconds: misskeyHQReactionDelay), + ); + } + await ref.read(notesProvider(account)).refresh(displayNote.id); + return; + } + final misskey = ref.read(misskeyPostContextProvider); + final note = ref.read(notesProvider(account)); + final MisskeyEmojiData? selectedEmoji; + if (isLikeOnly) { + selectedEmoji = const UnicodeEmojiData(char: "❤️"); + } else if (requestEmoji == null) { + selectedEmoji = await ref.read(appRouterProvider).push( + ReactionPickerRoute( + account: account, + isAcceptSensitive: displayNote.reactionAcceptance != + ReactionAcceptance.nonSensitiveOnly && + displayNote.reactionAcceptance != + ReactionAcceptance + .nonSensitiveOnlyForLocalLikeOnlyForRemote, + ), + ); + } else { + selectedEmoji = requestEmoji; + } + + if (selectedEmoji == null) return; + await misskey.notes.reactions.create( + NotesReactionsCreateRequest( + noteId: displayNote.id, + reaction: ":${selectedEmoji.baseName}:", + ), + ); + if (account.host == "misskey.io") { + await Future.delayed( + const Duration(milliseconds: misskeyHQReactionDelay), + ); + } + await note.refresh(displayNote.id); + }, + [displayNote], + ); + + final toggleReactionedRenote = useCallback(() { + ref.read(notesProvider(account)).updateNoteStatus( + note.id, + (status) => status.copyWith( + isReactionedRenote: !status.isReactionedRenote, + ), + ); + }); + final toggleLongNote = useCallback(() { + ref.read(notesProvider(account)).updateNoteStatus( + note.id, + (status) => status.copyWith( + isLongVisible: !status.isLongVisible, + ), + ); + }); + final toggleCwOpen = useCallback(() { + ref.read(notesProvider(account)).updateNoteStatus( + note.id, + (status) => status.copyWith( + isCwOpened: !status.isCwOpened, ), - decoration: widget.isDisplayBorder - ? BoxDecoration( - color: widget.recursive == 1 && - ref.read(noteModalSheetSharingModeProviding) - ? Theme.of(context).scaffoldBackgroundColor - : null, - border: Border( - left: displayNote.channel?.color != null - ? BorderSide( - color: Color( - 0xFF000000 | displayNote.channel!.color!), - width: 4) - : BorderSide.none, - bottom: BorderSide( - color: Theme.of(context).dividerColor, - width: 0.5, + ); + }); + + final buildParent = useCallback( + ({required child}) { + return MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: recursive > 1 + ? TextScaler.linear(MediaQuery.textScalerOf(context).scale(0.7)) + : null, + ), + child: RepaintBoundary( + key: globalKey.value, + child: Align( + alignment: Alignment.center, + child: Container( + constraints: const BoxConstraints(maxWidth: 800), + padding: EdgeInsets.only( + top: MediaQuery.textScalerOf(context).scale(5), + bottom: MediaQuery.textScalerOf(context).scale(5), + ), + decoration: isDisplayBorder + ? BoxDecoration( + //TODO: 動いていないっぽい + // color: widget.recursive == 1 && + // ref.read(noteModalSheetSharingModeProviding) + // ? Theme.of(context).scaffoldBackgroundColor + // : null, + border: Border( + bottom: BorderSide( + color: Theme.of(context).dividerColor, + width: 0.5, + ), + ), + ) + : BoxDecoration( + color: recursive == 1 + ? Theme.of(context).scaffoldBackgroundColor + : null, ), - ), - ) - : BoxDecoration( - color: widget.recursive == 1 - ? Theme.of(context).scaffoldBackgroundColor - : null), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, + child: child, + ), + ), + ), + ); + }, + [recursive, globalKey.value, displayNote.channel?.color], + ); + + return buildParent( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (isEmptyRenote) + Padding( + padding: const EdgeInsets.only(bottom: 2), + child: ChannelColorBarBox( + note: note, + child: RenoteHeader(note: note), + ), + ), + if (displayNote.reply != null && !isForceUnvisibleReply) + ChannelColorBarBox( + note: note, + child: MisskeyNote( + note: displayNote.reply!, + isDisplayBorder: false, + recursive: recursive + 1, + ), + ), + ChannelColorBarBox( + note: displayNote, + hideColorBar: !isDisplayBorder, + child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (isEmptyRenote) - Padding( - padding: const EdgeInsets.only(bottom: 2), - child: RenoteHeader( - note: widget.note, - loginAs: widget.loginAs, - ), - ), - if (displayNote.reply != null && !widget.isForceUnvisibleReply) - MisskeyNote( - note: displayNote.reply!, - isDisplayBorder: false, - recursive: widget.recursive + 1, - ), - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - AvatarIcon( - user: displayNote.user, - onTap: () => ref - .read(misskeyNoteNotifierProvider(account).notifier) - .navigateToUserPage( - context, - displayNote.user, - widget.loginAs, - ) - .expectFailure(context), - ), - const Padding(padding: EdgeInsets.only(left: 10)), - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, + AvatarIcon( + user: displayNote.user, + onTap: () async => ref + .read(misskeyNoteNotifierProvider.notifier) + .navigateToUserPage(displayNote.user), + ), + const Padding(padding: EdgeInsets.only(left: 10)), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NoteHeader1(displayNote: displayNote), + Row( children: [ - NoteHeader1( - displayNote: displayNote, - loginAs: widget.loginAs, + Expanded( + child: Text( + userId, + style: Theme.of(context).textTheme.bodySmall, + maxLines: 1, + overflow: TextOverflow.clip, + ), ), - Row( - children: [ - Expanded( + if (displayNote.user.instance != null) + GestureDetector( + onTap: () async => context.pushRoute( + FederationRoute( + accountContext: + ref.read(accountContextProvider), + host: displayNote.user.host!, + ), + ), + child: InkResponse( child: Text( - userId, + displayNote.user.instance?.name ?? "", style: Theme.of(context).textTheme.bodySmall, - maxLines: 1, - overflow: TextOverflow.clip, ), ), - if (displayNote.user.instance != null) - GestureDetector( - onTap: () => context.pushRoute( - FederationRoute( - account: widget.loginAs ?? account, - host: displayNote.user.host!)), - child: InkResponse( - child: Text( - displayNote.user.instance?.name ?? "", - style: Theme.of(context) - .textTheme - .bodySmall), - ), + ), + ], + ), + if (displayNote.cw != null) ...[ + MfmText( + mfmText: displayNote.cw ?? "", + host: displayNote.user.host, + emoji: displayNote.emojis, + isEnableAnimatedMFM: ref + .read(generalSettingsRepositoryProvider) + .settings + .enableAnimatedMFM, + ), + InNoteButton( + onPressed: toggleCwOpen, + child: Text( + isCwOpened + ? S.of(context).hide + : S.of(context).showCw, + ), + ), + ], + if (displayNote.cw == null || + displayNote.cw != null && isCwOpened) ...[ + if (isReactionedRenote) + SimpleMfmText( + "${(displayNote.text ?? "").substring(0, min((displayNote.text ?? "").length, 50))}..." + .replaceAll("\n\n", "\n"), + isNyaize: displayNote.user.isCat, + emojis: displayNote.emojis, + suffixSpan: [ + WidgetSpan( + child: InNoteButton( + onPressed: toggleReactionedRenote, + child: Text(S.of(context).showReactionedNote), ), + ), ], - ), - if (displayNote.cw != null) ...[ + ) + else ...[ + if (isLongVisible) MfmText( - mfmText: displayNote.cw ?? "", + mfmNode: displayTextNodes, host: displayNote.user.host, emoji: displayNote.emojis, + isNyaize: displayNote.user.isCat, isEnableAnimatedMFM: ref .read(generalSettingsRepositoryProvider) .settings .enableAnimatedMFM, - ), - InNoteButton( - onPressed: () { - ref - .read(notesProvider(account)) - .updateNoteStatus( - widget.note.id, - (status) => status.copyWith( - isCwOpened: !status.isCwOpened)); - }, - child: Text( - isCwOpened - ? S.of(context).hide - : S.of(context).showCw, - ), - ), - ], - if (displayNote.cw == null || - displayNote.cw != null && isCwOpened) ...[ - if (isReactionedRenote) - SimpleMfmText( - "${(displayNote.text ?? "").substring(0, min((displayNote.text ?? "").length, 50))}..." - .replaceAll("\n\n", "\n"), - isNyaize: displayNote.user.isCat, - emojis: displayNote.emojis, - suffixSpan: [ - WidgetSpan( - child: InNoteButton( - onPressed: () { - ref - .read(notesProvider(account)) - .updateNoteStatus( - widget.note.id, - (status) => status.copyWith( - isReactionedRenote: !status - .isReactionedRenote), - ); - }, - child: Text( - S.of(context).showReactionedNote), - ), - ) - ], - ) - else ...[ - if (isLongVisible) - MfmText( - mfmNode: displayTextNodes, - host: displayNote.user.host, - emoji: displayNote.emojis, - isNyaize: displayNote.user.isCat, - isEnableAnimatedMFM: ref - .read(generalSettingsRepositoryProvider) - .settings - .enableAnimatedMFM, - onEmojiTap: (emojiData) async => - await reactionControl( - ref, context, displayNote, - requestEmoji: emojiData), - suffixSpan: [ - if (!isEmptyRenote && - displayNote.renoteId != null && - (widget.recursive == 2 || - widget.isForceUnvisibleRenote)) - TextSpan( - text: " RN:...", - style: TextStyle( - color: Theme.of(context).primaryColor, - fontStyle: FontStyle.italic, - ), - ), - ], - ) - else - SimpleMfmText( - "${(displayNote.text ?? "").substring(0, min((displayNote.text ?? "").length, 150))}..." - .replaceAll("\n\n", "\n"), - emojis: displayNote.emojis, - isNyaize: displayNote.user.isCat, - suffixSpan: [ - WidgetSpan( - child: InNoteButton( - onPressed: () { - ref - .read(notesProvider(account)) - .updateNoteStatus( - widget.note.id, - (status) => status.copyWith( - isLongVisible: !status - .isLongVisible)); - }, - child: Text(S.of(context).showLongText), - ), - ) - ], - ), - MisskeyFileView( - files: displayNote.files, - height: 200 * - pow(0.5, widget.recursive - 1).toDouble(), + onEmojiTap: (emojiData) async => + await reactionControl( + requestEmoji: emojiData, ), - if (displayNote.poll != null) - NoteVote( - displayNote: displayNote, - poll: displayNote.poll!, - loginAs: widget.loginAs, - ), - if (isLongVisible && widget.recursive < 2) - ...links.map( - (link) => LinkPreview( - account: account, - link: link, - host: displayNote.user.host), - ), - if (displayNote.renoteId != null && - (widget.recursive < 2 && - !widget.isForceUnvisibleRenote)) - Container( - padding: const EdgeInsets.all(5), - child: DottedBorder( - color: - AppTheme.of(context).renoteBorderColor, - radius: - AppTheme.of(context).renoteBorderRadius, - strokeWidth: - AppTheme.of(context).renoteStrokeWidth, - dashPattern: - AppTheme.of(context).renoteDashPattern, - child: Padding( - padding: const EdgeInsets.all(5), - child: MisskeyNote( - note: displayNote.renote!, - isDisplayBorder: false, - recursive: widget.recursive + 1, - loginAs: widget.loginAs, - ), + suffixSpan: [ + if (!isEmptyRenote && + displayNote.renoteId != null && + (recursive == 2 || isForceUnvisibleRenote)) + TextSpan( + text: " RN:...", + style: TextStyle( + color: Theme.of(context).primaryColor, + fontStyle: FontStyle.italic, ), ), - ) - ] - ], - if (displayNote.reactions.isNotEmpty && - !isReactionedRenote) - const Padding(padding: EdgeInsets.only(bottom: 5)), - if (!isReactionedRenote) - Wrap( - spacing: - MediaQuery.textScalerOf(context).scale(5), - runSpacing: - MediaQuery.textScalerOf(context).scale(5), - children: [ - for (final reaction in displayNote - .reactions.entries - .mapIndexed((index, element) => - (index: index, element: element)) - .sorted((a, b) { - final primary = b.element.value - .compareTo(a.element.value); - if (primary != 0) return primary; - return a.index.compareTo(b.index); - }).take(isAllReactionVisible - ? displayNote.reactions.length - : 16)) - ReactionButton( - emojiData: MisskeyEmojiData.fromEmojiName( - emojiName: reaction.element.key, - repository: ref.read( - emojiRepositoryProvider(account)), - emojiInfo: displayNote.reactionEmojis), - reactionCount: reaction.element.value, - myReaction: displayNote.myReaction, - noteId: displayNote.id, - loginAs: widget.loginAs, + ], + ) + else + SimpleMfmText( + "${(displayNote.text ?? "").substring(0, min((displayNote.text ?? "").length, 150))}..." + .replaceAll("\n\n", "\n"), + emojis: displayNote.emojis, + isNyaize: displayNote.user.isCat, + suffixSpan: [ + WidgetSpan( + child: InNoteButton( + onPressed: toggleLongNote, + child: Text(S.of(context).showLongText), ), - if (!isAllReactionVisible && - displayNote.reactions.length > 16) - OutlinedButton( - style: AppTheme.of(context) - .reactionButtonStyle, - onPressed: () => setState(() { - isAllReactionVisible = true; - }), - child: Text(S.of(context).otherReactions( - displayNote.reactions.length - 16))), + ), ], ), - if (displayNote.channel != null) - NoteChannelView(channel: displayNote.channel!), - if (!isReactionedRenote) - Row( - mainAxisAlignment: widget.loginAs != null - ? MainAxisAlignment.end - : MainAxisAlignment.spaceAround, - mainAxisSize: MainAxisSize.max, - children: [ - if (widget.loginAs != null) ...[ - IconButton( - constraints: const BoxConstraints(), - padding: EdgeInsets.zero, - style: const ButtonStyle( - padding: MaterialStatePropertyAll( - EdgeInsets.zero), - minimumSize: MaterialStatePropertyAll( - Size(0, 0)), - tapTargetSize: - MaterialTapTargetSize.shrinkWrap, - ), - onPressed: () => ref - .read( - misskeyNoteNotifierProvider(account) - .notifier, - ) - .navigateToNoteDetailPage( - context, - displayNote, - widget.loginAs, - ) - .expectFailure(context), - icon: Icon( - Icons.u_turn_left, - size: MediaQuery.textScalerOf(context) - .scale(16), - color: Theme.of(context) - .textTheme - .bodySmall - ?.color, - )) - ] else ...[ - TextButton.icon( - onPressed: () { - context.pushRoute(NoteCreateRoute( - reply: displayNote, - initialAccount: account)); - }, - style: const ButtonStyle( - padding: MaterialStatePropertyAll( - EdgeInsets.zero), - minimumSize: - MaterialStatePropertyAll(Size(0, 0)), - tapTargetSize: - MaterialTapTargetSize.shrinkWrap, - ), - label: Text( - displayNote.repliesCount == 0 - ? "" - : displayNote.repliesCount.format(), - ), - icon: Icon( - Icons.reply, - size: MediaQuery.textScalerOf(context) - .scale(16), - color: Theme.of(context) - .textTheme - .bodySmall - ?.color, - ), + MisskeyFileView( + files: displayNote.files, + height: 200 * pow(0.5, recursive - 1).toDouble(), + noteUrl: + "https://${account.host}/notes/${displayNote.id}", + ), + if (displayNote.poll != null) + NoteVote( + displayNote: displayNote, + poll: displayNote.poll!, + ), + if (isLongVisible && recursive < 2) + ...links.map( + (link) => LinkPreview( + account: account, + link: link, + host: displayNote.user.host, + ), + ), + if (displayNote.renoteId != null && + (recursive < 2 && !isForceUnvisibleRenote)) + Container( + padding: const EdgeInsets.all(5), + child: DottedBorder( + color: AppTheme.of(context).renoteBorderColor, + radius: AppTheme.of(context).renoteBorderRadius, + strokeWidth: + AppTheme.of(context).renoteStrokeWidth, + dashPattern: + AppTheme.of(context).renoteDashPattern, + child: Padding( + padding: const EdgeInsets.all(5), + child: MisskeyNote( + note: displayNote.renote!, + isDisplayBorder: false, + recursive: recursive + 1, ), - RenoteButton( - displayNote: displayNote, + ), + ), + ), + ], + ], + if (displayNote.reactions.isNotEmpty && + !isReactionedRenote) + const Padding(padding: EdgeInsets.only(bottom: 5)), + if (!isReactionedRenote) + Wrap( + spacing: MediaQuery.textScalerOf(context).scale(5), + runSpacing: MediaQuery.textScalerOf(context).scale(5), + children: [ + for (final reaction in displayReactions) + ReactionButton( + emojiData: MisskeyEmojiData.fromEmojiName( + emojiName: reaction.element.key, + repository: ref.read( + emojiRepositoryProvider(account), ), - FooterReactionButton( - onPressed: () async => - await reactionControl( - ref, context, displayNote), - displayNote: displayNote, + emojiInfo: displayNote.reactionEmojis, + ), + reactionCount: reaction.element.value, + myReaction: displayNote.myReaction, + noteId: displayNote.id, + ), + if (!isAllReactionVisible.value && + displayNote.reactions.length > 16) + OutlinedButton( + style: AppTheme.of(context).reactionButtonStyle, + onPressed: () => + isAllReactionVisible.value = true, + child: Text( + S.of(context).otherReactions( + displayNote.reactions.length - 16, + ), + ), + ), + ], + ), + if (displayNote.channel != null) + NoteChannelView(channel: displayNote.channel!), + if (!isReactionedRenote) + Row( + mainAxisAlignment: !isPostAccountContext + ? MainAxisAlignment.end + : MainAxisAlignment.spaceAround, + mainAxisSize: MainAxisSize.max, + children: [ + if (isPostAccountContext) ...[ + TextButton.icon( + onPressed: () async => context.pushRoute( + NoteCreateRoute( + reply: displayNote, + initialAccount: account, ), - IconButton( - onPressed: () { - showModalBottomSheet( - context: context, - builder: (builder) { - return NoteModalSheet( - baseNote: widget.note, - targetNote: displayNote, - account: account, - noteBoundaryKey: globalKey, - ); - }); - }, - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - style: const ButtonStyle( - padding: MaterialStatePropertyAll( - EdgeInsets.zero), - minimumSize: - MaterialStatePropertyAll(Size(0, 0)), - tapTargetSize: - MaterialTapTargetSize.shrinkWrap, - ), - icon: Icon( - Icons.more_horiz, - size: MediaQuery.textScalerOf(context) - .scale(16), - color: Theme.of(context) - .textTheme - .bodySmall - ?.color, - ), + ), + style: const ButtonStyle( + padding: WidgetStatePropertyAll( + EdgeInsets.zero, ), - ] - ], + minimumSize: + WidgetStatePropertyAll(Size(0, 0)), + tapTargetSize: + MaterialTapTargetSize.shrinkWrap, + ), + label: Text( + displayNote.repliesCount == 0 + ? "" + : displayNote.repliesCount.format(), + ), + icon: Icon( + Icons.reply, + size: MediaQuery.textScalerOf(context) + .scale(16), + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, + ), + ), + RenoteButton(displayNote: displayNote), + FooterReactionButton( + onPressed: () async => await reactionControl(), + displayNote: displayNote, + ), + ], + IconButton( + onPressed: () async => context.pushRoute( + NoteModalRoute( + baseNote: note, + targetNote: displayNote, + accountContext: + ref.read(accountContextProvider), + noteBoundaryKey: globalKey.value, + ), + ), + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + style: const ButtonStyle( + padding: WidgetStatePropertyAll( + EdgeInsets.zero, + ), + minimumSize: WidgetStatePropertyAll(Size(0, 0)), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + icon: Icon( + Icons.more_horiz, + size: + MediaQuery.textScalerOf(context).scale(16), + color: Theme.of(context) + .textTheme + .bodySmall + ?.color, + ), ), - ], - ), - ), - ], + ], + ), + ], + ), ), ], ), ), - ), + ], ), ); } - - Future reactionControl( - WidgetRef ref, - BuildContext context, - Note displayNote, { - MisskeyEmojiData? requestEmoji, - }) async { - // 他のサーバーからログインしている場合は不可 - if (widget.loginAs != null) return; - - final account = AccountScope.of(context); - final isLikeOnly = - (displayNote.reactionAcceptance == ReactionAcceptance.likeOnly || - (displayNote.reactionAcceptance == - ReactionAcceptance.likeOnlyForRemote && - displayNote.user.host != null)); - if (displayNote.myReaction != null && requestEmoji != null) { - // すでにリアクション済み - return; - } - if (requestEmoji != null && - !ref - .read(generalSettingsRepositoryProvider) - .settings - .enableDirectReaction) { - // カスタム絵文字押下でのリアクション無効 - return; - } - if (requestEmoji != null && isLikeOnly) { - // いいねのみでカスタム絵文字押下 - return; - } - if (displayNote.myReaction != null && requestEmoji == null) { - if (await SimpleConfirmDialog.show( - context: context, - message: S.of(context).confirmDeleteReaction, - primary: S.of(context).cancelReaction, - secondary: S.of(context).cancel) != - true) { - return; - } - - await ref - .read(misskeyProvider(account)) - .notes - .reactions - .delete(NotesReactionsDeleteRequest(noteId: displayNote.id)); - if (account.host == "misskey.io") { - await Future.delayed( - const Duration(milliseconds: misskeyIOReactionDelay)); - } - await ref.read(notesProvider(account)).refresh(displayNote.id); - return; - } - final misskey = ref.read(misskeyProvider(account)); - final note = ref.read(notesProvider(account)); - final MisskeyEmojiData? selectedEmoji; - if (isLikeOnly) { - selectedEmoji = const UnicodeEmojiData(char: '❤️'); - } else if (requestEmoji == null) { - selectedEmoji = await showDialog( - context: context, - builder: (context) => ReactionPickerDialog( - account: account, - isAcceptSensitive: displayNote.reactionAcceptance != - ReactionAcceptance.nonSensitiveOnly && - displayNote.reactionAcceptance != - ReactionAcceptance - .nonSensitiveOnlyForLocalLikeOnlyForRemote, - )); - } else { - selectedEmoji = requestEmoji; - } - - if (selectedEmoji == null) return; - await misskey.notes.reactions.create(NotesReactionsCreateRequest( - noteId: displayNote.id, reaction: ":${selectedEmoji.baseName}:")); - if (account.host == "misskey.io") { - await Future.delayed( - const Duration(milliseconds: misskeyIOReactionDelay)); - } - await note.refresh(displayNote.id); - } } class NoteHeader1 extends ConsumerWidget { final Note displayNote; - final Account? loginAs; const NoteHeader1({ - super.key, required this.displayNote, - required this.loginAs, + super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { - final account = AccountScope.of(context); - return Row( children: [ Expanded( - child: Padding( - padding: const EdgeInsets.only(top: 2), - child: UserInformation(user: displayNote.user))), + child: Padding( + padding: const EdgeInsets.only(top: 2), + child: UserInformation(user: displayNote.user), + ), + ), if (displayNote.updatedAt != null) Padding( - padding: const EdgeInsets.only(left: 5, right: 5), - child: Icon(Icons.edit, - size: Theme.of(context).textTheme.bodySmall?.fontSize, - color: Theme.of(context).textTheme.bodySmall?.color)), + padding: const EdgeInsets.only(left: 5, right: 5), + child: Icon( + Icons.edit, + size: Theme.of(context).textTheme.bodySmall?.fontSize, + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ), GestureDetector( - onTap: () => ref - .read(misskeyNoteNotifierProvider(account).notifier) - .navigateToNoteDetailPage(context, displayNote, loginAs) - .expectFailure(context), + onTap: () async => ref + .read(misskeyNoteNotifierProvider.notifier) + .navigateToNoteDetailPage(displayNote), child: Text( displayNote.createdAt.differenceNow(context), textAlign: TextAlign.right, @@ -776,7 +877,7 @@ class NoteHeader1 extends ConsumerWidget { size: Theme.of(context).textTheme.bodySmall?.fontSize, color: Theme.of(context).textTheme.bodySmall?.color, ), - ) + ), ], ); } @@ -787,8 +888,8 @@ class RenoteHeader extends ConsumerWidget { final Account? loginAs; const RenoteHeader({ - super.key, required this.note, + super.key, this.loginAs, }); @@ -797,17 +898,15 @@ class RenoteHeader extends ConsumerWidget { final renoteTextStyle = Theme.of(context).textTheme.bodySmall?.copyWith( color: AppTheme.of(context).renoteBorderColor, ); - final account = AccountScope.of(context); return Row( children: [ const Padding(padding: EdgeInsets.only(left: 10)), Expanded( child: GestureDetector( - onTap: () => ref - .read(misskeyNoteNotifierProvider(account).notifier) - .navigateToUserPage(context, note.user, loginAs) - .expectFailure(context), + onTap: () async => ref + .read(misskeyNoteNotifierProvider.notifier) + .navigateToUserPage(note.user), child: SimpleMfmText( note.user.name ?? note.user.username, style: renoteTextStyle?.copyWith( @@ -820,7 +919,7 @@ class RenoteHeader extends ConsumerWidget { ? S.of(context).selfRenotedBy : S.of(context).renotedBy, style: renoteTextStyle, - ) + ), ], ), ), @@ -855,28 +954,60 @@ class RenoteHeader extends ConsumerWidget { size: renoteTextStyle?.fontSize, color: renoteTextStyle?.color, ), - ) + ), ], ); } } -class NoteChannelView extends StatelessWidget { - final NoteChannelInfo channel; +class ChannelColorBarBox extends StatelessWidget { + const ChannelColorBarBox({ + required this.note, + required this.child, + super.key, + this.hideColorBar = false, + }); - const NoteChannelView({super.key, required this.channel}); + final Note note; + final Widget child; + final bool hideColorBar; @override Widget build(BuildContext context) { - final account = AccountScope.of(context); + final channelColor = note.channel?.color; + return Padding( + padding: const EdgeInsets.only(left: 4), + child: DecoratedBox( + decoration: BoxDecoration( + border: Border( + left: !hideColorBar && channelColor != null + ? BorderSide( + color: Color(0xFF000000 | channelColor), + width: 4, + ) + : BorderSide.none, + ), + ), + child: child, + ), + ); + } +} +class NoteChannelView extends ConsumerWidget { + final NoteChannelInfo channel; + + const NoteChannelView({required this.channel, super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { return GestureDetector( - onTap: () { - context.pushRoute(ChannelDetailRoute( - account: account, + onTap: () async => context.pushRoute( + ChannelDetailRoute( + accountContext: ref.read(accountContextProvider), channelId: channel.id, - )); - }, + ), + ), child: Row( children: [ Icon( @@ -899,16 +1030,16 @@ class NoteChannelView extends StatelessWidget { } } -class RenoteButton extends StatelessWidget { +class RenoteButton extends ConsumerWidget { final Note displayNote; const RenoteButton({ - super.key, required this.displayNote, + super.key, }); @override - Widget build(BuildContext context) { - final account = AccountScope.of(context); + Widget build(BuildContext context, WidgetRef ref) { + final account = ref.read(accountContextProvider).getAccount; // 他人のノートで、ダイレクトまたはフォロワーのみへの公開の場合、リノート不可 if ((displayNote.visibility == NoteVisibility.specified || @@ -923,25 +1054,22 @@ class RenoteButton extends StatelessWidget { } return TextButton.icon( - onPressed: () => showModalBottomSheet( - context: context, - builder: (innerContext) => - RenoteModalSheet(note: displayNote, account: account)), - onLongPress: () => showDialog( - context: context, - builder: (context) => - RenoteUserDialog(account: account, noteId: displayNote.id)), + onPressed: () async => context + .pushRoute(RenoteModalRoute(note: displayNote, account: account)), + onLongPress: () async => context + .pushRoute(RenoteUserRoute(account: account, noteId: displayNote.id)), icon: Icon( Icons.repeat_rounded, size: MediaQuery.textScalerOf(context).scale(16), color: Theme.of(context).textTheme.bodySmall?.color, ), label: Text( - "${displayNote.renoteCount != 0 ? displayNote.renoteCount : ""}", - style: Theme.of(context).textTheme.bodySmall), + "${displayNote.renoteCount != 0 ? displayNote.renoteCount : ""}", + style: Theme.of(context).textTheme.bodySmall, + ), style: const ButtonStyle( - padding: MaterialStatePropertyAll(EdgeInsets.zero), - minimumSize: MaterialStatePropertyAll(Size(0, 0)), + padding: WidgetStatePropertyAll(EdgeInsets.zero), + minimumSize: WidgetStatePropertyAll(Size(0, 0)), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ); @@ -953,9 +1081,9 @@ class FooterReactionButton extends StatelessWidget { final VoidCallback onPressed; const FooterReactionButton({ - super.key, required this.displayNote, required this.onPressed, + super.key, }); @override @@ -974,18 +1102,19 @@ class FooterReactionButton extends StatelessWidget { icon = Icons.remove; } return IconButton( - onPressed: onPressed, - constraints: const BoxConstraints(), - padding: EdgeInsets.zero, - style: const ButtonStyle( - padding: MaterialStatePropertyAll(EdgeInsets.zero), - minimumSize: MaterialStatePropertyAll(Size(0, 0)), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - icon: Icon( - icon, - size: MediaQuery.textScalerOf(context).scale(16), - color: Theme.of(context).textTheme.bodySmall?.color, - )); + onPressed: onPressed, + constraints: const BoxConstraints(), + padding: EdgeInsets.zero, + style: const ButtonStyle( + padding: WidgetStatePropertyAll(EdgeInsets.zero), + minimumSize: WidgetStatePropertyAll(Size(0, 0)), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + icon: Icon( + icon, + size: MediaQuery.textScalerOf(context).scale(16), + color: Theme.of(context).textTheme.bodySmall?.color, + ), + ); } } diff --git a/lib/view/common/misskey_notes/network_image.dart b/lib/view/common/misskey_notes/network_image.dart index e5c734add..6b00385ff 100644 --- a/lib/view/common/misskey_notes/network_image.dart +++ b/lib/view/common/misskey_notes/network_image.dart @@ -1,8 +1,8 @@ -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:miria/providers.dart'; +import "package:cached_network_image/cached_network_image.dart"; +import "package:flutter/material.dart"; +import "package:flutter_svg/flutter_svg.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; enum ImageType { avatarIcon, @@ -26,9 +26,9 @@ class NetworkImageView extends ConsumerWidget { final BoxFit? fit; const NetworkImageView({ - super.key, required this.url, required this.type, + super.key, this.loadingBuilder, this.errorBuilder, this.width, @@ -61,7 +61,23 @@ class NetworkImageView extends ConsumerWidget { fit: fit, errorWidget: (context, url, error) => errorBuilder?.call(context, error, StackTrace.current) ?? - Container(), + Stack( + alignment: Alignment.center, + children: [ + Container( + color: const Color.fromARGB(255, 224, 224, 224), + ), + SvgPicture.asset( + "assets/images/miria_error.svg", + colorFilter: const ColorFilter.mode( + Color.fromARGB(255, 117, 117, 117), + BlendMode.srcIn, + ), + width: 48, + height: 48, + ), + ], + ), cacheManager: ref.read(cacheManagerProvider), width: width, height: height, diff --git a/lib/view/common/misskey_notes/note_modal_sheet.dart b/lib/view/common/misskey_notes/note_modal_sheet.dart deleted file mode 100644 index 4b05d5623..000000000 --- a/lib/view/common/misskey_notes/note_modal_sheet.dart +++ /dev/null @@ -1,359 +0,0 @@ -import 'dart:io'; -import 'dart:ui'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/common/misskey_notes/abuse_dialog.dart'; -import 'package:miria/view/common/misskey_notes/clip_modal_sheet.dart'; -import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; -import 'package:miria/view/note_create_page/note_create_page.dart'; -import 'package:miria/view/user_page/user_control_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:path/path.dart'; -import 'package:path_provider/path_provider.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:url_launcher/url_launcher_string.dart'; -import 'package:share_plus/share_plus.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -final noteModalSheetSharingModeProviding = StateProvider((ref) => false); - -class NoteModalSheet extends ConsumerWidget { - final Note baseNote; - final Note targetNote; - final Account account; - final GlobalKey noteBoundaryKey; - - const NoteModalSheet({ - super.key, - required this.baseNote, - required this.targetNote, - required this.account, - required this.noteBoundaryKey, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final accounts = ref.watch(accountRepositoryProvider); - return ListView( - children: [ - ListTile( - leading: const Icon(Icons.info_outline), - title: Text(S.of(context).detail), - onTap: () { - context - .pushRoute(NoteDetailRoute(note: targetNote, account: account)); - }, - ), - ListTile( - leading: const Icon(Icons.copy), - title: Text(S.of(context).copyContents), - onTap: () { - Clipboard.setData(ClipboardData(text: targetNote.text ?? "")); - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(S.of(context).doneCopy), duration: const Duration(seconds: 1)), - ); - }, - ), - ListTile( - leading: const Icon(Icons.link), - title: Text(S.of(context).copyLinks), - onTap: () { - Clipboard.setData( - ClipboardData( - text: "https://${account.host}/notes/${targetNote.id}", - ), - ); - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(S.of(context).doneCopy), duration: const Duration(seconds: 1)), - ); - }, - ), - ListTile( - leading: const Icon(Icons.person), - title: Text(S.of(context).user), - trailing: const Icon(Icons.keyboard_arrow_right), - onTap: () async { - final response = await ref - .read(misskeyProvider(account)) - .users - .show(UsersShowRequest(userId: targetNote.userId)); - if (!context.mounted) return; - showModalBottomSheet( - context: context, - builder: (context) => UserControlDialog( - account: account, - response: response, - ), - ); - }, - ), - ListTile( - leading: const Icon(Icons.open_in_browser), - title: Text(S.of(context).openBrowsers), - onTap: () async { - launchUrlString( - "https://${account.host}/notes/${targetNote.id}", - mode: LaunchMode.inAppWebView, - ); - - Navigator.of(context).pop(); - }, - ), - if (targetNote.user.host != null) - ListTile( - leading: const Icon(Icons.rocket_launch), - title: Text(S.of(context).openBrowsersAsRemote), - onTap: () async { - final uri = targetNote.url ?? targetNote.uri; - if (uri == null) return; - launchUrl(uri, mode: LaunchMode.inAppWebView); - - Navigator.of(context).pop(); - }, - ), - // ノートが連合なしのときは現在のアカウントと同じサーバーのアカウントが複数ある場合のみ表示する - if (!targetNote.localOnly || - accounts.where((e) => e.host == account.host).length > 1) - ListTile( - leading: const Icon(Icons.open_in_new), - title: Text(S.of(context).openInAnotherAccount), - onTap: () => ref - .read(misskeyNoteNotifierProvider(account).notifier) - .openNoteInOtherAccount(context, targetNote) - .expectFailure(context), - ), - ListTile( - leading: const Icon(Icons.share), - title: Text(S.of(context).shareNotes), - onTap: () { - ref.read(noteModalSheetSharingModeProviding.notifier).state = true; - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - Future(() async { - final box = context.findRenderObject() as RenderBox?; - final boundary = noteBoundaryKey.currentContext - ?.findRenderObject() as RenderRepaintBoundary; - final image = await boundary.toImage( - pixelRatio: View.of(context).devicePixelRatio, - ); - final byteData = - await image.toByteData(format: ImageByteFormat.png); - ref.read(noteModalSheetSharingModeProviding.notifier).state = - false; - - final path = - "${(await getApplicationDocumentsDirectory()).path}${separator}share.png"; - final file = File(path); - await file.writeAsBytes( - byteData!.buffer.asUint8List( - byteData.offsetInBytes, - byteData.lengthInBytes, - ), - ); - - final xFile = XFile(path, mimeType: "image/png"); - await Share.shareXFiles( - [xFile], - text: "https://${account.host}/notes/${targetNote.id}", - sharePositionOrigin: - box!.localToGlobal(Offset.zero) & box.size, - ); - }); - }); - }, - ), - FutureBuilder( - future: ref - .read(misskeyProvider(account)) - .notes - .state(NotesStateRequest(noteId: targetNote.id)), - builder: (_, snapshot) { - final data = snapshot.data; - return (data == null) - ? const Center(child: CircularProgressIndicator()) - : ListTile( - leading: const Icon(Icons.star_rounded), - onTap: () async { - if (data.isFavorited) { - ref - .read(misskeyProvider(account)) - .notes - .favorites - .delete( - NotesFavoritesDeleteRequest( - noteId: targetNote.id, - ), - ); - - Navigator.of(context).pop(); - } else { - ref - .read(misskeyProvider(account)) - .notes - .favorites - .create( - NotesFavoritesCreateRequest( - noteId: targetNote.id, - ), - ); - Navigator.of(context).pop(); - } - }, - title: Text(data.isFavorited - ? S.of(context).deleteFavorite - : S.of(context).favorite), - ); - }, - ), - ListTile( - leading: const Icon(Icons.attach_file), - title: Text(S.of(context).clip), - onTap: () { - Navigator.of(context).pop(); - - showModalBottomSheet( - context: context, - builder: (context2) => - ClipModalSheet(account: account, noteId: targetNote.id), - ); - }, - ), - ListTile( - leading: const Icon(Icons.repeat_rounded), - title: Text(S.of(context).notesAfterRenote), - onTap: () { - context.pushRoute( - NotesAfterRenoteRoute( - note: targetNote, - account: account, - ), - ); - }, - ), - if (baseNote.user.host == null && - baseNote.user.username == account.userId && - !(baseNote.text == null && - baseNote.renote != null && - baseNote.poll == null && - baseNote.files.isEmpty)) ...[ - if (account.i.policies.canEditNote) - ListTile( - leading: const Icon(Icons.edit), - title: Text(S.of(context).edit), - onTap: () async { - Navigator.of(context).pop(); - context.pushRoute( - NoteCreateRoute( - initialAccount: account, - note: targetNote, - noteCreationMode: NoteCreationMode.update, - ), - ); - }, - ), - ListTile( - leading: const Icon(Icons.delete), - title: Text(S.of(context).delete), - onTap: () async { - if (await showDialog( - context: context, - builder: (context) => SimpleConfirmDialog( - message: S.of(context).confirmDelete, - primary: S.of(context).doDeleting, - secondary: S.of(context).cancel, - ), - ) == - true) { - await ref - .read(misskeyProvider(account)) - .notes - .delete(NotesDeleteRequest(noteId: baseNote.id)); - ref.read(notesProvider(account)).delete(baseNote.id); - if (!context.mounted) return; - Navigator.of(context).pop(); - } - }, - ), - ListTile( - leading: const Icon(Icons.edit_outlined), - title: Text(S.of(context).deletedRecreate), - onTap: () async { - if (await showDialog( - context: context, - builder: (context) => SimpleConfirmDialog( - message: S.of(context).confirmDeletedRecreate, - primary: S.of(context).doDeleting, - secondary: S.of(context).cancel, - ), - ) == - true) { - await ref - .read(misskeyProvider(account)) - .notes - .delete(NotesDeleteRequest(noteId: targetNote.id)); - ref.read(notesProvider(account)).delete(targetNote.id); - if (!context.mounted) return; - Navigator.of(context).pop(); - context.pushRoute( - NoteCreateRoute( - initialAccount: account, - note: targetNote, - noteCreationMode: NoteCreationMode.recreate, - ), - ); - } - }, - ), - ], - if (baseNote.user.host == null && - baseNote.user.username == account.userId && - baseNote.renote != null && - baseNote.files.isEmpty && - baseNote.poll == null) ...[ - ListTile( - leading: const Icon(Icons.delete), - title: Text(S.of(context).deleteRenote), - onTap: () async { - await ref - .read(misskeyProvider(account)) - .notes - // unrenote ではないらしい - .delete(NotesDeleteRequest(noteId: baseNote.id)); - ref.read(notesProvider(account)).delete(baseNote.id); - if (!context.mounted) return; - Navigator.of(context).pop(); - }, - ), - ], - if (baseNote.user.host != null || - (baseNote.user.host == null && - baseNote.user.username != account.userId)) - ListTile( - leading: const Icon(Icons.report), - title: Text(S.of(context).reportAbuse), - onTap: () { - Navigator.of(context).pop(); - showDialog( - context: context, - builder: (context) => AbuseDialog( - account: account, - targetUser: targetNote.user, - defaultText: - "Note:\nhttps://${account.host}/notes/${targetNote.id}\n-----", - ), - ); - }, - ), - ], - ); - } -} diff --git a/lib/view/common/misskey_notes/note_vote.dart b/lib/view/common/misskey_notes/note_vote.dart index 33096327a..e7f4d4bc8 100644 --- a/lib/view/common/misskey_notes/note_vote.dart +++ b/lib/view/common/misskey_notes/note_vote.dart @@ -1,225 +1,222 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/extensions/date_time_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:collection/collection.dart"; +import "package:flutter/gestures.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/date_time_extension.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; -class NoteVote extends ConsumerStatefulWidget { +part "note_vote.g.dart"; + +@Riverpod(dependencies: [misskeyPostContext, notesWith]) +class NoteVoteNotifier extends _$NoteVoteNotifier { + @override + AsyncValue? build(Note note) => null; + + Future vote(int index) async { + final poll = note.poll!; + + final dialogValue = await ref + .read(dialogStateNotifierProvider.notifier) + .showDialog( + message: (context) => + S.of(context).confirmPoll(poll.choices[index].text), + actions: (context) => [S.of(context).doVoting, S.of(context).cancel], + ); + + if (dialogValue != 0) return false; + state = const AsyncLoading(); + + state = + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + await ref.read(misskeyPostContextProvider).notes.polls.vote( + NotesPollsVoteRequest( + noteId: note.id, + choice: index, + ), + ); + await ref.read(notesWithProvider).refresh(note.id); + }); + return true; + } +} + +class NoteVote extends HookConsumerWidget { const NoteVote({ - super.key, required this.displayNote, required this.poll, - required this.loginAs, + super.key, }); final Note displayNote; final NotePoll poll; - final Account? loginAs; - - @override - ConsumerState createState() => NoteVoteState(); -} - -class NoteVoteState extends ConsumerState { - var isOpened = false; - bool isAnyVotable() { - if (widget.loginAs != null) return false; - final expiresAt = widget.poll.expiresAt; + bool isAnyVotable(WidgetRef ref) { + if (!ref.read(accountContextProvider).isSame) return false; + final expiresAt = poll.expiresAt; return (expiresAt == null || expiresAt > DateTime.now()) && - ((widget.poll.multiple && widget.poll.choices.any((e) => !e.isVoted)) || - (!widget.poll.multiple && - widget.poll.choices.every((e) => !e.isVoted))); + ((poll.multiple && poll.choices.any((e) => !e.isVoted)) || + (!poll.multiple && poll.choices.every((e) => !e.isVoted))); } - bool isVotable(int choice) { - return isAnyVotable() && - ((!widget.poll.multiple) || - widget.poll.multiple && !widget.poll.choices[choice].isVoted); + bool isVotable(int choice, WidgetRef ref) { + return isAnyVotable(ref) && + ((!poll.multiple) || poll.multiple && !poll.choices[choice].isVoted); } @override - void initState() { - super.initState(); - - if (!isAnyVotable()) { - setState(() { - isOpened = true; - }); - } - } - - Future vote(int choice) async { - // 複数投票可能ですでに対象を投票しているか、 - // 単一投票でいずれかを投票している場合、なにもしない - if (!isVotable(choice)) { - return; - } - final account = AccountScope.of(context); - - final dialogValue = await showDialog( - context: context, - builder: (context2) => SimpleConfirmDialog( - message: - S.of(context).confirmPoll(widget.poll.choices[choice].text), - primary: S.of(context).doVoting, - secondary: S.of(context).cancel, - isMfm: true, - account: AccountScope.of(context), - )); - if (dialogValue == true) { - await ref.read(misskeyProvider(account)).notes.polls.vote( - NotesPollsVoteRequest(noteId: widget.displayNote.id, choice: choice)); - await ref.read(notesProvider(account)).refresh(widget.displayNote.id); - if (!widget.poll.multiple) { - if (!mounted) return; - setState(() { - isOpened = true; - }); - } - } - } - - @override - Widget build(BuildContext context) { - final totalVotes = widget.poll.choices.map((e) => e.votes).sum; - final expiresAt = widget.poll.expiresAt; + Widget build(BuildContext context, WidgetRef ref) { + final totalVotes = poll.choices.map((e) => e.votes).sum; + final expiresAt = poll.expiresAt; final isExpired = expiresAt != null && expiresAt < DateTime.now(); final differ = isExpired ? null - : widget.poll.expiresAt?.difference(DateTime.now()).format(context); + : poll.expiresAt?.difference(DateTime.now()).format(context); final colorTheme = AppTheme.of(context).colorTheme; + final isOpened = useState(useMemoized(() => !isAnyVotable(ref))); + + ref.watch(noteVoteNotifierProvider(displayNote)); + return Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ - for (final choice in widget.poll.choices.mapIndexed( - (index, element) => (index: index, element: element))) ...[ + for (final choice in poll.choices.mapIndexed( + (index, element) => (index: index, element: element), + )) ...[ SizedBox( - width: double.infinity, - child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all(color: Colors.transparent), - borderRadius: BorderRadius.circular(5), - color: isOpened ? null : colorTheme.accentedBackground, - gradient: isOpened - ? LinearGradient( - colors: [ - colorTheme.buttonGradateA, - colorTheme.buttonGradateB, - colorTheme.accentedBackground, - ], - stops: [ - 0, - choice.element.votes / totalVotes, - choice.element.votes / totalVotes, - ], - ) - : null, + width: double.infinity, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: Colors.transparent), + borderRadius: BorderRadius.circular(5), + color: isOpened.value ? null : colorTheme.accentedBackground, + gradient: isOpened.value + ? LinearGradient( + colors: [ + colorTheme.buttonGradateA, + colorTheme.buttonGradateB, + colorTheme.accentedBackground, + ], + stops: [ + 0, + choice.element.votes / totalVotes, + choice.element.votes / totalVotes, + ], + ) + : null, + ), + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + alignment: Alignment.centerLeft, + padding: const EdgeInsets.all(4), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), - child: ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.transparent, - alignment: Alignment.centerLeft, - padding: const EdgeInsets.all(4), - minimumSize: Size.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - onPressed: () { - vote(choice.index); - }, - child: Padding( - padding: const EdgeInsets.all(2), - child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all(color: Colors.transparent), - borderRadius: BorderRadius.circular(3), - color: Theme.of(context) - .scaffoldBackgroundColor - .withAlpha(215), - ), - child: Padding( - padding: const EdgeInsets.only(left: 3, right: 3), - child: SimpleMfmText( - choice.element.text, - style: Theme.of(context).textTheme.bodyMedium, - prefixSpan: [ - if (choice.element.isVoted) - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Icon( - Icons.check, - size: MediaQuery.textScalerOf(context).scale( - Theme.of(context) - .textTheme - .bodyMedium - ?.fontSize ?? - 22, - ), - color: Theme.of(context) - .textTheme - .bodyMedium - ?.color, + onPressed: () async { + // 複数投票可能ですでに対象を投票しているか、 + // 単一投票でいずれかを投票している場合、なにもしない + if (!isVotable(choice.index, ref)) { + return; + } + isOpened.value = await ref + .read(noteVoteNotifierProvider(displayNote).notifier) + .vote(choice.index); + }, + child: Padding( + padding: const EdgeInsets.all(2), + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all(color: Colors.transparent), + borderRadius: BorderRadius.circular(3), + color: Theme.of(context) + .scaffoldBackgroundColor + .withAlpha(215), + ), + child: Padding( + padding: const EdgeInsets.only(left: 3, right: 3), + child: SimpleMfmText( + choice.element.text, + style: Theme.of(context).textTheme.bodyMedium, + prefixSpan: [ + if (choice.element.isVoted) + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Icon( + Icons.check, + size: MediaQuery.textScalerOf(context).scale( + Theme.of(context) + .textTheme + .bodyMedium + ?.fontSize ?? + 22, ), + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color, ), - ], - suffixSpan: [ - const WidgetSpan( - child: Padding( + ), + ], + suffixSpan: [ + const WidgetSpan( + child: Padding( padding: EdgeInsets.only(left: 5), - )), - if (isOpened) - TextSpan( - text: S - .of(context) - .votesCount(choice.element.votes), - style: Theme.of(context).textTheme.bodySmall), - ], - ), + ), + ), + if (isOpened.value) + TextSpan( + text: S + .of(context) + .votesCount(choice.element.votes), + style: Theme.of(context).textTheme.bodySmall, + ), + ], ), ), ), ), - )), + ), + ), + ), const Padding(padding: EdgeInsets.only(bottom: 10)), ], - Text.rich(TextSpan( - children: [ - TextSpan(text: S.of(context).totalVotesCount(totalVotes)), - TextSpan( - text: isExpired - ? S.of(context).finished - : !isOpened - ? S.of(context).openResult - : isAnyVotable() - ? S.of(context).doVoting - : S.of(context).alreadyVoted, - recognizer: TapGestureRecognizer() - ..onTap = () => setState(() { - setState(() { - isOpened = !isOpened; - }); - }), - ), - const WidgetSpan( + Text.rich( + TextSpan( + children: [ + TextSpan(text: S.of(context).totalVotesCount(totalVotes)), + TextSpan( + text: isExpired + ? S.of(context).finished + : !isOpened.value + ? S.of(context).openResult + : isAnyVotable(ref) + ? S.of(context).doVoting + : S.of(context).alreadyVoted, + recognizer: TapGestureRecognizer() + ..onTap = () => isOpened.value = !isOpened.value, + ), + const WidgetSpan( child: Padding( - padding: EdgeInsets.only(left: 10), - )), - TextSpan( - text: differ == null ? "" : S.of(context).remainDiffer(differ)), - ], - style: Theme.of(context).textTheme.bodySmall, - )) + padding: EdgeInsets.only(left: 10), + ), + ), + TextSpan( + text: differ == null ? "" : S.of(context).remainDiffer(differ), + ), + ], + style: Theme.of(context).textTheme.bodySmall, + ), + ), ], ); } diff --git a/lib/view/common/misskey_notes/note_vote.g.dart b/lib/view/common/misskey_notes/note_vote.g.dart new file mode 100644 index 000000000..115a6f4af --- /dev/null +++ b/lib/view/common/misskey_notes/note_vote.g.dart @@ -0,0 +1,224 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'note_vote.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$noteVoteNotifierHash() => r'421dc72908e2fd868f90df56516d266c6a1de281'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$NoteVoteNotifier + extends BuildlessAutoDisposeNotifier { + late final Note note; + + AsyncValue? build( + Note note, + ); +} + +/// See also [NoteVoteNotifier]. +@ProviderFor(NoteVoteNotifier) +const noteVoteNotifierProvider = NoteVoteNotifierFamily(); + +/// See also [NoteVoteNotifier]. +class NoteVoteNotifierFamily extends Family { + /// See also [NoteVoteNotifier]. + const NoteVoteNotifierFamily(); + + static final Iterable _dependencies = [ + misskeyPostContextProvider, + notesWithProvider + ]; + + static final Iterable _allTransitiveDependencies = + { + misskeyPostContextProvider, + ...?misskeyPostContextProvider.allTransitiveDependencies, + notesWithProvider, + ...?notesWithProvider.allTransitiveDependencies + }; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'noteVoteNotifierProvider'; + + /// See also [NoteVoteNotifier]. + NoteVoteNotifierProvider call( + Note note, + ) { + return NoteVoteNotifierProvider( + note, + ); + } + + @visibleForOverriding + @override + NoteVoteNotifierProvider getProviderOverride( + covariant NoteVoteNotifierProvider provider, + ) { + return call( + provider.note, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(NoteVoteNotifier Function() create) { + return _$NoteVoteNotifierFamilyOverride(this, create); + } +} + +class _$NoteVoteNotifierFamilyOverride implements FamilyOverride { + _$NoteVoteNotifierFamilyOverride(this.overriddenFamily, this.create); + + final NoteVoteNotifier Function() create; + + @override + final NoteVoteNotifierFamily overriddenFamily; + + @override + NoteVoteNotifierProvider getProviderOverride( + covariant NoteVoteNotifierProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [NoteVoteNotifier]. +class NoteVoteNotifierProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [NoteVoteNotifier]. + NoteVoteNotifierProvider( + Note note, + ) : this._internal( + () => NoteVoteNotifier()..note = note, + from: noteVoteNotifierProvider, + name: r'noteVoteNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$noteVoteNotifierHash, + dependencies: NoteVoteNotifierFamily._dependencies, + allTransitiveDependencies: + NoteVoteNotifierFamily._allTransitiveDependencies, + note: note, + ); + + NoteVoteNotifierProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.note, + }) : super.internal(); + + final Note note; + + @override + AsyncValue? runNotifierBuild( + covariant NoteVoteNotifier notifier, + ) { + return notifier.build( + note, + ); + } + + @override + Override overrideWith(NoteVoteNotifier Function() create) { + return ProviderOverride( + origin: this, + override: NoteVoteNotifierProvider._internal( + () => create()..note = note, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + note: note, + ), + ); + } + + @override + (Note,) get argument { + return (note,); + } + + @override + AutoDisposeNotifierProviderElement + createElement() { + return _NoteVoteNotifierProviderElement(this); + } + + NoteVoteNotifierProvider _copyWith( + NoteVoteNotifier Function() create, + ) { + return NoteVoteNotifierProvider._internal( + () => create()..note = note, + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + note: note, + ); + } + + @override + bool operator ==(Object other) { + return other is NoteVoteNotifierProvider && other.note == note; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, note.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin NoteVoteNotifierRef on AutoDisposeNotifierProviderRef { + /// The parameter `note` of this provider. + Note get note; +} + +class _NoteVoteNotifierProviderElement + extends AutoDisposeNotifierProviderElement + with NoteVoteNotifierRef { + _NoteVoteNotifierProviderElement(super.provider); + + @override + Note get note => (origin as NoteVoteNotifierProvider).note; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/common/misskey_notes/player_embed.dart b/lib/view/common/misskey_notes/player_embed.dart index 5a46206b1..f1f0922ab 100644 --- a/lib/view/common/misskey_notes/player_embed.dart +++ b/lib/view/common/misskey_notes/player_embed.dart @@ -1,12 +1,12 @@ -import 'package:flutter/material.dart'; -import 'package:miria/model/summaly_result.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:webview_flutter/webview_flutter.dart'; -import 'package:webview_flutter_android/webview_flutter_android.dart'; -import 'package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart'; +import "package:flutter/material.dart"; +import "package:miria/model/summaly_result.dart"; +import "package:url_launcher/url_launcher.dart"; +import "package:webview_flutter/webview_flutter.dart"; +import "package:webview_flutter_android/webview_flutter_android.dart"; +import "package:webview_flutter_wkwebview/webview_flutter_wkwebview.dart"; class PlayerEmbed extends StatefulWidget { - const PlayerEmbed({super.key, required this.player}); + const PlayerEmbed({required this.player, super.key}); final Player player; @@ -54,7 +54,7 @@ class _PlayerEmbedState extends State { onNavigationRequest: (request) async { final url = Uri.tryParse(request.url); if (url != null && await canLaunchUrl(url)) { - launchUrl(url, mode: LaunchMode.externalApplication); + await launchUrl(url, mode: LaunchMode.externalApplication); } return NavigationDecision.prevent; }, diff --git a/lib/view/common/misskey_notes/reaction_button.dart b/lib/view/common/misskey_notes/reaction_button.dart index c9eaac4d8..732a760d4 100644 --- a/lib/view/common/misskey_notes/reaction_button.dart +++ b/lib/view/common/misskey_notes/reaction_button.dart @@ -1,56 +1,39 @@ -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:miria/const.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:miria/view/common/misskey_notes/custom_emoji.dart'; -import 'package:miria/view/common/misskey_notes/reaction_user_dialog.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class ReactionButton extends ConsumerStatefulWidget { +import "dart:math"; + +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/const.dart"; +import "package:miria/hooks/use_async.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:miria/view/common/misskey_notes/custom_emoji.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:misskey_dart/misskey_dart.dart"; + +class ReactionButton extends HookConsumerWidget { final MisskeyEmojiData emojiData; final int reactionCount; final String? myReaction; final String noteId; - final Account? loginAs; const ReactionButton({ - super.key, required this.emojiData, required this.reactionCount, required this.myReaction, required this.noteId, - required this.loginAs, + super.key, }); @override - ConsumerState createState() => ReactionButtonState(); -} - -class ReactionButtonState extends ConsumerState { - @override - void initState() { - super.initState(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - } - - @override - Widget build(BuildContext context) { - final emojiData = widget.emojiData; - final isMyReaction = (emojiData is CustomEmojiData && - widget.myReaction == emojiData.hostedName) || - (emojiData is UnicodeEmojiData && widget.myReaction == emojiData.char); + Widget build(BuildContext context, WidgetRef ref) { + final emojiData = this.emojiData; + final isMyReaction = + (emojiData is CustomEmojiData && myReaction == emojiData.hostedName) || + (emojiData is UnicodeEmojiData && myReaction == emojiData.char); final backgroundColor = isMyReaction ? AppTheme.of(context).reactionButtonMeReactedColor @@ -65,78 +48,86 @@ class ReactionButtonState extends ConsumerState { final borderColor = isMyReaction ? Theme.of(context).primaryColor : Colors.transparent; - return ElevatedButton( - onPressed: () async { - if (widget.loginAs != null) return; - // リアクション取り消し - final account = AccountScope.of(context); - if (isMyReaction) { - if (await SimpleConfirmDialog.show( - context: context, - message: S.of(context).confirmDeleteReaction, - primary: S.of(context).cancelReaction, - secondary: S.of(context).cancel) != - true) { - return; - } - + final reaction = useAsync(() async { + final accountContext = ref.read(accountContextProvider); + if (!accountContext.isSame) return; + // リアクション取り消し + final account = accountContext.postAccount; + if (isMyReaction) { + final dialogValue = + await ref.read(dialogStateNotifierProvider.notifier).showDialog( + message: (context) => S.of(context).confirmDeleteReaction, + actions: (context) => [ + S.of(context).cancelReaction, + S.of(context).cancel, + ], + ); + if (dialogValue != 0) return; + + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { await ref - .read(misskeyProvider(account)) + .read(misskeyPostContextProvider) .notes .reactions - .delete(NotesReactionsDeleteRequest(noteId: widget.noteId)); - if (account.host == "misskey.io") { + .delete(NotesReactionsDeleteRequest(noteId: noteId)); + if (account.host == "misskey.io" || account.host == "nijimiss.moe") { await Future.delayed( - const Duration(milliseconds: misskeyIOReactionDelay)); + const Duration(milliseconds: misskeyHQReactionDelay), + ); } - await ref.read(notesProvider(account)).refresh(widget.noteId); + await ref.read(notesWithProvider).refresh(noteId); + }); - return; - } + return; + } - // すでに別のリアクションを行っている - if (widget.myReaction != null) return; - - final String reactionString; - final emojiData = widget.emojiData; - switch (emojiData) { - case UnicodeEmojiData(): - reactionString = emojiData.char; - break; - case CustomEmojiData(): - if (!emojiData.isCurrentServer) return; - reactionString = ":${emojiData.baseName}:"; - break; - case NotEmojiData(): - return; - } + // すでに別のリアクションを行っている + if (myReaction != null) return; - await ref.read(misskeyProvider(account)).notes.reactions.create( - NotesReactionsCreateRequest( - noteId: widget.noteId, reaction: reactionString)); + final String reactionString; + switch (emojiData) { + case UnicodeEmojiData(): + reactionString = emojiData.char; + case CustomEmojiData(): + if (!emojiData.isCurrentServer) return; + reactionString = ":${emojiData.baseName}:"; + case NotEmojiData(): + return; + } + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + await ref.read(misskeyPostContextProvider).notes.reactions.create( + NotesReactionsCreateRequest( + noteId: noteId, + reaction: reactionString, + ), + ); // misskey.ioはただちにリアクションを反映してくれない if (account.host == "misskey.io") { await Future.delayed( - const Duration(milliseconds: misskeyIOReactionDelay)); + const Duration(milliseconds: misskeyHQReactionDelay), + ); } - await ref.read(notesProvider(account)).refresh(widget.noteId); - }, - onLongPress: () { - showDialog( - context: context, - builder: (context2) { - return ReactionUserDialog( - account: AccountScope.of(context), - emojiData: widget.emojiData, - noteId: widget.noteId); - }); + await ref.read(notesWithProvider).refresh(noteId); + }); + }); + + return ElevatedButton( + onPressed: reaction.executeOrNull, + onLongPress: () async { + await context.pushRoute( + ReactionUserRoute( + accountContext: ref.read(accountContextProvider), + emojiData: emojiData, + noteId: noteId, + ), + ); }, style: AppTheme.of(context).reactionButtonStyle.copyWith( - backgroundColor: MaterialStatePropertyAll(backgroundColor), - side: MaterialStatePropertyAll( + backgroundColor: WidgetStatePropertyAll(backgroundColor), + side: WidgetStatePropertyAll( BorderSide(color: borderColor), ), ), @@ -151,13 +142,13 @@ class ReactionButtonState extends ConsumerState { maxHeight: MediaQuery.textScalerOf(context).scale(24), ), child: CustomEmoji( - emojiData: widget.emojiData, + emojiData: emojiData, isAttachTooltip: false, ), ), const Padding(padding: EdgeInsets.only(left: 5)), Text( - widget.reactionCount.toString(), + reactionCount.toString(), style: TextStyle(color: foreground), ), ], diff --git a/lib/view/common/misskey_notes/reaction_user_dialog.dart b/lib/view/common/misskey_notes/reaction_user_dialog.dart index d39d0885a..255e4d18f 100644 --- a/lib/view/common/misskey_notes/reaction_user_dialog.dart +++ b/lib/view/common/misskey_notes/reaction_user_dialog.dart @@ -1,89 +1,93 @@ -import 'package:flutter/material.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/custom_emoji.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:miria/view/user_page/user_list_item.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/misskey_notes/custom_emoji.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:miria/view/user_page/user_list_item.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class ReactionUserDialog extends ConsumerWidget { - final Account account; +@RoutePage() +class ReactionUserDialog extends ConsumerWidget implements AutoRouteWrapper { + final AccountContext accountContext; final MisskeyEmojiData emojiData; final String noteId; const ReactionUserDialog({ - super.key, - required this.account, + required this.accountContext, required this.emojiData, required this.noteId, + super.key, }); + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); + @override Widget build(BuildContext context, WidgetRef ref) { final String type; - final handled = emojiData; - switch (handled) { - case CustomEmojiData(): - type = handled.hostedName; - break; - case UnicodeEmojiData(): - type = handled.char; - break; + switch (emojiData) { + case CustomEmojiData(:final hostedName): + type = hostedName; + case UnicodeEmojiData(:final char): + type = char; default: type = ""; } - return AccountScope( - account: account, - child: AlertDialog( - title: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CustomEmoji( - emojiData: emojiData, - ), - Text( - emojiData.baseName, - style: Theme.of(context).textTheme.bodySmall, - ) - ], - ), - content: SizedBox( - width: MediaQuery.of(context).size.width * 0.8, - height: MediaQuery.of(context).size.width * 0.8, - child: Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: PushableListView( - initializeFuture: () async { - final response = await ref - .read(misskeyProvider(account)) - .notes - .reactions - .reactions( - NotesReactionsRequest(noteId: noteId, type: type)); - return response.toList(); - }, - nextFuture: (item, index) async { - // 後方互換性のためにoffsetとuntilIdの両方をリクエストに含める - final response = await ref - .read(misskeyProvider(account)) - .notes - .reactions - .reactions(NotesReactionsRequest( - noteId: noteId, - type: type, - offset: index, - untilId: item.id)); - return response.toList(); - }, - itemBuilder: (context, item) => UserListItem(user: item.user), - showAd: false, - )), + return AlertDialog( + title: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CustomEmoji( + emojiData: emojiData, + ), + Text( + emojiData.baseName, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: MediaQuery.of(context).size.width * 0.8, + child: Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: PushableListView( + initializeFuture: () async { + final response = await ref + .read(misskeyGetContextProvider) + .notes + .reactions + .reactions( + NotesReactionsRequest(noteId: noteId, type: type), + ); + return response.toList(); + }, + nextFuture: (item, index) async { + // 後方互換性のためにoffsetとuntilIdの両方をリクエストに含める + final response = await ref + .read(misskeyGetContextProvider) + .notes + .reactions + .reactions( + NotesReactionsRequest( + noteId: noteId, + type: type, + offset: index, + untilId: item.id, + ), + ); + return response.toList(); + }, + itemBuilder: (context, item) => UserListItem(user: item.user), + showAd: false, + ), ), ), ); diff --git a/lib/view/common/misskey_notes/renote_modal_sheet.dart b/lib/view/common/misskey_notes/renote_modal_sheet.dart index b61d75212..5c15b3a0b 100644 --- a/lib/view/common/misskey_notes/renote_modal_sheet.dart +++ b/lib/view/common/misskey_notes/renote_modal_sheet.dart @@ -1,72 +1,160 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/extensions/note_visibility_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/settings_page/tab_settings_page/channel_select_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "dart:async"; -import 'local_only_icon.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/note_visibility_extension.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:miria/view/common/misskey_notes/local_only_icon.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; -class RenoteModalSheet extends ConsumerStatefulWidget { +part "renote_modal_sheet.g.dart"; + +@Riverpod(keepAlive: false) +class RenoteNotifier extends _$RenoteNotifier { + @override + AsyncValue? build(Account account, Note note) => null; + + /// チャンネル内にRenote + Future renoteInSpecificChannel() async { + state = const AsyncLoading(); + state = + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + await ref.read(misskeyProvider(this.account)).notes.create( + NotesCreateRequest( + renoteId: note.id, + localOnly: true, + channelId: note.channel!.id, + ), + ); + }); + } + + /// チャンネルにRenote + Future renoteInChannel(CommunityChannel channel) async { + state = const AsyncLoading(); + state = + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + await ref.read(misskeyProvider(this.account)).notes.create( + NotesCreateRequest( + renoteId: note.id, + channelId: channel.id, + localOnly: true, + ), + ); + }); + } + + /// 普通に引用Renote + Future renote(bool isLocalOnly, NoteVisibility visibility) async { + state = const AsyncLoading(); + state = + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + await ref.read(misskeyProvider(this.account)).notes.create( + NotesCreateRequest( + renoteId: note.id, + localOnly: isLocalOnly, + visibility: visibility, + ), + ); + }); + } +} + +@Riverpod(keepAlive: false) +class RenoteChannelNotifier extends _$RenoteChannelNotifier { + @override + AsyncValue? build(Account account) => null; + + /// Renoteの画面でチャンネル情報を取得する + Future findChannel(String channelId) async { + state = const AsyncLoading(); + state = await ref.read(dialogStateNotifierProvider.notifier).guard( + () async => await ref + .read(misskeyProvider(this.account)) + .channels + .show(ChannelsShowRequest(channelId: channelId)), + ); + } +} + +@RoutePage() +class RenoteModalSheet extends HookConsumerWidget { final Note note; final Account account; const RenoteModalSheet({ - super.key, required this.note, required this.account, + super.key, }); @override - ConsumerState createState() => - RenoteModalSheetState(); -} + Widget build(BuildContext context, WidgetRef ref) { + final channel = note.channel; + final notifier = renoteNotifierProvider(account, note).notifier; -class RenoteModalSheetState extends ConsumerState { - bool isLocalOnly = false; - var visibility = NoteVisibility.public; + ref + ..listen(renoteNotifierProvider(account, note), (_, next) { + if (next is! AsyncData) return; + unawaited(context.maybePop()); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).renoted), + duration: const Duration(seconds: 1), + ), + ); + }) + ..listen(renoteChannelNotifierProvider(account), (_, next) async { + if (next is! AsyncData || next == null) return; + unawaited(context.maybePop()); + await context.pushRoute( + NoteCreateRoute( + renote: note, + channel: next.value, + initialAccount: account, + ), + ); + }); - @override - void didChangeDependencies() { - super.didChangeDependencies(); + final renoteState = ref.watch(renoteNotifierProvider(account, note)); + final renoteChannelState = + ref.watch(renoteChannelNotifierProvider(account)); - final accountSettings = - ref.read(accountSettingsRepositoryProvider).fromAccount(widget.account); - isLocalOnly = accountSettings.defaultIsLocalOnly; - visibility = - accountSettings.defaultNoteVisibility == NoteVisibility.specified - ? NoteVisibility.followers - : accountSettings.defaultNoteVisibility; - } + final isLocalOnly = useState(false); + final visibility = useState(NoteVisibility.public); + useEffect( + () { + final accountSettings = + ref.read(accountSettingsRepositoryProvider).fromAccount(account); + isLocalOnly.value = accountSettings.defaultIsLocalOnly; + visibility.value = + accountSettings.defaultNoteVisibility == NoteVisibility.specified + ? NoteVisibility.followers + : accountSettings.defaultNoteVisibility; + return null; + }, + const [], + ); - @override - Widget build(BuildContext context) { - final channel = widget.note.channel; + if (renoteState is AsyncLoading || + renoteChannelState is AsyncLoading || + renoteState is AsyncData || + renoteChannelState is AsyncData) { + return const Center(child: CircularProgressIndicator.adaptive()); + } return ListView( children: [ if (channel != null) ...[ ListTile( - onTap: () async { - final scaffoldMessenger = ScaffoldMessenger.of(context); - final navigator = Navigator.of(context); - final localize = S.of(context); - await ref - .read(misskeyProvider(widget.account)) - .notes - .create(NotesCreateRequest( - renoteId: widget.note.id, - localOnly: true, - channelId: channel.id, - )); - scaffoldMessenger - .showSnackBar(SnackBar(content: Text(localize.renoted), duration: const Duration(seconds: 1))); - navigator.pop(); - }.expectFailure(context), + onTap: () async => + await ref.read(notifier).renoteInSpecificChannel(), leading: const SizedBox( height: 30, width: 30, @@ -74,15 +162,12 @@ class RenoteModalSheetState extends ConsumerState { children: [ Align( alignment: Alignment.bottomLeft, - child: Icon( - Icons.monitor, - size: 24, - ), + child: Icon(Icons.monitor, size: 24), ), Align( alignment: Alignment.topRight, - child: Icon(Icons.repeat, size: 18,) - ) + child: Icon(Icons.repeat, size: 18), + ), ], ), ), @@ -93,188 +178,152 @@ class RenoteModalSheetState extends ConsumerState { ), ListTile( onTap: () async { - final navigator = Navigator.of(context); - final channelsShowData = await ref - .read(misskeyProvider(widget.account)) - .channels - .show(ChannelsShowRequest(channelId: channel.id)); - if (!mounted) return; - navigator.pop(); - context.pushRoute(NoteCreateRoute( - renote: widget.note, - channel: channelsShowData, - initialAccount: widget.account)); - }.expectFailure(context), - leading: const SizedBox( - height: 30, - width: 30, - child: Stack( - children: [ - Align( - alignment: Alignment.bottomLeft, - child: Icon( - Icons.monitor, - size: 24, - ), - ), - Align( - alignment: Alignment.topRight, - child: Icon(Icons.format_quote, size: 18,) - ) - ], - ), + await ref + .read(renoteChannelNotifierProvider(account).notifier) + .findChannel(channel.id); + }, + leading: const SizedBox( + height: 30, + width: 30, + child: Stack( + children: [ + Align( + alignment: Alignment.bottomLeft, + child: Icon(Icons.monitor, size: 24), + ), + Align( + alignment: Alignment.topRight, + child: Icon(Icons.format_quote, size: 18), + ), + ], ), + ), title: Padding( padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), child: Text( - S.of(context).quotedRenoteInSpecificChannel(channel.name)), + S.of(context).quotedRenoteInSpecificChannel(channel.name), + ), ), ), ], - if (widget.note.channel?.allowRenoteToExternal != false) ...[ + if (note.channel?.allowRenoteToExternal != false) ...[ ListTile( - onTap: () async { - final scaffoldMessenger = ScaffoldMessenger.of(context); - final navigator = Navigator.of(context); - final localize = S.of(context); - await ref - .read(misskeyProvider(widget.account)) - .notes - .create(NotesCreateRequest( - renoteId: widget.note.id, - localOnly: isLocalOnly, - visibility: visibility, - )); - scaffoldMessenger - .showSnackBar(SnackBar(content: Text(localize.renoted), duration: const Duration(seconds: 1))); - navigator.pop(); - }.expectFailure(context), + onTap: () async => + ref.read(notifier).renote(isLocalOnly.value, visibility.value), leading: const Icon(Icons.repeat), - title: const Padding( - padding: EdgeInsets.only(top: 10.0, bottom: 10.0), - child: Text("Renote"), + title: Padding( + padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), + child: Text(S.of(context).renote), ), - subtitle: Row(children: [ - Expanded( - child: DropdownButton( - isExpanded: true, - items: [ - for (final element in NoteVisibility.values.where( - (element) => element != NoteVisibility.specified)) - DropdownMenuItem( - value: element, - child: Text(element.displayName(context)), - ), - ], - value: visibility, - onChanged: (value) => setState(() { - visibility = value ?? NoteVisibility.public; - }), + subtitle: Row( + children: [ + Expanded( + child: DropdownButton( + isExpanded: true, + items: [ + for (final element in NoteVisibility.values.where( + (element) => element != NoteVisibility.specified, + )) + DropdownMenuItem( + value: element, + child: Text(element.displayName(context)), + ), + ], + value: visibility.value, + onChanged: (value) => + visibility.value = value ?? NoteVisibility.public, + ), ), - ), - IconButton( - onPressed: () => setState(() { - isLocalOnly = !isLocalOnly; - }), - icon: isLocalOnly + IconButton( + onPressed: () => isLocalOnly.value = !isLocalOnly.value, + icon: isLocalOnly.value ? const LocalOnlyIcon() - : const Icon(Icons.rocket)), - ]), + : const Icon(Icons.rocket), + ), + ], + ), ), ListTile( - onTap: () { - final navigator = Navigator.of(context); - context.pushRoute(NoteCreateRoute( - renote: widget.note, initialAccount: widget.account)); - navigator.pop(); - }, - leading: const Icon(Icons.format_quote), - title: Text(S.of(context).quotedRenote)), + onTap: () async { + Navigator.of(context).pop(); + await context.pushRoute( + NoteCreateRoute(renote: note, initialAccount: account), + ); + }, + leading: const Icon(Icons.format_quote), + title: Text(S.of(context).quotedRenote), + ), ListTile( - onTap: () async { - final scaffoldMessenger = ScaffoldMessenger.of(context); - final navigator = Navigator.of(context); - final localize = S.of(context); - final selected = await showDialog( - context: context, - builder: (context) => - ChannelSelectDialog(account: widget.account)); - if (selected != null) { - await ref - .read(misskeyProvider(widget.account)) - .notes - .create(NotesCreateRequest( - renoteId: widget.note.id, - channelId: selected.id, - localOnly: true, - )); - - scaffoldMessenger - .showSnackBar(SnackBar(content: Text(localize.renoted), duration: const Duration(seconds: 1))); - navigator.pop(); - } - }.expectFailure(context), - leading: const SizedBox( - height: 30, - width: 30, - child: Stack( - children: [ - Align( - alignment: Alignment.bottomLeft, - child: Icon( - Icons.monitor, - size: 24, - ), - ), - Align( - alignment: Alignment.topRight, - child: Icon(Icons.repeat, size: 18,) - ) - ], - ), + onTap: () async { + final selected = await context.pushRoute( + ChannelSelectRoute(account: account), + ); + if (selected != null) { + await ref.read(notifier).renoteInChannel(selected); + } + }, + leading: const SizedBox( + height: 30, + width: 30, + child: Stack( + children: [ + Align( + alignment: Alignment.bottomLeft, + child: Icon(Icons.monitor, size: 24), + ), + Align( + alignment: Alignment.topRight, + child: Icon(Icons.repeat, size: 18), + ), + ], ), - title: Text(widget.note.channel != null + ), + title: Text( + note.channel != null ? S.of(context).renoteInOtherChannel - : S.of(context).renoteInChannel)), + : S.of(context).renoteInChannel, + ), + ), ListTile( - onTap: () async { - final navigator = Navigator.of(context); - final selected = await showDialog( - context: context, - builder: (context) => - ChannelSelectDialog(account: widget.account)); - if (!mounted) return; - if (selected != null) { - context.pushRoute(NoteCreateRoute( - renote: widget.note, - initialAccount: widget.account, - channel: selected)); - navigator.pop(); - } - }, - leading: const SizedBox( - height: 30, - width: 30, - child: Stack( - children: [ - Align( - alignment: Alignment.bottomLeft, - child: Icon( - Icons.monitor, - size: 24, - ), - ), - Align( - alignment: Alignment.topRight, - child: Icon(Icons.format_quote, size: 18,) - ) - ], + onTap: () async { + final selected = await context.pushRoute( + ChannelSelectRoute(account: account), + ); + if (!context.mounted) return; + if (selected == null) return; + await context.maybePop(); + if (!context.mounted) return; + await context.pushRoute( + NoteCreateRoute( + renote: note, + initialAccount: account, + channel: selected, ), + ); + }, + leading: const SizedBox( + height: 30, + width: 30, + child: Stack( + children: [ + Align( + alignment: Alignment.bottomLeft, + child: Icon(Icons.monitor, size: 24), + ), + Align( + alignment: Alignment.topRight, + child: Icon(Icons.format_quote, size: 18), + ), + ], ), - title: Text(widget.note.channel != null + ), + title: Text( + note.channel != null ? S.of(context).quotedRenoteInOtherChannel - : S.of(context).quotedRenoteInOtherChannel)), - ] + : S.of(context).quotedRenoteInChannel, + ), + ), + ], ], ); } diff --git a/lib/view/common/misskey_notes/renote_modal_sheet.g.dart b/lib/view/common/misskey_notes/renote_modal_sheet.g.dart new file mode 100644 index 000000000..b406c57bb --- /dev/null +++ b/lib/view/common/misskey_notes/renote_modal_sheet.g.dart @@ -0,0 +1,433 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'renote_modal_sheet.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$renoteNotifierHash() => r'2f8d167b63754ec318a4f0f76509e87798948ec6'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$RenoteNotifier + extends BuildlessAutoDisposeNotifier?> { + late final Account account; + late final Note note; + + AsyncValue? build( + Account account, + Note note, + ); +} + +/// See also [RenoteNotifier]. +@ProviderFor(RenoteNotifier) +const renoteNotifierProvider = RenoteNotifierFamily(); + +/// See also [RenoteNotifier]. +class RenoteNotifierFamily extends Family { + /// See also [RenoteNotifier]. + const RenoteNotifierFamily(); + + static const Iterable? _dependencies = null; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'renoteNotifierProvider'; + + /// See also [RenoteNotifier]. + RenoteNotifierProvider call( + Account account, + Note note, + ) { + return RenoteNotifierProvider( + account, + note, + ); + } + + @visibleForOverriding + @override + RenoteNotifierProvider getProviderOverride( + covariant RenoteNotifierProvider provider, + ) { + return call( + provider.account, + provider.note, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(RenoteNotifier Function() create) { + return _$RenoteNotifierFamilyOverride(this, create); + } +} + +class _$RenoteNotifierFamilyOverride implements FamilyOverride { + _$RenoteNotifierFamilyOverride(this.overriddenFamily, this.create); + + final RenoteNotifier Function() create; + + @override + final RenoteNotifierFamily overriddenFamily; + + @override + RenoteNotifierProvider getProviderOverride( + covariant RenoteNotifierProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [RenoteNotifier]. +class RenoteNotifierProvider + extends AutoDisposeNotifierProviderImpl?> { + /// See also [RenoteNotifier]. + RenoteNotifierProvider( + Account account, + Note note, + ) : this._internal( + () => RenoteNotifier() + ..account = account + ..note = note, + from: renoteNotifierProvider, + name: r'renoteNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$renoteNotifierHash, + dependencies: RenoteNotifierFamily._dependencies, + allTransitiveDependencies: + RenoteNotifierFamily._allTransitiveDependencies, + account: account, + note: note, + ); + + RenoteNotifierProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.account, + required this.note, + }) : super.internal(); + + final Account account; + final Note note; + + @override + AsyncValue? runNotifierBuild( + covariant RenoteNotifier notifier, + ) { + return notifier.build( + account, + note, + ); + } + + @override + Override overrideWith(RenoteNotifier Function() create) { + return ProviderOverride( + origin: this, + override: RenoteNotifierProvider._internal( + () => create() + ..account = account + ..note = note, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + account: account, + note: note, + ), + ); + } + + @override + ( + Account, + Note, + ) get argument { + return ( + account, + note, + ); + } + + @override + AutoDisposeNotifierProviderElement?> + createElement() { + return _RenoteNotifierProviderElement(this); + } + + RenoteNotifierProvider _copyWith( + RenoteNotifier Function() create, + ) { + return RenoteNotifierProvider._internal( + () => create() + ..account = account + ..note = note, + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + account: account, + note: note, + ); + } + + @override + bool operator ==(Object other) { + return other is RenoteNotifierProvider && + other.account == account && + other.note == note; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, account.hashCode); + hash = _SystemHash.combine(hash, note.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin RenoteNotifierRef on AutoDisposeNotifierProviderRef?> { + /// The parameter `account` of this provider. + Account get account; + + /// The parameter `note` of this provider. + Note get note; +} + +class _RenoteNotifierProviderElement extends AutoDisposeNotifierProviderElement< + RenoteNotifier, AsyncValue?> with RenoteNotifierRef { + _RenoteNotifierProviderElement(super.provider); + + @override + Account get account => (origin as RenoteNotifierProvider).account; + @override + Note get note => (origin as RenoteNotifierProvider).note; +} + +String _$renoteChannelNotifierHash() => + r'c519cce3b931c05ce41c7d31f69e849feca88bff'; + +abstract class _$RenoteChannelNotifier + extends BuildlessAutoDisposeNotifier?> { + late final Account account; + + AsyncValue? build( + Account account, + ); +} + +/// See also [RenoteChannelNotifier]. +@ProviderFor(RenoteChannelNotifier) +const renoteChannelNotifierProvider = RenoteChannelNotifierFamily(); + +/// See also [RenoteChannelNotifier]. +class RenoteChannelNotifierFamily extends Family { + /// See also [RenoteChannelNotifier]. + const RenoteChannelNotifierFamily(); + + static const Iterable? _dependencies = null; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'renoteChannelNotifierProvider'; + + /// See also [RenoteChannelNotifier]. + RenoteChannelNotifierProvider call( + Account account, + ) { + return RenoteChannelNotifierProvider( + account, + ); + } + + @visibleForOverriding + @override + RenoteChannelNotifierProvider getProviderOverride( + covariant RenoteChannelNotifierProvider provider, + ) { + return call( + provider.account, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(RenoteChannelNotifier Function() create) { + return _$RenoteChannelNotifierFamilyOverride(this, create); + } +} + +class _$RenoteChannelNotifierFamilyOverride implements FamilyOverride { + _$RenoteChannelNotifierFamilyOverride(this.overriddenFamily, this.create); + + final RenoteChannelNotifier Function() create; + + @override + final RenoteChannelNotifierFamily overriddenFamily; + + @override + RenoteChannelNotifierProvider getProviderOverride( + covariant RenoteChannelNotifierProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [RenoteChannelNotifier]. +class RenoteChannelNotifierProvider extends AutoDisposeNotifierProviderImpl< + RenoteChannelNotifier, AsyncValue?> { + /// See also [RenoteChannelNotifier]. + RenoteChannelNotifierProvider( + Account account, + ) : this._internal( + () => RenoteChannelNotifier()..account = account, + from: renoteChannelNotifierProvider, + name: r'renoteChannelNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$renoteChannelNotifierHash, + dependencies: RenoteChannelNotifierFamily._dependencies, + allTransitiveDependencies: + RenoteChannelNotifierFamily._allTransitiveDependencies, + account: account, + ); + + RenoteChannelNotifierProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.account, + }) : super.internal(); + + final Account account; + + @override + AsyncValue? runNotifierBuild( + covariant RenoteChannelNotifier notifier, + ) { + return notifier.build( + account, + ); + } + + @override + Override overrideWith(RenoteChannelNotifier Function() create) { + return ProviderOverride( + origin: this, + override: RenoteChannelNotifierProvider._internal( + () => create()..account = account, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + account: account, + ), + ); + } + + @override + (Account,) get argument { + return (account,); + } + + @override + AutoDisposeNotifierProviderElement?> createElement() { + return _RenoteChannelNotifierProviderElement(this); + } + + RenoteChannelNotifierProvider _copyWith( + RenoteChannelNotifier Function() create, + ) { + return RenoteChannelNotifierProvider._internal( + () => create()..account = account, + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + account: account, + ); + } + + @override + bool operator ==(Object other) { + return other is RenoteChannelNotifierProvider && other.account == account; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, account.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin RenoteChannelNotifierRef + on AutoDisposeNotifierProviderRef?> { + /// The parameter `account` of this provider. + Account get account; +} + +class _RenoteChannelNotifierProviderElement + extends AutoDisposeNotifierProviderElement?> with RenoteChannelNotifierRef { + _RenoteChannelNotifierProviderElement(super.provider); + + @override + Account get account => (origin as RenoteChannelNotifierProvider).account; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/common/misskey_notes/renote_user_dialog.dart b/lib/view/common/misskey_notes/renote_user_dialog.dart index 39fc25e0f..8312e7d6c 100644 --- a/lib/view/common/misskey_notes/renote_user_dialog.dart +++ b/lib/view/common/misskey_notes/renote_user_dialog.dart @@ -1,63 +1,69 @@ -import 'package:flutter/material.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/view/user_page/user_list_item.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:miria/view/user_page/user_list_item.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class RenoteUserDialog extends ConsumerWidget { +@RoutePage() +class RenoteUserDialog extends ConsumerWidget implements AutoRouteWrapper { final Account account; final String noteId; + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: account, child: this); + const RenoteUserDialog({ - super.key, required this.account, required this.noteId, + super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { - return AccountScope( - account: account, - child: AlertDialog( - title: Text(S.of(context).renotedUsers), - content: SizedBox( - width: MediaQuery.of(context).size.width * 0.8, - height: MediaQuery.of(context).size.width * 0.8, - child: Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: PushableListView( - initializeFuture: () async { - final response = await ref - .read(misskeyProvider(account)) - .notes - .renotes( - NotesRenoteRequest(noteId: noteId), - ); - ref - .read(notesProvider(account)) - .registerAll(response.where((e) => e.text != null)); - return response.toList(); - }, - nextFuture: (lastItem, _) async { - final response = await ref - .read(misskeyProvider(account)) - .notes - .renotes(NotesRenoteRequest( - noteId: noteId, untilId: lastItem.id)); - ref - .read(notesProvider(account)) - .registerAll(response.where((e) => e.text != null)); - return response.toList(); - }, - itemBuilder: (context, note) { - return UserListItem(user: note.user); - }, - showAd: false, - )), - ))); + return AlertDialog( + title: Text(S.of(context).renotedUsers), + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: MediaQuery.of(context).size.width * 0.8, + child: Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: PushableListView( + initializeFuture: () async { + final response = + await ref.read(misskeyGetContextProvider).notes.renotes( + NotesRenoteRequest(noteId: noteId), + ); + ref + .read(notesWithProvider) + .registerAll(response.where((e) => e.text != null)); + return response.toList(); + }, + nextFuture: (lastItem, _) async { + final response = + await ref.read(misskeyGetContextProvider).notes.renotes( + NotesRenoteRequest( + noteId: noteId, + untilId: lastItem.id, + ), + ); + ref + .read(notesWithProvider) + .registerAll(response.where((e) => e.text != null)); + return response.toList(); + }, + itemBuilder: (context, note) { + return UserListItem(user: note.user); + }, + showAd: false, + ), + ), + ), + ); } } diff --git a/lib/view/common/misskey_notes/twitter_embed.dart b/lib/view/common/misskey_notes/twitter_embed.dart index a26a37d5a..8352cfe34 100644 --- a/lib/view/common/misskey_notes/twitter_embed.dart +++ b/lib/view/common/misskey_notes/twitter_embed.dart @@ -1,11 +1,11 @@ -import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:webview_flutter/webview_flutter.dart'; +import "package:flutter/material.dart"; +import "package:url_launcher/url_launcher.dart"; +import "package:webview_flutter/webview_flutter.dart"; class TwitterEmbed extends StatefulWidget { const TwitterEmbed({ - super.key, required this.tweetId, + super.key, this.isDark = false, // https://developer.twitter.com/en/docs/twitter-for-websites/supported-languages @@ -36,7 +36,7 @@ class _TwitterEmbedState extends State { onNavigationRequest: (request) async { final url = Uri.tryParse(request.url); if (url != null && await canLaunchUrl(url)) { - launchUrl(url, mode: LaunchMode.externalApplication); + await launchUrl(url, mode: LaunchMode.externalApplication); } return NavigationDecision.prevent; }, diff --git a/lib/view/common/misskey_notes/video_dialog.dart b/lib/view/common/misskey_notes/video_dialog.dart deleted file mode 100644 index 419c9d88d..000000000 --- a/lib/view/common/misskey_notes/video_dialog.dart +++ /dev/null @@ -1,472 +0,0 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:volume_controller/volume_controller.dart'; -import 'package:media_kit/media_kit.dart'; -import 'package:media_kit_video/media_kit_video.dart'; -import 'package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart'; -import 'package:url_launcher/url_launcher_string.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class VideoDialog extends StatefulWidget { - const VideoDialog({super.key, required this.url, required this.fileType}); - - final String url; - final String fileType; - - @override - State createState() => _VideoDialogState(); -} - -class _VideoDialogState extends State { - late final videoKey = GlobalKey(); - late final player = Player(); - late final controller = VideoController(player); - late final bool isAudioFile; - - double aspectRatio = 1; - - int lastTapTime = 0; - bool isVisibleControlBar = false; - bool isEnabledButton = false; - bool isFullScreen = false; - Timer? timer; - - @override - void initState() { - super.initState(); - player.open(Media(widget.url)); - controller.rect.addListener(() { - final rect = controller.rect.value; - if (rect == null || rect.width == 0 || rect.height == 0) { - return; - } - setState(() { - aspectRatio = rect.width / rect.height; - }); - }); - isAudioFile = widget.fileType.startsWith(RegExp("audio")); - if (isAudioFile) { - isVisibleControlBar = true; - isEnabledButton = true; - } - ServicesBinding.instance.keyboard.addHandler(_onKey); - } - - @override - void dispose() { - player.dispose(); - ServicesBinding.instance.keyboard.removeHandler(_onKey); - VolumeController().removeListener(); - super.dispose(); - } - - bool _onKey(KeyEvent event) { - if (event is KeyDownEvent && - event.logicalKey == LogicalKeyboardKey.escape) { - if (!isFullScreen) { - Navigator.of(context).pop(); - return true; - } - } - return false; - } - - @override - Widget build(BuildContext context) { - final themeData = MaterialVideoControlsThemeData( - seekBarPositionColor: Theme.of(context).primaryColor, - seekBarThumbColor: Theme.of(context).primaryColor, - backdropColor: Colors.transparent, - volumeGesture: false, - brightnessGesture: false, - displaySeekBar: false, - automaticallyImplySkipNextButton: false, - automaticallyImplySkipPreviousButton: false, - primaryButtonBar: [], - bottomButtonBar: []); - - final themeDataFull = MaterialVideoControlsThemeData( - seekBarPositionColor: Theme.of(context).primaryColor, - seekBarThumbColor: Theme.of(context).primaryColor, - volumeGesture: false, - brightnessGesture: false, - automaticallyImplySkipNextButton: false, - automaticallyImplySkipPreviousButton: false, - bottomButtonBarMargin: - const EdgeInsets.only(left: 16.0, right: 8.0, bottom: 16.0), - seekBarMargin: const EdgeInsets.only(bottom: 16.0)); - - final themeDataDesktop = MaterialDesktopVideoControlsThemeData( - seekBarPositionColor: Theme.of(context).primaryColor, - seekBarThumbColor: Theme.of(context).primaryColor, - modifyVolumeOnScroll: false, - displaySeekBar: false, - automaticallyImplySkipNextButton: false, - automaticallyImplySkipPreviousButton: false, - primaryButtonBar: [], - bottomButtonBar: []); - - final themeDataDesktopFull = MaterialDesktopVideoControlsThemeData( - seekBarPositionColor: Theme.of(context).primaryColor, - seekBarThumbColor: Theme.of(context).primaryColor, - modifyVolumeOnScroll: false, - automaticallyImplySkipNextButton: false, - automaticallyImplySkipPreviousButton: false); - - return AlertDialog( - backgroundColor: Colors.transparent, - titlePadding: EdgeInsets.zero, - contentPadding: EdgeInsets.zero, - actionsPadding: EdgeInsets.zero, - insetPadding: EdgeInsets.zero, - content: SizedBox( - width: MediaQuery.of(context).size.width, - height: MediaQuery.of(context).size.height, - child: Stack( - children: [ - Positioned.fill( - child: Listener( - behavior: HitTestBehavior.translucent, - onPointerDown: (event) { - if (isAudioFile) return; - timer?.cancel(); - int now = DateTime.now().millisecondsSinceEpoch; - int elap = now - lastTapTime; - lastTapTime = now; - setState(() { - if (!isVisibleControlBar) { - isEnabledButton = true; - isVisibleControlBar = true; - } else if (elap > 500 && - (event.localPosition.dy + 100 < - MediaQuery.of(context).size.height && - max(event.localPosition.dx, - event.localPosition.dy) >= - 45)) { - isVisibleControlBar = false; - } - }); - }, - onPointerUp: (event) { - if (isAudioFile) return; - timer?.cancel(); - timer = Timer(const Duration(seconds: 2), () { - if (!mounted) return; - setState(() { - isVisibleControlBar = false; - }); - }); - }, - child: Dismissible( - key: const ValueKey(""), - behavior: HitTestBehavior.translucent, - direction: DismissDirection.vertical, - resizeDuration: null, - onDismissed: (_) => {Navigator.of(context).pop()}, - child: Stack( - children: [ - Align( - child: AspectRatio( - aspectRatio: aspectRatio, - child: MaterialVideoControlsTheme( - normal: themeData, - fullscreen: themeDataFull, - child: MaterialDesktopVideoControlsTheme( - normal: themeDataDesktop, - fullscreen: themeDataDesktopFull, - child: Video( - key: videoKey, - controller: controller, - controls: AdaptiveVideoControls, - fill: Colors.transparent, - onEnterFullscreen: () async { - isFullScreen = true; - await defaultEnterNativeFullscreen(); - videoKey.currentState - ?.update(fill: Colors.black); - }, - onExitFullscreen: () async { - await defaultExitNativeFullscreen(); - isFullScreen = false; - videoKey.currentState - ?.update(fill: Colors.transparent); - }, - )), - ))), - AnimatedOpacity( - curve: Curves.easeInOut, - opacity: isVisibleControlBar ? 1.0 : 0.0, - duration: const Duration(milliseconds: 500), - onEnd: () { - if (mounted && !isVisibleControlBar) { - setState(() { - isEnabledButton = false; - }); - } - }, - child: Visibility( - maintainState: true, - maintainAnimation: true, - visible: isEnabledButton, - child: Stack( - children: [ - Positioned( - bottom: 0, - child: _VideoControls( - controller: controller, - isAudioFile: isAudioFile, - onMenuPressed: () => { - showModalBottomSheet( - context: context, - builder: (innerContext) { - return ListView( - children: [ - ListTile( - leading: const Icon( - Icons.open_in_browser), - title: Text( - S.of(context).openBrowsers), - onTap: () async { - Navigator.of(innerContext) - .pop(); - Navigator.of(context).pop(); - launchUrlString( - widget.url, - mode: LaunchMode - .externalApplication, - ); - }), - if (!isAudioFile) - ListTile( - leading: const Icon( - Icons.fullscreen), - title: Text(S - .of(context) - .changeFullScreen), - onTap: () async { - Navigator.of(innerContext) - .pop(); - videoKey.currentState - ?.enterFullscreen(); - }, - ) - ], - ); - }, - ) - }, - ), - ), - Positioned( - left: 10, - top: 10, - child: RawMaterialButton( - onPressed: () { - Navigator.of(context).pop(); - }, - constraints: const BoxConstraints( - minWidth: 0, minHeight: 0), - materialTapTargetSize: - MaterialTapTargetSize.shrinkWrap, - padding: EdgeInsets.zero, - fillColor: Theme.of(context) - .scaffoldBackgroundColor - .withAlpha(200), - shape: const CircleBorder(), - child: Padding( - padding: const EdgeInsets.all(5), - child: Icon( - Icons.close, - color: Theme.of(context) - .textTheme - .bodyMedium - ?.color - ?.withAlpha(200), - ), - ), - ), - ), - ], - ), - ), - ) - ], - ), - ), - ), - ) - ], - ), - ), - ); - } -} - -class _VideoControls extends StatefulWidget { - final VideoController controller; - final double iconSize = 30.0; - final VoidCallback? onMenuPressed; - final bool isAudioFile; - - const _VideoControls( - {required this.controller, - required this.isAudioFile, - this.onMenuPressed}); - - @override - State<_VideoControls> createState() => _VideoControlState(); -} - -class _VideoControlState extends State<_VideoControls> { - final List subscriptions = []; - - late Duration position = widget.controller.player.state.position; - late Duration bufferPosition = widget.controller.player.state.buffer; - late Duration duration = widget.controller.player.state.duration; - - bool isSeeking = false; - bool isMute = false; - - @override - void initState() { - super.initState(); - if (subscriptions.isEmpty) { - subscriptions.addAll([ - widget.controller.player.stream.position.listen((event) { - setState(() { - if (!isSeeking) { - position = event; - } - }); - }), - widget.controller.player.stream.buffer.listen((event) { - setState(() { - bufferPosition = event; - }); - }), - widget.controller.player.stream.duration.listen((event) { - setState(() { - duration = event; - }); - }) - ]); - } - } - - @override - void dispose() { - super.dispose(); - for (final subscription in subscriptions) { - subscription.cancel(); - } - } - - @override - Widget build(BuildContext context) { - return Container( - padding: const EdgeInsets.only(left: 10, right: 10, top: 5), - width: MediaQuery.of(context).size.width, - height: 100, - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - border: Border( - top: BorderSide( - color: Theme.of(context).primaryColor, - ))), - child: Column(children: [ - Padding( - padding: const EdgeInsets.only(left: 0, right: 0, bottom: 10), - child: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Row(children: [ - Align( - alignment: Alignment.centerLeft, - child: IconButton( - iconSize: widget.iconSize, - onPressed: () => - widget.controller.player.playOrPause(), - icon: StreamBuilder( - stream: widget.controller.player.stream.playing, - builder: (context, playing) => Icon( - playing.data == true - ? Icons.pause - : Icons.play_arrow, - )), - ), - ), - Text(position.label(reference: duration), - textAlign: TextAlign.center), - const Text(" / "), - Text(duration.label(reference: duration), - textAlign: TextAlign.center), - ]), - ), - const Padding( - padding: EdgeInsets.only(right: 5), - ), - IconButton( - iconSize: widget.iconSize, - onPressed: () async { - await widget.controller.player - .setVolume(isMute ? 100 : 0); - isMute = !isMute; - }, - icon: StreamBuilder( - stream: widget.controller.player.stream.volume, - builder: (context, playing) => Icon( - playing.data == 0 - ? Icons.volume_off - : Icons.volume_up, - ), - )), - IconButton( - onPressed: widget.onMenuPressed, - icon: const Icon(Icons.more_horiz), - iconSize: widget.iconSize, - ) - ])), - Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: SliderTheme( - data: SliderThemeData( - overlayShape: SliderComponentShape.noOverlay, - trackHeight: 3.0, - thumbShape: const RoundSliderThumbShape( - enabledThumbRadius: 6.0)), - child: Slider( - thumbColor: Theme.of(context).primaryColor, - activeColor: Theme.of(context).primaryColor, - value: position.inMilliseconds.toDouble(), - secondaryTrackValue: - bufferPosition.inMilliseconds.toDouble(), - min: 0, - max: duration.inMilliseconds.toDouble(), - onChangeStart: (double value) { - isSeeking = true; - }, - onChanged: (double value) { - setState(() { - position = Duration(milliseconds: value.toInt()); - }); - }, - onChangeEnd: (double value) { - widget.controller.player.seek(position); - isSeeking = false; - }, - ))) - ]) - ]), - ); - } -} diff --git a/lib/view/common/misskey_server_list.dart b/lib/view/common/misskey_server_list.dart index 3897e438a..dafa655aa 100644 --- a/lib/view/common/misskey_server_list.dart +++ b/lib/view/common/misskey_server_list.dart @@ -1,10 +1,10 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/constants.dart'; -import 'package:miria/view/common/error_detail.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/state_notifier/common/misskey_server_list_notifier.dart"; +import "package:miria/view/common/constants.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:misskey_dart/misskey_dart.dart"; class MisskeyServerList extends ConsumerWidget { final bool isDisableUnloginable; @@ -12,9 +12,9 @@ class MisskeyServerList extends ConsumerWidget { final void Function(JoinMisskeyInstanceInfo) onTap; const MisskeyServerList({ + required this.onTap, super.key, this.isDisableUnloginable = false, - required this.onTap, }); @override @@ -94,7 +94,8 @@ class MisskeyServerList extends ConsumerWidget { const Padding(padding: EdgeInsets.only(top: 10)), Text( S.of(context).joiningServerUsers( - server.nodeInfo?.usage?.users?.total ?? 0), + server.nodeInfo?.usage?.users?.total ?? 0, + ), style: Theme.of(context).textTheme.bodySmall, ), const Padding(padding: EdgeInsets.only(top: 10)), @@ -123,7 +124,9 @@ class MisskeyServerList extends ConsumerWidget { }, ), error: (e, st) => ErrorDetail(error: e, stackTrace: st), - loading: () => const Center(child: CircularProgressIndicator()), + loading: () => const Center( + child: CircularProgressIndicator.adaptive(), + ), ), ), ], diff --git a/lib/view/common/modal_indicator.dart b/lib/view/common/modal_indicator.dart index 370d46244..fae21ca4b 100644 --- a/lib/view/common/modal_indicator.dart +++ b/lib/view/common/modal_indicator.dart @@ -1,7 +1,7 @@ /* * 汎用くるくるインジケータ */ -import 'package:flutter/material.dart'; +import "package:flutter/material.dart"; class IndicatorView { /* @@ -12,7 +12,7 @@ class IndicatorView { context, ModalOverlay( const Center( - child: CircularProgressIndicator(), + child: CircularProgressIndicator.adaptive(), ), isAndroidBackEnable: false, ), @@ -67,8 +67,12 @@ class ModalOverlay extends ModalRoute { } @override - Widget buildTransitions(BuildContext context, Animation animation, - Animation secondaryAnimation, Widget child) { + Widget buildTransitions( + BuildContext context, + Animation animation, + Animation secondaryAnimation, + Widget child, + ) { return FadeTransition( opacity: animation, child: ScaleTransition( diff --git a/lib/view/common/not_implements_dialog.dart b/lib/view/common/not_implements_dialog.dart index 74c7dee50..a3ba15b74 100644 --- a/lib/view/common/not_implements_dialog.dart +++ b/lib/view/common/not_implements_dialog.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import "package:flutter/material.dart"; class NotImplementationDialog extends StatelessWidget { const NotImplementationDialog({super.key}); diff --git a/lib/view/common/note_create/basic_keyboard.dart b/lib/view/common/note_create/basic_keyboard.dart index 411f5c3ab..02420e78a 100644 --- a/lib/view/common/note_create/basic_keyboard.dart +++ b/lib/view/common/note_create/basic_keyboard.dart @@ -1,15 +1,15 @@ -import 'package:flutter/cupertino.dart'; +import "package:flutter/cupertino.dart"; -import 'custom_keyboard_button.dart'; +import "package:miria/view/common/note_create/custom_keyboard_button.dart"; class BasicKeyboard extends StatelessWidget { final TextEditingController controller; final FocusNode focusNode; const BasicKeyboard({ - super.key, required this.controller, required this.focusNode, + super.key, }); @override diff --git a/lib/view/common/note_create/custom_keyboard_button.dart b/lib/view/common/note_create/custom_keyboard_button.dart index 8941780dd..5073970d3 100644 --- a/lib/view/common/note_create/custom_keyboard_button.dart +++ b/lib/view/common/note_create/custom_keyboard_button.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; -import 'package:miria/extensions/text_editing_controller_extension.dart'; +import "package:flutter/material.dart"; +import "package:miria/extensions/text_editing_controller_extension.dart"; class CustomKeyboardButton extends StatelessWidget { final String keyboard; @@ -10,10 +10,10 @@ class CustomKeyboardButton extends StatelessWidget { final void Function()? onTap; const CustomKeyboardButton({ - super.key, required this.keyboard, required this.controller, required this.focusNode, + super.key, String? displayText, this.afterInsert, this.onTap, diff --git a/lib/view/common/note_create/emoji_keyboard.dart b/lib/view/common/note_create/emoji_keyboard.dart index 1a5aa8e3f..8f64452d5 100644 --- a/lib/view/common/note_create/emoji_keyboard.dart +++ b/lib/view/common/note_create/emoji_keyboard.dart @@ -1,14 +1,15 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/input_completion_type.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/misskey_notes/custom_emoji.dart'; -import 'package:miria/view/common/note_create/basic_keyboard.dart'; -import 'package:miria/view/common/note_create/input_completation.dart'; -import 'package:miria/view/reaction_picker_dialog/reaction_picker_dialog.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/input_completion_type.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/misskey_notes/custom_emoji.dart"; +import "package:miria/view/common/note_create/basic_keyboard.dart"; +import "package:miria/view/common/note_create/input_completation.dart"; final _filteredEmojisProvider = NotifierProvider.autoDispose .family<_FilteredEmojis, List, Account>( @@ -19,13 +20,14 @@ class _FilteredEmojis extends AutoDisposeFamilyNotifier, Account> { @override List build(Account arg) { - ref.listen(inputCompletionTypeProvider, (_, type) { - _updateEmojis(type); - }); + ref.listen( + inputCompletionTypeProvider, + (_, type) async => _updateEmojis(type), + ); return ref.read(emojiRepositoryProvider(arg)).defaultEmojis(); } - void _updateEmojis(InputCompletionType type) async { + Future _updateEmojis(InputCompletionType type) async { if (type is Emoji) { state = await ref.read(emojiRepositoryProvider(arg)).searchEmojis(type.query); @@ -35,13 +37,11 @@ class _FilteredEmojis class EmojiKeyboard extends ConsumerWidget { const EmojiKeyboard({ - super.key, - required this.account, required this.controller, required this.focusNode, + super.key, }); - final Account account; final TextEditingController controller; final FocusNode focusNode; @@ -64,7 +64,6 @@ class EmojiKeyboard extends ConsumerWidget { offset: beforeSearchText.length + emoji.baseName.length + 2, ), ); - break; case UnicodeEmojiData(): controller.value = TextEditingValue( text: "$beforeSearchText${emoji.char}$after", @@ -72,7 +71,6 @@ class EmojiKeyboard extends ConsumerWidget { offset: beforeSearchText.length + emoji.char.length, ), ); - break; default: return; } @@ -81,7 +79,9 @@ class EmojiKeyboard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final filteredEmojis = ref.watch(_filteredEmojisProvider(account)); + final filteredEmojis = ref.watch( + _filteredEmojisProvider(ref.read(accountContextProvider).getAccount), + ); if (filteredEmojis.isEmpty) { return BasicKeyboard( @@ -108,13 +108,13 @@ class EmojiKeyboard extends ConsumerWidget { ), TextButton.icon( onPressed: () async { - final selected = await showDialog( - context: context, - builder: (context2) => ReactionPickerDialog( - account: account, + final selected = await context.pushRoute( + ReactionPickerRoute( + account: ref.read(accountContextProvider).getAccount, isAcceptSensitive: true, ), ); + if (selected != null) { insertEmoji(selected, ref); } diff --git a/lib/view/common/note_create/hashtag_keyboard.dart b/lib/view/common/note_create/hashtag_keyboard.dart index 6560e7c9a..ded687410 100644 --- a/lib/view/common/note_create/hashtag_keyboard.dart +++ b/lib/view/common/note_create/hashtag_keyboard.dart @@ -1,13 +1,13 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/extensions/text_editing_controller_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/input_completion_type.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/note_create/basic_keyboard.dart'; -import 'package:miria/view/common/note_create/custom_keyboard_button.dart'; -import 'package:miria/view/common/note_create/input_completation.dart'; -import 'package:misskey_dart/misskey_dart.dart' hide Hashtag; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/text_editing_controller_extension.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/input_completion_type.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/note_create/basic_keyboard.dart"; +import "package:miria/view/common/note_create/custom_keyboard_button.dart"; +import "package:miria/view/common/note_create/input_completation.dart"; +import "package:misskey_dart/misskey_dart.dart" hide Hashtag; final _hashtagsSearchProvider = AsyncNotifierProviderFamily<_HashtagsSearch, List, (String, Account)>(_HashtagsSearch.new); @@ -40,15 +40,16 @@ class _FilteredHashtags List build(Account arg) { ref.listen( inputCompletionTypeProvider, - (_, type) { - _updateHashtags(arg, type); - }, + (_, type) async => await _updateHashtags(arg, type), fireImmediately: true, ); return []; } - void _updateHashtags(Account account, InputCompletionType type) async { + Future _updateHashtags( + Account account, + InputCompletionType type, + ) async { if (type is Hashtag) { final query = type.query; if (query.isEmpty) { @@ -65,13 +66,11 @@ class _FilteredHashtags class HashtagKeyboard extends ConsumerWidget { const HashtagKeyboard({ - super.key, - required this.account, required this.controller, required this.focusNode, + super.key, }); - final Account account; final TextEditingController controller; final FocusNode focusNode; @@ -85,7 +84,9 @@ class HashtagKeyboard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final filteredHashtags = ref.watch(_filteredHashtagsProvider(account)); + final filteredHashtags = ref.watch( + _filteredHashtagsProvider(ref.read(accountContextProvider).getAccount), + ); if (filteredHashtags.isEmpty) { return BasicKeyboard( diff --git a/lib/view/common/note_create/input_completation.dart b/lib/view/common/note_create/input_completation.dart index 003408c3f..07e01a64e 100644 --- a/lib/view/common/note_create/input_completation.dart +++ b/lib/view/common/note_create/input_completation.dart @@ -1,96 +1,71 @@ -import 'dart:async'; - -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/extensions/text_editing_controller_extension.dart'; -import 'package:miria/model/input_completion_type.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/note_create/basic_keyboard.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/view/common/note_create/emoji_keyboard.dart'; -import 'package:miria/view/common/note_create/hashtag_keyboard.dart'; -import 'package:miria/view/common/note_create/mfm_fn_keyboard.dart'; +import "dart:async"; + +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/text_editing_controller_extension.dart"; +import "package:miria/model/input_completion_type.dart"; +import "package:miria/view/common/note_create/basic_keyboard.dart"; +import "package:miria/view/common/note_create/emoji_keyboard.dart"; +import "package:miria/view/common/note_create/hashtag_keyboard.dart"; +import "package:miria/view/common/note_create/mfm_fn_keyboard.dart"; final inputCompletionTypeProvider = StateProvider.autoDispose((ref) => Basic()); final inputComplementDelayedProvider = Provider((ref) => 300); -class InputComplement extends ConsumerStatefulWidget { +class InputComplement extends HookConsumerWidget { final TextEditingController controller; final AutoDisposeChangeNotifierProvider focusNode; const InputComplement({ - super.key, required this.controller, required this.focusNode, + super.key, }); @override - ConsumerState createState() => InputComplementState(); -} - -class InputComplementState extends ConsumerState { - bool isClose = true; - - @override - void initState() { - super.initState(); - - widget.controller.addListener(updateType); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - isClose = !ref.read(widget.focusNode).hasFocus; - } - - @override - void dispose() { - widget.controller.removeListener(updateType); + Widget build(BuildContext context, WidgetRef ref) { + final inputCompletionType = ref.watch(inputCompletionTypeProvider); + final focusNode = ref.watch(this.focusNode); - super.dispose(); - } + final isClose = useState(!ref.read(this.focusNode).hasFocus); - void updateType() { - ref.read(inputCompletionTypeProvider.notifier).state = - widget.controller.inputCompletionType; - } + useEffect( + () { + InputCompletionType updateType() => + ref.read(inputCompletionTypeProvider.notifier).state = + controller.inputCompletionType; + controller.addListener(updateType); - @override - Widget build(BuildContext context) { - final inputCompletionType = ref.watch(inputCompletionTypeProvider); - final focusNode = ref.watch(widget.focusNode); - final account = AccountScope.of(context); + return () => controller.removeListener(updateType); + }, + const [], + ); - ref.listen(widget.focusNode, (previous, next) async { + ref.listen(this.focusNode, (previous, next) async { if (!next.hasFocus) { await Future.delayed( Duration(milliseconds: ref.read(inputComplementDelayedProvider)), ); - if (!mounted) return; - if (!ref.read(widget.focusNode).hasFocus) { - setState(() { - isClose = true; - }); - } + if (!context.mounted) return; + if (ref.read(this.focusNode).hasFocus) return; + isClose.value = true; } else { - setState(() { - isClose = false; - }); + isClose.value = false; } }); - if (isClose) { + if (isClose.value) { return Container(); } return DecoratedBox( decoration: BoxDecoration( - border: - Border(top: BorderSide(color: Theme.of(context).primaryColor))), + border: Border(top: BorderSide(color: Theme.of(context).primaryColor)), + ), child: Row( children: [ Expanded( @@ -100,23 +75,17 @@ class InputComplementState extends ConsumerState { constraints: BoxConstraints(minWidth: MediaQuery.of(context).size.width), child: switch (inputCompletionType) { - Basic() => BasicKeyboard( - controller: widget.controller, - focusNode: focusNode, - ), - Emoji() => EmojiKeyboard( - account: account, - controller: widget.controller, - focusNode: focusNode, - ), + Basic() => + BasicKeyboard(controller: controller, focusNode: focusNode), + Emoji() => + EmojiKeyboard(controller: controller, focusNode: focusNode), MfmFn() => MfmFnKeyboard( - controller: widget.controller, + controller: controller, focusNode: focusNode, parentContext: context, ), Hashtag() => HashtagKeyboard( - account: account, - controller: widget.controller, + controller: controller, focusNode: focusNode, ), }, @@ -126,10 +95,11 @@ class InputComplementState extends ConsumerState { if (defaultTargetPlatform == TargetPlatform.android || defaultTargetPlatform == TargetPlatform.iOS) IconButton( - onPressed: () { - FocusManager.instance.primaryFocus?.unfocus(); - }, - icon: const Icon(Icons.keyboard_arrow_down)), + onPressed: () { + FocusManager.instance.primaryFocus?.unfocus(); + }, + icon: const Icon(Icons.keyboard_arrow_down), + ), ], ), ); diff --git a/lib/view/common/note_create/mfm_fn_keyboard.dart b/lib/view/common/note_create/mfm_fn_keyboard.dart index 1185c6e8e..2f1e94ae4 100644 --- a/lib/view/common/note_create/mfm_fn_keyboard.dart +++ b/lib/view/common/note_create/mfm_fn_keyboard.dart @@ -1,12 +1,12 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/extensions/text_editing_controller_extension.dart'; -import 'package:miria/model/input_completion_type.dart'; -import 'package:miria/view/common/color_picker_dialog.dart'; -import 'package:miria/view/common/date_time_picker.dart'; -import 'package:miria/view/common/note_create/basic_keyboard.dart'; -import 'package:miria/view/common/note_create/custom_keyboard_button.dart'; -import 'package:miria/view/common/note_create/input_completation.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/text_editing_controller_extension.dart"; +import "package:miria/model/input_completion_type.dart"; +import "package:miria/view/common/color_picker_dialog.dart"; +import "package:miria/view/common/date_time_picker.dart"; +import "package:miria/view/common/note_create/basic_keyboard.dart"; +import "package:miria/view/common/note_create/custom_keyboard_button.dart"; +import "package:miria/view/common/note_create/input_completation.dart"; class MfmFnArg { const MfmFnArg({ @@ -143,10 +143,10 @@ final filteredMfmFnArgsProvider = Provider.autoDispose>((ref) { class MfmFnKeyboard extends ConsumerWidget { const MfmFnKeyboard({ - super.key, required this.controller, required this.focusNode, required this.parentContext, + super.key, }); final TextEditingController controller; @@ -171,12 +171,14 @@ class MfmFnKeyboard extends ConsumerWidget { controller.insert(" $unixtime"); } } else if (mfmFnName == "fg" || mfmFnName == "bg") { - final result = await showDialog( - context: parentContext, - builder: (context) => const ColorPickerDialog()); + final result = await showDialog>( + context: parentContext, + builder: (context) => const ColorPickerDialog(), + ); if (result != null) { controller.insert( - ".color=${result.red.toRadixString(16).padLeft(2, "0")}${result.green.toRadixString(16).padLeft(2, "0")}${result.blue.toRadixString(16).padLeft(2, "0")} "); + ".color=${result.value.red.toRadixString(16).padLeft(2, "0")}${result.value.green.toRadixString(16).padLeft(2, "0")}${result.value.blue.toRadixString(16).padLeft(2, "0")} ", + ); } else { controller.insert(" "); } @@ -210,14 +212,15 @@ class MfmFnKeyboard extends ConsumerWidget { controller.insert(arg.name.substring(queryLength)); } } - if ((mfmFnName == "fg" || mfmFnName == "bg" || mfmFnName == "border") && arg.name == "color") { - final result = await showDialog( + if ((mfmFnName == "fg" || mfmFnName == "bg" || mfmFnName == "border") && + arg.name == "color") { + final result = await showDialog>( context: parentContext, builder: (context) => const ColorPickerDialog(), ); if (result != null) { controller.insert( - "=${result.red.toRadixString(16).padLeft(2, "0")}${result.green.toRadixString(16).padLeft(2, "0")}${result.blue.toRadixString(16).padLeft(2, "0")} ", + "=${result.value.red.toRadixString(16).padLeft(2, "0")}${result.value.green.toRadixString(16).padLeft(2, "0")}${result.value.blue.toRadixString(16).padLeft(2, "0")} ", ); } else { controller.insert("=f00 "); @@ -241,7 +244,7 @@ class MfmFnKeyboard extends ConsumerWidget { keyboard: arg.name, controller: controller, focusNode: focusNode, - onTap: () => insertMfmFnArg(arg), + onTap: () async => insertMfmFnArg(arg), ), ) .toList(), @@ -254,7 +257,7 @@ class MfmFnKeyboard extends ConsumerWidget { keyboard: name, controller: controller, focusNode: focusNode, - onTap: () => insertMfmFnName(name), + onTap: () async => insertMfmFnName(name), ), ) .toList(), diff --git a/lib/view/common/note_file_dialog/image_viewer.dart b/lib/view/common/note_file_dialog/image_viewer.dart new file mode 100644 index 000000000..df00114bf --- /dev/null +++ b/lib/view/common/note_file_dialog/image_viewer.dart @@ -0,0 +1,119 @@ +import "dart:math"; + +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/state_notifier/note_file_dialog/image_viewer_info_notifier.dart"; +import "package:miria/view/common/note_file_dialog/scale_notifier_interactive_viewer.dart"; +import "package:misskey_dart/misskey_dart.dart"; + +class ImageViewer extends HookConsumerWidget { + final DriveFile file; + final double maxScale; + const ImageViewer({ + required this.file, + super.key, + this.maxScale = 8.0, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final provider = ref.watch(imageViewerInfoNotifierProvider); + + final transformationController = useTransformationController(); + + final resetScale = useCallback( + () { + transformationController.value = Matrix4.identity(); + ref.read(imageViewerInfoNotifierProvider.notifier).reset(); + }, + [transformationController, 1.0], + ); + return Stack(children: [ + Positioned.fill( + child: Listener( + onPointerDown: (event) { + ref.read(imageViewerInfoNotifierProvider.notifier).addPointer(); + }, + onPointerUp: (event) { + if (provider.scale == 1.0 && provider.lastScale != 1.0) { + resetScale(); + } + ref.read(imageViewerInfoNotifierProvider.notifier).removePointer(); + }, + onPointerMove: (event) { + final prov = ref.read(imageViewerInfoNotifierProvider); + if (prov.isDoubleTap && prov.pointersCount == 1) { + final position = prov.lastTapLocalPosition; + final delta = event.localPosition - position!; + + final s = max( + min( + prov.lastScale + (delta.dy / 75.0), + maxScale, + ), + 1.0, + ); + ref.read(imageViewerInfoNotifierProvider.notifier).updateScale(s); + final v = transformationController.toScene(position); + + transformationController.value = Matrix4.identity() + ..scale(provider.scale); + + final v2 = transformationController.toScene(position) - v; + + transformationController.value = transformationController.value + .clone() + ..translate(v2.dx, v2.dy); + } + }, + child: GestureDetector( + onDoubleTapDown: (details) { + ref.read(imageViewerInfoNotifierProvider.notifier).update( + ref.read(imageViewerInfoNotifierProvider).copyWith( + lastScale: provider.scale, + isDoubleTap: true, + lastTapLocalPosition: details.localPosition, + ), + ); + }, + onDoubleTap: () { + if (provider.scale != 1.0) { + resetScale(); + } else { + final position = ref + .read(imageViewerInfoNotifierProvider) + .lastTapLocalPosition; + if (position == null) return; + transformationController.value = Matrix4.identity() + ..translate( + -position.dx * 2, + -position.dy * 2, + ) + ..scale(3.0); + ref.read(imageViewerInfoNotifierProvider.notifier).update( + ref.read(imageViewerInfoNotifierProvider).copyWith( + scale: 3.0, + isDoubleTap: false, + lastTapLocalPosition: null, + ), + ); + } + }, + child: ScaleNotifierInteractiveViewer( + imageUrl: file.url, + controller: transformationController, + onScaleChanged: (scaleUpdated) { + ref + .read(imageViewerInfoNotifierProvider.notifier) + .updateScale(scaleUpdated); + }, + maxScale: maxScale, + canChangeScale: !provider.isDoubleTap, + ), + ), + ), + ), + ]); + } +} diff --git a/lib/view/common/note_file_dialog/media_player.dart b/lib/view/common/note_file_dialog/media_player.dart new file mode 100644 index 000000000..4b3533d95 --- /dev/null +++ b/lib/view/common/note_file_dialog/media_player.dart @@ -0,0 +1,438 @@ +import "dart:async"; +import "dart:io"; +import "dart:math"; + +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:media_kit/media_kit.dart"; +import "package:media_kit_video/media_kit_video.dart"; +import "package:media_kit_video/media_kit_video_controls/src/controls/extensions/duration.dart"; +import "package:url_launcher/url_launcher_string.dart"; +import "package:volume_controller/volume_controller.dart"; + +class MediaPlayer extends StatefulWidget { + final String url; + final String fileType; + final String? thumbnailUrl; + + const MediaPlayer({ + required this.url, + required this.fileType, + this.thumbnailUrl, + super.key, + }); + + @override + MediaPlayerState createState() => MediaPlayerState(); +} + +class MediaPlayerState extends State { + late final videoKey = GlobalKey(); + late final player = Player(); + late final controller = VideoController(player); + late final bool isAudioFile; + final List subscriptions = []; + + double aspectRatio = 1; + + bool isVisibleControlBar = false; + bool isEnabledButton = false; + bool isFullscreen = false; + Timer? timer; + + Duration position = const Duration(); + Duration bufferPosition = const Duration(); + Duration duration = const Duration(); + final double iconSize = 30.0; + bool isSeeking = false; + + bool get isDesktop => + Platform.isWindows || Platform.isMacOS || Platform.isLinux; + + @override + void initState() { + super.initState(); + isAudioFile = widget.fileType.startsWith("audio"); + if (isAudioFile) { + isVisibleControlBar = true; + isEnabledButton = true; + } + + player.open(Media(widget.url)); + controller.rect.addListener(() { + final rect = controller.rect.value; + if (rect == null || rect.width == 0 || rect.height == 0) { + return; + } + setState(() { + aspectRatio = rect.width / rect.height; + }); + }); + + subscriptions.addAll([ + controller.player.stream.position.listen((event) { + setState(() { + if (!isSeeking) { + position = event; + } + }); + }), + controller.player.stream.buffer.listen((event) { + setState(() { + bufferPosition = event; + }); + }), + controller.player.stream.duration.listen((event) { + setState(() { + duration = event; + }); + }), + ]); + } + + @override + void dispose() { + Future.microtask(() async { + for (final subscription in subscriptions) { + await subscription.cancel(); + } + await player.dispose(); + }); + VolumeController().removeListener(); + super.dispose(); + } + + Future _showMenu() { + return showModalBottomSheet( + context: context, + builder: (innerContext) { + return ListView( + children: [ + ListTile( + leading: const Icon( + Icons.open_in_browser, + ), + title: Text( + S.of(context).openBrowsers, + ), + onTap: () async { + Navigator.of(innerContext).pop(); + Navigator.of(context).pop(); + await launchUrlString( + widget.url, + mode: LaunchMode.externalApplication, + ); + }, + ), + if (!isAudioFile) + ListTile( + leading: const Icon( + Icons.fullscreen, + ), + title: Text( + S.of(context).changeFullScreen, + ), + onTap: () async { + Navigator.of(innerContext).pop(); + await videoKey.currentState?.enterFullscreen(); + }, + ), + ], + ); + }, + ); + } + + @override + Widget build(BuildContext context) { + final themeData = MaterialVideoControlsThemeData( + seekBarPositionColor: Theme.of(context).primaryColor, + seekBarThumbColor: Theme.of(context).primaryColor, + backdropColor: Colors.transparent, + volumeGesture: false, + brightnessGesture: false, + displaySeekBar: false, + seekOnDoubleTap: false, + automaticallyImplySkipNextButton: false, + automaticallyImplySkipPreviousButton: false, + primaryButtonBar: [], + bottomButtonBar: [], + ); + + final themeDataFull = MaterialVideoControlsThemeData( + seekBarPositionColor: Theme.of(context).primaryColor, + seekBarThumbColor: Theme.of(context).primaryColor, + volumeGesture: false, + brightnessGesture: false, + displaySeekBar: true, + seekOnDoubleTap: true, + automaticallyImplySkipNextButton: false, + automaticallyImplySkipPreviousButton: false, + bottomButtonBarMargin: + const EdgeInsets.only(left: 16.0, right: 8.0, bottom: 16.0), + seekBarMargin: const EdgeInsets.only(bottom: 16.0), + ); + + final themeDataDesktop = MaterialDesktopVideoControlsThemeData( + seekBarPositionColor: Theme.of(context).primaryColor, + seekBarThumbColor: Theme.of(context).primaryColor, + modifyVolumeOnScroll: false, + displaySeekBar: false, + automaticallyImplySkipNextButton: false, + automaticallyImplySkipPreviousButton: false, + primaryButtonBar: [], + bottomButtonBar: [], + playAndPauseOnTap: false, + ); + + final themeDataDesktopFull = MaterialDesktopVideoControlsThemeData( + seekBarPositionColor: Theme.of(context).primaryColor, + seekBarThumbColor: Theme.of(context).primaryColor, + modifyVolumeOnScroll: false, + automaticallyImplySkipNextButton: false, + automaticallyImplySkipPreviousButton: false, + playAndPauseOnTap: false, + ); + + return Stack( + children: [ + Listener( + behavior: HitTestBehavior.translucent, + onPointerDown: (event) { + if (isAudioFile) return; + cancelHideTimer(); + setState(() { + isEnabledButton = true; + isVisibleControlBar = !isVisibleControlBar; + }); + }, + onPointerUp: (event) { + startHideTimer(); + }, + child: SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: Center( + child: Stack( + children: [ + Align( + child: AspectRatio( + aspectRatio: aspectRatio, + child: MaterialVideoControlsTheme( + normal: themeData, + fullscreen: themeDataFull, + child: MaterialDesktopVideoControlsTheme( + normal: themeDataDesktop, + fullscreen: themeDataDesktopFull, + child: Video( + key: videoKey, + controller: controller, + controls: AdaptiveVideoControls, + fill: Colors.transparent, + onEnterFullscreen: () async { + isFullscreen = true; + await defaultEnterNativeFullscreen(); + videoKey.currentState?.update(fill: Colors.black); + }, + onExitFullscreen: () async { + await defaultExitNativeFullscreen(); + isFullscreen = false; + videoKey.currentState + ?.update(fill: Colors.transparent); + }, + ), + ), + ), + ), + ), + if (!isDesktop) + SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: Container( + color: Colors.transparent, + ), + ), + ], + ), + ), + ), + ), + AnimatedOpacity( + curve: Curves.easeInOut, + opacity: isVisibleControlBar ? 1.0 : 0.0, + duration: const Duration(milliseconds: 500), + onEnd: () { + if (mounted && !isVisibleControlBar) { + setState(() { + isEnabledButton = false; + }); + } + }, + child: Visibility( + maintainState: true, + maintainAnimation: true, + visible: isEnabledButton, + child: Stack( + children: [ + Positioned( + bottom: 0, + child: Container( + padding: const EdgeInsets.only(left: 10, right: 10, top: 5), + width: MediaQuery.of(context).size.width, + height: 100, + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + border: Border( + top: BorderSide( + color: Theme.of(context).primaryColor, + ), + ), + ), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only( + left: 0, + right: 0, + bottom: 10, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: Row( + children: [ + Align( + alignment: Alignment.centerLeft, + child: IconButton( + iconSize: iconSize, + onPressed: () async { + cancelHideTimer(); + await controller.player.playOrPause(); + startHideTimer(); + }, + icon: StreamBuilder( + stream: + controller.player.stream.playing, + builder: (context, playing) => Icon( + playing.data == true + ? Icons.pause + : Icons.play_arrow, + ), + ), + ), + ), + Text( + "${position.label(reference: duration)} / ${duration.label(reference: duration)}", + textAlign: TextAlign.center, + ), + ], + ), + ), + IconButton( + iconSize: iconSize, + onPressed: () async { + cancelHideTimer(); + final isMute = + controller.player.state.volume == 0; + await controller.player + .setVolume(isMute ? 100 : 0); + startHideTimer(); + }, + icon: StreamBuilder( + stream: controller.player.stream.volume, + builder: (context, playing) => Icon( + playing.data == 0 + ? Icons.volume_off + : Icons.volume_up, + ), + ), + ), + IconButton( + onPressed: () async { + cancelHideTimer(); + await _showMenu(); + startHideTimer(); + }, + icon: const Icon(Icons.more_horiz), + iconSize: iconSize, + ), + ], + ), + ), + Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Expanded( + child: SliderTheme( + data: SliderThemeData( + overlayShape: SliderComponentShape.noOverlay, + trackHeight: 5.0, + thumbShape: const RoundSliderThumbShape( + enabledThumbRadius: 10.0, + ), + ), + child: Slider( + thumbColor: Theme.of(context).primaryColor, + activeColor: Theme.of(context).primaryColor, + value: min(position.inMilliseconds, + duration.inMilliseconds) + .toDouble(), + secondaryTrackValue: + bufferPosition.inMilliseconds.toDouble(), + min: 0, + max: duration.inMilliseconds.toDouble(), + onChangeStart: (value) { + cancelHideTimer(); + isSeeking = true; + }, + onChanged: (value) { + setState(() { + position = + Duration(milliseconds: value.toInt()); + }); + }, + onChangeEnd: (value) async { + await controller.player.seek(position); + isSeeking = false; + startHideTimer(); + }, + ), + ), + ), + ], + ), + ], + ), + ), + ), + ], + ), + ), + ), + ], + ); + } + + void startHideTimer() { + if (isAudioFile) return; + timer?.cancel(); + timer = Timer(const Duration(seconds: 2), () { + if (!mounted) return; + setState(() { + isVisibleControlBar = false; + }); + }); + } + + void cancelHideTimer() { + if (isAudioFile) return; + timer?.cancel(); + } +} diff --git a/lib/view/common/note_file_dialog/media_viewer.dart b/lib/view/common/note_file_dialog/media_viewer.dart new file mode 100644 index 000000000..a8d17752c --- /dev/null +++ b/lib/view/common/note_file_dialog/media_viewer.dart @@ -0,0 +1,65 @@ +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; +import "package:miria/view/common/note_file_dialog/media_player.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:misskey_dart/misskey_dart.dart"; + +class MediaViewer extends HookConsumerWidget { + final DriveFile file; + final bool autoPlay; + const MediaViewer({ + required this.file, + this.autoPlay = false, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isThumbnailVisible = useState(true); + final enabledAutoPlay = useState(false); + + useEffect( + () { + enabledAutoPlay.value = autoPlay; + return () {}; + }, + [], + ); + + final thumbnailWidget = GestureDetector( + onTap: () { + isThumbnailVisible.value = false; + }, + child: Stack( + fit: StackFit.passthrough, + alignment: Alignment.center, + children: [ + NetworkImageView( + url: file.thumbnailUrl.toString(), + type: ImageType.imageThumbnail, + fit: BoxFit.contain, + ), + Icon( + Icons.play_circle, + size: 100, + color: AppTheme.of(context).colorTheme.primary, + ), + ], + ), + ); + return SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: (enabledAutoPlay.value || + (!enabledAutoPlay.value && !isThumbnailVisible.value)) + ? MediaPlayer( + url: file.url, + fileType: file.type, + thumbnailUrl: file.thumbnailUrl, + ) + : thumbnailWidget, + ); + } +} diff --git a/lib/view/common/note_file_dialog/note_file_dialog.dart b/lib/view/common/note_file_dialog/note_file_dialog.dart new file mode 100644 index 000000000..08002cad0 --- /dev/null +++ b/lib/view/common/note_file_dialog/note_file_dialog.dart @@ -0,0 +1,223 @@ +import "dart:io"; + +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/state_notifier/common/download_file_notifier.dart"; +import "package:miria/state_notifier/note_file_dialog/image_viewer_info_notifier.dart"; +import "package:miria/view/common/note_file_dialog/image_viewer.dart"; +import "package:miria/view/common/note_file_dialog/media_viewer.dart"; +import "package:miria/view/common/note_file_dialog/unsupported_note_file.dart"; +import "package:miria/view/dialogs/simple_message_dialog.dart"; +import "package:misskey_dart/misskey_dart.dart"; + +class NoteFileDialog extends HookConsumerWidget { + final List driveFiles; + final int initialPage; + final String? noteUrl; + + const NoteFileDialog({ + required this.driveFiles, + required this.initialPage, + this.noteUrl, + super.key, + }); + + bool get isDesktop => + Platform.isWindows || Platform.isMacOS || Platform.isLinux; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final imageViewInfo = ref.watch(imageViewerInfoNotifierProvider); + final isAutoPlay = useState(true); + final isEnabledSaveButton = useState(true); + + useEffect( + () { + final f = driveFiles[initialPage].type.startsWith("image"); + isEnabledSaveButton.value = f; + return () {}; + }, + [], + ); + + final pageController = usePageController(initialPage: initialPage); + pageController.addListener(() { + final page = pageController.page!.round(); + final f = driveFiles[page].type.startsWith("image"); + isEnabledSaveButton.value = f; + isAutoPlay.value = false; + }); + + return Scaffold( + backgroundColor: Colors.transparent, + body: AlertDialog( + backgroundColor: Colors.transparent, + titlePadding: EdgeInsets.zero, + contentPadding: EdgeInsets.zero, + actionsPadding: EdgeInsets.zero, + insetPadding: EdgeInsets.zero, + content: SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: CallbackShortcuts( + bindings: { + const SingleActivator(LogicalKeyboardKey.arrowLeft): () async { + ref.read(imageViewerInfoNotifierProvider.notifier).reset(); + await pageController.previousPage( + duration: const Duration(milliseconds: 300), + curve: Curves.ease, + ); + }, + const SingleActivator(LogicalKeyboardKey.arrowRight): () async { + ref.read(imageViewerInfoNotifierProvider.notifier).reset(); + await pageController.nextPage( + duration: const Duration(milliseconds: 300), + curve: Curves.ease, + ); + }, + const SingleActivator(LogicalKeyboardKey.escape): () async { + Navigator.of(context).pop(); + }, + }, + child: Focus( + autofocus: true, + child: Dismissible( + key: const ValueKey(""), + behavior: HitTestBehavior.translucent, + direction: (!imageViewInfo.isDoubleTap && + imageViewInfo.scale == 1.0 && + imageViewInfo.pointersCount <= 1) + ? DismissDirection.vertical + : DismissDirection.none, + resizeDuration: null, + onDismissed: (_) => {Navigator.of(context).pop()}, + child: Stack( + children: [ + PageView( + controller: pageController, + physics: (!imageViewInfo.isDoubleTap && + imageViewInfo.scale == 1.0 && + imageViewInfo.pointersCount <= 1) + ? const ScrollPhysics() + : const NeverScrollableScrollPhysics(), + children: [ + for (final file in driveFiles) + if (file.type.startsWith("image")) + ImageViewer( + file: file, + ) + else if (file.type.startsWith(RegExp("video|audio"))) + MediaViewer( + file: file, + autoPlay: isAutoPlay.value, + ) + else + SizedBox( + width: MediaQuery.of(context).size.width, + height: MediaQuery.of(context).size.height, + child: UnsupportedNoteFile( + file: file, + showThumbnail: false, + noteUrl: noteUrl, + ), + ), + ], + ), + Positioned( + left: 10, + top: 10, + child: RawMaterialButton( + onPressed: () { + Navigator.of(context).pop(); + }, + constraints: + const BoxConstraints(minWidth: 0, minHeight: 0), + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + padding: EdgeInsets.zero, + fillColor: Theme.of(context) + .scaffoldBackgroundColor + .withAlpha(200), + shape: const CircleBorder(), + child: Padding( + padding: const EdgeInsets.all(5), + child: Icon( + Icons.close, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withAlpha(200), + ), + ), + ), + ), + if (isEnabledSaveButton.value && + (defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.iOS)) + Positioned( + right: 10, + top: 10, + child: RawMaterialButton( + onPressed: () async { + final page = pageController.page?.toInt(); + if (page == null) return; + final driveFile = driveFiles[page]; + final f = await ref + .read(downloadFileNotifierProvider.notifier) + .downloadFile(driveFile); + if (!context.mounted) return; + if (f != DownloadFileResult.succeeded) { + await showDialog( + context: context, + builder: (context) => SimpleMessageDialog( + message: + "${S.of(context).failedFileSave}\n[$f]", + ), + ); + return; + } + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + S.of(context).savedImage, + ), + duration: const Duration(seconds: 1), + ), + ); + }, + constraints: + const BoxConstraints(minWidth: 0, minHeight: 0), + materialTapTargetSize: + MaterialTapTargetSize.shrinkWrap, + padding: EdgeInsets.zero, + fillColor: Theme.of(context) + .scaffoldBackgroundColor + .withAlpha(200), + shape: const CircleBorder(), + child: Padding( + padding: const EdgeInsets.all(5), + child: Icon( + Icons.save, + color: Theme.of(context) + .textTheme + .bodyMedium + ?.color + ?.withAlpha(200), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ), + ), + ); + } +} diff --git a/lib/view/common/note_file_dialog/scale_notifier_interactive_viewer.dart b/lib/view/common/note_file_dialog/scale_notifier_interactive_viewer.dart new file mode 100644 index 000000000..7d4f9542a --- /dev/null +++ b/lib/view/common/note_file_dialog/scale_notifier_interactive_viewer.dart @@ -0,0 +1,62 @@ +import "package:flutter/material.dart"; +import "package:miria/view/common/interactive_viewer.dart" as iv; +import "package:miria/view/common/misskey_notes/network_image.dart"; + +class ScaleNotifierInteractiveViewer extends StatefulWidget { + final String imageUrl; + final TransformationController controller; + final void Function(double) onScaleChanged; + final double maxScale; + final bool canChangeScale; + + const ScaleNotifierInteractiveViewer({ + required this.imageUrl, + required this.controller, + required this.onScaleChanged, + required this.maxScale, + required this.canChangeScale, + super.key, + }); + + @override + State createState() => ScaleNotifierInteractiveViewerState(); +} + +class ScaleNotifierInteractiveViewerState + extends State { + var scale = 1.0; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: MediaQuery.of(context).size.width * 0.95, + height: MediaQuery.of(context).size.height * 0.95, + child: iv.InteractiveViewer( + maxScale: widget.maxScale, + canChangeScale: widget.canChangeScale, + // ピンチイン・ピンチアウト終了後の処理 + transformationController: widget.controller, + onInteractionEnd: (details) { + scale = widget.controller.value.getMaxScaleOnAxis(); + widget.onScaleChanged(scale); + }, + child: NetworkImageView( + url: widget.imageUrl, + type: ImageType.image, + loadingBuilder: ( + context, + child, + loadingProgress, + ) { + if (loadingProgress == null) return child; + return const SizedBox( + height: 48.0, + width: 48.0, + child: Center(child: CircularProgressIndicator.adaptive()), + ); + }, + ), + ), + ); + } +} diff --git a/lib/view/common/note_file_dialog/unsupported_note_file.dart b/lib/view/common/note_file_dialog/unsupported_note_file.dart new file mode 100644 index 000000000..ba2110e9b --- /dev/null +++ b/lib/view/common/note_file_dialog/unsupported_note_file.dart @@ -0,0 +1,94 @@ +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:url_launcher/url_launcher_string.dart"; + +class UnsupportedNoteFile extends StatelessWidget { + final DriveFile file; + final bool showThumbnail; + final String? noteUrl; + + const UnsupportedNoteFile({ + required this.file, + required this.showThumbnail, + this.noteUrl, + super.key, + }); + + @override + Widget build(BuildContext context) { + return Stack( + fit: StackFit.passthrough, + alignment: Alignment.center, + children: [ + if (showThumbnail) + NetworkImageView( + url: file.thumbnailUrl.toString(), + type: ImageType.imageThumbnail, + fit: BoxFit.contain, + ), + Container( + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + ), + ), + Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + S.of(context).unsupportedFile, + ), + const SizedBox(height: 30), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Icon( + Icons.file_present, + ), + const Padding(padding: EdgeInsets.only(left: 5)), + Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + file.name, + ), + ], + ), + ], + ), + const SizedBox(height: 30), + Wrap( + alignment: WrapAlignment.center, + spacing: 24, + runSpacing: 16, + children: [ + ElevatedButton.icon( + onPressed: () async { + await launchUrlString( + file.url, + mode: LaunchMode.externalApplication, + ); + }, + icon: const Icon(Icons.open_in_browser), + label: Text(S.of(context).openBrowsers), + ), + if (noteUrl != null) + ElevatedButton.icon( + onPressed: () async { + await launchUrlString( + noteUrl!, + mode: LaunchMode.externalApplication, + ); + }, + icon: const Icon(Icons.open_in_browser), + label: Text(S.of(context).openNoteInBrowsers), + ), + ], + ), + ], + ), + ], + ); + } +} diff --git a/lib/view/common/notification_icon.dart b/lib/view/common/notification_icon.dart index ef5c64a3f..4c2f4a635 100644 --- a/lib/view/common/notification_icon.dart +++ b/lib/view/common/notification_icon.dart @@ -1,9 +1,8 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; /// 通知アイコン class NotificationIcon extends ConsumerWidget { @@ -11,34 +10,49 @@ class NotificationIcon extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final hasUnread = ref.watch(iProvider(AccountScope.of(context).acct) - .select((value) => value.hasUnreadNotification)); + final acct = ref.watch( + accountContextProvider.select((value) => value.postAccount.acct), + ); + final hasUnread = ref.watch( + iProvider(acct).select((value) => value.hasUnreadNotification), + ); if (hasUnread) { return IconButton( - onPressed: () => context - .pushRoute(NotificationRoute(account: AccountScope.of(context))), - icon: Stack(children: [ + onPressed: () async => context.pushRoute( + NotificationRoute( + accountContext: ref.read(accountContextProvider), + ), + ), + icon: Stack( + children: [ const Icon(Icons.notifications), Transform.translate( - offset: const Offset(12, 12), - child: SizedBox( - width: 14, - height: 14, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.white, width: 1.5), - borderRadius: BorderRadius.circular(20), - color: Theme.of(context).primaryColor, - ), + offset: const Offset(12, 12), + child: SizedBox( + width: 14, + height: 14, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 1.5), + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).primaryColor, ), - )), - ])); + ), + ), + ), + ], + ), + ); } else { return IconButton( - onPressed: () => context - .pushRoute(NotificationRoute(account: AccountScope.of(context))), - icon: const Icon(Icons.notifications)); + onPressed: () async => context.pushRoute( + NotificationRoute( + accountContext: ref.read(accountContextProvider), + ), + ), + icon: const Icon(Icons.notifications), + ); } } } diff --git a/lib/view/common/pushable_listview.dart b/lib/view/common/pushable_listview.dart index 8408ec0da..8ca154c84 100644 --- a/lib/view/common/pushable_listview.dart +++ b/lib/view/common/pushable_listview.dart @@ -1,12 +1,16 @@ -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/model/general_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/view/common/error_notification.dart'; -import 'package:miria/view/common/misskey_ad.dart'; +import "dart:async"; -class PushableListView extends ConsumerStatefulWidget { +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/general_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:miria/view/common/error_notification.dart"; +import "package:miria/view/common/misskey_ad.dart"; + +class PushableListView extends HookConsumerWidget { final Future> Function() initializeFuture; final Future> Function(T, int) nextFuture; final Widget Function(BuildContext, T) itemBuilder; @@ -15,174 +19,161 @@ class PushableListView extends ConsumerStatefulWidget { final bool shrinkWrap; final ScrollPhysics? physics; final bool showAd; + final bool hideIsEmpty; const PushableListView({ - super.key, required this.initializeFuture, required this.nextFuture, required this.itemBuilder, + super.key, this.listKey = "", this.shrinkWrap = false, this.physics, this.additionalErrorInfo, this.showAd = true, + this.hideIsEmpty = false, }); @override - ConsumerState createState() => - PushableListViewState(); -} + Widget build(BuildContext context, WidgetRef ref) { + final isLoading = useState(false); + final error = useState<(Object?, StackTrace)?>(null); + final isFinalPage = useState(false); + final scrollController = useScrollController(); + final items = useState>([]); -class PushableListViewState extends ConsumerState> { - var isLoading = false; - (Object?, StackTrace)? error; - var isFinalPage = false; - final scrollController = ScrollController(); - - final items = []; - - void initialize() { - isLoading = true; - Future(() async { - try { - items - ..clear() - ..addAll(await widget.initializeFuture()); - if (!mounted) return; - setState(() { - isLoading = false; - }); - scrollController.animateTo(-scrollController.position.pixels, - duration: const Duration(milliseconds: 100), curve: Curves.easeIn); - } catch (e, s) { - if (kDebugMode) print(e); - if (mounted) { - setState(() { - error = (e, s); - isLoading = false; - }); + final initialize = useCallback( + () async { + isLoading.value = true; + isFinalPage.value = false; + items.value = []; + try { + final initialItems = await initializeFuture(); + items.value = initialItems; + isLoading.value = false; + await scrollController.animateTo( + -scrollController.position.pixels, + duration: const Duration(milliseconds: 100), + curve: Curves.easeIn, + ); + } catch (e, s) { + if (kDebugMode) print(e); + error.value = (e, s); + isLoading.value = false; } - rethrow; - } - }); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - if (items.isEmpty) { - initialize(); - } - } + }, + [initializeFuture, scrollController, listKey], + ); - @override - void didUpdateWidget(covariant PushableListView oldWidget) { - super.didUpdateWidget(oldWidget); - if (oldWidget.listKey != widget.listKey) { - setState(() { - items.clear(); - }); - initialize(); - } - } + useMemoized( + () => unawaited(initialize()), + [listKey], + ); - Future nextLoad() async { - if (isLoading || items.isEmpty) return; - Future(() async { - try { - if (!mounted) return; - setState(() { - isLoading = true; - }); - final result = await widget.nextFuture(items.last, items.length); - if (result.isEmpty) isFinalPage = true; - items.addAll(result); - if (!mounted) return; - setState(() { - isLoading = false; - }); - } catch (e) { - if (mounted) { - setState(() { - isLoading = false; - }); + final nextLoad = useCallback( + () async { + if (isLoading.value || items.value.isEmpty) return; + isLoading.value = true; + try { + final result = await nextFuture(items.value.last, items.value.length); + if (result.isEmpty) isFinalPage.value = true; + items.value = [...items.value, ...result]; + isLoading.value = false; + } catch (e) { + isLoading.value = false; } - rethrow; - } - }); - } + }, + [isLoading.value, items.value, nextFuture], + ); - @override - Widget build(BuildContext context) { return RefreshIndicator( onRefresh: () async { - setState(() { - items.clear(); - isLoading = true; - }); - initialize(); + items.value.clear(); + isLoading.value = true; + await initialize(); }, child: ListView.builder( - shrinkWrap: widget.shrinkWrap, - physics: widget.physics, - itemCount: items.length + 1, + shrinkWrap: shrinkWrap, + physics: physics, + itemCount: items.value.length + 1, controller: scrollController, itemBuilder: (context, index) { - if (items.length == index) { - if (isFinalPage) { + if (items.value.length == index) { + if (isFinalPage.value) { return Container(); } + if (isLoading.value) { + return const Center( + child: Padding( + padding: EdgeInsets.all(20), + child: CircularProgressIndicator.adaptive(), + ), + ); + } + + if (error.value != null) { + return ErrorDetail( + error: error.value!.$1, + stackTrace: error.value!.$2, + ); + } - if (ref.read(generalSettingsRepositoryProvider - .select((value) => value.settings.automaticPush)) == + if (items.value.isEmpty && !hideIsEmpty) { + return const Center( + child: Padding( + padding: EdgeInsets.all(10), + child: Text("なんもないで"), + ), + ); + } + + if (ref.read( + generalSettingsRepositoryProvider + .select((value) => value.settings.automaticPush), + ) == AutomaticPush.automatic) { - nextLoad(); + unawaited(nextLoad()); } return Column( children: [ - if (error != null) + if (error.value != null) Column( mainAxisAlignment: MainAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start, children: [ ErrorNotification( - error: error?.$1, - stackTrace: error?.$2, + error: error.value?.$1, + stackTrace: error.value?.$2, ), - widget.additionalErrorInfo?.call(context, error) ?? - const SizedBox.shrink() + additionalErrorInfo?.call(context, error.value) ?? + const SizedBox.shrink(), ], ), Center( - child: !isLoading - ? Padding( - padding: const EdgeInsets.only(top: 10, bottom: 10), - child: IconButton( - onPressed: nextLoad, - icon: const Icon(Icons.keyboard_arrow_down), - ), - ) - : const Padding( - padding: EdgeInsets.all(20), - child: CircularProgressIndicator()), - ) + child: Padding( + padding: const EdgeInsets.only(top: 10, bottom: 10), + child: IconButton( + onPressed: nextLoad, + icon: const Icon(Icons.keyboard_arrow_down), + ), + ), + ), ], ); } - if (index != 0 && (index == 3 || index % 30 == 0) && widget.showAd) { + if (index != 0 && (index == 3 || index % 30 == 0) && showAd) { return Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ - widget.itemBuilder(context, items[index]), + itemBuilder(context, items.value[index]), const MisskeyAd(), ], ); } else { - return widget.itemBuilder(context, items[index]); + return itemBuilder(context, items.value[index]); } }, ), diff --git a/lib/view/common/sending_elevated_button.dart b/lib/view/common/sending_elevated_button.dart new file mode 100644 index 000000000..6ea12d8f1 --- /dev/null +++ b/lib/view/common/sending_elevated_button.dart @@ -0,0 +1,15 @@ +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; + +class SendingElevatedButton extends StatelessWidget { + const SendingElevatedButton({super.key}); + + @override + Widget build(BuildContext context) { + return ElevatedButton.icon( + onPressed: null, + label: Text(S.of(context).sending), + icon: const CircularProgressIndicator.adaptive(), + ); + } +} diff --git a/lib/view/common/sharing_intent_listener.dart b/lib/view/common/sharing_intent_listener.dart index e200cb79b..d892614e9 100644 --- a/lib/view/common/sharing_intent_listener.dart +++ b/lib/view/common/sharing_intent_listener.dart @@ -1,21 +1,21 @@ -import 'dart:async'; +import "dart:async"; -import 'package:flutter/material.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:receive_sharing_intent/receive_sharing_intent.dart'; -import 'package:miria/router/app_router.dart'; +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:receive_sharing_intent/receive_sharing_intent.dart"; class SharingIntentListener extends ConsumerStatefulWidget { final AppRouter router; final Widget child; const SharingIntentListener({ - super.key, required this.router, required this.child, + super.key, }); @override @@ -38,27 +38,35 @@ class SharingIntentListenerState extends ConsumerState { ReceiveSharingIntent.getMediaStream().listen((event) { final items = event.map((e) => e.path).toList(); if (account.length == 1) { - widget.router.push(NoteCreateRoute( - initialMediaFiles: items, - initialAccount: account.first, - )); + widget.router.push( + NoteCreateRoute( + initialMediaFiles: items, + initialAccount: account.first, + ), + ); } else { - widget.router.push(SharingAccountSelectRoute( - filePath: items, - )); + widget.router.push( + SharingAccountSelectRoute( + filePath: items, + ), + ); } }); intentDataTextStreamSubscription = ReceiveSharingIntent.getTextStream().listen((event) { if (account.length == 1) { - widget.router.push(NoteCreateRoute( - initialText: event, - initialAccount: account.first, - )); + widget.router.push( + NoteCreateRoute( + initialText: event, + initialAccount: account.first, + ), + ); } else { - widget.router.push(SharingAccountSelectRoute( - sharingText: event, - )); + widget.router.push( + SharingAccountSelectRoute( + sharingText: event, + ), + ); } }); } diff --git a/lib/view/common/tab_icon_view.dart b/lib/view/common/tab_icon_view.dart index 88655524c..8d343843a 100644 --- a/lib/view/common/tab_icon_view.dart +++ b/lib/view/common/tab_icon_view.dart @@ -1,11 +1,10 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/model/tab_icon.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/custom_emoji.dart'; +import "package:flutter/cupertino.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/model/tab_icon.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/misskey_notes/custom_emoji.dart"; class TabIconView extends ConsumerWidget { final TabIcon? icon; @@ -13,8 +12,8 @@ class TabIconView extends ConsumerWidget { final double? size; const TabIconView({ - super.key, required this.icon, + super.key, this.color, this.size, }); @@ -35,8 +34,11 @@ class TabIconView extends ConsumerWidget { return CustomEmoji( emojiData: MisskeyEmojiData.fromEmojiName( emojiName: ":$customEmoji:", - repository: - ref.read(emojiRepositoryProvider(AccountScope.of(context))), + repository: ref.read( + emojiRepositoryProvider( + ref.read(accountContextProvider).getAccount, + ), + ), ), size: iconSize, forceSquare: true, diff --git a/lib/view/common/timeline_listview.dart b/lib/view/common/timeline_listview.dart index d7cca4cc2..7339a5429 100644 --- a/lib/view/common/timeline_listview.dart +++ b/lib/view/common/timeline_listview.dart @@ -1,9 +1,9 @@ -import 'dart:math' as math; +import "dart:math" as math; -import 'package:flutter/gestures.dart' show DragStartBehavior; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; +import "package:flutter/gestures.dart" show DragStartBehavior; +import "package:flutter/material.dart"; +import "package:flutter/rendering.dart"; +import "package:flutter/widgets.dart"; /// Infinite ListView /// @@ -12,14 +12,14 @@ import 'package:flutter/widgets.dart'; class TimelineListView extends StatefulWidget { /// See [ListView.builder] const TimelineListView.builder({ - Key? key, + required this.itemBuilder, + super.key, this.scrollDirection = Axis.vertical, this.reverse = false, this.controller, this.physics, this.padding, this.itemExtent, - required this.itemBuilder, this.itemCount, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, @@ -30,19 +30,18 @@ class TimelineListView extends StatefulWidget { this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, this.restorationId, this.clipBehavior = Clip.hardEdge, - }) : separatorBuilder = null, - super(key: key); + }) : separatorBuilder = null; /// See [ListView.separated] const TimelineListView.separated({ - Key? key, + required this.itemBuilder, + required this.separatorBuilder, + super.key, this.scrollDirection = Axis.vertical, this.reverse = false, this.controller, this.physics, this.padding, - required this.itemBuilder, - required this.separatorBuilder, this.itemCount, this.addAutomaticKeepAlives = true, this.addRepaintBoundaries = true, @@ -53,8 +52,7 @@ class TimelineListView extends StatefulWidget { this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual, this.restorationId, this.clipBehavior = Clip.hardEdge, - }) : itemExtent = null, - super(key: key); + }) : itemExtent = null; /// See: [ScrollView.scrollDirection] final Axis scrollDirection; @@ -122,16 +120,12 @@ class _TimelineListViewState extends State { _InfiniteScrollPosition? _negativeOffset; - //FIXME static - static double minMaxExtent = 0.0; - void timingCallback(_) { - final extent = _negativeOffset?.maxScrollExtent; - if (extent != null) { - minMaxExtent = -extent; - } else { - minMaxExtent = 0; - } + final negativeOffset = _negativeOffset; + if (negativeOffset == null) return; + final extent = negativeOffset.maxScrollExtent; + negativeOffset._minMaxExtent = -extent; + _effectiveController._offset = -extent; } @override @@ -163,60 +157,65 @@ class _TimelineListViewState extends State { @override Widget build(BuildContext context) { - final List slivers = _buildSlivers(context, negative: false); - final List negativeSlivers = _buildSlivers(context, negative: true); - final AxisDirection axisDirection = _getDirection(context); + final slivers = _buildSlivers(context, negative: false); + final negativeSlivers = _buildSlivers(context, negative: true); + final axisDirection = _getDirection(context); final scrollPhysics = widget.physics ?? const AlwaysScrollableScrollPhysics(); return Scrollable( axisDirection: axisDirection, controller: _effectiveController, physics: scrollPhysics, - viewportBuilder: (BuildContext context, ViewportOffset offset) { - return Builder(builder: (BuildContext context) { - /// Build negative [ScrollPosition] for the negative scrolling [Viewport]. - final state = Scrollable.of(context); - final negativeOffset = _InfiniteScrollPosition( - physics: scrollPhysics, - context: state, - initialPixels: -offset.pixels, - keepScrollOffset: _effectiveController.keepScrollOffset, - negativeScroll: true, - ); - _negativeOffset = negativeOffset; - - /// Keep the negative scrolling [Viewport] positioned to the [ScrollPosition]. - offset.addListener(() { - negativeOffset._forceNegativePixels(offset.pixels); - }); - - /// Stack the two [Viewport]s on top of each other so they move in sync. - return Stack( - children: [ - Viewport( - axisDirection: flipAxisDirection(axisDirection), - anchor: 1.0 - widget.anchor, - offset: negativeOffset, - slivers: negativeSlivers, - cacheExtent: widget.cacheExtent, - ), - Viewport( - axisDirection: axisDirection, - anchor: widget.anchor, - offset: offset, - slivers: slivers, - cacheExtent: widget.cacheExtent, - ), - ], - ); - }); + viewportBuilder: (context, offset) { + return Builder( + builder: (context) { + /// Build negative [ScrollPosition] for the negative scrolling [Viewport]. + final state = Scrollable.of(context); + final negativeOffset = _InfiniteScrollPosition( + physics: scrollPhysics, + context: state, + initialPixels: -offset.pixels, + keepScrollOffset: _effectiveController.keepScrollOffset, + negativeScroll: true, + ); + _negativeOffset = negativeOffset; + + /// Keep the negative scrolling [Viewport] positioned to the [ScrollPosition]. + offset.addListener(() { + negativeOffset._forceNegativePixels(offset.pixels); + }); + + /// Stack the two [Viewport]s on top of each other so they move in sync. + return Stack( + children: [ + Viewport( + axisDirection: flipAxisDirection(axisDirection), + anchor: 1.0 - widget.anchor, + offset: negativeOffset, + slivers: negativeSlivers, + cacheExtent: widget.cacheExtent, + ), + Viewport( + axisDirection: axisDirection, + anchor: widget.anchor, + offset: offset, + slivers: slivers, + cacheExtent: widget.cacheExtent, + ), + ], + ); + }, + ); }, ); } AxisDirection _getDirection(BuildContext context) { return getAxisDirectionFromAxisReverseAndDirectionality( - context, widget.scrollDirection, widget.reverse); + context, + widget.scrollDirection, + widget.reverse, + ); } List _buildSlivers(BuildContext context, {bool negative = false}) { @@ -236,22 +235,21 @@ class _TimelineListViewState extends State { : positiveChildrenDelegate, ), padding: EdgeInsets.zero, - ) + ), ]; } SliverChildDelegate get negativeChildrenDelegate { return SliverChildBuilderDelegate( - (BuildContext context, int index) { + (context, index) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - final extent = (_negativeOffset?.hasContentDimensions ?? false) - ? _negativeOffset?.maxScrollExtent + final negativeOffset = _negativeOffset; + if (negativeOffset == null) return; + final extent = negativeOffset.hasContentDimensions + ? negativeOffset.maxScrollExtent : null; - if (extent != null) { - minMaxExtent = -extent; - } else { - minMaxExtent = 0; - } + negativeOffset._minMaxExtent = -(extent ?? 0); + _controller?._offset = -(extent ?? 0); }); final separatorBuilder = widget.separatorBuilder; if (separatorBuilder != null) { @@ -274,7 +272,7 @@ class _TimelineListViewState extends State { final itemCount = widget.itemCount; return SliverChildBuilderDelegate( (separatorBuilder != null) - ? (BuildContext context, int index) { + ? (context, index) { final itemIndex = index ~/ 2; return index.isEven ? widget.itemBuilder(context, itemIndex) @@ -293,21 +291,44 @@ class _TimelineListViewState extends State { void debugFillProperties(DiagnosticPropertiesBuilder properties) { super.debugFillProperties(properties); properties - .add(EnumProperty('scrollDirection', widget.scrollDirection)); - properties.add(FlagProperty('reverse', - value: widget.reverse, ifTrue: 'reversed', showName: true)); - properties.add(DiagnosticsProperty( - 'controller', widget.controller, - showName: false, defaultValue: null)); - properties.add(DiagnosticsProperty('physics', widget.physics, - showName: false, defaultValue: null)); - properties.add(DiagnosticsProperty( - 'padding', widget.padding, - defaultValue: null)); - properties.add( - DoubleProperty('itemExtent', widget.itemExtent, defaultValue: null)); - properties.add( - DoubleProperty('cacheExtent', widget.cacheExtent, defaultValue: null)); + ..add(EnumProperty("scrollDirection", widget.scrollDirection)) + ..add( + FlagProperty( + "reverse", + value: widget.reverse, + ifTrue: "reversed", + showName: true, + ), + ) + ..add( + DiagnosticsProperty( + "controller", + widget.controller, + showName: false, + defaultValue: null, + ), + ) + ..add( + DiagnosticsProperty( + "physics", + widget.physics, + showName: false, + defaultValue: null, + ), + ) + ..add( + DiagnosticsProperty( + "padding", + widget.padding, + defaultValue: null, + ), + ) + ..add( + DoubleProperty("itemExtent", widget.itemExtent, defaultValue: null), + ) + ..add( + DoubleProperty("cacheExtent", widget.cacheExtent, defaultValue: null), + ); } } @@ -315,14 +336,10 @@ class _TimelineListViewState extends State { class TimelineScrollController extends ScrollController { /// Creates a new [TimelineScrollController] TimelineScrollController({ - double initialScrollOffset = 0.0, - bool keepScrollOffset = true, - String? debugLabel, - }) : super( - initialScrollOffset: initialScrollOffset, - keepScrollOffset: keepScrollOffset, - debugLabel: debugLabel, - ) { + super.initialScrollOffset, + super.keepScrollOffset, + super.debugLabel, + }) { addListener(() { final currentPosition = position.pixels; _previousPosition = currentPosition; @@ -331,8 +348,11 @@ class TimelineScrollController extends ScrollController { } @override - ScrollPosition createScrollPosition(ScrollPhysics physics, - ScrollContext context, ScrollPosition? oldPosition) { + ScrollPosition createScrollPosition( + ScrollPhysics physics, + ScrollContext context, + ScrollPosition? oldPosition, + ) { return _InfiniteScrollPosition( physics: physics, context: context, @@ -340,13 +360,17 @@ class TimelineScrollController extends ScrollController { keepScrollOffset: keepScrollOffset, oldPosition: oldPosition, debugLabel: debugLabel, + negativePredicate: () => _offset, ); } + double _offset = 0.0; + double _previousPosition = 0.0; double _previousMaxExtent = 0.0; void forceScrollToTop() { + if (isDisposed) return; if (positions.isEmpty) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { forceScrollToTop(); @@ -356,10 +380,22 @@ class TimelineScrollController extends ScrollController { scrollToTop(); } + bool isDisposed = false; + + @override + void dispose() { + if (isDisposed) return; + isDisposed = true; + super.dispose(); + } + void scrollToTop() { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (isDisposed) return; + if (positions.isEmpty) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (isDisposed) return; scrollToTop(); }); return; @@ -373,6 +409,8 @@ class TimelineScrollController extends ScrollController { if (_previousPosition == _previousMaxExtent) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (isDisposed) return; + scrollToTop(); }); } @@ -382,23 +420,18 @@ class TimelineScrollController extends ScrollController { class _InfiniteScrollPosition extends ScrollPositionWithSingleContext { _InfiniteScrollPosition({ - required ScrollPhysics physics, - required ScrollContext context, - double? initialPixels = 0.0, - bool keepScrollOffset = true, - ScrollPosition? oldPosition, - String? debugLabel, + required super.physics, + required super.context, + super.initialPixels, + super.keepScrollOffset, + super.oldPosition, + super.debugLabel, this.negativeScroll = false, - }) : super( - physics: physics, - context: context, - initialPixels: initialPixels, - keepScrollOffset: keepScrollOffset, - oldPosition: oldPosition, - debugLabel: debugLabel, - ); + this.negativePredicate, + }); final bool negativeScroll; + final double Function()? negativePredicate; void _forceNegativePixels(double value) { super.forcePixels(-value); @@ -419,5 +452,7 @@ class _InfiniteScrollPosition extends ScrollPositionWithSingleContext { } @override - double get minScrollExtent => _TimelineListViewState.minMaxExtent; + double get minScrollExtent => negativePredicate?.call() ?? _minMaxExtent; + + double _minMaxExtent = 0; } diff --git a/lib/view/copy_modal_sheet/copy_note_modal_sheet.dart b/lib/view/copy_modal_sheet/copy_note_modal_sheet.dart new file mode 100644 index 000000000..448668c76 --- /dev/null +++ b/lib/view/copy_modal_sheet/copy_note_modal_sheet.dart @@ -0,0 +1,54 @@ +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:miria/view/themes/app_theme.dart"; + +class CopyNoteModalSheet extends ConsumerWidget { + final String note; + + const CopyNoteModalSheet({ + required this.note, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + body: Padding( + padding: const EdgeInsets.all(10), + child: ListView( + children: [ + ListTile( + title: Text(S.of(context).detail), + trailing: IconButton( + onPressed: () { + Clipboard.setData( + ClipboardData(text: note), + ); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).doneCopy), + duration: const Duration(seconds: 1), + ), + ); + }, + icon: const Icon(Icons.copy), + tooltip: S.of(context).copyContents, + ), + ), + Card( + child: Padding( + padding: const EdgeInsets.all(10), + child: SelectableText( + note, + style: AppTheme.of(context).monospaceStyle, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/view/debug_info_page/debug_info_page.dart b/lib/view/debug_info_page/debug_info_page.dart index 2926de25f..ed520db69 100644 --- a/lib/view/debug_info_page/debug_info_page.dart +++ b/lib/view/debug_info_page/debug_info_page.dart @@ -1,4 +1,4 @@ -import 'package:flutter/material.dart'; +import "package:flutter/material.dart"; class DebugInfoPage extends StatefulWidget { const DebugInfoPage({super.key}); @@ -11,9 +11,10 @@ class DebugInfoPageState extends State { @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar(title: const Text("デバッグ")), - body: ListView( - children: const [ListTile()], - )); + appBar: AppBar(title: const Text("デバッグ")), + body: ListView( + children: const [ListTile()], + ), + ); } } diff --git a/lib/view/dialogs/note_detail_dialog.dart b/lib/view/dialogs/note_detail_dialog.dart index 57351fee7..eb2a8ab6f 100644 --- a/lib/view/dialogs/note_detail_dialog.dart +++ b/lib/view/dialogs/note_detail_dialog.dart @@ -1,12 +1,12 @@ -import 'package:flutter/material.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/repository/time_line_repository.dart'; -import 'package:miria/view/common/misskey_notes/custom_emoji.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/time_line_repository.dart"; +import "package:miria/view/common/misskey_notes/custom_emoji.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:misskey_dart/misskey_dart.dart"; class NoteDetailDialog extends ConsumerStatefulWidget { final Note note; @@ -14,10 +14,10 @@ class NoteDetailDialog extends ConsumerStatefulWidget { final ChangeNotifierProvider timeLineRepository; const NoteDetailDialog({ - super.key, required this.note, required this.timeLineRepository, required this.account, + super.key, }); @override @@ -37,16 +37,21 @@ class NoteDetailDialogState extends ConsumerState { setState(() { foundEmojis.clear(); if (reactionTextField.text.isNotEmpty) { - foundEmojis.addAll(ref - .read(emojiRepositoryProvider(widget.account)) - .emoji - ?.where((element) => - element.emoji.baseName.contains(reactionTextField.text) || - element.aliases - .any((e) => e.contains(reactionTextField.text))) - .take(10) - .map((e) => e.emoji) ?? - []); + foundEmojis.addAll( + ref + .read(emojiRepositoryProvider(widget.account)) + .emoji + ?.where( + (element) => + element.emoji.baseName + .contains(reactionTextField.text) || + element.aliases + .any((e) => e.contains(reactionTextField.text)), + ) + .take(10) + .map((e) => e.emoji) ?? + [], + ); } }); }); diff --git a/lib/view/dialogs/simple_confirm_dialog.dart b/lib/view/dialogs/simple_confirm_dialog.dart index 0771dc79d..bc5182863 100644 --- a/lib/view/dialogs/simple_confirm_dialog.dart +++ b/lib/view/dialogs/simple_confirm_dialog.dart @@ -1,7 +1,7 @@ -import 'package:flutter/material.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; +import "package:flutter/material.dart"; +import "package:miria/model/account.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; class SimpleConfirmDialog extends StatelessWidget { final String message; @@ -19,51 +19,57 @@ class SimpleConfirmDialog extends StatelessWidget { Account? account, }) async => await showDialog( - context: context, - builder: (context) => SimpleConfirmDialog( - message: message, - primary: primary, - secondary: secondary, - isMfm: isMfm, - account: account, - )); + context: context, + builder: (context) => SimpleConfirmDialog( + message: message, + primary: primary, + secondary: secondary, + isMfm: isMfm, + account: account, + ), + ); const SimpleConfirmDialog({ - super.key, required this.message, required this.primary, required this.secondary, + super.key, this.isMfm = false, this.account, - }) : assert(isMfm == false || (isMfm == true && account != null)); + }) : assert(!isMfm || (isMfm && account != null)); @override Widget build(BuildContext context) { if (isMfm) { - return AccountScope( - account: account!, - child: AlertDialog( - content: SimpleMfmText(message), - actions: [ - OutlinedButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text(secondary)), - ElevatedButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text(primary)) - ], - )); + return AccountContextScope.as( + account: account!, + child: AlertDialog( + content: SimpleMfmText(message), + actions: [ + OutlinedButton( + onPressed: () => Navigator.of(context).pop(false), + child: Text(secondary), + ), + ElevatedButton( + onPressed: () => Navigator.of(context).pop(true), + child: Text(primary), + ), + ], + ), + ); } return AlertDialog( content: Text(message), actions: [ OutlinedButton( - onPressed: () => Navigator.of(context).pop(false), - child: Text(secondary)), + onPressed: () => Navigator.of(context).pop(false), + child: Text(secondary), + ), ElevatedButton( - onPressed: () => Navigator.of(context).pop(true), - child: Text(primary)) + onPressed: () => Navigator.of(context).pop(true), + child: Text(primary), + ), ], ); } diff --git a/lib/view/dialogs/simple_message_dialog.dart b/lib/view/dialogs/simple_message_dialog.dart index ac14d1bdf..6402e6dc5 100644 --- a/lib/view/dialogs/simple_message_dialog.dart +++ b/lib/view/dialogs/simple_message_dialog.dart @@ -1,17 +1,18 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; class SimpleMessageDialog extends StatelessWidget { final String message; static Future show(BuildContext context, String message) async => await showDialog( - context: context, - builder: (context) => SimpleMessageDialog(message: message)); + context: context, + builder: (context) => SimpleMessageDialog(message: message), + ); const SimpleMessageDialog({ - super.key, required this.message, + super.key, }); @override @@ -20,10 +21,11 @@ class SimpleMessageDialog extends StatelessWidget { content: Text(message), actions: [ ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text(S.of(context).done)) + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text(S.of(context).done), + ), ], ); } diff --git a/lib/view/explore_page/explore_hashtags.dart b/lib/view/explore_page/explore_hashtags.dart index 74ef1e8bf..4b3d8b45a 100644 --- a/lib/view/explore_page/explore_hashtags.dart +++ b/lib/view/explore_page/explore_hashtags.dart @@ -1,21 +1,14 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/futable_list_builder.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class ExploreHashtags extends ConsumerStatefulWidget { - const ExploreHashtags({super.key}); - - @override - ConsumerState createState() => ExploreHashtagsState(); -} +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/futable_list_builder.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:misskey_dart/misskey_dart.dart"; enum HashtagListType { localTrend, @@ -23,92 +16,98 @@ enum HashtagListType { remote, } -class ExploreHashtagsState extends ConsumerState { - var hashtagListType = HashtagListType.localTrend; +class ExploreHashtags extends HookConsumerWidget { + const ExploreHashtags({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final hashtagListType = useState(HashtagListType.localTrend); + return Column( children: [ Padding( - padding: const EdgeInsets.only(top: 3, bottom: 3), - child: LayoutBuilder( - builder: (context, constraints) => ToggleButtons( - constraints: BoxConstraints.expand( - width: constraints.maxWidth / 3 - - Theme.of(context) - .toggleButtonsTheme - .borderWidth! - .toInt() * - 3), - onPressed: (index) => setState(() { - hashtagListType = HashtagListType.values[index]; - }), - isSelected: [ - for (final element in HashtagListType.values) - element == hashtagListType - ], - children: [ - Text(S.of(context).trend), - Text(S.of(context).local), - Text(S.of(context).remote), - ]))), - if (hashtagListType == HashtagListType.localTrend) - Expanded( - child: FutureListView( - future: ref - .read(misskeyProvider(AccountScope.of(context))) - .hashtags - .trend(), - builder: (context, item) => - Hashtag(hashtag: item.tag, usersCount: item.usersCount)), + padding: const EdgeInsets.only(top: 3, bottom: 3), + child: LayoutBuilder( + builder: (context, constraints) => ToggleButtons( + constraints: BoxConstraints.expand( + width: constraints.maxWidth / 3 - + Theme.of(context).toggleButtonsTheme.borderWidth!.toInt() * + 3, + ), + onPressed: (index) => + hashtagListType.value = HashtagListType.values[index], + isSelected: [ + for (final element in HashtagListType.values) + element == hashtagListType.value, + ], + children: [ + Text(S.of(context).trend), + Text(S.of(context).local), + Text(S.of(context).remote), + ], + ), ), - if (hashtagListType == HashtagListType.local) - Expanded( - child: FutureListView( - future: ref - .read(misskeyProvider(AccountScope.of(context))) - .hashtags - .list(const HashtagsListRequest( + ), + switch (hashtagListType.value) { + HashtagListType.localTrend => Expanded( + child: FutureListView( + future: ref.read(misskeyGetContextProvider).hashtags.trend(), + builder: (context, item) => + Hashtag(hashtag: item.tag, usersCount: item.usersCount), + ), + ), + HashtagListType.local => Expanded( + child: FutureListView( + future: ref.read(misskeyGetContextProvider).hashtags.list( + const HashtagsListRequest( limit: 50, attachedToLocalUserOnly: true, - sort: - HashtagsListSortType.attachedLocalUsersDescendant)), + sort: HashtagsListSortType.attachedLocalUsersDescendant, + ), + ), builder: (context, item) => Hashtag( - hashtag: item.tag, - usersCount: item.attachedLocalUsersCount)), - ), - if (hashtagListType == HashtagListType.remote) - Expanded( - child: FutureListView( - future: ref - .read(misskeyProvider(AccountScope.of(context))) - .hashtags - .list(const HashtagsListRequest( + hashtag: item.tag, + usersCount: item.attachedLocalUsersCount, + ), + ), + ), + HashtagListType.remote => Expanded( + child: FutureListView( + future: ref.read(misskeyGetContextProvider).hashtags.list( + const HashtagsListRequest( limit: 50, attachedToRemoteUserOnly: true, - sort: HashtagsListSortType - .attachedRemoteUsersDescendant)), + sort: + HashtagsListSortType.attachedRemoteUsersDescendant, + ), + ), builder: (context, item) => Hashtag( - hashtag: item.tag, - usersCount: item.attachedRemoteUsersCount)), - ), + hashtag: item.tag, + usersCount: item.attachedRemoteUsersCount, + ), + ), + ), + }, ], ); } } -class Hashtag extends StatelessWidget { +class Hashtag extends ConsumerWidget { final String hashtag; final int usersCount; - const Hashtag({super.key, required this.hashtag, required this.usersCount}); + const Hashtag({required this.hashtag, required this.usersCount, super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return ListTile( - onTap: () => context.pushRoute( - HashtagRoute(hashtag: hashtag, account: AccountScope.of(context))), + onTap: () async => context.pushRoute( + HashtagRoute( + hashtag: hashtag, + accountContext: ref.read(accountContextProvider), + ), + ), title: Text("#$hashtag", style: AppTheme.of(context).hashtagStyle), trailing: MfmText(mfmText: S.of(context).joiningHashtagUsers(usersCount)), ); diff --git a/lib/view/explore_page/explore_highlight.dart b/lib/view/explore_page/explore_highlight.dart index 2d0288712..3e4bb7cd4 100644 --- a/lib/view/explore_page/explore_highlight.dart +++ b/lib/view/explore_page/explore_highlight.dart @@ -1,28 +1,20 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class ExploreHighlight extends ConsumerStatefulWidget { +class ExploreHighlight extends HookConsumerWidget { const ExploreHighlight({ super.key, }); @override - ConsumerState createState() => - ExploreHighlightState(); -} - -class ExploreHighlightState extends ConsumerState { - bool isNote = true; - - @override - Widget build(BuildContext context) { - final account = AccountScope.of(context); + Widget build(BuildContext context, WidgetRef ref) { + final isNote = useState(true); return Padding( padding: const EdgeInsets.only(right: 10), child: Column( @@ -31,71 +23,72 @@ class ExploreHighlightState extends ConsumerState { padding: const EdgeInsets.only(top: 3, bottom: 3, left: 10), child: LayoutBuilder( builder: (context, constraints) => ToggleButtons( - constraints: BoxConstraints.expand( - width: constraints.maxWidth / 2 - - Theme.of(context) - .toggleButtonsTheme - .borderWidth! - .toInt() * - 2), - onPressed: (index) => setState(() { - isNote = index == 0; - }), - isSelected: [ - isNote, - !isNote - ], - children: [ - Text(S.of(context).note), - Text(S.of(context).searchVoteTab) - ]), + constraints: BoxConstraints.expand( + width: constraints.maxWidth / 2 - + Theme.of(context) + .toggleButtonsTheme + .borderWidth! + .toInt() * + 2, + ), + onPressed: (index) => isNote.value = index == 0, + isSelected: [ + isNote.value, + !isNote.value, + ], + children: [ + Text(S.of(context).note), + Text(S.of(context).searchVoteTab), + ], + ), ), ), Expanded( child: PushableListView( - listKey: isNote, + listKey: isNote.value, initializeFuture: () async { final Iterable note; - if (isNote) { + if (isNote.value) { note = await ref - .read(misskeyProvider(account)) + .read(misskeyGetContextProvider) .notes .featured(const NotesFeaturedRequest()); } else { note = await ref - .read(misskeyProvider(account)) + .read(misskeyGetContextProvider) .notes .polls .recommendation(const NotesPollsRecommendationRequest()); } - ref.read(notesProvider(account)).registerAll(note); + ref.read(notesWithProvider).registerAll(note); return note.toList(); }, nextFuture: (item, index) async { final Iterable note; - if (isNote) { - note = await ref - .read(misskeyProvider(account)) - .notes - .featured(NotesFeaturedRequest( - offset: index, - untilId: item.id, - )); + if (isNote.value) { + note = + await ref.read(misskeyGetContextProvider).notes.featured( + NotesFeaturedRequest( + offset: index, + untilId: item.id, + ), + ); } else { note = await ref - .read(misskeyProvider(account)) + .read(misskeyGetContextProvider) .notes .polls .recommendation( - NotesPollsRecommendationRequest(offset: index)); + NotesPollsRecommendationRequest(offset: index), + ); } - ref.read(notesProvider(account)).registerAll(note); + ref.read(notesWithProvider).registerAll(note); return note.toList(); }, itemBuilder: (context, item) => MisskeyNote(note: item), ), - ) + ), ], ), ); diff --git a/lib/view/explore_page/explore_page.dart b/lib/view/explore_page/explore_page.dart index a04b8fbd0..cb24c69fe 100644 --- a/lib/view/explore_page/explore_page.dart +++ b/lib/view/explore_page/explore_page.dart @@ -1,66 +1,63 @@ -import 'package:auto_route/annotations.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/explore_page/explore_hashtags.dart'; -import 'package:miria/view/explore_page/explore_highlight.dart'; -import 'package:miria/view/explore_page/explore_pages.dart'; -import 'package:miria/view/explore_page/explore_plays.dart'; -import 'package:miria/view/explore_page/explore_role.dart'; -import 'package:miria/view/explore_page/explore_server.dart'; -import 'package:miria/view/explore_page/explore_users.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/explore_page/explore_hashtags.dart"; +import "package:miria/view/explore_page/explore_highlight.dart"; +import "package:miria/view/explore_page/explore_pages.dart"; +import "package:miria/view/explore_page/explore_plays.dart"; +import "package:miria/view/explore_page/explore_role.dart"; +import "package:miria/view/explore_page/explore_server.dart"; +import "package:miria/view/explore_page/explore_users.dart"; @RoutePage() -class ExplorePage extends ConsumerStatefulWidget { - final Account account; +class ExplorePage extends ConsumerWidget implements AutoRouteWrapper { + final AccountContext accountContext; const ExplorePage({ + required this.accountContext, super.key, - required this.account, }); @override - ConsumerState createState() => ExplorePageState(); -} + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); -class ExplorePageState extends ConsumerState { @override - Widget build(BuildContext context) { - return AccountScope( - account: widget.account, - child: DefaultTabController( - length: 7, - child: Scaffold( - appBar: AppBar( - title: Text(S.of(context).explore), - bottom: TabBar( - isScrollable: true, - tabs: [ - Tab(text: S.of(context).highlight), - Tab(text: S.of(context).user), - Tab(text: S.of(context).role), - Tab(text: S.of(context).page), - Tab(text: S.of(context).flash), - Tab(text: S.of(context).hashtag), - Tab(text: S.of(context).otherServers), - ], - tabAlignment: TabAlignment.center, - ), - ), - body: const TabBarView( - children: [ - ExploreHighlight(), - ExploreUsers(), - ExploreRole(), - ExplorePages(), - ExplorePlay(), - ExploreHashtags(), - ExploreServer(), - ], - ), + Widget build(BuildContext context, WidgetRef ref) { + return DefaultTabController( + length: 7, + child: Scaffold( + appBar: AppBar( + title: Text(S.of(context).explore), + bottom: TabBar( + isScrollable: true, + tabs: [ + Tab(text: S.of(context).highlight), + Tab(text: S.of(context).user), + Tab(text: S.of(context).role), + Tab(text: S.of(context).page), + Tab(text: S.of(context).flash), + Tab(text: S.of(context).hashtag), + Tab(text: S.of(context).otherServers), + ], + tabAlignment: TabAlignment.center, ), - )); + ), + body: const TabBarView( + children: [ + ExploreHighlight(), + ExploreUsers(), + ExploreRole(), + ExplorePages(), + ExplorePlay(), + ExploreHashtags(), + ExploreServer(), + ], + ), + ), + ); } } diff --git a/lib/view/explore_page/explore_pages.dart b/lib/view/explore_page/explore_pages.dart index df31e2c36..018716e9d 100644 --- a/lib/view/explore_page/explore_pages.dart +++ b/lib/view/explore_page/explore_pages.dart @@ -1,46 +1,45 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/futable_list_builder.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/futable_list_builder.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; -class ExplorePages extends ConsumerStatefulWidget { +class ExplorePages extends ConsumerWidget { const ExplorePages({super.key}); @override - ConsumerState createState() => ExplorePagesState(); -} - -class ExplorePagesState extends ConsumerState { - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Padding( padding: const EdgeInsets.only(left: 10, right: 10), - child: FutureListView(future: () async { - final result = await ref - .read(misskeyProvider(AccountScope.of(context))) - .pages - .featured(); - return result.toList(); - }(), builder: (context, item) { - return ListTile( - onTap: () { - context.pushRoute( - MisskeyRouteRoute(account: AccountScope.of(context), page: item), - ); - }, - title: MfmText( + child: FutureListView( + future: () async { + final result = + await ref.read(misskeyGetContextProvider).pages.featured(); + return result.toList(); + }(), + builder: (context, item) { + return ListTile( + onTap: () async { + await context.pushRoute( + MisskeyRouteRoute( + accountContext: ref.read(accountContextProvider), + page: item, + ), + ); + }, + title: MfmText( mfmText: item.title, style: Theme.of(context) .textTheme .bodyMedium - ?.copyWith(fontWeight: FontWeight.bold)), - subtitle: MfmText(mfmText: item.summary ?? ""), - ); - }), + ?.copyWith(fontWeight: FontWeight.bold), + ), + subtitle: MfmText(mfmText: item.summary ?? ""), + ); + }, + ), ); } } diff --git a/lib/view/explore_page/explore_plays.dart b/lib/view/explore_page/explore_plays.dart index b12644d3d..ea2986116 100644 --- a/lib/view/explore_page/explore_plays.dart +++ b/lib/view/explore_page/explore_plays.dart @@ -1,43 +1,49 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/futable_list_builder.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:url_launcher/url_launcher.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/futable_list_builder.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:url_launcher/url_launcher.dart"; -class ExplorePlay extends ConsumerStatefulWidget { +class ExplorePlay extends ConsumerWidget { const ExplorePlay({super.key}); @override - ConsumerState createState() => ExplorePagesState(); -} - -class ExplorePagesState extends ConsumerState { - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Padding( padding: const EdgeInsets.only(left: 10, right: 10), - child: FutureListView(future: () async { - final result = await ref - .read(misskeyProvider(AccountScope.of(context))) - .flash - .featured(); - return result.toList(); - }(), builder: (context, item) { - return ListTile( - onTap: () async { - await launchUrl( + child: FutureListView( + future: Future(() async { + final result = await ref + .read(misskeyGetContextProvider) + .flash + .featured(const FlashFeaturedRequest()); + return result.toList(); + }), + builder: (context, item) { + return ListTile( + onTap: () async { + await launchUrl( Uri( - scheme: "https", - host: AccountScope.of(context).host, - pathSegments: ["play", item.id]), - mode: LaunchMode.externalApplication); - }, - title: MfmText(mfmText: item.title, style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold)), - subtitle: MfmText(mfmText: item.summary), - ); - }), + scheme: "https", + host: ref.read(accountContextProvider).getAccount.host, + pathSegments: ["play", item.id], + ), + mode: LaunchMode.externalApplication, + ); + }, + title: MfmText( + mfmText: item.title, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.bold), + ), + subtitle: MfmText(mfmText: item.summary), + ); + }, + ), ); } } diff --git a/lib/view/explore_page/explore_role.dart b/lib/view/explore_page/explore_role.dart index 554f88943..2359c0912 100644 --- a/lib/view/explore_page/explore_role.dart +++ b/lib/view/explore_page/explore_role.dart @@ -1,15 +1,14 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/futable_list_builder.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:miria/view/common/misskey_notes/network_image.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/futable_list_builder.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; +import "package:misskey_dart/misskey_dart.dart"; class ExploreRole extends ConsumerWidget { const ExploreRole({super.key}); @@ -19,58 +18,65 @@ class ExploreRole extends ConsumerWidget { return Padding( padding: const EdgeInsets.only(left: 10, right: 10), child: FutureListView( - future: () async { - final response = await ref - .read(misskeyProvider(AccountScope.of(context))) - .roles - .list(); + future: Future(() async { + final response = + await ref.read(misskeyGetContextProvider).roles.list(); - return response - .where((element) => element.usersCount != 0) - .sorted((a, b) => b.displayOrder.compareTo(a.displayOrder)); - }(), - builder: (context, item) => RoleListItem(item: item)), + return response + .where((element) => element.usersCount != 0) + .sorted((a, b) => b.displayOrder.compareTo(a.displayOrder)); + }), + builder: (context, item) => RoleListItem(item: item), + ), ); } } -class RoleListItem extends StatelessWidget { +class RoleListItem extends ConsumerWidget { final RolesListResponse item; - const RoleListItem({super.key, required this.item}); + const RoleListItem({required this.item, super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final iconHeight = MediaQuery.textScalerOf(context) .scale(Theme.of(context).textTheme.bodyMedium!.fontSize!); return ListTile( - onTap: () { - context.pushRoute(ExploreRoleUsersRoute( - item: item, account: AccountScope.of(context))); + onTap: () async { + await context.pushRoute( + ExploreRoleUsersRoute( + item: item, + accountContext: ref.read(accountContextProvider), + ), + ); }, title: Text.rich( - TextSpan(children: [ - if (item.iconUrl != null) - WidgetSpan( + TextSpan( + children: [ + if (item.iconUrl != null) + WidgetSpan( alignment: PlaceholderAlignment.middle, child: Padding( padding: const EdgeInsets.only(right: 10), child: NetworkImageView( + height: iconHeight, + loadingBuilder: (context, _, __) => SizedBox( + width: iconHeight, height: iconHeight, - loadingBuilder: (context, _, __) => SizedBox( - width: iconHeight, - height: iconHeight, - ), - errorBuilder: (context, e, s) => const SizedBox( - width: 1, - height: 1, - ), - url: item.iconUrl!.toString(), - type: ImageType.avatarIcon), - )), - TextSpan(text: item.name), - ]), + ), + errorBuilder: (context, e, s) => const SizedBox( + width: 1, + height: 1, + ), + url: item.iconUrl!.toString(), + type: ImageType.avatarIcon, + ), + ), + ), + TextSpan(text: item.name), + ], + ), ), subtitle: Text(item.description ?? ""), trailing: diff --git a/lib/view/explore_page/explore_role_users_page.dart b/lib/view/explore_page/explore_role_users_page.dart index 48e3a6a06..fe04353a9 100644 --- a/lib/view/explore_page/explore_role_users_page.dart +++ b/lib/view/explore_page/explore_role_users_page.dart @@ -1,92 +1,92 @@ -import 'package:auto_route/annotations.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:miria/view/user_page/user_list_item.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:miria/view/user_page/user_list_item.dart"; +import "package:misskey_dart/misskey_dart.dart"; @RoutePage() -class ExploreRoleUsersPage extends ConsumerWidget { +class ExploreRoleUsersPage extends ConsumerWidget implements AutoRouteWrapper { final RolesListResponse item; - final Account account; + final AccountContext accountContext; const ExploreRoleUsersPage({ - super.key, required this.item, - required this.account, + required this.accountContext, + super.key, }); + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); + @override Widget build(BuildContext context, WidgetRef ref) { return DefaultTabController( length: 2, - child: AccountScope( - account: account, - child: Scaffold( - appBar: AppBar( - title: Text(item.name), - bottom: TabBar( - tabs: [ - Tab(text: S.of(context).user), - Tab(text: S.of(context).timeline), - ], - ), - ), - body: TabBarView( - children: [ - PushableListView( - initializeFuture: () async { - final response = await ref - .read(misskeyProvider(account)) - .roles - .users(RolesUsersRequest(roleId: item.id)); - return response.toList(); - }, - nextFuture: (lastItem, _) async { - final response = - await ref.read(misskeyProvider(account)).roles.users( - RolesUsersRequest( - roleId: item.id, - untilId: lastItem.id, - ), - ); - return response.toList(); - }, - itemBuilder: (context, item) => UserListItem( - user: item.user, - isDetail: true, - ), - ), - PushableListView( - initializeFuture: () async { - final response = await ref - .read(misskeyProvider(account)) - .roles - .notes(RolesNotesRequest(roleId: item.id)); - ref.read(notesProvider(account)).registerAll(response); - return response.toList(); - }, - nextFuture: (lastItem, _) async { - final response = - await ref.read(misskeyProvider(account)).roles.notes( - RolesNotesRequest( - roleId: item.id, - untilId: lastItem.id, - ), - ); - ref.read(notesProvider(account)).registerAll(response); - return response.toList(); - }, - itemBuilder: (context, note) => MisskeyNote(note: note), - ), + child: Scaffold( + appBar: AppBar( + title: Text(item.name), + bottom: TabBar( + tabs: [ + Tab(text: S.of(context).user), + Tab(text: S.of(context).timeline), ], ), ), + body: TabBarView( + children: [ + PushableListView( + initializeFuture: () async { + final response = await ref + .read(misskeyGetContextProvider) + .roles + .users(RolesUsersRequest(roleId: item.id)); + return response.toList(); + }, + nextFuture: (lastItem, _) async { + final response = + await ref.read(misskeyGetContextProvider).roles.users( + RolesUsersRequest( + roleId: item.id, + untilId: lastItem.id, + ), + ); + return response.toList(); + }, + itemBuilder: (context, item) => UserListItem( + user: item.user, + isDetail: true, + ), + ), + PushableListView( + initializeFuture: () async { + final response = await ref + .read(misskeyGetContextProvider) + .roles + .notes(RolesNotesRequest(roleId: item.id)); + ref.read(notesWithProvider).registerAll(response); + return response.toList(); + }, + nextFuture: (lastItem, _) async { + final response = + await ref.read(misskeyGetContextProvider).roles.notes( + RolesNotesRequest( + roleId: item.id, + untilId: lastItem.id, + ), + ); + ref.read(notesWithProvider).registerAll(response); + return response.toList(); + }, + itemBuilder: (context, note) => MisskeyNote(note: note), + ), + ], + ), ), ); } diff --git a/lib/view/explore_page/explore_server.dart b/lib/view/explore_page/explore_server.dart index 9d5e57aac..cfc58b3a8 100644 --- a/lib/view/explore_page/explore_server.dart +++ b/lib/view/explore_page/explore_server.dart @@ -1,25 +1,25 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_server_list.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/misskey_server_list.dart"; -class ExploreServer extends ConsumerStatefulWidget { +class ExploreServer extends ConsumerWidget { const ExploreServer({super.key}); @override - ConsumerState createState() => ExploreServerState(); -} - -class ExploreServerState extends ConsumerState { - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Padding( padding: const EdgeInsets.only(left: 10, right: 10), child: MisskeyServerList( - onTap: (item) => context.pushRoute(FederationRoute( - account: AccountScope.of(context), host: item.url))), + onTap: (item) async => context.pushRoute( + FederationRoute( + accountContext: ref.read(accountContextProvider), + host: item.url, + ), + ), + ), ); } } diff --git a/lib/view/explore_page/explore_users.dart b/lib/view/explore_page/explore_users.dart index 3a4658074..2f4e6a5f0 100644 --- a/lib/view/explore_page/explore_users.dart +++ b/lib/view/explore_page/explore_users.dart @@ -1,18 +1,20 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/extensions/users_sort_type_extension.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:miria/view/user_page/user_list_item.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/users_sort_type_extension.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:miria/view/user_page/user_list_item.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; -class ExploreUsers extends ConsumerStatefulWidget { - const ExploreUsers({super.key}); +part "explore_users.g.dart"; - @override - ConsumerState createState() => ExploreUsersState(); +@Riverpod(dependencies: [misskeyGetContext]) +Future> _pinnedUser(_PinnedUserRef ref) async { + return (await ref.read(misskeyGetContextProvider).pinnedUsers()).toList(); } enum ExploreUserType { @@ -21,30 +23,16 @@ enum ExploreUserType { remote, } -class ExploreUsersState extends ConsumerState { - final List pinnedUser = []; - var exploreUserType = ExploreUserType.pinned; - var sortType = UsersSortType.followerDescendant; - var isDetailOpen = false; +class ExploreUsers extends HookConsumerWidget { + const ExploreUsers({super.key}); @override - void didChangeDependencies() { - super.didChangeDependencies(); - Future(() async { - final response = await ref - .read(misskeyProvider(AccountScope.of(context))) - .pinnedUsers(); - if (!mounted) return; - setState(() { - pinnedUser - ..clear() - ..addAll(response); - }); - }); - } + Widget build(BuildContext context, WidgetRef ref) { + final exploreUserType = useState(ExploreUserType.pinned); + final sortType = useState(UsersSortType.followerDescendant); + final isDetailOpen = useState(false); + final pinnedUser = ref.watch(_pinnedUserProvider); - @override - Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(left: 10, right: 10), child: Column( @@ -60,108 +48,116 @@ class ExploreUsersState extends ConsumerState { padding: const EdgeInsets.only(top: 3, bottom: 3), child: LayoutBuilder( builder: (context, constraints) => ToggleButtons( - constraints: BoxConstraints.expand( - width: constraints.maxWidth / 3 - - Theme.of(context) - .toggleButtonsTheme - .borderWidth! - .toInt() * - 3), - onPressed: (index) => setState(() { - exploreUserType = - ExploreUserType.values[index]; - }), - isSelected: [ - for (final element in ExploreUserType.values) - element == exploreUserType - ], - children: [ - Text(S.of(context).pinnedUser), - Text(S.of(context).local), - Text(S.of(context).remote), - ]), + constraints: BoxConstraints.expand( + width: constraints.maxWidth / 3 - + Theme.of(context) + .toggleButtonsTheme + .borderWidth! + .toInt() * + 3, + ), + onPressed: (index) => exploreUserType.value = + ExploreUserType.values[index], + isSelected: [ + for (final element in ExploreUserType.values) + element == exploreUserType.value, + ], + children: [ + Text(S.of(context).pinnedUser), + Text(S.of(context).local), + Text(S.of(context).remote), + ], + ), ), ), ), IconButton( - onPressed: exploreUserType == ExploreUserType.pinned - ? null - : () { - setState(() { - isDetailOpen = !isDetailOpen; - }); - }, - icon: Icon(isDetailOpen + onPressed: exploreUserType == ExploreUserType.pinned + ? null + : () => isDetailOpen.value = !isDetailOpen.value, + icon: Icon( + isDetailOpen.value ? Icons.keyboard_arrow_up - : Icons.keyboard_arrow_down)), + : Icons.keyboard_arrow_down, + ), + ), ], ), - if (isDetailOpen) ...[ + if (isDetailOpen.value) ...[ Row( children: [ Expanded( - child: Text(S.of(context).sort, - textAlign: TextAlign.center)), + child: + Text(S.of(context).sort, textAlign: TextAlign.center), + ), Expanded( child: DropdownButton( - items: [ - for (final sortType in UsersSortType.values) - DropdownMenuItem( - value: sortType, - child: Text(sortType.displayName(context)), - ), - ], - value: sortType, - onChanged: (e) { - setState(() { - sortType = e ?? UsersSortType.followerDescendant; - }); - }), - ) + items: [ + for (final sortType in UsersSortType.values) + DropdownMenuItem( + value: sortType, + child: Text(sortType.displayName(context)), + ), + ], + value: sortType.value, + onChanged: (e) => sortType.value = + e ?? UsersSortType.followerDescendant, + ), + ), ], ), ], ], ), - if (exploreUserType == ExploreUserType.pinned) - Expanded( - child: ListView.builder( - itemCount: pinnedUser.length, - itemBuilder: (context, index) => UserListItem( - user: pinnedUser[index], - isDetail: true, - ), - ), - ) + if (exploreUserType.value == ExploreUserType.pinned) + switch (pinnedUser) { + AsyncLoading() => + const Center(child: CircularProgressIndicator.adaptive()), + AsyncError(:final error, :final stackTrace) => + ErrorDetail(error: error, stackTrace: stackTrace), + AsyncData(:final value) => Expanded( + child: ListView.builder( + itemCount: value.length, + itemBuilder: (context, index) => UserListItem( + user: value[index], + isDetail: true, + ), + ), + ) + } else Expanded( child: PushableListView( - listKey: Object.hashAll([sortType, exploreUserType]), + listKey: Object.hashAll( + [sortType.value, exploreUserType.value], + ), initializeFuture: () async { - final response = await ref - .read(misskeyProvider(AccountScope.of(context))) - .users - .users(UsersUsersRequest( - sort: sortType, - state: UsersState.alive, - origin: exploreUserType == ExploreUserType.remote - ? Origin.remote - : Origin.local, - )); + final response = + await ref.read(misskeyGetContextProvider).users.users( + UsersUsersRequest( + sort: sortType.value, + state: UsersState.alive, + origin: exploreUserType.value == + ExploreUserType.remote + ? Origin.remote + : Origin.local, + ), + ); return response.toList(); }, nextFuture: (_, index) async { - final response = await ref - .read(misskeyProvider(AccountScope.of(context))) - .users - .users(UsersUsersRequest( - sort: sortType, - state: UsersState.alive, - offset: index, - origin: exploreUserType == ExploreUserType.remote - ? Origin.remote - : Origin.local, - )); + final response = + await ref.read(misskeyGetContextProvider).users.users( + UsersUsersRequest( + sort: sortType.value, + state: UsersState.alive, + offset: index, + origin: exploreUserType.value == + ExploreUserType.remote + ? Origin.remote + : Origin.local, + ), + ); return response.toList(); }, itemBuilder: (context, user) => UserListItem( @@ -169,7 +165,7 @@ class ExploreUsersState extends ConsumerState { isDetail: true, ), ), - ) + ), ], ), ); diff --git a/lib/view/explore_page/explore_users.g.dart b/lib/view/explore_page/explore_users.g.dart new file mode 100644 index 000000000..47a0d0c79 --- /dev/null +++ b/lib/view/explore_page/explore_users.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'explore_users.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$pinnedUserHash() => r'042fec5f8f75f588ec4457af44e7f2e627a3e7f2'; + +/// See also [_pinnedUser]. +@ProviderFor(_pinnedUser) +final _pinnedUserProvider = + AutoDisposeFutureProvider>.internal( + _pinnedUser, + name: r'_pinnedUserProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$pinnedUserHash, + dependencies: [misskeyGetContextProvider], + allTransitiveDependencies: { + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies + }, +); + +typedef _PinnedUserRef = AutoDisposeFutureProviderRef>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/favorited_note_page/favorited_note_page.dart b/lib/view/favorited_note_page/favorited_note_page.dart index 93b744133..77305de6c 100644 --- a/lib/view/favorited_note_page/favorited_note_page.dart +++ b/lib/view/favorited_note_page/favorited_note_page.dart @@ -1,53 +1,55 @@ -import 'package:auto_route/annotations.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; @RoutePage() -class FavoritedNotePage extends ConsumerWidget { - final Account account; +class FavoritedNotePage extends ConsumerWidget implements AutoRouteWrapper { + final AccountContext accountContext; - const FavoritedNotePage({super.key, required this.account}); + const FavoritedNotePage({required this.accountContext, super.key}); + + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); @override Widget build(BuildContext context, WidgetRef ref) { return Scaffold( - appBar: AppBar( - title: Text(S.of(context).favorite), + appBar: AppBar( + title: Text(S.of(context).favorite), + ), + body: Padding( + padding: const EdgeInsets.only(right: 10), + child: PushableListView( + initializeFuture: () async { + final response = await ref + .read(misskeyPostContextProvider) + .i + .favorites(const IFavoritesRequest()); + ref + .read(notesWithProvider) + .registerAll(response.map((e) => e.note)); + return response.map((e) => e.note).toList(); + }, + nextFuture: (lastItem, _) async { + final response = await ref + .read(misskeyPostContextProvider) + .i + .favorites(IFavoritesRequest(untilId: lastItem.id)); + ref + .read(notesWithProvider) + .registerAll(response.map((e) => e.note)); + return response.map((e) => e.note).toList(); + }, + itemBuilder: (context, item) => MisskeyNote(note: item), ), - body: AccountScope( - account: account, - child: Padding( - padding: const EdgeInsets.only(right: 10), - child: PushableListView( - initializeFuture: () async { - final response = await ref - .read(misskeyProvider(account)) - .i - .favorites(const IFavoritesRequest()); - ref - .read(notesProvider(account)) - .registerAll(response.map((e) => e.note)); - return response.map((e) => e.note).toList(); - }, - nextFuture: (lastItem, _) async { - final response = await ref - .read(misskeyProvider(account)) - .i - .favorites(IFavoritesRequest(untilId: lastItem.id)); - ref - .read(notesProvider(account)) - .registerAll(response.map((e) => e.note)); - return response.map((e) => e.note).toList(); - }, - itemBuilder: (context, item) => MisskeyNote(note: item), - ), - ))); + ), + ); } } diff --git a/lib/view/federation_page/federation_ads.dart b/lib/view/federation_page/federation_ads.dart index fff5e2eb9..a4cac84cb 100644 --- a/lib/view/federation_page/federation_ads.dart +++ b/lib/view/federation_page/federation_ads.dart @@ -1,21 +1,14 @@ -import 'package:flutter/cupertino.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/view/common/misskey_notes/network_image.dart'; -import 'package:miria/view/federation_page/federation_page.dart'; -import 'package:url_launcher/url_launcher.dart'; +import "package:flutter/cupertino.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:url_launcher/url_launcher.dart"; -class FederationAds extends ConsumerStatefulWidget { - const FederationAds({super.key}); - @override - ConsumerState createState() => FederationAdsState(); -} +class FederationAds extends StatelessWidget { + final List ads; -class FederationAdsState extends ConsumerState { + const FederationAds({required this.ads, super.key}); @override Widget build(BuildContext context) { - final ads = ref.watch( - federationPageFederationDataProvider.select((value) => value?.ads)); - if (ads == null) return const SizedBox.shrink(); return ListView.builder( itemCount: ads.length, itemBuilder: (context, index) { @@ -23,9 +16,12 @@ class FederationAdsState extends ConsumerState { return Padding( padding: const EdgeInsets.all(10), child: GestureDetector( - onTap: () => launchUrl(ad.url), - child: NetworkImageView( - url: ad.imageUrl.toString(), type: ImageType.ad)), + onTap: () async => launchUrl(ad.url), + child: NetworkImageView( + url: ad.imageUrl.toString(), + type: ImageType.ad, + ), + ), ); }, ); diff --git a/lib/view/federation_page/federation_announcements.dart b/lib/view/federation_page/federation_announcements.dart index 21ffa3c19..df309ffa6 100644 --- a/lib/view/federation_page/federation_announcements.dart +++ b/lib/view/federation_page/federation_announcements.dart @@ -1,35 +1,24 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/extensions/date_time_extension.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:miria/view/common/misskey_notes/network_image.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/date_time_extension.dart"; +import "package:miria/hooks/use_async.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class FederationAnnouncements extends ConsumerStatefulWidget { +class FederationAnnouncements extends HookConsumerWidget { final String host; - const FederationAnnouncements({ - super.key, - required this.host, - }); + const FederationAnnouncements({required this.host, super.key}); @override - ConsumerState createState() => - FederationAnnouncementsState(); -} - -class FederationAnnouncementsState - extends ConsumerState { - var isActive = true; + Widget build(BuildContext context, WidgetRef ref) { + final isActive = useState(true); - @override - Widget build(BuildContext context) { - final account = AccountScope.of(context); - final isCurrentServer = widget.host == AccountScope.of(context).host; return Column( children: [ Row( @@ -38,26 +27,26 @@ class FederationAnnouncementsState child: Center( child: ToggleButtons( isSelected: [ - isActive, - !isActive, + isActive.value, + !isActive.value, ], onPressed: (value) { - setState(() { - switch (value) { - case 0: - isActive = true; - case 1: - isActive = false; - } - }); + switch (value) { + case 0: + isActive.value = true; + case 1: + isActive.value = false; + } }, children: [ Padding( - padding: const EdgeInsets.only(left: 5, right: 5), - child: Text(S.of(context).activeAnnouncements)), + padding: const EdgeInsets.only(left: 5, right: 5), + child: Text(S.of(context).activeAnnouncements), + ), Padding( - padding: const EdgeInsets.only(left: 5, right: 5), - child: Text(S.of(context).inactiveAnnouncements)), + padding: const EdgeInsets.only(left: 5, right: 5), + child: Text(S.of(context).inactiveAnnouncements), + ), ], ), ), @@ -66,154 +55,137 @@ class FederationAnnouncementsState ), Expanded( child: PushableListView( - listKey: isActive, - initializeFuture: () async { - final Iterable response; - final request = - AnnouncementsRequest(isActive: isActive, limit: 10); - if (isCurrentServer) { - response = await ref - .read(misskeyProvider(account)) - .announcements(request); - } else { - response = await ref - .read(misskeyWithoutAccountProvider(widget.host)) - .announcements(request); - } - return response.toList(); - }, - nextFuture: (lastItem, offset) async { - final Iterable response; - // 互換性のためにuntilIdとoffsetを両方いれる - final request = AnnouncementsRequest( - untilId: lastItem.id, - isActive: isActive, - limit: 30, - offset: offset); - if (isCurrentServer) { - response = await ref - .read(misskeyProvider(account)) - .announcements(request); - } else { - response = await ref - .read(misskeyWithoutAccountProvider(widget.host)) - .announcements(request); - } - return response.toList(); - }, - itemBuilder: (context, data) => - Announcement(data: data, host: widget.host)), + listKey: isActive.value, + initializeFuture: () async { + final Iterable response; + final request = + AnnouncementsRequest(isActive: isActive.value, limit: 10); + response = await ref + .read(misskeyGetContextProvider) + .announcements(request); + return response.toList(); + }, + nextFuture: (lastItem, offset) async { + final Iterable response; + // 互換性のためにuntilIdとoffsetを両方いれる + final request = AnnouncementsRequest( + untilId: lastItem.id, + isActive: isActive.value, + limit: 30, + offset: offset, + ); + response = await ref + .read(misskeyGetContextProvider) + .announcements(request); + return response.toList(); + }, + itemBuilder: (context, data) => + Announcement(initialData: data, host: host), + ), ), ], ); } } -class Announcement extends ConsumerStatefulWidget { - final AnnouncementsResponse data; +class Announcement extends HookConsumerWidget { + final AnnouncementsResponse initialData; final String host; const Announcement({ - super.key, - required this.data, + required this.initialData, required this.host, + super.key, }); @override - ConsumerState createState() => AnnouncementState(); -} + Widget build(BuildContext context, WidgetRef ref) { + final data = useState(initialData); + final icon = data.value.icon; + final imageUrl = data.value.imageUrl; -class AnnouncementState extends ConsumerState { - late AnnouncementsResponse data; - - @override - void initState() { - super.initState(); - data = widget.data; - } + final confirm = useAsync(() async { + if (data.value.needConfirmationToRead == true) { + final isConfirmed = + await ref.read(dialogStateNotifierProvider.notifier).showDialog( + message: (context) => + S.of(context).confirmAnnouncementsRead(data.value.title), + actions: (context) => [ + S.of(context).readAnnouncement, + S.of(context).didNotReadAnnouncement, + ], + ); + if (isConfirmed != 0) return; + } + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + await ref.read(misskeyPostContextProvider).i.readAnnouncement( + IReadAnnouncementRequest( + announcementId: data.value.id, + ), + ); + data.value = data.value.copyWith(isRead: true); + }); + }); - @override - Widget build(BuildContext context) { - final icon = data.icon; - final imageUrl = data.imageUrl; return Padding( - padding: const EdgeInsets.all(10), - child: Card( - child: Padding( - padding: const EdgeInsets.all(10), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - if (data.forYou == true) - Text(S.of(context).announcementsForYou, - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(color: Theme.of(context).primaryColor)), - Row( - children: [ - if (icon != null) AnnouncementIcon(iconType: icon), - Expanded( - child: MfmText( - mfmText: data.title, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - ], - ), - Align( - alignment: Alignment.centerRight, - child: Text(data.createdAt.format(context)), + padding: const EdgeInsets.all(10), + child: Card( + child: Padding( + padding: const EdgeInsets.all(10), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + if (data.value.forYou == true) + Text( + S.of(context).announcementsForYou, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(color: Theme.of(context).primaryColor), ), - const Padding(padding: EdgeInsets.only(top: 10)), - MfmText( - mfmText: data.text, - host: AccountScope.of(context).host == widget.host - ? null - : widget.host, - ), - if (imageUrl != null) - Center( - child: Padding( - padding: const EdgeInsets.all(10), - child: NetworkImageView( - url: imageUrl.toString(), - type: ImageType.image, - ), + Row( + children: [ + if (icon != null) AnnouncementIcon(iconType: icon), + Expanded( + child: MfmText( + mfmText: data.value.title, + style: Theme.of(context).textTheme.titleMedium, ), ), - if (AccountScope.of(context).host == widget.host && - data.isRead == false) - ElevatedButton( - onPressed: () async { - final account = AccountScope.of(context); - if (data.needConfirmationToRead == true) { - final isConfirmed = await SimpleConfirmDialog.show( - context: context, - message: S - .of(context) - .confirmAnnouncementsRead(data.title), - primary: S.of(context).readAnnouncement, - secondary: S.of(context).didNotReadAnnouncement); - if (isConfirmed != true) return; - } - - await ref - .read(misskeyProvider(account)) - .i - .readAnnouncement(IReadAnnouncementRequest( - announcementId: data.id)); - setState(() { - data = data.copyWith(isRead: true); - }); - }, - child: Text(S.of(context).done)) - ], - ), + ], + ), + Align( + alignment: Alignment.centerRight, + child: Text(data.value.createdAt.format(context)), + ), + const Padding(padding: EdgeInsets.only(top: 10)), + MfmText( + mfmText: data.value.text, + host: ref.read(accountContextProvider).isSame ? null : host, + ), + if (imageUrl != null) + Center( + child: Padding( + padding: const EdgeInsets.all(10), + child: NetworkImageView( + url: imageUrl.toString(), + type: ImageType.image, + ), + ), + ), + if (ref.read(accountContextProvider).isSame && + data.value.isRead == false) + ElevatedButton( + onPressed: confirm.executeOrNull, + child: Text(S.of(context).done), + ), + ], ), - )); + ), + ), + ); } } @@ -221,8 +193,8 @@ class AnnouncementIcon extends StatelessWidget { final AnnouncementIconType iconType; const AnnouncementIcon({ - super.key, required this.iconType, + super.key, }); @override diff --git a/lib/view/federation_page/federation_custom_emojis.dart b/lib/view/federation_page/federation_custom_emojis.dart index 8c3fa88de..415fed85f 100644 --- a/lib/view/federation_page/federation_custom_emojis.dart +++ b/lib/view/federation_page/federation_custom_emojis.dart @@ -1,137 +1,152 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/error_detail.dart'; -import 'package:miria/view/common/misskey_notes/custom_emoji.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:miria/view/common/misskey_notes/custom_emoji.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; -class FederationCustomEmojis extends ConsumerStatefulWidget { +part "federation_custom_emojis.g.dart"; + +@Riverpod(dependencies: [misskeyGetContext]) +Future>> fetchEmoji( + FetchEmojiRef ref, + String host, + MetaResponse meta, +) async { + final result = await ref.read(misskeyGetContextProvider).emojis(); + + return result.emojis.groupListsBy((e) => e.category ?? ""); +} + +class FederationCustomEmojis extends ConsumerWidget { final String host; final MetaResponse meta; const FederationCustomEmojis({ - super.key, required this.host, required this.meta, + super.key, }); @override - ConsumerState createState() => - FederationCustomEmojisState(); -} - -class FederationCustomEmojisState - extends ConsumerState { - var isLoading = false; - (Object?, StackTrace)? error; - - Map> emojis = {}; - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - - Future(() async { - final result = await ref - .read(misskeyProvider(Account.demoAccount(widget.host, widget.meta))) - .emojis(); - emojis - ..clear() - ..addAll(result.emojis.groupListsBy((e) => e.category ?? "")); - if (!mounted) return; - setState(() {}); - }); - } + Widget build(BuildContext context, WidgetRef ref) { + final emoji = ref.watch(fetchEmojiProvider(host, meta)); - @override - Widget build(BuildContext context) { - if (isLoading) return const Center(child: CircularProgressIndicator()); - if (error != null) { - return ErrorDetail( - error: error?.$1, - stackTrace: error?.$2, - ); - } - return ListView.builder( - itemCount: emojis.length, - itemBuilder: (context, index) { - final entry = emojis.entries.toList()[index]; - return ExpansionTile( - title: Text(entry.key), - childrenPadding: EdgeInsets.zero, - children: [ - for (final element in entry.value) - SizedBox( - width: double.infinity, - child: Padding( - padding: const EdgeInsets.only( - left: 10, right: 10, top: 5, bottom: 5), - child: Card( + return switch (emoji) { + AsyncLoading() => const Center( + child: CircularProgressIndicator.adaptive(), + ), + AsyncError(:final error, :final stackTrace) => ErrorDetail( + error: error, + stackTrace: stackTrace, + ), + AsyncData(:final value) => ListView.builder( + itemCount: value.length, + itemBuilder: (context, index) { + final entry = value.entries.toList()[index]; + return ExpansionTile( + title: Text(entry.key), + childrenPadding: EdgeInsets.zero, + children: [ + for (final element in entry.value) + SizedBox( + width: double.infinity, child: Padding( padding: const EdgeInsets.only( - left: 10, right: 10, bottom: 10), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text.rich(TextSpan(children: [ - WidgetSpan( - child: CustomEmoji( - emojiData: CustomEmojiData( - baseName: element.name, - hostedName: widget.host, - url: element.url, - isCurrentServer: false, - isSensitive: element.isSensitive), - fontSizeRatio: 2, + left: 10, + right: 10, + top: 5, + bottom: 5, + ), + child: Card( + child: Padding( + padding: const EdgeInsets.only( + left: 10, + right: 10, + bottom: 10, + ), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text.rich( + TextSpan( + children: [ + WidgetSpan( + child: CustomEmoji( + emojiData: CustomEmojiData( + baseName: element.name, + hostedName: host, + url: element.url, + isCurrentServer: false, + isSensitive: element.isSensitive, + ), + fontSizeRatio: 2, + ), + alignment: PlaceholderAlignment.middle, + ), + const WidgetSpan( + child: Padding( + padding: EdgeInsets.only(left: 10), + ), + ), + TextSpan( + text: ":${element.name}:", + style: Theme.of(context) + .textTheme + .titleMedium, + ), + if (element.isSensitive) + WidgetSpan( + child: Container( + decoration: BoxDecoration( + color: + Theme.of(context).primaryColor, + ), + child: Padding( + padding: const EdgeInsets.only( + left: 3, + right: 3, + ), + child: Text( + S.of(context).sensitive, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith( + color: Colors.white, + ), + ), + ), + ), + ), + ], + ), + ), + if (element.aliases.isNotEmpty) ...[ + const Padding( + padding: EdgeInsets.only(top: 10), ), - alignment: PlaceholderAlignment.middle), - const WidgetSpan( - child: Padding( - padding: EdgeInsets.only(left: 10))), - TextSpan( - text: ":${element.name}:", - style: Theme.of(context).textTheme.titleMedium, - ), - if (element.isSensitive) - WidgetSpan( - child: Container( - decoration: BoxDecoration( - color: Theme.of(context).primaryColor), - child: Padding( - padding: - const EdgeInsets.only(left: 3, right: 3), - child: Text( - S.of(context).sensitive, - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(color: Colors.white), - ), + Text( + element.aliases.join(" "), + style: Theme.of(context).textTheme.bodyMedium, ), - )), - ])), - if (element.aliases.isNotEmpty) ...[ - const Padding(padding: EdgeInsets.only(top: 10)), - Text( - element.aliases.join(" "), - style: Theme.of(context).textTheme.bodyMedium, - ), - ] - ], + ], + ], + ), + ), ), ), ), - ), - ) - ], - ); - }, - ); + ], + ); + }, + ) + }; } } diff --git a/lib/view/federation_page/federation_custom_emojis.g.dart b/lib/view/federation_page/federation_custom_emojis.g.dart new file mode 100644 index 000000000..bcbed691b --- /dev/null +++ b/lib/view/federation_page/federation_custom_emojis.g.dart @@ -0,0 +1,232 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'federation_custom_emojis.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$fetchEmojiHash() => r'9384ae1ca986e12b77f51e9b692a0355d6deaebc'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [fetchEmoji]. +@ProviderFor(fetchEmoji) +const fetchEmojiProvider = FetchEmojiFamily(); + +/// See also [fetchEmoji]. +class FetchEmojiFamily extends Family { + /// See also [fetchEmoji]. + const FetchEmojiFamily(); + + static final Iterable _dependencies = [ + misskeyGetContextProvider + ]; + + static final Iterable _allTransitiveDependencies = + { + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies + }; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'fetchEmojiProvider'; + + /// See also [fetchEmoji]. + FetchEmojiProvider call( + String host, + MetaResponse meta, + ) { + return FetchEmojiProvider( + host, + meta, + ); + } + + @visibleForOverriding + @override + FetchEmojiProvider getProviderOverride( + covariant FetchEmojiProvider provider, + ) { + return call( + provider.host, + provider.meta, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith( + FutureOr>> Function(FetchEmojiRef ref) create) { + return _$FetchEmojiFamilyOverride(this, create); + } +} + +class _$FetchEmojiFamilyOverride implements FamilyOverride { + _$FetchEmojiFamilyOverride(this.overriddenFamily, this.create); + + final FutureOr>> Function(FetchEmojiRef ref) create; + + @override + final FetchEmojiFamily overriddenFamily; + + @override + FetchEmojiProvider getProviderOverride( + covariant FetchEmojiProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [fetchEmoji]. +class FetchEmojiProvider + extends AutoDisposeFutureProvider>> { + /// See also [fetchEmoji]. + FetchEmojiProvider( + String host, + MetaResponse meta, + ) : this._internal( + (ref) => fetchEmoji( + ref as FetchEmojiRef, + host, + meta, + ), + from: fetchEmojiProvider, + name: r'fetchEmojiProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$fetchEmojiHash, + dependencies: FetchEmojiFamily._dependencies, + allTransitiveDependencies: + FetchEmojiFamily._allTransitiveDependencies, + host: host, + meta: meta, + ); + + FetchEmojiProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.host, + required this.meta, + }) : super.internal(); + + final String host; + final MetaResponse meta; + + @override + Override overrideWith( + FutureOr>> Function(FetchEmojiRef ref) create, + ) { + return ProviderOverride( + origin: this, + override: FetchEmojiProvider._internal( + (ref) => create(ref as FetchEmojiRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + host: host, + meta: meta, + ), + ); + } + + @override + ( + String, + MetaResponse, + ) get argument { + return ( + host, + meta, + ); + } + + @override + AutoDisposeFutureProviderElement>> createElement() { + return _FetchEmojiProviderElement(this); + } + + FetchEmojiProvider _copyWith( + FutureOr>> Function(FetchEmojiRef ref) create, + ) { + return FetchEmojiProvider._internal( + (ref) => create(ref as FetchEmojiRef), + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + host: host, + meta: meta, + ); + } + + @override + bool operator ==(Object other) { + return other is FetchEmojiProvider && + other.host == host && + other.meta == meta; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, host.hashCode); + hash = _SystemHash.combine(hash, meta.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin FetchEmojiRef on AutoDisposeFutureProviderRef>> { + /// The parameter `host` of this provider. + String get host; + + /// The parameter `meta` of this provider. + MetaResponse get meta; +} + +class _FetchEmojiProviderElement + extends AutoDisposeFutureProviderElement>> + with FetchEmojiRef { + _FetchEmojiProviderElement(super.provider); + + @override + String get host => (origin as FetchEmojiProvider).host; + @override + MetaResponse get meta => (origin as FetchEmojiProvider).meta; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/federation_page/federation_info.dart b/lib/view/federation_page/federation_info.dart index 30597e328..e983bb10e 100644 --- a/lib/view/federation_page/federation_info.dart +++ b/lib/view/federation_page/federation_info.dart @@ -1,153 +1,143 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/extensions/string_extensions.dart'; -import 'package:miria/model/federation_data.dart'; -import 'package:miria/view/common/constants.dart'; -import 'package:miria/view/common/error_detail.dart'; -import 'package:miria/view/common/misskey_notes/network_image.dart'; -import 'package:miria/view/federation_page/federation_page.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_html/flutter_html.dart'; -import 'package:url_launcher/url_launcher_string.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_html/flutter_html.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/string_extensions.dart"; +import "package:miria/model/federation_data.dart"; +import "package:miria/view/common/constants.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:url_launcher/url_launcher.dart"; +import "package:url_launcher/url_launcher_string.dart"; -class FederationInfo extends ConsumerStatefulWidget { - final String host; - - const FederationInfo({ - super.key, - required this.host, - }); - - @override - ConsumerState createState() => FederationInfoState(); -} - -class FederationInfoState extends ConsumerState { - (Object?, StackTrace)? error; - FederationData? data; +class FederationInfo extends ConsumerWidget { + final FederationData data; + const FederationInfo({required this.data, super.key}); @override - Widget build(BuildContext context) { - final data = ref.watch(federationPageFederationDataProvider); - if (data != null) { - final description = data.description; - return SingleChildScrollView( - child: Padding( - padding: - const EdgeInsets.only(left: 10, top: 10, right: 10, bottom: 20), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (data.bannerUrl != null) - NetworkImageView( - url: data.bannerUrl!.toString(), type: ImageType.other), - Row( - children: [ - if (data.faviconUrl != null) - SizedBox( - width: 32, - child: NetworkImageView( - url: data.faviconUrl!.toString(), - type: ImageType.serverIcon, - ), + Widget build(BuildContext context, WidgetRef ref) { + final description = data.description; + return SingleChildScrollView( + child: Padding( + padding: + const EdgeInsets.only(left: 10, top: 10, right: 10, bottom: 20), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (data.bannerUrl != null) + NetworkImageView( + url: data.bannerUrl!.toString(), + type: ImageType.other, + ), + Row( + children: [ + if (data.faviconUrl != null) + SizedBox( + width: 32, + child: NetworkImageView( + url: data.faviconUrl!.toString(), + type: ImageType.serverIcon, ), - const Padding(padding: EdgeInsets.only(left: 10)), - Expanded( - child: Text( - data.name, - style: Theme.of(context).textTheme.headlineMedium, + ), + const Padding(padding: EdgeInsets.only(left: 10)), + Expanded( + child: Text( + data.name, + style: Theme.of(context).textTheme.headlineMedium, + ), + ), + ], + ), + const Padding(padding: EdgeInsets.only(top: 5)), + Html( + data: description, + style: { + "a": Style(color: AppTheme.of(context).linkStyle.color), + }, + onLinkTap: (url, _, __) async { + await launchUrlString(url.toString()); + }, + ), + const Padding(padding: EdgeInsets.only(top: 5)), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + Text( + data.usersCount.format(ifNull: "???"), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge, ), - ) - ], - ), - const Padding(padding: EdgeInsets.only(top: 5)), - Html( - data: description, - style: {"a": Style(color: AppTheme.of(context).linkStyle.color)}, - onLinkTap: (url, _, __, ___) { - launchUrlString(url.toString()); - } - ), - const Padding(padding: EdgeInsets.only(top: 5)), - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Column( + Text( + S.of(context).user, + textAlign: TextAlign.center, + ), + ], + ), + Column( + children: [ + Text( + data.notesCount.format(ifNull: "???"), + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge, + ), + Text( + S.of(context).federatedPosts, + textAlign: TextAlign.center, + ), + ], + ), + ], + ), + const Padding(padding: EdgeInsets.only(top: 10)), + Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + children: [ + TableRow( + children: [ + Text( + S.of(context).software, + textAlign: TextAlign.center, + ), + Text("${data.softwareName} ${data.softwareVersion}"), + ], + ), + if (data.languages.isNotEmpty) + TableRow( children: [ Text( - data.usersCount.format(ifNull: "???"), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge, - ), - Text( - S.of(context).user, + S.of(context).language, textAlign: TextAlign.center, ), + Text(data.languages.join(", ")), ], ), - Column( + if (data.maintainerName != null) + TableRow( children: [ Text( - data.notesCount.format(ifNull: "???"), - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge, - ), - Text( - S.of(context).federatedPosts, + S.of(context).administrator, textAlign: TextAlign.center, ), + Text("${data.maintainerName}"), ], ), - ], - ), - const Padding(padding: EdgeInsets.only(top: 10)), - Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - children: [ + if (data.maintainerEmail != null) TableRow( children: [ Text( - S.of(context).software, + S.of(context).contact, textAlign: TextAlign.center, ), - Text( - "${data.softwareName} ${data.softwareVersion}", - ) + Text("${data.maintainerEmail}"), ], ), - if (data.languages.isNotEmpty) - TableRow(children: [ - Text(S.of(context).language, textAlign: TextAlign.center), - Text( - data.languages.join(", "), - ) - ]), - if (data.maintainerName != null) - TableRow(children: [ - Text( - S.of(context).administrator, - textAlign: TextAlign.center, - ), - Text( - "${data.maintainerName}", - ) - ]), - if (data.maintainerEmail != null) - TableRow(children: [ - Text( - S.of(context).contact, - textAlign: TextAlign.center, - ), - Text( - "${data.maintainerEmail}", - ) - ]), - if (data.serverRules.isNotEmpty) - TableRow(children: [ + if (data.serverRules.isNotEmpty) + TableRow( + children: [ Text( S.of(context).serverRules, textAlign: TextAlign.center, @@ -158,74 +148,75 @@ class FederationInfoState extends ConsumerState { children: [ for (final rule in data.serverRules.indexed) Html( - data: "${(rule.$1 + 1)}. ${rule.$2}
", - style: {"a": Style(color: AppTheme.of(context).linkStyle.color)}, - onLinkTap: (url, _, __, ___) { - launchUrlString(url.toString()); - } - ) + data: "${rule.$1 + 1}. ${rule.$2}
", + style: { + "a": Style( + color: AppTheme.of(context).linkStyle.color, + ), + }, + onLinkTap: (url, _, __) async { + await launchUrlString(url.toString()); + }, + ), ], - ) - ]), - if (data.tosUrl != null) - TableRow(children: [ + ), + ], + ), + if (data.tosUrl != null) + TableRow( + children: [ Text( S.of(context).tos, textAlign: TextAlign.center, ), GestureDetector( - onTap: () => launchUrl(Uri.parse(data.tosUrl!)), + onTap: () async => launchUrl(Uri.parse(data.tosUrl!)), child: Text( data.tosUrl!.toString().tight, style: AppTheme.of(context).linkStyle, ), - ) - ]), - if (data.privacyPolicyUrl != null) - TableRow(children: [ + ), + ], + ), + if (data.privacyPolicyUrl != null) + TableRow( + children: [ Text( S.of(context).privacyPolicy, textAlign: TextAlign.center, ), GestureDetector( - onTap: () => + onTap: () async => launchUrl(Uri.parse(data.privacyPolicyUrl!)), child: Text( data.privacyPolicyUrl!.toString().tight, style: AppTheme.of(context).linkStyle, ), - ) - ]), - if (data.impressumUrl != null) - TableRow(children: [ + ), + ], + ), + if (data.impressumUrl != null) + TableRow( + children: [ Text( S.of(context).impressum, textAlign: TextAlign.center, ), GestureDetector( - onTap: () => launchUrl(Uri.parse(data.impressumUrl!)), + onTap: () async => + launchUrl(Uri.parse(data.impressumUrl!)), child: Text( data.impressumUrl!.toString().tight, style: AppTheme.of(context).linkStyle, ), - ) - ]), - ], - ), - ], - ), + ), + ], + ), + ], + ), + ], ), - ); - } - if (error != null) { - ErrorDetail( - error: error?.$1, - stackTrace: error?.$2, - ); - } - - return const Center( - child: CircularProgressIndicator(), + ), ); } } diff --git a/lib/view/federation_page/federation_page.dart b/lib/view/federation_page/federation_page.dart index 9c7a0b474..7971b4af1 100644 --- a/lib/view/federation_page/federation_page.dart +++ b/lib/view/federation_page/federation_page.dart @@ -1,226 +1,136 @@ -import 'package:auto_route/annotations.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/federation_data.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/federation_page/federation_ads.dart'; -import 'package:miria/view/federation_page/federation_announcements.dart'; -import 'package:miria/view/federation_page/federation_custom_emojis.dart'; -import 'package:miria/view/federation_page/federation_info.dart'; -import 'package:miria/view/federation_page/federation_timeline.dart'; -import 'package:miria/view/federation_page/federation_users.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:miria/view/search_page/note_search.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/federation_data.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:miria/view/federation_page/federation_ads.dart"; +import "package:miria/view/federation_page/federation_announcements.dart"; +import "package:miria/view/federation_page/federation_custom_emojis.dart"; +import "package:miria/view/federation_page/federation_info.dart"; +import "package:miria/view/federation_page/federation_timeline.dart"; +import "package:miria/view/federation_page/federation_users.dart"; +import "package:miria/view/search_page/note_search.dart"; @RoutePage() -class FederationPage extends ConsumerStatefulWidget { - final Account account; +class FederationPage extends ConsumerWidget implements AutoRouteWrapper { + final AccountContext accountContext; final String host; const FederationPage({ - super.key, - required this.account, + required this.accountContext, required this.host, + super.key, }); @override - ConsumerState createState() => FederationPageState(); -} - -final federationPageFederationDataProvider = - StateProvider.autoDispose((ref) => null); - -class FederationPageState extends ConsumerState { - @override - void didChangeDependencies() { - super.didChangeDependencies(); - Future(() async { - try { - final account = widget.account; - if (widget.host == account.host) { - // 自分のサーバーの場合 - final metaResponse = await ref.read(misskeyProvider(account)).meta(); - final statsResponse = - await ref.read(misskeyProvider(account)).stats(); - ref.read(federationPageFederationDataProvider.notifier).state = - FederationData( - bannerUrl: metaResponse.bannerUrl?.toString(), - faviconUrl: metaResponse.iconUrl?.toString(), - tosUrl: metaResponse.tosUrl?.toString(), - privacyPolicyUrl: (metaResponse.privacyPolicyUrl)?.toString(), - impressumUrl: (metaResponse.impressumUrl)?.toString(), - repositoryUrl: (metaResponse.repositoryUrl).toString(), - name: metaResponse.name ?? "", - description: metaResponse.description ?? "", - usersCount: statsResponse.originalUsersCount, - notesCount: statsResponse.originalNotesCount, - maintainerName: metaResponse.maintainerName, - maintainerEmail: metaResponse.maintainerEmail, - serverRules: metaResponse.serverRules, - reactionCount: statsResponse.reactionsCount, - softwareName: "misskey", - softwareVersion: metaResponse.version, - languages: metaResponse.langs, - ads: metaResponse.ads, - meta: metaResponse, - - // 自分のサーバーが非対応ということはない - isSupportedAnnouncement: true, - isSupportedEmoji: true, - isSupportedLocalTimeline: true); - - await ref - .read(emojiRepositoryProvider( - Account.demoAccount(widget.host, metaResponse))) - .loadFromSourceIfNeed(); - } else { - final federation = await ref - .read(misskeyProvider(widget.account)) - .federation - .showInstance(FederationShowInstanceRequest(host: widget.host)); - MetaResponse? misskeyMeta; - - bool isSupportedEmoji = false; - bool isSupportedAnnouncement = false; - bool isSupportedLocalTimeline = false; - - if (federation.softwareName == "fedibird" || - federation.softwareName == "mastodon") { - // already known unsupported software. - } else { - try { - // Misskeyサーバーかもしれなかったら追加の情報を取得 - - final misskeyServer = - ref.read(misskeyWithoutAccountProvider(widget.host)); - final endpoints = await misskeyServer.endpoints(); - - if (endpoints.contains("announcement")) { - isSupportedAnnouncement = true; - } - - // 絵文字が取得できなければローカルタイムラインを含め非対応 - if (endpoints.contains("emojis")) { - isSupportedEmoji = true; - - if (endpoints.contains("notes/local-timeline")) { - isSupportedLocalTimeline = true; - } - } - - misskeyMeta = await misskeyServer.meta(); - await ref - .read(emojiRepositoryProvider( - Account.demoAccount(widget.host, misskeyMeta))) - .loadFromSourceIfNeed(); - } catch (e) {} - ; - } - - ref.read(federationPageFederationDataProvider.notifier).state = - FederationData( - bannerUrl: (misskeyMeta?.bannerUrl)?.toString(), - faviconUrl: (federation.faviconUrl)?.toString(), - tosUrl: (misskeyMeta?.tosUrl)?.toString(), - privacyPolicyUrl: (misskeyMeta?.privacyPolicyUrl)?.toString(), - impressumUrl: (misskeyMeta?.impressumUrl)?.toString(), - repositoryUrl: (misskeyMeta?.repositoryUrl)?.toString(), - name: misskeyMeta?.name ?? federation.name, - description: misskeyMeta?.description ?? federation.description, - maintainerName: misskeyMeta?.maintainerName, - maintainerEmail: misskeyMeta?.maintainerEmail, - usersCount: federation.usersCount, - notesCount: federation.notesCount, - softwareName: federation.softwareName ?? "", - softwareVersion: - misskeyMeta?.version ?? federation.softwareVersion ?? "", - languages: misskeyMeta?.langs ?? [], - ads: misskeyMeta?.ads ?? [], - serverRules: misskeyMeta?.serverRules ?? [], - isSupportedEmoji: isSupportedEmoji, - isSupportedLocalTimeline: isSupportedLocalTimeline, - isSupportedAnnouncement: isSupportedAnnouncement, - meta: misskeyMeta, - ); - } - - if (!mounted) return; - setState(() {}); - } catch (e, s) { - print(e); - print(s); - if (!mounted) return; - } - }); - } + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); @override - Widget build(BuildContext context) { - final metaResponse = ref.watch(federationPageFederationDataProvider); - final adsAvailable = metaResponse?.ads.isNotEmpty == true; - final isMisskey = metaResponse?.isSupportedEmoji == true; - final isAnotherHost = widget.account.host != widget.host; - final isSupportedTimeline = - isMisskey && metaResponse?.isSupportedLocalTimeline == true; - final enableLocalTimeline = isSupportedTimeline && - metaResponse?.meta?.policies?.ltlAvailable == true; - final enableSearch = isSupportedTimeline && - metaResponse?.meta?.policies?.canSearchNotes == true; + Widget build(BuildContext context, WidgetRef ref) { + final federate = ref.watch(federationStateProvider(host)); - return AccountScope( - account: widget.account, - child: DefaultTabController( - length: 1 + - (isAnotherHost ? 1 : 0) + - (adsAvailable ? 1 : 0) + - (isMisskey ? 1 : 0) + - (isSupportedTimeline ? 1 : 0) + - (enableLocalTimeline ? 1 : 0) + - (enableSearch ? 1 : 0), - child: Scaffold( - appBar: AppBar( - title: Text(widget.host), - bottom: TabBar( - isScrollable: true, - tabs: [ - Tab(text: S.of(context).serverInformation), - if (isAnotherHost) Tab(text: S.of(context).user), - if (adsAvailable) Tab(text: S.of(context).ad), - if (isMisskey) Tab(text: S.of(context).announcement), - if (isSupportedTimeline) Tab(text: S.of(context).customEmoji), - if (isSupportedTimeline) - Tab(text: S.of(context).localTimelineAbbr), - if (enableSearch) Tab(text: S.of(context).search), - ], - tabAlignment: TabAlignment.center, - ), - ), - body: TabBarView( - children: [ - FederationInfo(host: widget.host), - if (isAnotherHost) FederationUsers(host: widget.host), - if (adsAvailable) const FederationAds(), - if (isMisskey) FederationAnnouncements(host: widget.host), - if (isSupportedTimeline) - FederationCustomEmojis( - host: widget.host, meta: metaResponse!.meta!), - if (isSupportedTimeline) - FederationTimeline( - host: widget.host, meta: metaResponse!.meta!), - if (enableSearch) - AccountScope( - account: - Account.demoAccount(widget.host, metaResponse!.meta!), - child: NoteSearch( - focusNode: FocusNode(), - )), - ], - ), + return switch (federate) { + AsyncLoading() => Scaffold( + appBar: AppBar(title: Text(host)), + body: const Center(child: CircularProgressIndicator.adaptive()), + ), + AsyncError(:final error, :final stackTrace) => Scaffold( + appBar: AppBar(title: Text(host)), + body: ErrorDetail(error: error, stackTrace: stackTrace), + ), + AsyncData(:final value) => Builder( + builder: (context) { + final adsAvailable = value.ads.isNotEmpty; + final isMisskey = value.isSupportedEmoji; + final isAnotherHost = accountContext.postAccount.host != host; + final isSupportedTimeline = + isMisskey && value.isSupportedLocalTimeline; + final enableLocalTimeline = isSupportedTimeline && + value.meta?.policies?.ltlAvailable == true; + final enableSearch = isSupportedTimeline && + value.meta?.policies?.canSearchNotes == true; + + return DefaultTabController( + length: 1 + + (isAnotherHost ? 1 : 0) + + (adsAvailable ? 1 : 0) + + (isMisskey ? 1 : 0) + + (isSupportedTimeline ? 2 : 0) + + (enableLocalTimeline ? 1 : 0) + + (enableSearch ? 1 : 0), + child: Scaffold( + appBar: AppBar( + title: Text(host), + bottom: TabBar( + isScrollable: true, + tabs: [ + Tab(text: S.of(context).serverInformation), + if (isAnotherHost) Tab(text: S.of(context).user), + if (adsAvailable) Tab(text: S.of(context).ad), + if (isMisskey) Tab(text: S.of(context).announcement), + if (isSupportedTimeline) + Tab(text: S.of(context).customEmoji), + if (isSupportedTimeline) + Tab(text: S.of(context).localTimelineAbbr), + if (enableSearch) Tab(text: S.of(context).search), + ], + tabAlignment: TabAlignment.center, + ), + ), + body: TabBarView( + children: [ + FederationInfo(data: value), + if (isAnotherHost) FederationUsers(host: host), + if (adsAvailable) FederationAds(ads: [...value.ads]), + if (isMisskey) + AccountContextScope( + context: AccountContext( + getAccount: Account.demoAccount(host, value.meta), + postAccount: accountContext.postAccount, + ), + child: FederationAnnouncements(host: host), + ), + if (isSupportedTimeline) + AccountContextScope( + context: AccountContext( + getAccount: Account.demoAccount(host, value.meta), + postAccount: accountContext.postAccount, + ), + child: FederationCustomEmojis( + host: host, + meta: value.meta!, + ), + ), + if (isSupportedTimeline) + AccountContextScope( + context: AccountContext( + getAccount: Account.demoAccount(host, value.meta), + postAccount: accountContext.postAccount, + ), + child: FederationTimeline( + host: host, + meta: value.meta!, + ), + ), + if (enableSearch) + AccountContextScope( + context: AccountContext( + getAccount: Account.demoAccount(host, value.meta), + postAccount: accountContext.postAccount, + ), + child: NoteSearch(focusNode: FocusNode()), + ), + ], + ), + ), + ); + }, ), - ), - ); + }; } } diff --git a/lib/view/federation_page/federation_timeline.dart b/lib/view/federation_page/federation_timeline.dart index a903d20ea..85391ee72 100644 --- a/lib/view/federation_page/federation_timeline.dart +++ b/lib/view/federation_page/federation_timeline.dart @@ -1,61 +1,47 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; -import '../../model/account.dart'; - -class FederationTimeline extends ConsumerStatefulWidget { +class FederationTimeline extends ConsumerWidget { final String host; final MetaResponse meta; const FederationTimeline({ - super.key, required this.host, required this.meta, + super.key, }); @override - ConsumerState createState() => - FederationTimelineState(); -} - -class FederationTimelineState extends ConsumerState { - @override - Widget build(BuildContext context) { - final demoAccount = Account.demoAccount(widget.host, widget.meta); - - return AccountScope( - account: demoAccount, - child: Padding( - padding: const EdgeInsets.only(right: 10), - child: PushableListView( - initializeFuture: () async { - final result = await ref - .read(misskeyProvider(demoAccount)) - .notes - .localTimeline(const NotesLocalTimelineRequest()); - ref.read(notesProvider(demoAccount)).registerAll(result); - return result.toList(); - }, - nextFuture: (lastItem, _) async { - final result = await ref - .read(misskeyProvider(demoAccount)) - .notes - .localTimeline( - NotesLocalTimelineRequest(untilId: lastItem.id)); - ref.read(notesProvider(demoAccount)).registerAll(result); - return result.toList(); - }, - itemBuilder: (context2, item) => Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: MisskeyNote( - note: item, - loginAs: AccountScope.of(context), - ))), + Widget build(BuildContext context, WidgetRef ref) { + return Padding( + padding: const EdgeInsets.only(right: 10), + child: PushableListView( + initializeFuture: () async { + final result = await ref + .read(misskeyGetContextProvider) + .notes + .localTimeline(const NotesLocalTimelineRequest()); + ref.read(notesWithProvider).registerAll(result); + return result.toList(); + }, + nextFuture: (lastItem, _) async { + final result = + await ref.read(misskeyGetContextProvider).notes.localTimeline( + NotesLocalTimelineRequest(untilId: lastItem.id), + ); + ref.read(notesWithProvider).registerAll(result); + return result.toList(); + }, + itemBuilder: (context2, item) => Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: MisskeyNote( + note: item, + ), + ), ), ); } diff --git a/lib/view/federation_page/federation_users.dart b/lib/view/federation_page/federation_users.dart index 9905dac44..8d30a11ec 100644 --- a/lib/view/federation_page/federation_users.dart +++ b/lib/view/federation_page/federation_users.dart @@ -1,32 +1,30 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:miria/view/user_page/user_list_item.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:miria/view/user_page/user_list_item.dart"; +import "package:misskey_dart/misskey_dart.dart"; class FederationUsers extends ConsumerWidget { final String host; const FederationUsers({ - super.key, required this.host, + super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { - final account = AccountScope.of(context); return PushableListView( initializeFuture: () async { final response = await ref - .read(misskeyProvider(account)) + .read(misskeyGetContextProvider) .federation .users(FederationUsersRequest(host: host)); return response.toList(); }, nextFuture: (lastItem, _) async { final response = await ref - .read(misskeyProvider(account)) + .read(misskeyGetContextProvider) .federation .users(FederationUsersRequest(host: host, untilId: lastItem.id)); return response.toList(); diff --git a/lib/view/games_page/misskey_games_page.dart b/lib/view/games_page/misskey_games_page.dart index d870ef122..413bd07e5 100644 --- a/lib/view/games_page/misskey_games_page.dart +++ b/lib/view/games_page/misskey_games_page.dart @@ -1,45 +1,27 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:url_launcher/url_launcher_string.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; +import "package:url_launcher/url_launcher_string.dart"; + +part "misskey_games_page.g.dart"; @RoutePage() -class MisskeyGamesPage extends ConsumerStatefulWidget { - final Account account; +class MisskeyGamesPage extends ConsumerWidget implements AutoRouteWrapper { + final AccountContext accountContext; - const MisskeyGamesPage({super.key, required this.account}); + const MisskeyGamesPage({required this.accountContext, super.key}); @override - ConsumerState createState() => MisskeyGamesPageState(); -} - -class MisskeyGamesPageState extends ConsumerState { - bool isLoaded = false; - List reversiInvitations = []; - List reversiWaiting = []; + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); @override - void didChangeDependencies() { - super.didChangeDependencies(); - Future(() async { - reversiInvitations = (await ref - .read(misskeyProvider(widget.account)) - .reversi - .invitations()) - .toList(); - - setState(() { - isLoaded = true; - }); - }); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return Scaffold( appBar: AppBar( title: Text(S.of(context).misskeyGames), @@ -48,38 +30,56 @@ class MisskeyGamesPageState extends ConsumerState { children: [ ListTile( title: Text(S.of(context).cookieCliker), - onTap: () => launchUrlString( - "https://${widget.account.host}/clicker", + onTap: () async => launchUrlString( + "https://${accountContext.postAccount.host}/clicker", mode: LaunchMode.externalApplication, ), ), ListTile( title: Text(S.of(context).bubbleGame), - onTap: () => launchUrlString( - "https://${widget.account.host}/bubble-game", + onTap: () async => launchUrlString( + "https://${accountContext.postAccount.host}/bubble-game", mode: LaunchMode.externalApplication, ), ), ListTile( title: Text(S.of(context).reversi), - subtitle: !isLoaded - ? Text(S.of(context).loading) - : reversiInvitations.isEmpty - ? Text(S.of(context).nonInvitedReversi) - : Text( - S.of(context).invitedReversi( - reversiInvitations - .map((e) => e.name ?? e.username) - .join(", "), - ), - ), - onTap: () => launchUrlString( - "https://${widget.account.host}/reversi", + subtitle: const ReversiInvite(), + onTap: () async => launchUrlString( + "https://${accountContext.postAccount.host}/reversi", mode: LaunchMode.externalApplication, ), - ) + ), ], ), ); } } + +@Riverpod(dependencies: [misskeyPostContext]) +Future> _fetchReversiData( + _FetchReversiDataRef ref, +) async { + return [...await ref.read(misskeyPostContextProvider).reversi.invitations()]; +} + +class ReversiInvite extends ConsumerWidget { + const ReversiInvite({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final reversiInvitation = ref.watch(_fetchReversiDataProvider); + + return switch (reversiInvitation) { + AsyncLoading() => Text(S.of(context).loading), + AsyncData>(:final value) => value.isEmpty + ? Text(S.of(context).nonInvitedReversi) + : Text( + S.of(context).invitedReversi( + value.map((e) => e.name ?? e.username).join(", "), + ), + ), + AsyncError() => const SizedBox.shrink(), + }; + } +} diff --git a/lib/view/games_page/misskey_games_page.g.dart b/lib/view/games_page/misskey_games_page.g.dart new file mode 100644 index 000000000..fc12cc0af --- /dev/null +++ b/lib/view/games_page/misskey_games_page.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'misskey_games_page.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$fetchReversiDataHash() => r'38cce5ae9d15fe973565196ea2c1dfc283c8c7df'; + +/// See also [_fetchReversiData]. +@ProviderFor(_fetchReversiData) +final _fetchReversiDataProvider = + AutoDisposeFutureProvider>.internal( + _fetchReversiData, + name: r'_fetchReversiDataProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$fetchReversiDataHash, + dependencies: [misskeyPostContextProvider], + allTransitiveDependencies: { + misskeyPostContextProvider, + ...?misskeyPostContextProvider.allTransitiveDependencies + }, +); + +typedef _FetchReversiDataRef = AutoDisposeFutureProviderRef>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/hashtag_page/hashtag_page.dart b/lib/view/hashtag_page/hashtag_page.dart index 659ba4ddb..d7b3542b0 100644 --- a/lib/view/hashtag_page/hashtag_page.dart +++ b/lib/view/hashtag_page/hashtag_page.dart @@ -1,61 +1,68 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; @RoutePage() -class HashtagPage extends ConsumerWidget { +class HashtagPage extends ConsumerWidget implements AutoRouteWrapper { final String hashtag; - final Account account; + final AccountContext accountContext; const HashtagPage({ - super.key, required this.hashtag, - required this.account, + required this.accountContext, + super.key, }); + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); + @override Widget build(BuildContext context, WidgetRef ref) { - return AccountScope( - account: account, - child: Scaffold( - appBar: AppBar(title: Text("#$hashtag")), - body: Padding( - padding: const EdgeInsets.only(right: 10), - child: PushableListView( - initializeFuture: () async { - final response = await ref - .read(misskeyProvider(account)) - .notes - .searchByTag(NotesSearchByTagRequest(tag: hashtag)); - ref.read(notesProvider(account)).registerAll(response); - return response.toList(); - }, - nextFuture: (lastItem, _) async { - final response = await ref - .read(misskeyProvider(account)) - .notes - .searchByTag(NotesSearchByTagRequest( - tag: hashtag, untilId: lastItem.id)); - ref.read(notesProvider(account)).registerAll(response); - return response.toList(); - }, - itemBuilder: (context, item) => MisskeyNote(note: item), + return Scaffold( + appBar: AppBar(title: Text("#$hashtag")), + body: Padding( + padding: const EdgeInsets.only(right: 10), + child: PushableListView( + initializeFuture: () async { + final response = await ref + .read(misskeyGetContextProvider) + .notes + .searchByTag(NotesSearchByTagRequest(tag: hashtag)); + ref.read(notesWithProvider).registerAll(response); + return response.toList(); + }, + nextFuture: (lastItem, _) async { + final response = + await ref.read(misskeyGetContextProvider).notes.searchByTag( + NotesSearchByTagRequest( + tag: hashtag, + untilId: lastItem.id, + ), + ); + ref.read(notesWithProvider).registerAll(response); + return response.toList(); + }, + itemBuilder: (context, item) => MisskeyNote(note: item), + ), + ), + floatingActionButton: FloatingActionButton( + onPressed: () async { + await context.pushRoute( + NoteCreateRoute( + initialAccount: accountContext.postAccount, + initialText: "#$hashtag", ), - ), - floatingActionButton: FloatingActionButton( - onPressed: () { - context.pushRoute(NoteCreateRoute( - initialAccount: account, initialText: "#$hashtag")); - }, - child: const Icon(Icons.edit), - ), - )); + ); + }, + child: const Icon(Icons.edit), + ), + ); } } diff --git a/lib/view/login_page/api_key_login.dart b/lib/view/login_page/api_key_login.dart index d0390b752..68973f8a0 100644 --- a/lib/view/login_page/api_key_login.dart +++ b/lib/view/login_page/api_key_login.dart @@ -1,14 +1,15 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/util/punycode.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/common/modal_indicator.dart'; -import 'package:miria/view/login_page/centraing_widget.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/view/login_page/misskey_server_list_dialog.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/account_repository.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/util/punycode.dart"; +import "package:miria/view/common/error_dialog_handler.dart"; +import "package:miria/view/common/modal_indicator.dart"; +import "package:miria/view/login_page/centraing_widget.dart"; +import "package:miria/view/login_page/misskey_server_list_dialog.dart"; class ApiKeyLogin extends ConsumerStatefulWidget { const ApiKeyLogin({super.key}); @@ -36,9 +37,12 @@ class APiKeyLoginState extends ConsumerState { .loginAsToken(toAscii(serverController.text), apiKeyController.text); if (!mounted) return; - context.pushRoute(TimeLineRoute( + await context.pushRoute( + TimeLineRoute( initialTabSetting: - ref.read(tabSettingsRepositoryProvider).tabSettings.first)); + ref.read(tabSettingsRepositoryProvider).tabSettings.first, + ), + ); } catch (e) { rethrow; } finally { @@ -59,52 +63,64 @@ class APiKeyLoginState extends ConsumerState { 1: FlexColumnWidth(), }, children: [ - TableRow(children: [ - Text(S.of(context).server), - TextField( - controller: serverController, - decoration: InputDecoration( + TableRow( + children: [ + Text(S.of(context).server), + TextField( + controller: serverController, + decoration: InputDecoration( prefixIcon: const Icon(Icons.dns), suffixIcon: IconButton( - onPressed: () async { - final url = await showDialog( - context: context, - builder: (context) => - const MisskeyServerListDialog()); - if (url != null && url.isNotEmpty) { - serverController.text = url; - } - }, - icon: const Icon(Icons.search))), - ), - ]), - TableRow(children: [ - const Padding(padding: EdgeInsets.only(bottom: 10)), - Container() - ]), - TableRow(children: [ - Padding( - padding: const EdgeInsets.only(right: 20), - child: Text(S.of(context).apiKey), - ), - TextField( - controller: apiKeyController, - decoration: - const InputDecoration(prefixIcon: Icon(Icons.key)), - ) - ]), + onPressed: () async { + final url = await showDialog( + context: context, + builder: (context) => + const MisskeyServerListDialog(), + ); + if (url != null && url.isNotEmpty) { + serverController.text = url; + } + }, + icon: const Icon(Icons.search), + ), + ), + ), + ], + ), + TableRow( + children: [ + const Padding(padding: EdgeInsets.only(bottom: 10)), + Container(), + ], + ), + TableRow( + children: [ + Padding( + padding: const EdgeInsets.only(right: 20), + child: Text(S.of(context).apiKey), + ), + TextField( + controller: apiKeyController, + decoration: + const InputDecoration(prefixIcon: Icon(Icons.key)), + ), + ], + ), // ], - TableRow(children: [ - Container(), - Padding( - padding: const EdgeInsets.only(top: 10), - child: ElevatedButton( - onPressed: () { - login().expectFailure(context); + TableRow( + children: [ + Container(), + Padding( + padding: const EdgeInsets.only(top: 10), + child: ElevatedButton( + onPressed: () async { + await login().expectFailure(context); }, - child: Text(S.of(context).login)), - ) - ]) + child: Text(S.of(context).login), + ), + ), + ], + ), ], ), ], diff --git a/lib/view/login_page/centraing_widget.dart b/lib/view/login_page/centraing_widget.dart index 59c03d18a..5cc79df94 100644 --- a/lib/view/login_page/centraing_widget.dart +++ b/lib/view/login_page/centraing_widget.dart @@ -1,16 +1,20 @@ -import 'package:flutter/cupertino.dart'; +import "package:flutter/cupertino.dart"; class CenteringWidget extends StatelessWidget { final Widget child; - const CenteringWidget({super.key, required this.child}); + const CenteringWidget({required this.child, super.key}); @override Widget build(BuildContext context) { return Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 600), - child: Padding( - padding: const EdgeInsets.only(left: 10, right: 10), child: child))); + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 600), + child: Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: child, + ), + ), + ); } } diff --git a/lib/view/login_page/login_page.dart b/lib/view/login_page/login_page.dart index 227530a1d..f3bd8c2d0 100644 --- a/lib/view/login_page/login_page.dart +++ b/lib/view/login_page/login_page.dart @@ -1,38 +1,33 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/view/login_page/api_key_login.dart'; -import 'package:miria/view/login_page/mi_auth_login.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:miria/view/login_page/api_key_login.dart"; +import "package:miria/view/login_page/mi_auth_login.dart"; @RoutePage() -class LoginPage extends ConsumerStatefulWidget { +class LoginPage extends StatelessWidget { const LoginPage({super.key}); - @override - ConsumerState createState() => LoginPageState(); -} - -class LoginPageState extends ConsumerState { @override Widget build(BuildContext context) { return DefaultTabController( length: 2, child: Scaffold( - appBar: AppBar( - title: Text(S.of(context).login), - bottom: TabBar( - isScrollable: true, - tabs: [ - Tab(text: S.of(context).loginAsMiAuth), - Tab(text: S.of(context).loginAsAPIKey), - ], - tabAlignment: TabAlignment.center, - ), + appBar: AppBar( + title: Text(S.of(context).login), + bottom: TabBar( + isScrollable: true, + tabs: [ + Tab(text: S.of(context).loginAsMiAuth), + Tab(text: S.of(context).loginAsAPIKey), + ], + tabAlignment: TabAlignment.center, ), - body: const TabBarView( - children: [MiAuthLogin(), ApiKeyLogin()], - )), + ), + body: const TabBarView( + children: [MiAuthLogin(), ApiKeyLogin()], + ), + ), ); } } diff --git a/lib/view/login_page/mi_auth_login.dart b/lib/view/login_page/mi_auth_login.dart index 00afc8299..3195b938a 100644 --- a/lib/view/login_page/mi_auth_login.dart +++ b/lib/view/login_page/mi_auth_login.dart @@ -1,14 +1,16 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/util/punycode.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/common/modal_indicator.dart'; -import 'package:miria/view/login_page/centraing_widget.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/view/login_page/misskey_server_list_dialog.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_riverpod/flutter_riverpod.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/account_repository.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/util/punycode.dart"; +import "package:miria/view/common/error_dialog_handler.dart"; +import "package:miria/view/common/modal_indicator.dart"; +import "package:miria/view/login_page/centraing_widget.dart"; +import "package:miria/view/login_page/misskey_server_list_dialog.dart"; class MiAuthLogin extends ConsumerStatefulWidget { const MiAuthLogin({super.key}); @@ -34,7 +36,7 @@ class MiAuthLoginState extends ConsumerState { .read(accountRepositoryProvider.notifier) .validateMiAuth(toAscii(serverController.text)); if (!mounted) return; - context.pushRoute( + await context.pushRoute( TimeLineRoute( initialTabSetting: ref.read(tabSettingsRepositoryProvider).tabSettings.first, @@ -50,71 +52,87 @@ class MiAuthLoginState extends ConsumerState { @override Widget build(BuildContext context) { return CenteringWidget( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - columnWidths: const { - 0: IntrinsicColumnWidth(), - 1: FlexColumnWidth(), - }, - children: [ - TableRow(children: [ - Text(S.of(context).server), - TextField( - controller: serverController, - decoration: InputDecoration( - prefixIcon: const Icon(Icons.dns), - suffixIcon: IconButton( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Table( + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + columnWidths: const { + 0: IntrinsicColumnWidth(), + 1: FlexColumnWidth(), + }, + children: [ + TableRow( + children: [ + Text(S.of(context).server), + TextField( + controller: serverController, + decoration: InputDecoration( + prefixIcon: const Icon(Icons.dns), + suffixIcon: IconButton( onPressed: () async { final url = await showDialog( - context: context, - builder: (context) => - const MisskeyServerListDialog()); + context: context, + builder: (context) => + const MisskeyServerListDialog(), + ); if (url != null && url.isNotEmpty) { serverController.text = url; } }, - icon: const Icon(Icons.search))), + icon: const Icon(Icons.search), + ), + ), + ), + ], ), - ]), - TableRow(children: [ - const Padding(padding: EdgeInsets.only(bottom: 10)), - Container() - ]), - TableRow(children: [ - Container(), - ElevatedButton( - onPressed: () { - ref - .read(accountRepositoryProvider.notifier) - .openMiAuth(toAscii(serverController.text)) - .expectFailure(context); - setState(() { - isAuthed = true; - }); - }, - child: Text(isAuthed - ? S.of(context).reauthorizate - : S.of(context).authorizate), + TableRow( + children: [ + const Padding(padding: EdgeInsets.only(bottom: 10)), + Container(), + ], ), - ]), - TableRow(children: [ - const Padding(padding: EdgeInsets.only(bottom: 10)), - Container() - ]), - if (isAuthed) - TableRow(children: [ - Container(), - ElevatedButton( - onPressed: () => login().expectFailure(context), - child: Text(S.of(context).didAuthorize), + TableRow( + children: [ + Container(), + ElevatedButton( + onPressed: () async { + await ref + .read(accountRepositoryProvider.notifier) + .openMiAuth(toAscii(serverController.text)) + .expectFailure(context); + setState(() { + isAuthed = true; + }); + }, + child: Text( + isAuthed + ? S.of(context).reauthorizate + : S.of(context).authorizate, + ), + ), + ], + ), + TableRow( + children: [ + const Padding(padding: EdgeInsets.only(bottom: 10)), + Container(), + ], + ), + if (isAuthed) + TableRow( + children: [ + Container(), + ElevatedButton( + onPressed: () async => login().expectFailure(context), + child: Text(S.of(context).didAuthorize), + ), + ], ), - ]), - ], - ), - ], - )); + ], + ), + ], + ), + ); } } diff --git a/lib/view/login_page/misskey_server_list_dialog.dart b/lib/view/login_page/misskey_server_list_dialog.dart index 676b10822..69b0fa055 100644 --- a/lib/view/login_page/misskey_server_list_dialog.dart +++ b/lib/view/login_page/misskey_server_list_dialog.dart @@ -1,28 +1,23 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/view/common/misskey_server_list.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:miria/view/common/misskey_server_list.dart"; -class MisskeyServerListDialog extends ConsumerStatefulWidget { +@RoutePage() +class MisskeyServerListDialog extends StatelessWidget { const MisskeyServerListDialog({super.key}); - @override - ConsumerState createState() => - MisskeyServerListDialogState(); -} - -class MisskeyServerListDialogState - extends ConsumerState { @override Widget build(BuildContext context) { return AlertDialog( title: Text(S.of(context).chooseLoginServer), content: SizedBox( - width: double.maxFinite, - child: MisskeyServerList( - isDisableUnloginable: true, - onTap: (item) => Navigator.of(context).pop(item.url), - )), + width: double.maxFinite, + child: MisskeyServerList( + isDisableUnloginable: true, + onTap: (item) async => context.maybePop(item.url), + ), + ), ); } } diff --git a/lib/view/login_page/password_login.dart b/lib/view/login_page/password_login.dart index 68fec366a..f0c7a1538 100644 --- a/lib/view/login_page/password_login.dart +++ b/lib/view/login_page/password_login.dart @@ -1,9 +1,10 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/login_page/centraing_widget.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/account_repository.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/login_page/centraing_widget.dart"; class PasswordLogin extends ConsumerStatefulWidget { const PasswordLogin({super.key}); @@ -27,30 +28,37 @@ class PasswordLoginState extends ConsumerState { Future login() async { await ref.read(accountRepositoryProvider.notifier).loginAsPassword( - serverController.text, userController.text, passwordController.text); + serverController.text, + userController.text, + passwordController.text, + ); if (!mounted) return; - context.pushRoute(TimeLineRoute( + await context.pushRoute( + TimeLineRoute( initialTabSetting: - ref.read(tabSettingsRepositoryProvider).tabSettings.first)); + ref.read(tabSettingsRepositoryProvider).tabSettings.first, + ), + ); } @override Widget build(BuildContext context) { return CenteringWidget( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ const Text("この機能はめいどるふぃんなどを想定していますが、現状機能しません。"), Table( - defaultVerticalAlignment: TableCellVerticalAlignment.middle, - columnWidths: const { - 0: IntrinsicColumnWidth(), - 1: FlexColumnWidth(), - }, - children: [ - TableRow(children: [ + defaultVerticalAlignment: TableCellVerticalAlignment.middle, + columnWidths: const { + 0: IntrinsicColumnWidth(), + 1: FlexColumnWidth(), + }, + children: [ + TableRow( + children: [ const Text("サーバー"), TextField( enabled: false, @@ -58,47 +66,63 @@ class PasswordLoginState extends ConsumerState { decoration: const InputDecoration(prefixIcon: Icon(Icons.dns)), ), - ]), - TableRow(children: [ + ], + ), + TableRow( + children: [ const Padding(padding: EdgeInsets.only(bottom: 10)), - Container() - ]), - TableRow(children: [ + Container(), + ], + ), + TableRow( + children: [ const Text("ユーザー名"), TextField( - enabled: false, - controller: userController, - decoration: const InputDecoration( - prefixIcon: Icon(Icons.account_circle))) - ]), - TableRow(children: [ + enabled: false, + controller: userController, + decoration: const InputDecoration( + prefixIcon: Icon(Icons.account_circle), + ), + ), + ], + ), + TableRow( + children: [ const Padding(padding: EdgeInsets.only(bottom: 10)), - Container() - ]), - TableRow(children: [ + Container(), + ], + ), + TableRow( + children: [ const Padding( - padding: EdgeInsets.only(right: 20), - child: Text("パスワード")), + padding: EdgeInsets.only(right: 20), + child: Text("パスワード"), + ), TextField( enabled: false, controller: passwordController, decoration: const InputDecoration(prefixIcon: Icon(Icons.key)), obscureText: true, - ) - ]), - TableRow(children: [ + ), + ], + ), + TableRow( + children: [ Container(), Padding( padding: const EdgeInsets.only(top: 10), child: ElevatedButton( - onPressed: () { - login(); - }, - child: const Text("ログイン")), - ) - ]) - ]) - ])); + onPressed: () async => login(), + child: const Text("ログイン"), + ), + ), + ], + ), + ], + ), + ], + ), + ); } } diff --git a/lib/view/misskey_page_page/misskey_page_notifier.dart b/lib/view/misskey_page_page/misskey_page_notifier.dart new file mode 100644 index 000000000..c9132acaa --- /dev/null +++ b/lib/view/misskey_page_page/misskey_page_notifier.dart @@ -0,0 +1,74 @@ +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +part "misskey_page_notifier.freezed.dart"; +part "misskey_page_notifier.g.dart"; + +@freezed +class MisskeyPageNotifierState with _$MisskeyPageNotifierState { + const factory MisskeyPageNotifierState({ + required Page page, + AsyncValue? likeOr, + }) = _MisskeyPageNotifierState; +} + +@Riverpod(dependencies: [accountContext, misskeyGetContext, misskeyPostContext]) +class MisskeyPageNotifier extends _$MisskeyPageNotifier { + @override + Future build(String pageId) async { + return MisskeyPageNotifierState( + page: await ref + .read(misskeyGetContextProvider) + .pages + .show(PagesShowRequest(pageId: pageId)), + ); + } + + Future likeOr() async { + final before = await future; + + if (ref.read(accountContextProvider).postAccount.i.id == + before.page.userId) { + await ref.read(dialogStateNotifierProvider.notifier).showSimpleDialog( + message: (context) => S.of(context).canNotFavoriteMyPage, + ); + return; + } + state = AsyncData(before.copyWith(likeOr: const AsyncLoading())); + final likeOrResult = + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + if (before.page.isLiked ?? false) { + await ref + .read(misskeyPostContextProvider) + .pages + .unlike(PagesUnlikeRequest(pageId: pageId)); + state = AsyncData( + before.copyWith( + page: before.page.copyWith( + isLiked: false, + likedCount: before.page.likedCount - 1, + ), + ), + ); + } else { + await ref + .read(misskeyPostContextProvider) + .pages + .like(PagesLikeRequest(pageId: pageId)); + state = AsyncData( + before.copyWith( + page: before.page.copyWith( + isLiked: true, + likedCount: before.page.likedCount + 1, + ), + ), + ); + } + }); + state = AsyncData((await future).copyWith(likeOr: likeOrResult)); + } +} diff --git a/lib/view/misskey_page_page/misskey_page_notifier.freezed.dart b/lib/view/misskey_page_page/misskey_page_notifier.freezed.dart new file mode 100644 index 000000000..9368a9a8d --- /dev/null +++ b/lib/view/misskey_page_page/misskey_page_notifier.freezed.dart @@ -0,0 +1,181 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'misskey_page_notifier.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$MisskeyPageNotifierState { + Page get page => throw _privateConstructorUsedError; + AsyncValue? get likeOr => throw _privateConstructorUsedError; + + /// Create a copy of MisskeyPageNotifierState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $MisskeyPageNotifierStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $MisskeyPageNotifierStateCopyWith<$Res> { + factory $MisskeyPageNotifierStateCopyWith(MisskeyPageNotifierState value, + $Res Function(MisskeyPageNotifierState) then) = + _$MisskeyPageNotifierStateCopyWithImpl<$Res, MisskeyPageNotifierState>; + @useResult + $Res call({Page page, AsyncValue? likeOr}); + + $PageCopyWith<$Res> get page; +} + +/// @nodoc +class _$MisskeyPageNotifierStateCopyWithImpl<$Res, + $Val extends MisskeyPageNotifierState> + implements $MisskeyPageNotifierStateCopyWith<$Res> { + _$MisskeyPageNotifierStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of MisskeyPageNotifierState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? page = null, + Object? likeOr = freezed, + }) { + return _then(_value.copyWith( + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as Page, + likeOr: freezed == likeOr + ? _value.likeOr + : likeOr // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + ) as $Val); + } + + /// Create a copy of MisskeyPageNotifierState + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $PageCopyWith<$Res> get page { + return $PageCopyWith<$Res>(_value.page, (value) { + return _then(_value.copyWith(page: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$MisskeyPageNotifierStateImplCopyWith<$Res> + implements $MisskeyPageNotifierStateCopyWith<$Res> { + factory _$$MisskeyPageNotifierStateImplCopyWith( + _$MisskeyPageNotifierStateImpl value, + $Res Function(_$MisskeyPageNotifierStateImpl) then) = + __$$MisskeyPageNotifierStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({Page page, AsyncValue? likeOr}); + + @override + $PageCopyWith<$Res> get page; +} + +/// @nodoc +class __$$MisskeyPageNotifierStateImplCopyWithImpl<$Res> + extends _$MisskeyPageNotifierStateCopyWithImpl<$Res, + _$MisskeyPageNotifierStateImpl> + implements _$$MisskeyPageNotifierStateImplCopyWith<$Res> { + __$$MisskeyPageNotifierStateImplCopyWithImpl( + _$MisskeyPageNotifierStateImpl _value, + $Res Function(_$MisskeyPageNotifierStateImpl) _then) + : super(_value, _then); + + /// Create a copy of MisskeyPageNotifierState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? page = null, + Object? likeOr = freezed, + }) { + return _then(_$MisskeyPageNotifierStateImpl( + page: null == page + ? _value.page + : page // ignore: cast_nullable_to_non_nullable + as Page, + likeOr: freezed == likeOr + ? _value.likeOr + : likeOr // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + )); + } +} + +/// @nodoc + +class _$MisskeyPageNotifierStateImpl implements _MisskeyPageNotifierState { + const _$MisskeyPageNotifierStateImpl({required this.page, this.likeOr}); + + @override + final Page page; + @override + final AsyncValue? likeOr; + + @override + String toString() { + return 'MisskeyPageNotifierState(page: $page, likeOr: $likeOr)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$MisskeyPageNotifierStateImpl && + (identical(other.page, page) || other.page == page) && + (identical(other.likeOr, likeOr) || other.likeOr == likeOr)); + } + + @override + int get hashCode => Object.hash(runtimeType, page, likeOr); + + /// Create a copy of MisskeyPageNotifierState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$MisskeyPageNotifierStateImplCopyWith<_$MisskeyPageNotifierStateImpl> + get copyWith => __$$MisskeyPageNotifierStateImplCopyWithImpl< + _$MisskeyPageNotifierStateImpl>(this, _$identity); +} + +abstract class _MisskeyPageNotifierState implements MisskeyPageNotifierState { + const factory _MisskeyPageNotifierState( + {required final Page page, + final AsyncValue? likeOr}) = _$MisskeyPageNotifierStateImpl; + + @override + Page get page; + @override + AsyncValue? get likeOr; + + /// Create a copy of MisskeyPageNotifierState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$MisskeyPageNotifierStateImplCopyWith<_$MisskeyPageNotifierStateImpl> + get copyWith => throw _privateConstructorUsedError; +} diff --git a/lib/view/misskey_page_page/misskey_page_notifier.g.dart b/lib/view/misskey_page_page/misskey_page_notifier.g.dart new file mode 100644 index 000000000..27b57ce1c --- /dev/null +++ b/lib/view/misskey_page_page/misskey_page_notifier.g.dart @@ -0,0 +1,229 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'misskey_page_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$misskeyPageNotifierHash() => + r'ee6d060c8b5eddfefb89f6062692fdfa8b0e4d47'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$MisskeyPageNotifier + extends BuildlessAutoDisposeAsyncNotifier { + late final String pageId; + + FutureOr build( + String pageId, + ); +} + +/// See also [MisskeyPageNotifier]. +@ProviderFor(MisskeyPageNotifier) +const misskeyPageNotifierProvider = MisskeyPageNotifierFamily(); + +/// See also [MisskeyPageNotifier]. +class MisskeyPageNotifierFamily extends Family { + /// See also [MisskeyPageNotifier]. + const MisskeyPageNotifierFamily(); + + static final Iterable _dependencies = [ + accountContextProvider, + misskeyGetContextProvider, + misskeyPostContextProvider + ]; + + static final Iterable _allTransitiveDependencies = + { + accountContextProvider, + ...?accountContextProvider.allTransitiveDependencies, + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies, + misskeyPostContextProvider, + ...?misskeyPostContextProvider.allTransitiveDependencies + }; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'misskeyPageNotifierProvider'; + + /// See also [MisskeyPageNotifier]. + MisskeyPageNotifierProvider call( + String pageId, + ) { + return MisskeyPageNotifierProvider( + pageId, + ); + } + + @visibleForOverriding + @override + MisskeyPageNotifierProvider getProviderOverride( + covariant MisskeyPageNotifierProvider provider, + ) { + return call( + provider.pageId, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(MisskeyPageNotifier Function() create) { + return _$MisskeyPageNotifierFamilyOverride(this, create); + } +} + +class _$MisskeyPageNotifierFamilyOverride implements FamilyOverride { + _$MisskeyPageNotifierFamilyOverride(this.overriddenFamily, this.create); + + final MisskeyPageNotifier Function() create; + + @override + final MisskeyPageNotifierFamily overriddenFamily; + + @override + MisskeyPageNotifierProvider getProviderOverride( + covariant MisskeyPageNotifierProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [MisskeyPageNotifier]. +class MisskeyPageNotifierProvider extends AutoDisposeAsyncNotifierProviderImpl< + MisskeyPageNotifier, MisskeyPageNotifierState> { + /// See also [MisskeyPageNotifier]. + MisskeyPageNotifierProvider( + String pageId, + ) : this._internal( + () => MisskeyPageNotifier()..pageId = pageId, + from: misskeyPageNotifierProvider, + name: r'misskeyPageNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$misskeyPageNotifierHash, + dependencies: MisskeyPageNotifierFamily._dependencies, + allTransitiveDependencies: + MisskeyPageNotifierFamily._allTransitiveDependencies, + pageId: pageId, + ); + + MisskeyPageNotifierProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.pageId, + }) : super.internal(); + + final String pageId; + + @override + FutureOr runNotifierBuild( + covariant MisskeyPageNotifier notifier, + ) { + return notifier.build( + pageId, + ); + } + + @override + Override overrideWith(MisskeyPageNotifier Function() create) { + return ProviderOverride( + origin: this, + override: MisskeyPageNotifierProvider._internal( + () => create()..pageId = pageId, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + pageId: pageId, + ), + ); + } + + @override + (String,) get argument { + return (pageId,); + } + + @override + AutoDisposeAsyncNotifierProviderElement createElement() { + return _MisskeyPageNotifierProviderElement(this); + } + + MisskeyPageNotifierProvider _copyWith( + MisskeyPageNotifier Function() create, + ) { + return MisskeyPageNotifierProvider._internal( + () => create()..pageId = pageId, + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + pageId: pageId, + ); + } + + @override + bool operator ==(Object other) { + return other is MisskeyPageNotifierProvider && other.pageId == pageId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, pageId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin MisskeyPageNotifierRef + on AutoDisposeAsyncNotifierProviderRef { + /// The parameter `pageId` of this provider. + String get pageId; +} + +class _MisskeyPageNotifierProviderElement + extends AutoDisposeAsyncNotifierProviderElement with MisskeyPageNotifierRef { + _MisskeyPageNotifierProviderElement(super.provider); + + @override + String get pageId => (origin as MisskeyPageNotifierProvider).pageId; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/misskey_page_page/misskey_page_page.dart b/lib/view/misskey_page_page/misskey_page_page.dart index 46c1cd918..0b6dce40d 100644 --- a/lib/view/misskey_page_page/misskey_page_page.dart +++ b/lib/view/misskey_page_page/misskey_page_page.dart @@ -1,132 +1,151 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mfm_parser/mfm_parser.dart' hide MfmText; -import 'package:miria/extensions/list_mfm_node_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/constants.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/common/image_dialog.dart'; -import 'package:miria/view/common/misskey_notes/link_preview.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/misskey_notes/network_image.dart'; -import 'package:miria/view/dialogs/simple_message_dialog.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:miria/view/user_page/user_list_item.dart'; -import 'package:misskey_dart/misskey_dart.dart' as misskey; -import 'package:miria/view/common/account_scope.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:mfm_parser/mfm_parser.dart" hide MfmText; +import "package:miria/extensions/list_mfm_node_extension.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/constants.dart"; +import "package:miria/view/common/note_file_dialog/note_file_dialog.dart"; +import "package:miria/view/common/misskey_notes/link_preview.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; +import "package:miria/view/misskey_page_page/misskey_page_notifier.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:miria/view/user_page/user_list_item.dart"; +import "package:misskey_dart/misskey_dart.dart" as misskey; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; +import "package:url_launcher/url_launcher.dart"; + +part "misskey_page_page.g.dart"; @RoutePage() -class MisskeyPagePage extends ConsumerWidget { - final Account account; +class MisskeyPagePage extends ConsumerWidget implements AutoRouteWrapper { + final AccountContext accountContext; final misskey.Page page; const MisskeyPagePage({ - super.key, - required this.account, + required this.accountContext, required this.page, + super.key, }); + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); @override Widget build(BuildContext context, WidgetRef ref) { - return AccountScope( - account: account, - child: Scaffold( - appBar: AppBar(title: Text(S.of(context).page)), - body: Padding( - padding: const EdgeInsets.only(left: 10, right: 10, bottom: 20), - child: Align( - alignment: Alignment.topCenter, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 800), - child: SingleChildScrollView( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, + return Scaffold( + appBar: AppBar(title: Text(S.of(context).page)), + body: Padding( + padding: const EdgeInsets.only(left: 10, right: 10, bottom: 20), + child: Align( + alignment: Alignment.topCenter, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + MfmText( + mfmText: page.title, + style: Theme.of(context).textTheme.headlineSmall, + ), + MfmText( + mfmText: page.summary ?? "", + style: Theme.of(context).textTheme.bodySmall, + ), + const Divider(), + if (page.eyeCatchingImage != null) + NetworkImageView( + url: page.eyeCatchingImage!.url, + type: ImageType.image, + ), + for (final content in page.content) + PageContent(content: content, page: page), + const Divider(), + Text(S.of(context).pageWrittenBy), + UserListItem(user: page.user), + Row( children: [ - MfmText( - mfmText: page.title, - style: Theme.of(context).textTheme.headlineSmall), - MfmText( - mfmText: page.summary ?? "", - style: Theme.of(context).textTheme.bodySmall), - const Divider(), - if (page.eyeCatchingImage != null) - NetworkImageView( - url: page.eyeCatchingImage!.url, - type: ImageType.image), - for (final content in page.content) - PageContent(content: content, page: page), - const Divider(), - Text(S.of(context).pageWrittenBy), - UserListItem(user: page.user), - Row( - children: [ - PageLikeButton( - initialLiked: page.isLiked ?? false, - likeCount: page.likedCount, - pageId: page.id, - userId: page.userId, - ), - const Padding(padding: EdgeInsets.only(left: 10)), - GestureDetector( - onTap: () => launchUrl(Uri( - scheme: "https", - host: account.host, - pathSegments: [ - "@${page.user.username}", - "pages", - page.name - ])), - child: Text( - S.of(context).openBrowsers, - style: AppTheme.of(context).linkStyle, - ), - ), - ], + PageLikeButton( + initialLiked: page.isLiked ?? false, + likeCount: page.likedCount, + pageId: page.id, + userId: page.userId, ), - Align( - alignment: Alignment.centerRight, - child: Column( - mainAxisAlignment: MainAxisAlignment.end, - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text(S.of(context).pageCreatedAt(page.createdAt)), - Text(S.of(context).pageUpdatedAt(page.updatedAt)), - ], + const Padding(padding: EdgeInsets.only(left: 10)), + GestureDetector( + onTap: () async => launchUrl( + Uri( + scheme: "https", + host: accountContext.getAccount.host, + pathSegments: [ + "@${page.user.username}", + "pages", + page.name, + ], + ), + ), + child: Text( + S.of(context).openBrowsers, + style: AppTheme.of(context).linkStyle, ), ), ], ), - ), + Align( + alignment: Alignment.centerRight, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(S.of(context).pageCreatedAt(page.createdAt)), + Text(S.of(context).pageUpdatedAt(page.updatedAt)), + ], + ), + ), + ], ), ), ), - )); + ), + ), + ); } } +@Riverpod(dependencies: [misskeyGetContext, notesWith]) +Future fetchNote(FetchNoteRef ref, String noteId) async { + final note = await ref + .read(misskeyGetContextProvider) + .notes + .show(misskey.NotesShowRequest(noteId: noteId)); + ref.read(notesWithProvider).registerNote(note); + return note; +} + class PageContent extends ConsumerWidget { final misskey.AbstractPageContent content; final misskey.Page page; const PageContent({ - super.key, required this.content, required this.page, + super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { final content = this.content; if (content case misskey.PageText(:final text?)) { - final account = AccountScope.of(context); + final account = ref.read(accountContextProvider).getAccount; final nodes = const MfmParser().parse(text); return Column( children: [ @@ -139,121 +158,111 @@ class PageContent extends ConsumerWidget { link: link, host: account.host, ), - ) + ), ], ); } if (content case misskey.PageImage(:final fileId?)) { - final url = - page.attachedFiles.firstWhereOrNull((e) => e.id == fileId)?.url; - final thumbnailUrl = page.attachedFiles - .firstWhereOrNull((e) => e.id == fileId) - ?.thumbnailUrl; - if (url != null) { + final file = page.attachedFiles.firstWhereOrNull((e) => e.id == fileId); + if (file != null) { + final url = file.url; + + final thumbnailUrl = page.attachedFiles + .firstWhereOrNull((e) => e.id == content.fileId) + ?.thumbnailUrl; return GestureDetector( - onTap: () => showDialog( - context: context, - builder: (context) => - ImageDialog(imageUrlList: [url], initialPage: 0)), - child: NetworkImageView( - url: thumbnailUrl ?? url, type: ImageType.image)); + onTap: () async => showDialog( + context: context, + builder: (context) => + NoteFileDialog(driveFiles: [file], initialPage: 0), + ), + child: NetworkImageView( + url: thumbnailUrl ?? url, + type: ImageType.image, + ), + ); } else { return const SizedBox.shrink(); } } if (content case misskey.PageNote(note: final noteId?)) { - return FutureBuilder( - future: (() async { - final account = AccountScope.of(context); - final note = await ref - .read(misskeyProvider(account)) - .notes - .show(misskey.NotesShowRequest(noteId: noteId)); - ref.read(notesProvider(account)).registerNote(note); - return note; - })(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done && - snapshot.data != null) { - return MisskeyNote(note: snapshot.data!); - } else if (snapshot.hasError) { - return Text(S.of(context).thrownError); - } else { - return const Center( - child: SizedBox( - width: 20, height: 20, child: CircularProgressIndicator()), - ); - } - }, - ); + final note = ref.watch(fetchNoteProvider(noteId)); + return switch (note) { + AsyncLoading() => const Center( + child: SizedBox.square( + dimension: 20, + child: CircularProgressIndicator.adaptive(), + ), + ), + AsyncError() => Text(S.of(context).thrownError), + AsyncData(:final value) => MisskeyNote(note: value) + }; } if (content is misskey.PageSection) { return Padding( padding: const EdgeInsets.only(left: 5, right: 5), child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - MfmText( - mfmText: content.title ?? "", - style: Theme.of(context).textTheme.titleLarge), - for (final child in content.children) - PageContent(content: child, page: page) - ]), + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + MfmText( + mfmText: content.title ?? "", + style: Theme.of(context).textTheme.titleLarge, + ), + for (final child in content.children) + PageContent(content: child, page: page), + ], + ), ); } return SizedBox( width: double.infinity, child: Card( - child: Column(children: [ - Padding( - padding: const EdgeInsets.all(8.0), - child: Text(S.of(context).unsupportedPage), - ), - ]), + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: Text(S.of(context).unsupportedPage), + ), + ], + ), ), ); } } -class PageLikeButton extends ConsumerStatefulWidget { +class PageLikeButton extends ConsumerWidget { final bool initialLiked; final int likeCount; final String pageId; final String userId; const PageLikeButton({ - super.key, required this.initialLiked, required this.likeCount, required this.pageId, required this.userId, + super.key, }); - @override - ConsumerState createState() => PageLikeButtonState(); -} - -class PageLikeButtonState extends ConsumerState { - late bool liked = widget.initialLiked; - late int likeCount = widget.likeCount; + Widget build(BuildContext context, WidgetRef ref) { + final provider = misskeyPageNotifierProvider(pageId); + final liked = ref.watch( + provider.select((value) => value.valueOrNull?.page.isLiked ?? false), + ); + final likeCount = ref.watch( + provider.select((value) => value.valueOrNull?.page.likedCount ?? 0), + ); + final isLoading = ref.watch( + provider.select((value) => value.valueOrNull?.likeOr is AsyncLoading), + ); - @override - Widget build(BuildContext context) { if (liked) { return ElevatedButton.icon( - onPressed: () async { - await ref - .read(misskeyProvider(AccountScope.of(context))) - .pages - .unlike(misskey.PagesUnlikeRequest(pageId: widget.pageId)); - setState(() { - liked = false; - likeCount--; - }); - }.expectFailure(context), + onPressed: + isLoading ? null : () async => ref.read(provider.notifier).likeOr(), icon: Icon( Icons.favorite, size: MediaQuery.textScalerOf(context) @@ -263,21 +272,8 @@ class PageLikeButtonState extends ConsumerState { ); } else { return OutlinedButton.icon( - onPressed: () async { - if (AccountScope.of(context).i.id == widget.userId) { - SimpleMessageDialog.show( - context, S.of(context).canNotFavoriteMyPage); - return; - } - await ref - .read(misskeyProvider(AccountScope.of(context))) - .pages - .like(misskey.PagesLikeRequest(pageId: widget.pageId)); - setState(() { - liked = true; - likeCount++; - }); - }.expectFailure(context), + onPressed: + isLoading ? null : () async => ref.read(provider.notifier).likeOr(), icon: Icon( Icons.favorite, size: MediaQuery.textScalerOf(context) diff --git a/lib/view/misskey_page_page/misskey_page_page.g.dart b/lib/view/misskey_page_page/misskey_page_page.g.dart new file mode 100644 index 000000000..19a18a4a3 --- /dev/null +++ b/lib/view/misskey_page_page/misskey_page_page.g.dart @@ -0,0 +1,207 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'misskey_page_page.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$fetchNoteHash() => r'93baa5042998814028ccf0597aeee9d344c2e189'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [fetchNote]. +@ProviderFor(fetchNote) +const fetchNoteProvider = FetchNoteFamily(); + +/// See also [fetchNote]. +class FetchNoteFamily extends Family { + /// See also [fetchNote]. + const FetchNoteFamily(); + + static final Iterable _dependencies = [ + misskeyGetContextProvider, + notesWithProvider + ]; + + static final Iterable _allTransitiveDependencies = + { + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies, + notesWithProvider, + ...?notesWithProvider.allTransitiveDependencies + }; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'fetchNoteProvider'; + + /// See also [fetchNote]. + FetchNoteProvider call( + String noteId, + ) { + return FetchNoteProvider( + noteId, + ); + } + + @visibleForOverriding + @override + FetchNoteProvider getProviderOverride( + covariant FetchNoteProvider provider, + ) { + return call( + provider.noteId, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(FutureOr Function(FetchNoteRef ref) create) { + return _$FetchNoteFamilyOverride(this, create); + } +} + +class _$FetchNoteFamilyOverride implements FamilyOverride { + _$FetchNoteFamilyOverride(this.overriddenFamily, this.create); + + final FutureOr Function(FetchNoteRef ref) create; + + @override + final FetchNoteFamily overriddenFamily; + + @override + FetchNoteProvider getProviderOverride( + covariant FetchNoteProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [fetchNote]. +class FetchNoteProvider extends AutoDisposeFutureProvider { + /// See also [fetchNote]. + FetchNoteProvider( + String noteId, + ) : this._internal( + (ref) => fetchNote( + ref as FetchNoteRef, + noteId, + ), + from: fetchNoteProvider, + name: r'fetchNoteProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$fetchNoteHash, + dependencies: FetchNoteFamily._dependencies, + allTransitiveDependencies: FetchNoteFamily._allTransitiveDependencies, + noteId: noteId, + ); + + FetchNoteProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.noteId, + }) : super.internal(); + + final String noteId; + + @override + Override overrideWith( + FutureOr Function(FetchNoteRef ref) create, + ) { + return ProviderOverride( + origin: this, + override: FetchNoteProvider._internal( + (ref) => create(ref as FetchNoteRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + noteId: noteId, + ), + ); + } + + @override + (String,) get argument { + return (noteId,); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _FetchNoteProviderElement(this); + } + + FetchNoteProvider _copyWith( + FutureOr Function(FetchNoteRef ref) create, + ) { + return FetchNoteProvider._internal( + (ref) => create(ref as FetchNoteRef), + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + noteId: noteId, + ); + } + + @override + bool operator ==(Object other) { + return other is FetchNoteProvider && other.noteId == noteId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, noteId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin FetchNoteRef on AutoDisposeFutureProviderRef { + /// The parameter `noteId` of this provider. + String get noteId; +} + +class _FetchNoteProviderElement extends AutoDisposeFutureProviderElement + with FetchNoteRef { + _FetchNoteProviderElement(super.provider); + + @override + String get noteId => (origin as FetchNoteProvider).noteId; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/note_create_page/channel_area.dart b/lib/view/note_create_page/channel_area.dart index ab96bf250..f8d08c181 100644 --- a/lib/view/note_create_page/channel_area.dart +++ b/lib/view/note_create_page/channel_area.dart @@ -1,15 +1,15 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/state_notifier/note_create_page/note_create_state_notifier.dart"; class ChannelArea extends ConsumerWidget { const ChannelArea({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final channel = ref.watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.channel)); + final channel = ref.watch( + noteCreateNotifierProvider.select((value) => value.channel), + ); if (channel == null) return Container(); return Align( diff --git a/lib/view/note_create_page/create_file_view.dart b/lib/view/note_create_page/create_file_view.dart index b713f670e..36362bb8a 100644 --- a/lib/view/note_create_page/create_file_view.dart +++ b/lib/view/note_create_page/create_file_view.dart @@ -1,56 +1,55 @@ -import 'dart:typed_data'; - -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/image_file.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/note_create_page/file_settings_dialog.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/image_file.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/state_notifier/note_create_page/note_create_state_notifier.dart"; +import "package:miria/view/note_create_page/file_settings_dialog.dart"; class CreateFileView extends ConsumerWidget { final int index; final MisskeyPostFile file; const CreateFileView({ - super.key, required this.file, required this.index, + super.key, }); Future onTap(BuildContext context, WidgetRef ref) async { if (defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS || defaultTargetPlatform == TargetPlatform.android) { - final account = AccountScope.of(context); - context.pushRoute(PhotoEditRoute( - account: AccountScope.of(context), + await context.pushRoute( + PhotoEditRoute( + accountContext: ref.read(accountContextProvider), file: file, onSubmit: (result) { ref - .read(noteCreateProvider(account).notifier) + .read(noteCreateNotifierProvider.notifier) .setFileContent(file, result); - })); + }, + ), + ); } } Future detailTap(BuildContext context, WidgetRef ref) async { - final account = AccountScope.of(context); final result = await showDialog( - context: context, builder: (context) => FileSettingsDialog(file: file)); + context: context, + builder: (context) => FileSettingsDialog(file: file), + ); if (result == null) return; ref - .read(noteCreateProvider(account).notifier) + .read(noteCreateNotifierProvider.notifier) .setFileMetaData(index, result); } void delete(BuildContext context, WidgetRef ref) { - ref - .read(noteCreateProvider(AccountScope.of(context)).notifier) - .deleteFile(index); + ref.read(noteCreateNotifierProvider.notifier).deleteFile(index); } @override @@ -59,50 +58,84 @@ class CreateFileView extends ConsumerWidget { switch (data) { case ImageFile(): - return Column( - children: [ - SizedBox( - height: 200, - child: GestureDetector( - onTap: () async => await onTap(context, ref), - child: Image.memory(data.data)), - ), - Row( + return Card.outlined( + child: SizedBox( + width: 210, + child: Column( children: [ - if (data.isNsfw) const Icon(Icons.details_rounded), - Text(data.fileName), - IconButton( - onPressed: () => detailTap(context, ref), - icon: const Icon(Icons.more_vert)), - IconButton( - onPressed: () => delete(context, ref), - icon: const Icon(Icons.delete)), + Container( + padding: const EdgeInsets.all(5), + child: SizedBox( + height: 200, + child: GestureDetector( + onTap: () async => await onTap(context, ref), + child: Image.memory(data.data), + ), + ), + ), + Row( + children: [ + if (data.isNsfw) const Icon(Icons.details_rounded), + if (!data.isNsfw) const SizedBox(width: 5), + Expanded( + child: Text( + data.fileName, + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + onPressed: () async => detailTap(context, ref), + icon: const Icon(Icons.more_vert), + ), + IconButton( + onPressed: () => delete(context, ref), + icon: const Icon(Icons.delete), + ), + ], + ), ], - ) - ], + ), + ), ); case ImageFileAlreadyPostedFile(): - return Column( - children: [ - SizedBox( - height: 200, - child: GestureDetector( - onTap: () async => await onTap(context, ref), - child: Image.memory(data.data)), - ), - Row( + return Card.outlined( + child: SizedBox( + width: 210, + child: Column( children: [ - if (data.isNsfw) const Icon(Icons.details_rounded), - Text(data.fileName), - IconButton( - onPressed: () => detailTap(context, ref), - icon: const Icon(Icons.more_vert)), - IconButton( - onPressed: () => delete(context, ref), - icon: const Icon(Icons.delete)), + Container( + padding: const EdgeInsets.all(5), + child: SizedBox( + height: 200, + child: GestureDetector( + onTap: () async => await onTap(context, ref), + child: Image.memory(data.data), + ), + ), + ), + Row( + children: [ + if (data.isNsfw) const Icon(Icons.details_rounded), + if (!data.isNsfw) const SizedBox(width: 5), + Expanded( + child: Text( + data.fileName, + overflow: TextOverflow.ellipsis, + ), + ), + IconButton( + onPressed: () async => detailTap(context, ref), + icon: const Icon(Icons.more_vert), + ), + IconButton( + onPressed: () => delete(context, ref), + icon: const Icon(Icons.delete), + ), + ], + ), ], - ) - ], + ), + ), ); case UnknownFile(): return Text(data.fileName); diff --git a/lib/view/note_create_page/cw_text_area.dart b/lib/view/note_create_page/cw_text_area.dart index 0c224a1d3..dd3bc2e28 100644 --- a/lib/view/note_create_page/cw_text_area.dart +++ b/lib/view/note_create_page/cw_text_area.dart @@ -1,64 +1,50 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class CwTextArea extends ConsumerStatefulWidget { +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/state_notifier/note_create_page/note_create_state_notifier.dart"; +import "package:miria/view/themes/app_theme.dart"; + +class CwTextArea extends HookConsumerWidget { const CwTextArea({super.key}); @override - ConsumerState createState() => CwTextAreaState(); -} - -class CwTextAreaState extends ConsumerState { - final cwController = TextEditingController(); - - @override - void initState() { - super.initState(); - + Widget build(BuildContext context, WidgetRef ref) { + final cwController = useTextEditingController(); cwController.addListener(() { ref - .watch(noteCreateProvider(AccountScope.of(context)).notifier) + .watch(noteCreateNotifierProvider.notifier) .setCwText(cwController.text); }); - } - @override - void dispose() { - cwController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { ref.listen( - noteCreateProvider(AccountScope.of(context)) - .select((value) => value.cwText), + noteCreateNotifierProvider.select((value) => value.cwText), (_, next) { if (next != cwController.text) cwController.text = next; }, ); - final cw = ref.watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.isCw)); + final cw = ref.watch( + noteCreateNotifierProvider.select((value) => value.isCw), + ); if (!cw) return const SizedBox.shrink(); return Padding( padding: const EdgeInsets.only(bottom: 10), child: Container( decoration: BoxDecoration( - border: Border( - bottom: BorderSide(color: Theme.of(context).dividerColor))), + border: Border( + bottom: BorderSide(color: Theme.of(context).dividerColor), + ), + ), padding: const EdgeInsets.only(bottom: 10), child: TextField( controller: cwController, keyboardType: TextInputType.multiline, decoration: AppTheme.of(context).noteTextStyle.copyWith( - hintText: S.of(context).contentWarning, - contentPadding: const EdgeInsets.all(5)), + hintText: S.of(context).contentWarning, + contentPadding: const EdgeInsets.all(5), + ), ), ), ); diff --git a/lib/view/note_create_page/cw_toggle_button.dart b/lib/view/note_create_page/cw_toggle_button.dart index bd8c7882e..9820c3f1a 100644 --- a/lib/view/note_create_page/cw_toggle_button.dart +++ b/lib/view/note_create_page/cw_toggle_button.dart @@ -1,19 +1,18 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/state_notifier/note_create_page/note_create_state_notifier.dart"; class CwToggleButton extends ConsumerWidget { const CwToggleButton({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final cw = ref.watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.isCw)); + final cw = ref.watch( + noteCreateNotifierProvider.select((value) => value.isCw), + ); return IconButton( - onPressed: () => ref - .read(noteCreateProvider(AccountScope.of(context)).notifier) - .toggleCw(), - icon: Icon(cw ? Icons.visibility_off : Icons.remove_red_eye)); + onPressed: () => ref.read(noteCreateNotifierProvider.notifier).toggleCw(), + icon: Icon(cw ? Icons.visibility_off : Icons.remove_red_eye), + ); } } diff --git a/lib/view/note_create_page/drive_file_select_dialog.dart b/lib/view/note_create_page/drive_file_select_dialog.dart index 8b51bdbe3..2fe1941be 100644 --- a/lib/view/note_create_page/drive_file_select_dialog.dart +++ b/lib/view/note_create_page/drive_file_select_dialog.dart @@ -1,61 +1,61 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/misskey_notes/network_image.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class DriveFileSelectDialog extends ConsumerStatefulWidget { +@RoutePage>() +class DriveFileSelectDialog extends HookConsumerWidget + implements AutoRouteWrapper { final Account account; final bool allowMultiple; const DriveFileSelectDialog({ - super.key, required this.account, + super.key, this.allowMultiple = false, }); @override - ConsumerState createState() => - DriveFileSelectDialogState(); -} - -class DriveFileSelectDialogState extends ConsumerState { - final List path = []; - final List files = []; + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: account, child: this); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final path = useState>([]); + final files = useState>([]); + return AlertDialog( title: AppBar( leading: IconButton( - onPressed: path.isEmpty + onPressed: path.value.isEmpty ? null - : () { - setState(() { - path.removeLast(); - }); - }, + : () => path.value = [...path.value..removeLast()], icon: const Icon(Icons.arrow_back), ), - title: path.isEmpty + title: path.value.isEmpty ? Text(S.of(context).chooseFile) - : Text(path.map((e) => e.name).join("/")), + : Text(path.value.map((e) => e.name).join("/")), actions: [ - if (files.isNotEmpty) + if (files.value.isNotEmpty) Center( child: Text( - "(${files.length})", + "(${files.value.length})", style: Theme.of(context).textTheme.titleMedium, ), ), - if (widget.allowMultiple) + if (allowMultiple) IconButton( - onPressed: - files.isEmpty ? null : () => Navigator.of(context).pop(files), + onPressed: files.value.isEmpty + ? null + : () => Navigator.of(context).pop(files.value), icon: const Icon(Icons.check), ), ], @@ -68,62 +68,65 @@ class DriveFileSelectDialogState extends ConsumerState { child: Column( children: [ PushableListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - showAd: false, - initializeFuture: () async { - final misskey = ref.read(misskeyProvider(widget.account)); - final response = await misskey.drive.folders.folders( - DriveFoldersRequest( - folderId: path.isEmpty ? null : path.last.id)); - return response.toList(); - }, - nextFuture: (lastItem, _) async { - final misskey = ref.read(misskeyProvider(widget.account)); - final response = await misskey.drive.folders.folders( - DriveFoldersRequest( - untilId: lastItem.id, - folderId: path.isEmpty ? null : path.last.id)); - return response.toList(); - }, - listKey: path.map((e) => e.id).join("/"), - itemBuilder: (context, item) { - return ListTile( - leading: const Icon(Icons.folder), - title: Text(item.name), - onTap: () { - setState(() { - path.add(item); - }); - }, - ); - }), + hideIsEmpty: true, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + showAd: false, + initializeFuture: () async { + final misskey = ref.read(misskeyGetContextProvider); + final response = await misskey.drive.folders.folders( + DriveFoldersRequest( + folderId: path.value.isEmpty ? null : path.value.last.id, + ), + ); + return response.toList(); + }, + nextFuture: (lastItem, _) async { + final misskey = ref.read(misskeyGetContextProvider); + final response = await misskey.drive.folders.folders( + DriveFoldersRequest( + untilId: lastItem.id, + folderId: path.value.isEmpty ? null : path.value.last.id, + ), + ); + return response.toList(); + }, + listKey: path.value.map((e) => e.id).join("/"), + itemBuilder: (context, item) { + return ListTile( + leading: const Icon(Icons.folder), + title: Text(item.name), + onTap: () => path.value = [...path.value, item], + ); + }, + ), PushableListView( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), showAd: false, initializeFuture: () async { - final misskey = ref.read(misskeyProvider(widget.account)); + final misskey = ref.read(misskeyGetContextProvider); final response = await misskey.drive.files.files( DriveFilesRequest( - folderId: path.isEmpty ? null : path.last.id, + folderId: path.value.isEmpty ? null : path.value.last.id, ), ); return response.toList(); }, nextFuture: (lastItem, _) async { - final misskey = ref.read(misskeyProvider(widget.account)); + final misskey = ref.read(misskeyGetContextProvider); final response = await misskey.drive.files.files( DriveFilesRequest( untilId: lastItem.id, - folderId: path.isEmpty ? null : path.last.id, + folderId: path.value.isEmpty ? null : path.value.last.id, ), ); return response.toList(); }, - listKey: path.map((e) => e.id).join("/"), + listKey: path.value.map((e) => e.id).join("/"), itemBuilder: (context, item) { - final isSelected = files.any((file) => file.id == item.id); + final isSelected = + files.value.any((file) => file.id == item.id); return Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: InkWell( @@ -131,21 +134,21 @@ class DriveFileSelectDialogState extends ConsumerState { borderRadius: BorderRadius.circular(5), ), onTap: () { - if (widget.allowMultiple) { - setState(() { - if (isSelected) { - files.removeWhere((file) => file.id == item.id); - } else { - files.add(item); - } - }); + if (allowMultiple) { + if (isSelected) { + files.value = files.value + .where((file) => file.id != item.id) + .toList(); + } else { + files.value = [...files.value, item]; + } } else { - Navigator.of(context).pop(item); + Navigator.of(context).pop([item]); } }, child: Container( padding: const EdgeInsets.all(10), - decoration: (widget.allowMultiple && isSelected) + decoration: (allowMultiple && isSelected) ? BoxDecoration( color: AppTheme.of(context) .currentDisplayTabColor diff --git a/lib/view/note_create_page/drive_modal_sheet.dart b/lib/view/note_create_page/drive_modal_sheet.dart index 869075025..31047b826 100644 --- a/lib/view/note_create_page/drive_modal_sheet.dart +++ b/lib/view/note_create_page/drive_modal_sheet.dart @@ -1,8 +1,10 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; enum DriveModalSheetReturnValue { upload, drive } +@RoutePage() class DriveModalSheet extends StatelessWidget { const DriveModalSheet({super.key}); @@ -13,16 +15,13 @@ class DriveModalSheet extends StatelessWidget { ListTile( title: Text(S.of(context).uploadFile), leading: const Icon(Icons.upload), - onTap: () { - Navigator.of(context).pop(DriveModalSheetReturnValue.upload); - }, + onTap: () async => + context.maybePop(DriveModalSheetReturnValue.upload), ), ListTile( title: Text(S.of(context).fromDrive), leading: const Icon(Icons.cloud_outlined), - onTap: () { - Navigator.of(context).pop(DriveModalSheetReturnValue.drive); - }, + onTap: () async => context.maybePop(DriveModalSheetReturnValue.drive), ), ], ); diff --git a/lib/view/note_create_page/file_preview.dart b/lib/view/note_create_page/file_preview.dart index 02bb7b1ca..120b9fb60 100644 --- a/lib/view/note_create_page/file_preview.dart +++ b/lib/view/note_create_page/file_preview.dart @@ -1,25 +1,26 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/note_create_page/create_file_view.dart'; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/state_notifier/note_create_page/note_create_state_notifier.dart"; +import "package:miria/view/note_create_page/create_file_view.dart"; class FilePreview extends ConsumerWidget { const FilePreview({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final files = ref.watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.files)); + final files = ref.watch( + noteCreateNotifierProvider.select((value) => value.files), + ); return SingleChildScrollView( scrollDirection: Axis.horizontal, child: Row( children: [ for (final file in files.mapIndexed((index, e) => (index, e))) Padding( - padding: const EdgeInsets.only(right: 10), - child: CreateFileView(file: file.$2, index: file.$1)) + padding: const EdgeInsets.only(right: 10), + child: CreateFileView(file: file.$2, index: file.$1), + ), ], ), ); diff --git a/lib/view/note_create_page/file_settings_dialog.dart b/lib/view/note_create_page/file_settings_dialog.dart index 4cf087d0f..9d8fa5829 100644 --- a/lib/view/note_create_page/file_settings_dialog.dart +++ b/lib/view/note_create_page/file_settings_dialog.dart @@ -1,57 +1,39 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/image_file.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/image_file.dart"; class FileSettingsDialogResult { final String fileName; final bool isNsfw; final String caption; - const FileSettingsDialogResult( - {required this.fileName, required this.isNsfw, required this.caption}); + const FileSettingsDialogResult({ + required this.fileName, + required this.isNsfw, + required this.caption, + }); } -class FileSettingsDialog extends ConsumerStatefulWidget { +class FileSettingsDialog extends HookConsumerWidget { final MisskeyPostFile file; - const FileSettingsDialog({super.key, required this.file}); - - @override - ConsumerState createState() => - FileSettingsDialogState(); -} - -class FileSettingsDialogState extends ConsumerState { - late final TextEditingController fileNameController; - late final TextEditingController captionController; - bool isNsfw = false; - - @override - void initState() { - super.initState(); - - fileNameController = TextEditingController(text: widget.file.fileName); - captionController = TextEditingController(text: widget.file.caption); - isNsfw = widget.file.isNsfw; - } - - @override - void dispose() { - fileNameController.dispose(); - captionController.dispose(); - super.dispose(); - } + const FileSettingsDialog({required this.file, super.key}); String generateRandomText() { - var str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" - .split(""); - str.shuffle(); + final str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" + .split("") + ..shuffle(); return str.take(10).join(""); } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final fileNameController = useTextEditingController(text: file.fileName); + final captionController = useTextEditingController(text: file.caption); + final isNsfw = useState(file.isNsfw); + return AlertDialog( contentPadding: const EdgeInsets.all(10), content: SizedBox( @@ -67,25 +49,27 @@ class FileSettingsDialogState extends ConsumerState { TextField( controller: fileNameController, decoration: const InputDecoration( - prefixIcon: Icon(Icons.badge_outlined)), + prefixIcon: Icon(Icons.badge_outlined), + ), ), TextButton( - onPressed: () { - final period = fileNameController.text.lastIndexOf("."); - if (period == -1) { - fileNameController.text = generateRandomText(); - } else { - fileNameController.text = generateRandomText() + - fileNameController.text.substring(period); - } - }, - child: Text(S.of(context).randomizeFileName)), + onPressed: () { + final period = fileNameController.text.lastIndexOf("."); + if (period == -1) { + fileNameController.text = generateRandomText(); + } else { + fileNameController.text = generateRandomText() + + fileNameController.text.substring(period); + } + }, + child: Text(S.of(context).randomizeFileName), + ), const Padding(padding: EdgeInsets.only(top: 10)), CheckboxListTile( - value: isNsfw, + value: isNsfw.value, title: Text(S.of(context).markAsSensitive), subtitle: Text(S.of(context).sensitiveSubTitle), - onChanged: (value) => setState(() => isNsfw = !isNsfw), + onChanged: (value) => isNsfw.value = !isNsfw.value, controlAffinity: ListTileControlAffinity.leading, ), const Padding(padding: EdgeInsets.only(top: 10)), @@ -95,7 +79,8 @@ class FileSettingsDialogState extends ConsumerState { maxLines: null, minLines: 5, decoration: const InputDecoration( - prefixIcon: Icon(Icons.subtitles_outlined)), + prefixIcon: Icon(Icons.subtitles_outlined), + ), ), ], ), @@ -103,14 +88,17 @@ class FileSettingsDialogState extends ConsumerState { ), actions: [ ElevatedButton( - onPressed: () { - Navigator.of(context).pop(FileSettingsDialogResult( + onPressed: () { + Navigator.of(context).pop( + FileSettingsDialogResult( fileName: fileNameController.text, - isNsfw: isNsfw, + isNsfw: isNsfw.value, caption: captionController.text, - )); - }, - child: Text(S.of(context).done)), + ), + ); + }, + child: Text(S.of(context).done), + ), ], ); } diff --git a/lib/view/note_create_page/mfm_preview.dart b/lib/view/note_create_page/mfm_preview.dart index 54b71e30c..df5779853 100644 --- a/lib/view/note_create_page/mfm_preview.dart +++ b/lib/view/note_create_page/mfm_preview.dart @@ -1,20 +1,22 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/state_notifier/note_create_page/note_create_state_notifier.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; class MfmPreview extends ConsumerWidget { const MfmPreview({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final previewText = ref.watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.text)); + final previewText = ref.watch( + noteCreateNotifierProvider.select((value) => value.text), + ); final replyTo = ref - .watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.replyTo)) + .watch( + noteCreateNotifierProvider.select((value) => value.replyTo), + ) .map((e) => "@${e.username}${e.host == null ? " " : "@${e.host}"} ") .join(""); @@ -22,7 +24,7 @@ class MfmPreview extends ConsumerWidget { padding: const EdgeInsets.all(5), child: MfmText( mfmText: "$replyTo$previewText", - isNyaize: AccountScope.of(context).i.isCat, + isNyaize: ref.read(accountContextProvider).postAccount.i.isCat, ), ); } diff --git a/lib/view/note_create_page/note_create_page.dart b/lib/view/note_create_page/note_create_page.dart index 9e1f9d65a..ce199c4db 100644 --- a/lib/view/note_create_page/note_create_page.dart +++ b/lib/view/note_create_page/note_create_page.dart @@ -1,34 +1,32 @@ -import 'dart:io'; +import "dart:async"; -import 'package:auto_route/annotations.dart'; -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:miria/extensions/text_editing_controller_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/state_notifier/note_create_page/note_create_state_notifier.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/common/modal_indicator.dart'; -import 'package:miria/view/note_create_page/renote_area.dart'; -import 'package:miria/view/note_create_page/reply_area.dart'; -import 'package:miria/view/note_create_page/reply_to_area.dart'; -import 'package:miria/view/note_create_page/vote_area.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:miria/view/note_create_page/note_create_setting_top.dart'; -import 'package:miria/view/note_create_page/note_emoji.dart'; -import 'package:miria/view/reaction_picker_dialog/reaction_picker_dialog.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -import 'channel_area.dart'; -import 'cw_text_area.dart'; -import 'cw_toggle_button.dart'; -import 'file_preview.dart'; -import 'mfm_preview.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/text_editing_controller_extension.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/state_notifier/note_create_page/note_create_state_notifier.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/modal_indicator.dart"; +import "package:miria/view/note_create_page/channel_area.dart"; +import "package:miria/view/note_create_page/cw_text_area.dart"; +import "package:miria/view/note_create_page/cw_toggle_button.dart"; +import "package:miria/view/note_create_page/file_preview.dart"; +import "package:miria/view/note_create_page/mfm_preview.dart"; +import "package:miria/view/note_create_page/note_create_setting_top.dart"; +import "package:miria/view/note_create_page/note_emoji.dart"; +import "package:miria/view/note_create_page/renote_area.dart"; +import "package:miria/view/note_create_page/reply_area.dart"; +import "package:miria/view/note_create_page/reply_to_area.dart"; +import "package:miria/view/note_create_page/vote_area.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:misskey_dart/misskey_dart.dart"; final noteInputTextProvider = ChangeNotifierProvider.autoDispose((ref) { @@ -42,7 +40,7 @@ final noteFocusProvider = enum NoteCreationMode { update, recreate } @RoutePage() -class NoteCreatePage extends ConsumerStatefulWidget { +class NoteCreatePage extends HookConsumerWidget implements AutoRouteWrapper { final Account initialAccount; final String? initialText; final List? initialMediaFiles; @@ -54,8 +52,8 @@ class NoteCreatePage extends ConsumerStatefulWidget { final NoteCreationMode? noteCreationMode; const NoteCreatePage({ - super.key, required this.initialAccount, + super.key, this.initialText, this.initialMediaFiles, this.exitOnNoted = false, @@ -66,126 +64,124 @@ class NoteCreatePage extends ConsumerStatefulWidget { this.noteCreationMode, }); - @override - ConsumerState createState() => NoteCreatePageState(); -} - -class NoteCreatePageState extends ConsumerState { - late final focusNode = ref.watch(noteFocusProvider); - var isFirstChangeDependenciesCalled = false; - - NoteCreate get data => ref.read(noteCreateProvider(widget.initialAccount)); - NoteCreateNotifier get notifier => - ref.read(noteCreateProvider(widget.initialAccount).notifier); - static const shareExtensionMethodChannel = MethodChannel("info.shiosyakeyakini.miria/share_extension"); @override - void initState() { - super.initState(); - } + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: initialAccount, child: this); @override - void didChangeDependencies() { - super.didChangeDependencies(); - if (isFirstChangeDependenciesCalled) return; - isFirstChangeDependenciesCalled = true; - Future(() async { - notifier.initialize( - widget.channel, - widget.initialText, - widget.initialMediaFiles, - widget.note, - widget.renote, - widget.reply, - widget.noteCreationMode, - ); + Widget build(BuildContext context, WidgetRef ref) { + final focusNode = ref.watch(noteFocusProvider); + final notifier = ref.read(noteCreateNotifierProvider.notifier); + final controller = ref.watch(noteInputTextProvider); - ref.read(noteInputTextProvider).addListener(() { - notifier.setContentText(ref.read(noteInputTextProvider).text); - }); - focusNode.addListener(() { - notifier.setContentTextFocused(focusNode.hasFocus); - }); - }); - } + useEffect( + () { + WidgetsBinding.instance.addPostFrameCallback((timestamp) async { + await notifier.initialize( + channel, + initialText, + initialMediaFiles, + note, + renote, + reply, + noteCreationMode, + ); + }); - @override - Widget build(BuildContext context) { - ref.listen( - noteCreateProvider(widget.initialAccount).select((value) => value.text), - (_, next) { - if (next != ref.read(noteInputTextProvider).text) { - ref.read(noteInputTextProvider).text = next; - } + controller.addListener(() { + notifier.setContentText(ref.read(noteInputTextProvider).text); + }); + focusNode.addListener(() { + notifier.setContentTextFocused(focusNode.hasFocus); + }); + return () => {}; }, + const [], ); - ref.listen( - noteCreateProvider(widget.initialAccount) - .select((value) => value.isNoteSending), (_, next) { - switch (next) { - case NoteSendStatus.sending: - IndicatorView.showIndicator(context); - break; - case NoteSendStatus.finished: - IndicatorView.hideIndicator(context); - if (widget.exitOnNoted) { - shareExtensionMethodChannel.invokeMethod("exit"); - } else { - Navigator.of(context).pop(); + + ref + ..listen( + noteCreateNotifierProvider.select((value) => value.text), + (_, next) { + if (next != ref.read(noteInputTextProvider).text) { + ref.read(noteInputTextProvider).text = next; } + }, + ) + ..listen( + noteCreateNotifierProvider.select((value) => value.isNoteSending), + (_, next) async { + switch (next) { + case NoteSendStatus.sending: + IndicatorView.showIndicator(context); + case NoteSendStatus.finished: + IndicatorView.hideIndicator(context); + if (exitOnNoted) { + await shareExtensionMethodChannel.invokeMethod("exit"); + } else { + Navigator.of(context).pop(); + } - break; - case NoteSendStatus.error: - IndicatorView.hideIndicator(context); - break; - case null: - break; - } - }); + case NoteSendStatus.error: + IndicatorView.hideIndicator(context); + case null: + break; + } + }); final noteDecoration = AppTheme.of(context).noteTextStyle.copyWith( - hintText: (widget.renote != null || widget.reply != null) + hintText: (renote != null || reply != null) ? S.of(context).replyNotePlaceholder : S.of(context).defaultNotePlaceholder, contentPadding: const EdgeInsets.all(5), ); - return AccountScope( - account: widget.initialAccount, - child: Scaffold( - appBar: AppBar( - title: Text(S.of(context).note), - actions: [ - IconButton( - onPressed: () async => - await notifier.note(context).expectFailure(context), - icon: const Icon(Icons.send)) - ], - ), - resizeToAvoidBottomInset: true, - body: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.only(left: 5, right: 5), - child: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - if (widget.noteCreationMode != NoteCreationMode.update) - const NoteCreateSettingTop() - else - const Padding(padding: EdgeInsets.only(top: 30)), - const ChannelArea(), - const ReplyArea(), - const ReplyToArea(), - const CwTextArea(), - TextField( - controller: ref.watch(noteInputTextProvider), + return Scaffold( + appBar: AppBar( + title: Text(S.of(context).note), + actions: [ + IconButton( + onPressed: () async => await notifier.note(), + icon: const Icon(Icons.send), + ), + ], + ), + resizeToAvoidBottomInset: true, + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.only(left: 5, right: 5), + child: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + if (noteCreationMode != NoteCreationMode.update) + const NoteCreateSettingTop() + else + const Padding(padding: EdgeInsets.only(top: 30)), + const ChannelArea(), + const ReplyArea(), + const ReplyToArea(), + const CwTextArea(), + Focus( + onKeyEvent: (node, event) { + if (event is KeyDownEvent) { + if (event.logicalKey == LogicalKeyboardKey.enter && + HardwareKeyboard.instance.isControlPressed) { + unawaited(notifier.note()); + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + }, + child: TextField( + controller: controller, focusNode: focusNode, maxLines: null, minLines: 5, @@ -193,80 +189,77 @@ class NoteCreatePageState extends ConsumerState { decoration: noteDecoration, autofocus: true, ), - Row( - children: [ - if (widget.noteCreationMode != - NoteCreationMode.update) ...[ - IconButton( - onPressed: () async => - await notifier.chooseFile(context), - icon: const Icon(Icons.image)), - if (widget.noteCreationMode != - NoteCreationMode.update) - IconButton( - onPressed: () { - ref - .read(noteCreateProvider( - widget.initialAccount) - .notifier) - .toggleVote(); - }, - icon: const Icon(Icons.how_to_vote)), - ], - const CwToggleButton(), - if (widget.noteCreationMode != - NoteCreationMode.update) - IconButton( - onPressed: () => notifier.addReplyUser(context), - icon: const Icon(Icons.mail_outline)), + ), + Row( + children: [ + if (noteCreationMode != NoteCreationMode.update) ...[ IconButton( - onPressed: () async { - final selectedEmoji = - await showDialog( - context: context, - builder: (context) => - ReactionPickerDialog( - account: data.account, - isAcceptSensitive: true, - )); - if (selectedEmoji == null) return; - switch (selectedEmoji) { - case CustomEmojiData(): - ref - .read(noteInputTextProvider) - .insert(":${selectedEmoji.baseName}:"); - break; - case UnicodeEmojiData(): - ref - .read(noteInputTextProvider) - .insert(selectedEmoji.char); - break; - default: - break; - } - ref.read(noteFocusProvider).requestFocus(); + onPressed: () async => await notifier.chooseFile(), + icon: const Icon(Icons.image), + ), + if (noteCreationMode != NoteCreationMode.update) + IconButton( + onPressed: () { + ref + .read(noteCreateNotifierProvider.notifier) + .toggleVote(); }, - icon: const Icon(Icons.tag_faces)) + icon: const Icon(Icons.how_to_vote), + ), ], - ), - const MfmPreview(), - if (widget.noteCreationMode != NoteCreationMode.update) - const FilePreview() - else if (widget.note?.files.isNotEmpty == true) - Text(S.of(context).hasMediaButCannotEdit), - const RenoteArea(), - if (widget.noteCreationMode != NoteCreationMode.update) - const VoteArea() - else if (widget.note?.poll != null) - Text(S.of(context).hasVoteButCannotEdit), - ], - ), + const CwToggleButton(), + if (noteCreationMode != NoteCreationMode.update) + IconButton( + onPressed: () async => notifier.addReplyUser(), + icon: const Icon(Icons.mail_outline), + ), + IconButton( + onPressed: () async { + final selectedEmoji = + await context.pushRoute( + ReactionPickerRoute( + account: ref + .read(accountContextProvider) + .postAccount, + isAcceptSensitive: true, + ), + ); + if (selectedEmoji == null) return; + switch (selectedEmoji) { + case CustomEmojiData(): + ref + .read(noteInputTextProvider) + .insert(":${selectedEmoji.baseName}:"); + case UnicodeEmojiData(): + ref + .read(noteInputTextProvider) + .insert(selectedEmoji.char); + default: + break; + } + ref.read(noteFocusProvider).requestFocus(); + }, + icon: const Icon(Icons.tag_faces), + ), + ], + ), + const MfmPreview(), + if (noteCreationMode != NoteCreationMode.update) + const FilePreview() + else if (note?.files.isNotEmpty == true) + Text(S.of(context).hasMediaButCannotEdit), + const RenoteArea(), + if (noteCreationMode != NoteCreationMode.update) + const VoteArea() + else if (note?.poll != null) + Text(S.of(context).hasVoteButCannotEdit), + ], ), ), ), - const NoteEmoji(), - ], - ), + ), + const NoteEmoji(), + ], ), ); } diff --git a/lib/view/note_create_page/note_create_setting_top.dart b/lib/view/note_create_page/note_create_setting_top.dart index 56595adf4..87191a197 100644 --- a/lib/view/note_create_page/note_create_setting_top.dart +++ b/lib/view/note_create_page/note_create_setting_top.dart @@ -1,69 +1,75 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/avatar_icon.dart'; -import 'package:miria/view/common/misskey_notes/local_only_icon.dart'; -import 'package:miria/view/note_create_page/note_visibility_dialog.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/view/note_create_page/reaction_acceptance_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/material.dart"; +import "package:flutter_svg/flutter_svg.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/state_notifier/note_create_page/note_create_state_notifier.dart"; +import "package:miria/view/common/avatar_icon.dart"; +import "package:miria/view/common/misskey_notes/local_only_icon.dart"; +import "package:miria/view/note_create_page/note_visibility_dialog.dart"; +import "package:miria/view/note_create_page/reaction_acceptance_dialog.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class NoteCreateSettingTop extends ConsumerWidget { - const NoteCreateSettingTop({super.key}); - - IconData resolveVisibilityIcon(NoteVisibility visibility) { - switch (visibility) { - case NoteVisibility.public: - return Icons.public; - case NoteVisibility.home: - return Icons.home; - case NoteVisibility.followers: - return Icons.lock_outline; - case NoteVisibility.specified: - return Icons.mail; - } +IconData resolveVisibilityIcon(NoteVisibility visibility) { + switch (visibility) { + case NoteVisibility.public: + return Icons.public; + case NoteVisibility.home: + return Icons.home; + case NoteVisibility.followers: + return Icons.lock_outline; + case NoteVisibility.specified: + return Icons.mail; } +} + +class AcceptanceIcon extends StatelessWidget { + final ReactionAcceptance? acceptance; + const AcceptanceIcon({required this.acceptance, super.key}); - Widget resolveAcceptanceIcon( - ReactionAcceptance? acceptance, BuildContext context) { - switch (acceptance) { - case null: - return SvgPicture.asset( + @override + Widget build(BuildContext context) { + return switch (acceptance) { + null => SvgPicture.asset( "assets/images/play_shapes_FILL0_wght400_GRAD0_opsz48.svg", - color: Theme.of(context).textTheme.bodyMedium!.color, + colorFilter: ColorFilter.mode( + Theme.of(context).textTheme.bodyMedium!.color ?? + const Color(0xff5f6368), + BlendMode.srcIn, + ), width: 28, height: 28, - ); - case ReactionAcceptance.likeOnly: - return const Icon(Icons.favorite_border); - case ReactionAcceptance.likeOnlyForRemote: - return const Icon(Icons.add_reaction_outlined); - case ReactionAcceptance.nonSensitiveOnly: - return const Icon(Icons.shield_outlined); - case ReactionAcceptance.nonSensitiveOnlyForLocalLikeOnlyForRemote: - return const Icon(Icons.add_moderator_outlined); - } + ), + ReactionAcceptance.likeOnly => const Icon(Icons.favorite_border), + ReactionAcceptance.likeOnlyForRemote => + const Icon(Icons.add_reaction_outlined), + ReactionAcceptance.nonSensitiveOnly => const Icon(Icons.shield_outlined), + ReactionAcceptance.nonSensitiveOnlyForLocalLikeOnlyForRemote => + const Icon(Icons.add_moderator_outlined), + }; } +} + +class NoteCreateSettingTop extends ConsumerWidget { + const NoteCreateSettingTop({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final notifier = - ref.read(noteCreateProvider(AccountScope.of(context)).notifier); + final notifier = ref.read(noteCreateNotifierProvider.notifier); final noteVisibility = ref.watch( - noteCreateProvider(AccountScope.of(context)) - .select((value) => value.noteVisibility)); + noteCreateNotifierProvider.select((value) => value.noteVisibility), + ); final reactionAcceptance = ref.watch( - noteCreateProvider(AccountScope.of(context)) - .select((value) => value.reactionAcceptance)); - final isLocal = ref.watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.localOnly)); + noteCreateNotifierProvider.select((value) => value.reactionAcceptance), + ); + final isLocal = ref.watch( + noteCreateNotifierProvider.select((value) => value.localOnly), + ); return Row( children: [ const Padding(padding: EdgeInsets.only(left: 5)), AvatarIcon( - user: AccountScope.of(context).i, + user: ref.read(accountContextProvider).postAccount.i, height: Theme.of(context).iconButtonTheme.style?.iconSize?.resolve({}) ?? 32, @@ -71,34 +77,43 @@ class NoteCreateSettingTop extends ConsumerWidget { Expanded(child: Container()), Builder( builder: (context2) => IconButton( - onPressed: () async { - final result = await showModalBottomSheet( - context: context2, - builder: (context3) => NoteVisibilityDialog( - account: AccountScope.of(context), - )); - if (result != null) { - notifier.setNoteVisibility(result); + onPressed: () async { + final result = await showModalBottomSheet( + context: context2, + builder: (context3) => NoteVisibilityDialog( + account: ref.read(accountContextProvider).postAccount, + ), + ); + if (result != null) { + if (result == NoteVisibility.public && + !await ref + .read(noteCreateNotifierProvider.notifier) + .validateNoteVisibility(NoteVisibility.public)) { + return; } - }, - icon: Icon(resolveVisibilityIcon(noteVisibility))), + + notifier.setNoteVisibility(result); + } + }, + icon: Icon(resolveVisibilityIcon(noteVisibility)), + ), ), IconButton( + onPressed: () async => notifier.toggleLocalOnly(), + icon: isLocal ? const LocalOnlyIcon() : const Icon(Icons.rocket), + ), + Builder( + builder: (context2) => IconButton( onPressed: () async { - notifier.toggleLocalOnly(context); + final result = await showModalBottomSheet( + context: context2, + builder: (context) => const ReactionAcceptanceDialog(), + ); + notifier.setReactionAcceptance(result); }, - icon: isLocal ? const LocalOnlyIcon() : const Icon(Icons.rocket)), - Builder( - builder: (context2) => IconButton( - onPressed: () async { - final result = - await showModalBottomSheet( - context: context2, - builder: (context) => - const ReactionAcceptanceDialog()); - notifier.setReactionAcceptance(result); - }, - icon: resolveAcceptanceIcon(reactionAcceptance, context2))), + icon: AcceptanceIcon(acceptance: reactionAcceptance), + ), + ), ], ); } diff --git a/lib/view/note_create_page/note_emoji.dart b/lib/view/note_create_page/note_emoji.dart index 53d4b6fec..b26d31e73 100644 --- a/lib/view/note_create_page/note_emoji.dart +++ b/lib/view/note_create_page/note_emoji.dart @@ -1,8 +1,7 @@ -import 'package:flutter/material.dart'; -import 'package:miria/view/common/note_create/input_completation.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import 'note_create_page.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/view/common/note_create/input_completation.dart"; +import "package:miria/view/note_create_page/note_create_page.dart"; class NoteEmoji extends ConsumerWidget { const NoteEmoji({super.key}); @@ -12,10 +11,11 @@ class NoteEmoji extends ConsumerWidget { final baseHeight = MediaQuery.textScalerOf(context) .scale((Theme.of(context).textTheme.bodyMedium?.fontSize ?? 22) * 1.35); return SizedBox( - height: baseHeight + 40, - child: InputComplement( - controller: ref.read(noteInputTextProvider), - focusNode: noteFocusProvider, - )); + height: baseHeight + 40, + child: InputComplement( + controller: ref.read(noteInputTextProvider), + focusNode: noteFocusProvider, + ), + ); } } diff --git a/lib/view/note_create_page/note_visibility_dialog.dart b/lib/view/note_create_page/note_visibility_dialog.dart index b9193ee08..b747b613d 100644 --- a/lib/view/note_create_page/note_visibility_dialog.dart +++ b/lib/view/note_create_page/note_visibility_dialog.dart @@ -1,16 +1,15 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:misskey_dart/misskey_dart.dart"; class NoteVisibilityDialog extends ConsumerWidget { final Account account; const NoteVisibilityDialog({ - super.key, required this.account, + super.key, }); @override @@ -18,12 +17,9 @@ class NoteVisibilityDialog extends ConsumerWidget { return ListView( children: [ ListTile( - onTap: () { - if (ref - .read(noteCreateProvider(account).notifier) - .validateNoteVisibility(NoteVisibility.public, context)) { - Navigator.of(context).pop(NoteVisibility.public); - } + onTap: () async { + if (!context.mounted) return; + Navigator.of(context).pop(NoteVisibility.public); }, leading: const Icon(Icons.public), title: Text(S.of(context).public), diff --git a/lib/view/note_create_page/reaction_acceptance_dialog.dart b/lib/view/note_create_page/reaction_acceptance_dialog.dart index 747b5527f..edc299168 100644 --- a/lib/view/note_create_page/reaction_acceptance_dialog.dart +++ b/lib/view/note_create_page/reaction_acceptance_dialog.dart @@ -1,7 +1,7 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_svg/flutter_svg.dart"; +import "package:misskey_dart/misskey_dart.dart"; class ReactionAcceptanceDialog extends StatelessWidget { const ReactionAcceptanceDialog({super.key}); @@ -14,7 +14,11 @@ class ReactionAcceptanceDialog extends StatelessWidget { onTap: () => Navigator.of(context).pop(null), leading: SvgPicture.asset( "assets/images/play_shapes_FILL0_wght400_GRAD0_opsz48.svg", - color: Theme.of(context).textTheme.bodyMedium!.color, + colorFilter: ColorFilter.mode( + Theme.of(context).textTheme.bodyMedium!.color ?? + const Color(0xff5f6368), + BlendMode.srcIn, + ), width: 28, height: 28, ), @@ -39,11 +43,12 @@ class ReactionAcceptanceDialog extends StatelessWidget { ), ListTile( onTap: () => Navigator.of(context).pop( - ReactionAcceptance.nonSensitiveOnlyForLocalLikeOnlyForRemote), + ReactionAcceptance.nonSensitiveOnlyForLocalLikeOnlyForRemote, + ), leading: const Icon(Icons.add_moderator_outlined), title: Text(S.of(context).favoriteNonSensitiveOnlyAndLikeOnlyForRemote), - ) + ), ], ); } diff --git a/lib/view/note_create_page/renote_area.dart b/lib/view/note_create_page/renote_area.dart index ebd90adbd..d2a8f2536 100644 --- a/lib/view/note_create_page/renote_area.dart +++ b/lib/view/note_create_page/renote_area.dart @@ -1,16 +1,16 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/state_notifier/note_create_page/note_create_state_notifier.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; class RenoteArea extends ConsumerWidget { const RenoteArea({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final renote = ref.watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.renote)); + final renote = ref.watch( + noteCreateNotifierProvider.select((value) => value.renote), + ); if (renote != null) { return Column( diff --git a/lib/view/note_create_page/reply_area.dart b/lib/view/note_create_page/reply_area.dart index 45167e8ec..cd25f6a2f 100644 --- a/lib/view/note_create_page/reply_area.dart +++ b/lib/view/note_create_page/reply_area.dart @@ -1,16 +1,16 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/state_notifier/note_create_page/note_create_state_notifier.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; class ReplyArea extends ConsumerWidget { const ReplyArea({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final reply = ref.watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.reply)); + final reply = ref.watch( + noteCreateNotifierProvider.select((value) => value.reply), + ); if (reply != null) { return MediaQuery( diff --git a/lib/view/note_create_page/reply_to_area.dart b/lib/view/note_create_page/reply_to_area.dart index f5cf20c2d..65d264202 100644 --- a/lib/view/note_create_page/reply_to_area.dart +++ b/lib/view/note_create_page/reply_to_area.dart @@ -1,18 +1,18 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/avatar_icon.dart'; -import 'package:miria/view/themes/app_theme.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/state_notifier/note_create_page/note_create_state_notifier.dart"; +import "package:miria/view/common/avatar_icon.dart"; +import "package:miria/view/themes/app_theme.dart"; class ReplyToArea extends ConsumerWidget { const ReplyToArea({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final repliesTo = ref.watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.replyTo)); + final repliesTo = ref.watch( + noteCreateNotifierProvider.select((value) => value.replyTo), + ); if (repliesTo.isEmpty) { return const SizedBox.shrink(); @@ -42,12 +42,12 @@ class ReplyToArea extends ConsumerWidget { Text( "@${replyTo.username}${replyTo.host == null ? "" : "@${replyTo.host}"}", style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: AppTheme.of(context).mentionStyle.color), + color: AppTheme.of(context).mentionStyle.color, + ), ), IconButton( - onPressed: () => ref - .read( - noteCreateProvider(AccountScope.of(context)).notifier) + onPressed: () async => ref + .read(noteCreateNotifierProvider.notifier) .deleteReplyUser(replyTo), icon: Icon( Icons.remove, @@ -58,8 +58,8 @@ class ReplyToArea extends ConsumerWidget { constraints: const BoxConstraints(), padding: EdgeInsets.zero, style: const ButtonStyle( - padding: MaterialStatePropertyAll(EdgeInsets.zero), - minimumSize: MaterialStatePropertyAll(Size(0, 0)), + padding: WidgetStatePropertyAll(EdgeInsets.zero), + minimumSize: WidgetStatePropertyAll(Size(0, 0)), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), ), @@ -67,16 +67,13 @@ class ReplyToArea extends ConsumerWidget { ], ), IconButton( - onPressed: () { - ref - .read(noteCreateProvider(AccountScope.of(context)).notifier) - .addReplyUser(context); - }, + onPressed: () async => + ref.read(noteCreateNotifierProvider.notifier).addReplyUser(), constraints: const BoxConstraints(), padding: EdgeInsets.zero, style: const ButtonStyle( - padding: MaterialStatePropertyAll(EdgeInsets.zero), - minimumSize: MaterialStatePropertyAll(Size.zero), + padding: WidgetStatePropertyAll(EdgeInsets.zero), + minimumSize: WidgetStatePropertyAll(Size.zero), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), icon: Icon( diff --git a/lib/view/note_create_page/vote_area.dart b/lib/view/note_create_page/vote_area.dart index 523046972..c302dbca9 100644 --- a/lib/view/note_create_page/vote_area.dart +++ b/lib/view/note_create_page/vote_area.dart @@ -1,27 +1,21 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/extensions/date_time_extension.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/state_notifier/note_create_page/note_create_state_notifier.dart'; - -import '../common/account_scope.dart'; - -class VoteArea extends ConsumerStatefulWidget { +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/date_time_extension.dart"; +import "package:miria/state_notifier/note_create_page/note_create_state_notifier.dart"; + +class VoteArea extends ConsumerWidget { const VoteArea({super.key}); @override - ConsumerState createState() => VoteAreaState(); -} - -class VoteAreaState extends ConsumerState { - @override - Widget build(BuildContext context) { - final expireType = ref.watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.voteExpireType)); - final isVote = ref.watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.isVote)); + Widget build(BuildContext context, WidgetRef ref) { + final expireType = ref.watch( + noteCreateNotifierProvider.select((value) => value.voteExpireType), + ); + final isVote = + ref.watch(noteCreateNotifierProvider.select((value) => value.isVote)); if (!isVote) { return Container(); @@ -34,9 +28,7 @@ class VoteAreaState extends ConsumerState { const VoteContentList(), ElevatedButton( onPressed: () { - ref - .read(noteCreateProvider(AccountScope.of(context)).notifier) - .addVoteContent(); + ref.read(noteCreateNotifierProvider.notifier).addVoteContent(); }, child: Text(S.of(context).addChoice), ), @@ -49,18 +41,14 @@ class VoteAreaState extends ConsumerState { } } -class VoteContentList extends ConsumerStatefulWidget { +class VoteContentList extends ConsumerWidget { const VoteContentList({super.key}); @override - ConsumerState createState() => VoteContentListState(); -} - -class VoteContentListState extends ConsumerState { - @override - Widget build(BuildContext context) { - final contentCount = ref.watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.voteContentCount)); + Widget build(BuildContext context, WidgetRef ref) { + final contentCount = ref.watch( + noteCreateNotifierProvider.select((value) => value.voteContentCount), + ); return ListView( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), @@ -71,57 +59,39 @@ class VoteContentListState extends ConsumerState { } } -class VoteContentListItem extends ConsumerStatefulWidget { +class VoteContentListItem extends HookConsumerWidget { final int index; - const VoteContentListItem({super.key, required this.index}); - - @override - ConsumerState createState() => - VoteContentListItemState(); -} - -class VoteContentListItemState extends ConsumerState { - final controller = TextEditingController(); - @override - void initState() { - super.initState(); - - controller.addListener(() { - ref - .read(noteCreateProvider(AccountScope.of(context)).notifier) - .setVoteContent(widget.index, controller.text); - }); - } + const VoteContentListItem({required this.index, super.key}); @override - void didUpdateWidget(covariant VoteContentListItem oldWidget) { - super.didUpdateWidget(oldWidget); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - controller.text = ref - .read(noteCreateProvider(AccountScope.of(context))) - .voteContent[widget.index]; - }); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - controller.text = ref - .read(noteCreateProvider(AccountScope.of(context))) - .voteContent[widget.index]; + Widget build(BuildContext context, WidgetRef ref) { + final initial = useMemoized( + () => ref.read(noteCreateNotifierProvider).voteContent[index], + ); + final controller = useTextEditingController(text: initial); + ref.listen( + noteCreateNotifierProvider.select( + (value) => + value.voteContent.length <= index ? "" : value.voteContent[index], + ), (_, next) { + controller.text = next; }); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { + useEffect( + () { + controller.addListener(() { + if (ref.read(noteCreateNotifierProvider).voteContent.length <= + index) { + return; + } + ref + .read(noteCreateNotifierProvider.notifier) + .setVoteContent(index, controller.text); + }); + return null; + }, + [index], + ); return Padding( padding: const EdgeInsets.only(bottom: 5), child: Row( @@ -130,17 +100,18 @@ class VoteContentListItemState extends ConsumerState { child: TextField( controller: controller, decoration: InputDecoration( - hintText: S.of(context).choiceNumber(widget.index + 1), + hintText: S.of(context).choiceNumber(index + 1), ), ), ), IconButton( - onPressed: () { - ref - .read(noteCreateProvider(AccountScope.of(context)).notifier) - .deleteVoteContent(widget.index); - }, - icon: const Icon(Icons.close)), + onPressed: () { + ref + .read(noteCreateNotifierProvider.notifier) + .deleteVoteContent(index); + }, + icon: const Icon(Icons.close), + ), ], ), ); @@ -155,13 +126,13 @@ class MultipleVoteRadioButton extends ConsumerWidget { return Row( children: [ Switch( - value: ref.watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.isVoteMultiple)), - onChanged: (value) { - ref - .read(noteCreateProvider(AccountScope.of(context)).notifier) - .toggleVoteMultiple(); - }), + value: ref.watch( + noteCreateNotifierProvider.select((value) => value.isVoteMultiple), + ), + onChanged: (value) { + ref.read(noteCreateNotifierProvider.notifier).toggleVoteMultiple(); + }, + ), Expanded(child: Text(S.of(context).canMultipleChoice)), ], ); @@ -174,156 +145,140 @@ class VoteDuration extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return DropdownButton( - value: ref.watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.voteExpireType)), - items: [ - for (final item in VoteExpireType.values) - DropdownMenuItem( - value: item, - child: Text(item.displayText(context)), - ) - ], - onChanged: (item) { - if (item == null) return; - ref - .read(noteCreateProvider(AccountScope.of(context)).notifier) - .setVoteExpireType(item); - }); + value: ref.watch( + noteCreateNotifierProvider.select((value) => value.voteExpireType), + ), + items: [ + for (final item in VoteExpireType.values) + DropdownMenuItem( + value: item, + child: Text(item.displayText(context)), + ), + ], + onChanged: (item) { + if (item == null) return; + ref.read(noteCreateNotifierProvider.notifier).setVoteExpireType(item); + }, + ); } } -class VoteUntilDate extends ConsumerStatefulWidget { - const VoteUntilDate({super.key}); +class VoteUntilDate extends ConsumerWidget { + const VoteUntilDate({ + super.key, + }); @override - ConsumerState createState() => VoteUntilDateState(); -} - -class VoteUntilDateState extends ConsumerState { - @override - Widget build(BuildContext context) { - final date = ref.watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.voteDate)); + Widget build(BuildContext context, WidgetRef ref) { + final date = ref.watch( + noteCreateNotifierProvider.select((value) => value.voteDate), + ); return Padding( padding: const EdgeInsets.only(top: 10), child: GestureDetector( onTap: () async { - final account = AccountScope.of(context); final resultDate = await showDatePicker( - context: context, - initialDate: date ?? DateTime.now(), - firstDate: DateTime.now(), - lastDate: - DateTime(2999, 12, 31)); //TODO: misskeyの日付のデータピッカーどこまで行く? + context: context, + initialDate: date ?? DateTime.now(), + firstDate: DateTime.now(), + lastDate: DateTime(2999, 12, 31), + ); //TODO: misskeyの日付のデータピッカーどこまで行く? if (resultDate == null) return; - if (!mounted) return; + if (!context.mounted) return; final resultTime = await showTimePicker( - context: context, - initialTime: TimeOfDay( - hour: date?.hour ?? DateTime.now().hour, - minute: date?.minute ?? DateTime.now().minute)); + context: context, + initialTime: TimeOfDay( + hour: date?.hour ?? DateTime.now().hour, + minute: date?.minute ?? DateTime.now().minute, + ), + ); if (resultTime == null) return; - ref.read(noteCreateProvider(account).notifier).setVoteExpireDate( - DateTime(resultDate.year, resultDate.month, resultDate.day, - resultTime.hour, resultTime.minute, 0)); + ref.read(noteCreateNotifierProvider.notifier).setVoteExpireDate( + DateTime( + resultDate.year, + resultDate.month, + resultDate.day, + resultTime.hour, + resultTime.minute, + 0, + ), + ); }, child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all(color: Theme.of(context).primaryColor), - borderRadius: BorderRadius.circular(10)), - child: Padding( - padding: const EdgeInsets.all(5), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.max, - children: [ - const Icon(Icons.date_range), - const Padding(padding: EdgeInsets.only(left: 10)), - Expanded( - child: Text( - date?.formatUntilSeconds(context) ?? "", - ), - ), - ]), - )), + decoration: BoxDecoration( + border: Border.all(color: Theme.of(context).primaryColor), + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(5), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + const Icon(Icons.date_range), + const Padding(padding: EdgeInsets.only(left: 10)), + Expanded( + child: Text( + date?.formatUntilSeconds(context) ?? "", + ), + ), + ], + ), + ), + ), ), ); } } -class VoteUntilDuration extends ConsumerStatefulWidget { +class VoteUntilDuration extends HookConsumerWidget { const VoteUntilDuration({super.key}); @override - ConsumerState createState() => - VoteUntilDurationState(); -} - -class VoteUntilDurationState extends ConsumerState { - final controller = TextEditingController(); - - @override - void initState() { - super.initState(); - Future(() async { - controller.addListener(() { - final value = int.tryParse(controller.text); - if (value == null) return; - ref - .read(noteCreateProvider(AccountScope.of(context)).notifier) - .setVoteDuration(value); - }); + Widget build(BuildContext context, WidgetRef ref) { + final controller = useTextEditingController( + text: ref.read(noteCreateNotifierProvider).voteDuration?.toString() ?? "", + ); + controller.addListener(() { + final value = int.tryParse(controller.text); + if (value == null) return; + ref.read(noteCreateNotifierProvider.notifier).setVoteDuration(value); }); - } - - @override - void dispose() { - super.dispose(); - controller.dispose(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - controller.text = ref - .read(noteCreateProvider(AccountScope.of(context))) - .voteDuration - ?.toString() ?? - ""; - } - @override - Widget build(BuildContext context) { return Row( children: [ Expanded( child: TextField( - controller: controller, - decoration: const InputDecoration(prefixIcon: Icon(Icons.timer)), - keyboardType: const TextInputType.numberWithOptions(), - inputFormatters: [FilteringTextInputFormatter.digitsOnly]), + controller: controller, + decoration: const InputDecoration(prefixIcon: Icon(Icons.timer)), + keyboardType: const TextInputType.numberWithOptions(), + inputFormatters: [FilteringTextInputFormatter.digitsOnly], + ), ), const Padding( padding: EdgeInsets.only(left: 10), ), DropdownButton( - items: [ - for (final item in VoteExpireDurationType.values) - DropdownMenuItem( - value: item, - child: Text(item.displayText(context)), - ), - ], - value: ref.watch(noteCreateProvider(AccountScope.of(context)) - .select((value) => value.voteDurationType)), - onChanged: (value) { - if (value == null) return; - ref - .read(noteCreateProvider(AccountScope.of(context)).notifier) - .setVoteDurationType(value); - }), + items: [ + for (final item in VoteExpireDurationType.values) + DropdownMenuItem( + value: item, + child: Text(item.displayText(context)), + ), + ], + value: ref.watch( + noteCreateNotifierProvider + .select((value) => value.voteDurationType), + ), + onChanged: (value) { + if (value == null) return; + ref + .read(noteCreateNotifierProvider.notifier) + .setVoteDurationType(value); + }, + ), ], ); } diff --git a/lib/view/note_detail_page/note_detail_page.dart b/lib/view/note_detail_page/note_detail_page.dart index 288d45812..3768fbb4a 100644 --- a/lib/view/note_detail_page/note_detail_page.dart +++ b/lib/view/note_detail_page/note_detail_page.dart @@ -1,148 +1,158 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/extensions/date_time_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/date_time_extension.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +part "note_detail_page.g.dart"; + +@Riverpod(dependencies: [misskeyGetContext, notesWith]) +Future _notesShow(_NotesShowRef ref, String noteId) async { + final note = await ref + .read(misskeyGetContextProvider) + .notes + .show(NotesShowRequest(noteId: noteId)); + + ref.read(notesWithProvider).registerNote(note); + + return note; +} + +@Riverpod(dependencies: [misskeyGetContext, notesWith]) +Future> _conversation(_ConversationRef ref, String noteId) async { + final conversationResult = await ref + .read(misskeyGetContextProvider) + .notes + .conversation(NotesConversationRequest(noteId: noteId)); + ref.read(notesWithProvider).registerAll(conversationResult); + ref + .read(notesWithProvider) + .registerAll(conversationResult.map((e) => e.reply).whereNotNull()); + + return [ + ...[...conversationResult].reversed, + ]; +} @RoutePage() -class NoteDetailPage extends ConsumerStatefulWidget { +class NoteDetailPage extends ConsumerWidget implements AutoRouteWrapper { final Note note; - final Account account; + final AccountContext accountContext; const NoteDetailPage({ - super.key, required this.note, - required this.account, + required this.accountContext, + super.key, }); @override - ConsumerState createState() => NoteDetailPageState(); -} - -class NoteDetailPageState extends ConsumerState { - List conversations = []; - Note? actualShow; - - bool isLoading = true; + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); @override - void didChangeDependencies() { - super.didChangeDependencies(); + Widget build(BuildContext context, WidgetRef ref) { + final notesShow = ref.watch(_notesShowProvider(note.id)); + final conversation = ref.watch(_conversationProvider(note.id)); - Future(() async { - actualShow = await ref - .read(misskeyProvider(widget.account)) - .notes - .show(NotesShowRequest(noteId: widget.note.id)); - ref.read(notesProvider(widget.account)).registerNote(actualShow!); - final conversationResult = await ref - .read(misskeyProvider(widget.account)) - .notes - .conversation(NotesConversationRequest(noteId: widget.note.id)); - ref.read(notesProvider(widget.account)).registerAll(conversationResult); - ref - .read(notesProvider(widget.account)) - .registerAll(conversationResult.map((e) => e.reply).whereNotNull()); - conversations - ..clear() - ..addAll(conversationResult.toList().reversed); - if (!mounted) return; - setState(() { - isLoading = false; - }); - }); - } - - @override - Widget build(BuildContext context) { - return AccountScope( - account: widget.account, - child: Scaffold( - appBar: AppBar(title: Text(S.of(context).note)), - body: Padding( - padding: const EdgeInsets.only(right: 10, top: 10, bottom: 10), - child: isLoading - ? const Center(child: CircularProgressIndicator()) - : SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - ListView.builder( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - itemCount: conversations.length, - itemBuilder: (context, index) { - return MisskeyNote( - note: conversations[index], - isForceUnvisibleRenote: true, - isForceUnvisibleReply: true, - ); - }), - MisskeyNote( - note: actualShow!, - recursive: 1, - isForceUnvisibleReply: true, - isDisplayBorder: false, - isForceVisibleLong: true, - ), - const Padding(padding: EdgeInsets.only(top: 5)), - Text( - S.of(context).noteCreatedAt( - actualShow!.createdAt - .formatUntilMilliSeconds(context), - ), + return Scaffold( + appBar: AppBar(title: Text(S.of(context).note)), + body: Padding( + padding: const EdgeInsets.only(right: 10, top: 10, bottom: 10), + child: switch (notesShow) { + AsyncLoading() => const Center( + child: CircularProgressIndicator.adaptive(), + ), + AsyncError(:final error, :final stackTrace) => + ErrorDetail(error: error, stackTrace: stackTrace), + AsyncData(:final value) => SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + switch (conversation) { + AsyncLoading() => const SizedBox.square( + dimension: 100, + child: CircularProgressIndicator.adaptive(), ), - const Padding(padding: EdgeInsets.only(top: 5)), - const Divider(), - const Padding(padding: EdgeInsets.only(top: 5)), - Padding( - padding: const EdgeInsets.only(left: 20), - child: PushableListView( - physics: const NeverScrollableScrollPhysics(), - shrinkWrap: true, - initializeFuture: () async { - final repliesResult = await ref - .read(misskeyProvider(widget.account)) - .notes - .children(NotesChildrenRequest( - noteId: widget.note.id)); - ref - .read(notesProvider(widget.account)) - .registerAll(repliesResult); - return repliesResult.toList(); - }, - nextFuture: (lastItem, _) async { - final repliesResult = await ref - .read(misskeyProvider(widget.account)) - .notes - .children(NotesChildrenRequest( - noteId: widget.note.id, - untilId: lastItem.id)); - ref - .read(notesProvider(widget.account)) - .registerAll(repliesResult); - return repliesResult.toList(); - }, - itemBuilder: (context, item) { - return MisskeyNote( - note: item, - recursive: 1, - isForceUnvisibleRenote: true, - isForceUnvisibleReply: true, - ); - }), + AsyncError(:final error, :final stackTrace) => + ErrorDetail(error: error, stackTrace: stackTrace), + AsyncData(:final value) => ListView.builder( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + itemCount: value.length, + itemBuilder: (context, index) { + return MisskeyNote( + note: value[index], + isForceUnvisibleRenote: true, + isForceUnvisibleReply: true, + ); + }, ), - ], + }, + MisskeyNote( + note: value, + recursive: 1, + isForceUnvisibleReply: true, + isDisplayBorder: false, + isForceVisibleLong: true, + ), + const Padding(padding: EdgeInsets.only(top: 5)), + Text( + S.of(context).noteCreatedAt( + value.createdAt.formatUntilMilliSeconds(context), + ), + ), + const Padding(padding: EdgeInsets.only(top: 5)), + const Divider(), + const Padding(padding: EdgeInsets.only(top: 5)), + Padding( + padding: const EdgeInsets.only(left: 20), + child: PushableListView( + physics: const NeverScrollableScrollPhysics(), + shrinkWrap: true, + hideIsEmpty: true, + initializeFuture: () async { + final repliesResult = await ref + .read(misskeyGetContextProvider) + .notes + .children(NotesChildrenRequest(noteId: note.id)); + ref.read(notesWithProvider).registerAll(repliesResult); + return repliesResult.toList(); + }, + nextFuture: (lastItem, _) async { + final repliesResult = await ref + .read(misskeyGetContextProvider) + .notes + .children( + NotesChildrenRequest( + noteId: note.id, + untilId: lastItem.id, + ), + ); + ref.read(notesWithProvider).registerAll(repliesResult); + return repliesResult.toList(); + }, + itemBuilder: (context, item) { + return MisskeyNote( + note: item, + recursive: 1, + isForceUnvisibleRenote: true, + isForceUnvisibleReply: true, + ); + }, + ), ), - ), - ), + ], + ), + ) + }, ), ); } diff --git a/lib/view/note_detail_page/note_detail_page.g.dart b/lib/view/note_detail_page/note_detail_page.g.dart new file mode 100644 index 000000000..16e111150 --- /dev/null +++ b/lib/view/note_detail_page/note_detail_page.g.dart @@ -0,0 +1,387 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'note_detail_page.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$notesShowHash() => r'63ae72993a53ae11435a15f7dea995e715e75d03'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [_notesShow]. +@ProviderFor(_notesShow) +const _notesShowProvider = _NotesShowFamily(); + +/// See also [_notesShow]. +class _NotesShowFamily extends Family { + /// See also [_notesShow]. + const _NotesShowFamily(); + + static final Iterable _dependencies = [ + misskeyGetContextProvider, + notesWithProvider + ]; + + static final Iterable _allTransitiveDependencies = + { + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies, + notesWithProvider, + ...?notesWithProvider.allTransitiveDependencies + }; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'_notesShowProvider'; + + /// See also [_notesShow]. + _NotesShowProvider call( + String noteId, + ) { + return _NotesShowProvider( + noteId, + ); + } + + @visibleForOverriding + @override + _NotesShowProvider getProviderOverride( + covariant _NotesShowProvider provider, + ) { + return call( + provider.noteId, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(FutureOr Function(_NotesShowRef ref) create) { + return _$NotesShowFamilyOverride(this, create); + } +} + +class _$NotesShowFamilyOverride implements FamilyOverride { + _$NotesShowFamilyOverride(this.overriddenFamily, this.create); + + final FutureOr Function(_NotesShowRef ref) create; + + @override + final _NotesShowFamily overriddenFamily; + + @override + _NotesShowProvider getProviderOverride( + covariant _NotesShowProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [_notesShow]. +class _NotesShowProvider extends AutoDisposeFutureProvider { + /// See also [_notesShow]. + _NotesShowProvider( + String noteId, + ) : this._internal( + (ref) => _notesShow( + ref as _NotesShowRef, + noteId, + ), + from: _notesShowProvider, + name: r'_notesShowProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$notesShowHash, + dependencies: _NotesShowFamily._dependencies, + allTransitiveDependencies: + _NotesShowFamily._allTransitiveDependencies, + noteId: noteId, + ); + + _NotesShowProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.noteId, + }) : super.internal(); + + final String noteId; + + @override + Override overrideWith( + FutureOr Function(_NotesShowRef ref) create, + ) { + return ProviderOverride( + origin: this, + override: _NotesShowProvider._internal( + (ref) => create(ref as _NotesShowRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + noteId: noteId, + ), + ); + } + + @override + (String,) get argument { + return (noteId,); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _NotesShowProviderElement(this); + } + + _NotesShowProvider _copyWith( + FutureOr Function(_NotesShowRef ref) create, + ) { + return _NotesShowProvider._internal( + (ref) => create(ref as _NotesShowRef), + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + noteId: noteId, + ); + } + + @override + bool operator ==(Object other) { + return other is _NotesShowProvider && other.noteId == noteId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, noteId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin _NotesShowRef on AutoDisposeFutureProviderRef { + /// The parameter `noteId` of this provider. + String get noteId; +} + +class _NotesShowProviderElement extends AutoDisposeFutureProviderElement + with _NotesShowRef { + _NotesShowProviderElement(super.provider); + + @override + String get noteId => (origin as _NotesShowProvider).noteId; +} + +String _$conversationHash() => r'6135b7e553bb745dd69625bc4aaa17efbf72aec5'; + +/// See also [_conversation]. +@ProviderFor(_conversation) +const _conversationProvider = _ConversationFamily(); + +/// See also [_conversation]. +class _ConversationFamily extends Family { + /// See also [_conversation]. + const _ConversationFamily(); + + static final Iterable _dependencies = [ + misskeyGetContextProvider, + notesWithProvider + ]; + + static final Iterable _allTransitiveDependencies = + { + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies, + notesWithProvider, + ...?notesWithProvider.allTransitiveDependencies + }; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'_conversationProvider'; + + /// See also [_conversation]. + _ConversationProvider call( + String noteId, + ) { + return _ConversationProvider( + noteId, + ); + } + + @visibleForOverriding + @override + _ConversationProvider getProviderOverride( + covariant _ConversationProvider provider, + ) { + return call( + provider.noteId, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith( + FutureOr> Function(_ConversationRef ref) create) { + return _$ConversationFamilyOverride(this, create); + } +} + +class _$ConversationFamilyOverride implements FamilyOverride { + _$ConversationFamilyOverride(this.overriddenFamily, this.create); + + final FutureOr> Function(_ConversationRef ref) create; + + @override + final _ConversationFamily overriddenFamily; + + @override + _ConversationProvider getProviderOverride( + covariant _ConversationProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [_conversation]. +class _ConversationProvider extends AutoDisposeFutureProvider> { + /// See also [_conversation]. + _ConversationProvider( + String noteId, + ) : this._internal( + (ref) => _conversation( + ref as _ConversationRef, + noteId, + ), + from: _conversationProvider, + name: r'_conversationProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$conversationHash, + dependencies: _ConversationFamily._dependencies, + allTransitiveDependencies: + _ConversationFamily._allTransitiveDependencies, + noteId: noteId, + ); + + _ConversationProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.noteId, + }) : super.internal(); + + final String noteId; + + @override + Override overrideWith( + FutureOr> Function(_ConversationRef ref) create, + ) { + return ProviderOverride( + origin: this, + override: _ConversationProvider._internal( + (ref) => create(ref as _ConversationRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + noteId: noteId, + ), + ); + } + + @override + (String,) get argument { + return (noteId,); + } + + @override + AutoDisposeFutureProviderElement> createElement() { + return _ConversationProviderElement(this); + } + + _ConversationProvider _copyWith( + FutureOr> Function(_ConversationRef ref) create, + ) { + return _ConversationProvider._internal( + (ref) => create(ref as _ConversationRef), + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + noteId: noteId, + ); + } + + @override + bool operator ==(Object other) { + return other is _ConversationProvider && other.noteId == noteId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, noteId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin _ConversationRef on AutoDisposeFutureProviderRef> { + /// The parameter `noteId` of this provider. + String get noteId; +} + +class _ConversationProviderElement + extends AutoDisposeFutureProviderElement> with _ConversationRef { + _ConversationProviderElement(super.provider); + + @override + String get noteId => (origin as _ConversationProvider).noteId; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/note_modal_sheet/note_modal_sheet.dart b/lib/view/note_modal_sheet/note_modal_sheet.dart new file mode 100644 index 000000000..2550b9f53 --- /dev/null +++ b/lib/view/note_modal_sheet/note_modal_sheet.dart @@ -0,0 +1,509 @@ +import "dart:async"; +import "dart:io"; +import "dart:ui"; + +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter/rendering.dart"; +import "package:flutter/services.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/account_repository.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/state_notifier/common/misskey_notes/misskey_note_notifier.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:miria/view/copy_modal_sheet/copy_note_modal_sheet.dart"; +import "package:miria/view/note_create_page/note_create_page.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:path/path.dart"; +import "package:path_provider/path_provider.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; +import "package:share_plus/share_plus.dart"; +import "package:url_launcher/url_launcher.dart"; +import "package:url_launcher/url_launcher_string.dart"; + +part "note_modal_sheet.freezed.dart"; +part "note_modal_sheet.g.dart"; + +@freezed +class NoteModalSheetState with _$NoteModalSheetState { + factory NoteModalSheetState({ + AsyncValue? noteState, + @Default(false) bool isSharingMode, + AsyncValue? user, + AsyncValue? delete, + AsyncValue? deleteRecreate, + AsyncValue? favorite, + }) = _NoteModalSheetState; + + const NoteModalSheetState._(); + + bool get isLoading => + user is AsyncLoading || + delete is AsyncLoading || + deleteRecreate is AsyncLoading || + favorite is AsyncLoading; +} + +@Riverpod( + keepAlive: false, + dependencies: [ + misskeyPostContext, + misskeyGetContext, + accountContext, + notesWith, + ], +) +class NoteModalSheetNotifier extends _$NoteModalSheetNotifier { + @override + NoteModalSheetState build(Note note) { + if (ref.read(accountContextProvider).isSame) { + state = NoteModalSheetState(noteState: const AsyncLoading()); + unawaited(_status()); + return state; + } else { + return NoteModalSheetState(); + } + } + + Future _status() async { + state = state.copyWith( + noteState: await ref.read(dialogStateNotifierProvider.notifier).guard( + () async => ref + .read(misskeyPostContextProvider) + .notes + .state(NotesStateRequest(noteId: note.id)), + ), + ); + } + + Future user() async { + state = state.copyWith(user: const AsyncLoading()); + state = state.copyWith( + user: await ref.read(dialogStateNotifierProvider.notifier).guard( + () async => await ref.read(misskeyGetContextProvider).users.show( + UsersShowRequest(userId: note.userId), + ), + ), + ); + } + + Future favorite() async { + final isFavorited = state.noteState?.valueOrNull?.isFavorited; + if (isFavorited == null) return; + state = state.copyWith(favorite: const AsyncLoading()); + state = state.copyWith( + favorite: + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + if (isFavorited) { + await ref.read(misskeyPostContextProvider).notes.favorites.delete( + NotesFavoritesDeleteRequest(noteId: note.id), + ); + } else { + await ref.read(misskeyPostContextProvider).notes.favorites.create( + NotesFavoritesCreateRequest(noteId: note.id), + ); + } + }), + ); + } + + Future copyAsImage( + RenderBox box, + RenderRepaintBoundary boundary, + double devicePixelRatio, + ) async { + final image = await boundary.toImage(pixelRatio: devicePixelRatio); + final byteData = await image.toByteData(format: ImageByteFormat.png); + state = state.copyWith(isSharingMode: false); + final host = ref.read(accountContextProvider).postAccount.host; + + final path = + "${(await getApplicationDocumentsDirectory()).path}${separator}share.png"; + final file = File(path); + await file.writeAsBytes( + byteData!.buffer.asUint8List( + byteData.offsetInBytes, + byteData.lengthInBytes, + ), + ); + + final xFile = XFile(path, mimeType: "image/png"); + await Share.shareXFiles( + [xFile], + text: "https://$host/notes/${note.id}", + sharePositionOrigin: box.localToGlobal(Offset.zero) & box.size, + ); + } + + Future unRenote() async { + state = state.copyWith(delete: const AsyncLoading()); + state = state.copyWith( + delete: await ref.read(dialogStateNotifierProvider.notifier).guard( + () async => await ref + .read(misskeyPostContextProvider) + .notes + .delete(NotesDeleteRequest(noteId: note.id)), + ), + ); + } + + Future delete() async { + final confirm = + await ref.read(dialogStateNotifierProvider.notifier).showDialog( + message: (context) => S.of(context).confirmDelete, + actions: (context) => [ + S.of(context).doDeleting, + S.of(context).cancel, + ], + ); + if (confirm != 0) return; + state = state.copyWith(delete: const AsyncLoading()); + state = state.copyWith( + delete: await ref.read(dialogStateNotifierProvider.notifier).guard( + () async { + await ref + .read(misskeyPostContextProvider) + .notes + .delete(NotesDeleteRequest(noteId: note.id)); + ref.read(notesWithProvider).delete(note.id); + }, + ), + ); + } + + Future deleteRecreate() async { + final confirm = + await ref.read(dialogStateNotifierProvider.notifier).showDialog( + message: (context) => S.of(context).confirmDeletedRecreate, + actions: (context) => [ + S.of(context).doDeleting, + S.of(context).cancel, + ], + ); + if (confirm != 0) return false; + state = state.copyWith(deleteRecreate: const AsyncLoading()); + state = state.copyWith( + deleteRecreate: + await ref.read(dialogStateNotifierProvider.notifier).guard( + () async { + await ref + .read(misskeyPostContextProvider) + .notes + .delete(NotesDeleteRequest(noteId: note.id)); + ref.read(notesWithProvider).delete(note.id); + }, + ), + ); + return true; + } +} + +@RoutePage() +class NoteModalSheet extends ConsumerWidget implements AutoRouteWrapper { + final Note baseNote; + final Note targetNote; + final AccountContext accountContext; + final GlobalKey noteBoundaryKey; + + const NoteModalSheet({ + required this.baseNote, + required this.targetNote, + required this.accountContext, + required this.noteBoundaryKey, + super.key, + }); + + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final accounts = ref.watch(accountRepositoryProvider); + final notifierProvider = noteModalSheetNotifierProvider(targetNote); + + ref.listen(notifierProvider.select((value) => value.user), (_, next) async { + switch (next) { + case AsyncData(:final value): + await context.pushRoute( + UserControlRoute( + account: accountContext.postAccount, + response: value, + ), + ); + case null: + case AsyncLoading(): + case AsyncError(): + } + }); + final noteStatus = + ref.watch(notifierProvider.select((value) => value.noteState)); + + if (ref.read(notifierProvider).isLoading) { + return const Center( + child: SizedBox( + width: 100, + height: 100, + child: CircularProgressIndicator.adaptive(), + ), + ); + } + + return ListView( + children: [ + ListTile( + leading: const Icon(Icons.info_outline), + title: Text(S.of(context).detail), + onTap: () async => context.pushRoute( + NoteDetailRoute( + note: targetNote, + accountContext: accountContext, + ), + ), + ), + ListTile( + leading: const Icon(Icons.copy), + title: Text(S.of(context).copyContents), + onTap: () async { + await Clipboard.setData(ClipboardData(text: targetNote.text ?? "")); + if (!context.mounted) return; + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).doneCopy), + duration: const Duration(seconds: 1), + ), + ); + }, + trailing: IconButton( + onPressed: () async { + Navigator.of(context).pop(); + await showModalBottomSheet( + context: context, + builder: (context) => CopyNoteModalSheet( + note: targetNote.text ?? "", + ), + ); + }, + icon: const Icon(Icons.edit_note), + tooltip: S.of(context).detail, + ), + ), + ListTile( + leading: const Icon(Icons.link), + title: Text(S.of(context).copyLinks), + onTap: () async { + await Clipboard.setData( + ClipboardData( + text: + "https://${accountContext.getAccount.host}/notes/${targetNote.id}", + ), + ); + if (!context.mounted) return; + Navigator.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).doneCopy), + duration: const Duration(seconds: 1), + ), + ); + }, + ), + ListTile( + leading: const Icon(Icons.person), + title: Text(S.of(context).user), + trailing: const Icon(Icons.keyboard_arrow_right), + onTap: () async => ref.read(notifierProvider.notifier).user(), + ), + ListTile( + leading: const Icon(Icons.open_in_browser), + title: Text(S.of(context).openBrowsers), + onTap: () async { + await launchUrlString( + "https://${accountContext.getAccount.host}/notes/${targetNote.id}", + mode: LaunchMode.externalApplication, + ); + + if (!context.mounted) return; + Navigator.of(context).pop(); + }, + ), + if (targetNote.user.host != null) + ListTile( + leading: const Icon(Icons.rocket_launch), + title: Text(S.of(context).openBrowsersAsRemote), + onTap: () async { + final uri = targetNote.url ?? targetNote.uri; + if (uri == null) return; + launchUrl(uri, mode: LaunchMode.externalApplication); + if (!context.mounted) return; + Navigator.of(context).pop(); + }, + ), + // ノートが連合なしのときは現在のアカウントと同じサーバーのアカウントが複数ある場合のみ表示する + if (!targetNote.localOnly || + accounts + .where((e) => e.host == accountContext.postAccount.host) + .length > + 1) + ListTile( + leading: const Icon(Icons.open_in_new), + title: Text(S.of(context).openInAnotherAccount), + onTap: () async => ref + .read(misskeyNoteNotifierProvider.notifier) + .openNoteInOtherAccount(targetNote), + ), + ListTile( + leading: const Icon(Icons.share), + title: Text(S.of(context).shareNotes), + onTap: () { + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + Future(() async { + final box = context.findRenderObject() as RenderBox?; + if (box == null) return; + final boundary = noteBoundaryKey.currentContext! + .findRenderObject()! as RenderRepaintBoundary; + await ref.read(notifierProvider.notifier).copyAsImage( + box, + boundary, + View.of(context).devicePixelRatio, + ); + }); + }); + }, + ), + if (accountContext.isSame) + switch (noteStatus) { + null => const SizedBox.shrink(), + AsyncLoading() => const Center( + child: CircularProgressIndicator.adaptive(), + ), + AsyncError() => Text(S.of(context).thrownError), + AsyncData(:final value) => ListTile( + leading: const Icon(Icons.star_rounded), + onTap: () async { + await ref.read(notifierProvider.notifier).favorite(); + if (!context.mounted) return; + Navigator.of(context).pop(); + }, + title: Text( + value.isFavorited + ? S.of(context).deleteFavorite + : S.of(context).favorite, + ), + ) + }, + if (accountContext.isSame) + ListTile( + leading: const Icon(Icons.attach_file), + title: Text(S.of(context).clip), + onTap: () async { + Navigator.of(context).pop(); + await context.pushRoute( + ClipModalRoute( + account: accountContext.postAccount, + noteId: targetNote.id, + ), + ); + }, + ), + ListTile( + leading: const Icon(Icons.repeat_rounded), + title: Text(S.of(context).notesAfterRenote), + onTap: () async => context.pushRoute( + NotesAfterRenoteRoute( + note: targetNote, + accountContext: accountContext, + ), + ), + ), + if (accountContext.isSame && + baseNote.user.host == null && + baseNote.user.username == accountContext.postAccount.userId && + !(baseNote.text == null && + baseNote.cw == null && + baseNote.renote != null && + baseNote.poll == null && + baseNote.files.isEmpty)) ...[ + if (accountContext.postAccount.i.policies.canEditNote) + ListTile( + leading: const Icon(Icons.edit), + title: Text(S.of(context).edit), + onTap: () async { + Navigator.of(context).pop(); + await context.pushRoute( + NoteCreateRoute( + initialAccount: accountContext.postAccount, + note: targetNote, + noteCreationMode: NoteCreationMode.update, + ), + ); + }, + ), + ListTile( + leading: const Icon(Icons.delete), + title: Text(S.of(context).delete), + onTap: () async { + await ref.read(notifierProvider.notifier).delete(); + if (!context.mounted) return; + Navigator.of(context).pop(); + }, + ), + ListTile( + leading: const Icon(Icons.edit_outlined), + title: Text(S.of(context).deletedRecreate), + onTap: () async { + final result = + await ref.read(notifierProvider.notifier).deleteRecreate(); + if (!result || !context.mounted) return; + Navigator.of(context).pop(); + await context.pushRoute( + NoteCreateRoute( + initialAccount: accountContext.postAccount, + noteCreationMode: NoteCreationMode.recreate, + note: targetNote, + ), + ); + }, + ), + ], + if (accountContext.isSame && + baseNote.user.host == null && + baseNote.user.username == accountContext.postAccount.userId && + baseNote.renote != null && + baseNote.text == null && + baseNote.cw == null && + baseNote.files.isEmpty && + baseNote.poll == null) ...[ + ListTile( + leading: const Icon(Icons.delete), + title: Text(S.of(context).deleteRenote), + onTap: () async => ref.read(notifierProvider.notifier).unRenote(), + ), + ], + if (accountContext.isSame && baseNote.user.host != null || + (baseNote.user.host == null && + baseNote.user.username != accountContext.postAccount.userId)) + ListTile( + leading: const Icon(Icons.report), + title: Text(S.of(context).reportAbuse), + onTap: () async { + // Navigator.of(context).pop(); + await context.pushRoute( + AbuseRoute( + account: accountContext.postAccount, + targetUser: targetNote.user, + defaultText: + "Note:\nhttps://${accountContext.postAccount.host}/notes/${targetNote.id}\n-----", + ), + ); + }, + ), + ], + ); + } +} diff --git a/lib/view/note_modal_sheet/note_modal_sheet.freezed.dart b/lib/view/note_modal_sheet/note_modal_sheet.freezed.dart new file mode 100644 index 000000000..29199c1e4 --- /dev/null +++ b/lib/view/note_modal_sheet/note_modal_sheet.freezed.dart @@ -0,0 +1,257 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'note_modal_sheet.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$NoteModalSheetState { + AsyncValue? get noteState => + throw _privateConstructorUsedError; + bool get isSharingMode => throw _privateConstructorUsedError; + AsyncValue? get user => throw _privateConstructorUsedError; + AsyncValue? get delete => throw _privateConstructorUsedError; + AsyncValue? get deleteRecreate => throw _privateConstructorUsedError; + AsyncValue? get favorite => throw _privateConstructorUsedError; + + /// Create a copy of NoteModalSheetState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $NoteModalSheetStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $NoteModalSheetStateCopyWith<$Res> { + factory $NoteModalSheetStateCopyWith( + NoteModalSheetState value, $Res Function(NoteModalSheetState) then) = + _$NoteModalSheetStateCopyWithImpl<$Res, NoteModalSheetState>; + @useResult + $Res call( + {AsyncValue? noteState, + bool isSharingMode, + AsyncValue? user, + AsyncValue? delete, + AsyncValue? deleteRecreate, + AsyncValue? favorite}); +} + +/// @nodoc +class _$NoteModalSheetStateCopyWithImpl<$Res, $Val extends NoteModalSheetState> + implements $NoteModalSheetStateCopyWith<$Res> { + _$NoteModalSheetStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of NoteModalSheetState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? noteState = freezed, + Object? isSharingMode = null, + Object? user = freezed, + Object? delete = freezed, + Object? deleteRecreate = freezed, + Object? favorite = freezed, + }) { + return _then(_value.copyWith( + noteState: freezed == noteState + ? _value.noteState + : noteState // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + isSharingMode: null == isSharingMode + ? _value.isSharingMode + : isSharingMode // ignore: cast_nullable_to_non_nullable + as bool, + user: freezed == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + delete: freezed == delete + ? _value.delete + : delete // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + deleteRecreate: freezed == deleteRecreate + ? _value.deleteRecreate + : deleteRecreate // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + favorite: freezed == favorite + ? _value.favorite + : favorite // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$NoteModalSheetStateImplCopyWith<$Res> + implements $NoteModalSheetStateCopyWith<$Res> { + factory _$$NoteModalSheetStateImplCopyWith(_$NoteModalSheetStateImpl value, + $Res Function(_$NoteModalSheetStateImpl) then) = + __$$NoteModalSheetStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {AsyncValue? noteState, + bool isSharingMode, + AsyncValue? user, + AsyncValue? delete, + AsyncValue? deleteRecreate, + AsyncValue? favorite}); +} + +/// @nodoc +class __$$NoteModalSheetStateImplCopyWithImpl<$Res> + extends _$NoteModalSheetStateCopyWithImpl<$Res, _$NoteModalSheetStateImpl> + implements _$$NoteModalSheetStateImplCopyWith<$Res> { + __$$NoteModalSheetStateImplCopyWithImpl(_$NoteModalSheetStateImpl _value, + $Res Function(_$NoteModalSheetStateImpl) _then) + : super(_value, _then); + + /// Create a copy of NoteModalSheetState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? noteState = freezed, + Object? isSharingMode = null, + Object? user = freezed, + Object? delete = freezed, + Object? deleteRecreate = freezed, + Object? favorite = freezed, + }) { + return _then(_$NoteModalSheetStateImpl( + noteState: freezed == noteState + ? _value.noteState + : noteState // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + isSharingMode: null == isSharingMode + ? _value.isSharingMode + : isSharingMode // ignore: cast_nullable_to_non_nullable + as bool, + user: freezed == user + ? _value.user + : user // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + delete: freezed == delete + ? _value.delete + : delete // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + deleteRecreate: freezed == deleteRecreate + ? _value.deleteRecreate + : deleteRecreate // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + favorite: freezed == favorite + ? _value.favorite + : favorite // ignore: cast_nullable_to_non_nullable + as AsyncValue?, + )); + } +} + +/// @nodoc + +class _$NoteModalSheetStateImpl extends _NoteModalSheetState { + _$NoteModalSheetStateImpl( + {this.noteState, + this.isSharingMode = false, + this.user, + this.delete, + this.deleteRecreate, + this.favorite}) + : super._(); + + @override + final AsyncValue? noteState; + @override + @JsonKey() + final bool isSharingMode; + @override + final AsyncValue? user; + @override + final AsyncValue? delete; + @override + final AsyncValue? deleteRecreate; + @override + final AsyncValue? favorite; + + @override + String toString() { + return 'NoteModalSheetState(noteState: $noteState, isSharingMode: $isSharingMode, user: $user, delete: $delete, deleteRecreate: $deleteRecreate, favorite: $favorite)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$NoteModalSheetStateImpl && + (identical(other.noteState, noteState) || + other.noteState == noteState) && + (identical(other.isSharingMode, isSharingMode) || + other.isSharingMode == isSharingMode) && + (identical(other.user, user) || other.user == user) && + (identical(other.delete, delete) || other.delete == delete) && + (identical(other.deleteRecreate, deleteRecreate) || + other.deleteRecreate == deleteRecreate) && + (identical(other.favorite, favorite) || + other.favorite == favorite)); + } + + @override + int get hashCode => Object.hash(runtimeType, noteState, isSharingMode, user, + delete, deleteRecreate, favorite); + + /// Create a copy of NoteModalSheetState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$NoteModalSheetStateImplCopyWith<_$NoteModalSheetStateImpl> get copyWith => + __$$NoteModalSheetStateImplCopyWithImpl<_$NoteModalSheetStateImpl>( + this, _$identity); +} + +abstract class _NoteModalSheetState extends NoteModalSheetState { + factory _NoteModalSheetState( + {final AsyncValue? noteState, + final bool isSharingMode, + final AsyncValue? user, + final AsyncValue? delete, + final AsyncValue? deleteRecreate, + final AsyncValue? favorite}) = _$NoteModalSheetStateImpl; + _NoteModalSheetState._() : super._(); + + @override + AsyncValue? get noteState; + @override + bool get isSharingMode; + @override + AsyncValue? get user; + @override + AsyncValue? get delete; + @override + AsyncValue? get deleteRecreate; + @override + AsyncValue? get favorite; + + /// Create a copy of NoteModalSheetState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$NoteModalSheetStateImplCopyWith<_$NoteModalSheetStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/view/note_modal_sheet/note_modal_sheet.g.dart b/lib/view/note_modal_sheet/note_modal_sheet.g.dart new file mode 100644 index 000000000..39a63ab2b --- /dev/null +++ b/lib/view/note_modal_sheet/note_modal_sheet.g.dart @@ -0,0 +1,232 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'note_modal_sheet.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$noteModalSheetNotifierHash() => + r'6e16e66d1e8ce6671ac6ba8bd9bb208c9b7b6652'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +abstract class _$NoteModalSheetNotifier + extends BuildlessAutoDisposeNotifier { + late final Note note; + + NoteModalSheetState build( + Note note, + ); +} + +/// See also [NoteModalSheetNotifier]. +@ProviderFor(NoteModalSheetNotifier) +const noteModalSheetNotifierProvider = NoteModalSheetNotifierFamily(); + +/// See also [NoteModalSheetNotifier]. +class NoteModalSheetNotifierFamily extends Family { + /// See also [NoteModalSheetNotifier]. + const NoteModalSheetNotifierFamily(); + + static final Iterable _dependencies = { + misskeyPostContextProvider, + misskeyGetContextProvider, + accountContextProvider, + notesWithProvider + }; + + static final Iterable _allTransitiveDependencies = + { + misskeyPostContextProvider, + ...?misskeyPostContextProvider.allTransitiveDependencies, + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies, + accountContextProvider, + ...?accountContextProvider.allTransitiveDependencies, + notesWithProvider, + ...?notesWithProvider.allTransitiveDependencies + }; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'noteModalSheetNotifierProvider'; + + /// See also [NoteModalSheetNotifier]. + NoteModalSheetNotifierProvider call( + Note note, + ) { + return NoteModalSheetNotifierProvider( + note, + ); + } + + @visibleForOverriding + @override + NoteModalSheetNotifierProvider getProviderOverride( + covariant NoteModalSheetNotifierProvider provider, + ) { + return call( + provider.note, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(NoteModalSheetNotifier Function() create) { + return _$NoteModalSheetNotifierFamilyOverride(this, create); + } +} + +class _$NoteModalSheetNotifierFamilyOverride implements FamilyOverride { + _$NoteModalSheetNotifierFamilyOverride(this.overriddenFamily, this.create); + + final NoteModalSheetNotifier Function() create; + + @override + final NoteModalSheetNotifierFamily overriddenFamily; + + @override + NoteModalSheetNotifierProvider getProviderOverride( + covariant NoteModalSheetNotifierProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [NoteModalSheetNotifier]. +class NoteModalSheetNotifierProvider extends AutoDisposeNotifierProviderImpl< + NoteModalSheetNotifier, NoteModalSheetState> { + /// See also [NoteModalSheetNotifier]. + NoteModalSheetNotifierProvider( + Note note, + ) : this._internal( + () => NoteModalSheetNotifier()..note = note, + from: noteModalSheetNotifierProvider, + name: r'noteModalSheetNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$noteModalSheetNotifierHash, + dependencies: NoteModalSheetNotifierFamily._dependencies, + allTransitiveDependencies: + NoteModalSheetNotifierFamily._allTransitiveDependencies, + note: note, + ); + + NoteModalSheetNotifierProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.note, + }) : super.internal(); + + final Note note; + + @override + NoteModalSheetState runNotifierBuild( + covariant NoteModalSheetNotifier notifier, + ) { + return notifier.build( + note, + ); + } + + @override + Override overrideWith(NoteModalSheetNotifier Function() create) { + return ProviderOverride( + origin: this, + override: NoteModalSheetNotifierProvider._internal( + () => create()..note = note, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + note: note, + ), + ); + } + + @override + (Note,) get argument { + return (note,); + } + + @override + AutoDisposeNotifierProviderElement createElement() { + return _NoteModalSheetNotifierProviderElement(this); + } + + NoteModalSheetNotifierProvider _copyWith( + NoteModalSheetNotifier Function() create, + ) { + return NoteModalSheetNotifierProvider._internal( + () => create()..note = note, + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + note: note, + ); + } + + @override + bool operator ==(Object other) { + return other is NoteModalSheetNotifierProvider && other.note == note; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, note.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin NoteModalSheetNotifierRef + on AutoDisposeNotifierProviderRef { + /// The parameter `note` of this provider. + Note get note; +} + +class _NoteModalSheetNotifierProviderElement + extends AutoDisposeNotifierProviderElement with NoteModalSheetNotifierRef { + _NoteModalSheetNotifierProviderElement(super.provider); + + @override + Note get note => (origin as NoteModalSheetNotifierProvider).note; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/notes_after_renote_page/notes_after_renote_page.dart b/lib/view/notes_after_renote_page/notes_after_renote_page.dart index 43deb3614..998254a13 100644 --- a/lib/view/notes_after_renote_page/notes_after_renote_page.dart +++ b/lib/view/notes_after_renote_page/notes_after_renote_page.dart @@ -1,28 +1,32 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; @RoutePage() -class NotesAfterRenotePage extends ConsumerStatefulWidget { +class NotesAfterRenotePage extends ConsumerStatefulWidget + implements AutoRouteWrapper { final Note note; - final Account account; + final AccountContext accountContext; const NotesAfterRenotePage({ - super.key, required this.note, - required this.account, + required this.accountContext, + super.key, }); @override ConsumerState createState() => _NotesAfterRenotePageState(); + + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); } class _NotesAfterRenotePageState extends ConsumerState { @@ -30,7 +34,7 @@ class _NotesAfterRenotePageState extends ConsumerState { @override Widget build(BuildContext context) { - final misskey = ref.watch(misskeyProvider(widget.account)); + final misskey = ref.watch(misskeyGetContextProvider); return Scaffold( appBar: AppBar(title: Text(S.of(context).notesAfterRenote)), @@ -43,9 +47,7 @@ class _NotesAfterRenotePageState extends ConsumerState { notesAfterRenote, lastRenoteId, ) = await getNotesAfterRenote(misskey); - ref - .read(notesProvider(widget.account)) - .registerAll(notesAfterRenote); + ref.read(notesWithProvider).registerAll(notesAfterRenote); setState(() { untilId = lastRenoteId; }); @@ -59,19 +61,14 @@ class _NotesAfterRenotePageState extends ConsumerState { misskey, untilId: untilId, ); - ref - .read(notesProvider(widget.account)) - .registerAll(notesAfterRenote); + ref.read(notesWithProvider).registerAll(notesAfterRenote); setState(() { untilId = lastRenoteId; }); return notesAfterRenote; }, itemBuilder: (context, item) { - return AccountScope( - account: widget.account, - child: MisskeyNote(note: item), - ); + return MisskeyNote(note: item); }, ), ), diff --git a/lib/view/notification_page/notification_page.dart b/lib/view/notification_page/notification_page.dart index d99975847..476b5d9f9 100644 --- a/lib/view/notification_page/notification_page.dart +++ b/lib/view/notification_page/notification_page.dart @@ -1,138 +1,142 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/extensions/date_time_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/notification_page/notification_page_data.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/common/misskey_notes/custom_emoji.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart' +import "package:auto_route/auto_route.dart"; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/date_time_extension.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/account_repository.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:miria/view/common/misskey_notes/custom_emoji.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart" as misskey_note; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/view/user_page/user_list_item.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:miria/view/common/pushable_listview.dart"; +import "package:miria/view/notification_page/notification_page_data.dart"; +import "package:miria/view/user_page/user_list_item.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +part "notification_page.g.dart"; @RoutePage() -class NotificationPage extends ConsumerStatefulWidget { - const NotificationPage({super.key, required this.account}); - final Account account; +class NotificationPage extends ConsumerWidget implements AutoRouteWrapper { + final AccountContext accountContext; - @override - ConsumerState createState() => - NotificationPageState(); -} + const NotificationPage({required this.accountContext, super.key}); -class NotificationPageState extends ConsumerState { @override - Widget build(BuildContext context) { - final misskey = ref.read(misskeyProvider(widget.account)); + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); + @override + Widget build(BuildContext context, WidgetRef ref) { + final misskey = ref.read(misskeyPostContextProvider); return DefaultTabController( length: 3, - child: AccountScope( - account: widget.account, - child: Scaffold( - appBar: AppBar( - title: Text(S.of(context).notification), - bottom: TabBar(tabs: [ - Tab(text: S.of(context).notificationAll), - Tab(text: S.of(context).notificationForMe), - Tab(text: S.of(context).notificationDirect) - ])), - body: Padding( - padding: const EdgeInsets.only(left: 5.0, right: 5.0), - child: TabBarView( - children: [ - PushableListView( - initializeFuture: () async { - final localize = S.of(context); - final result = await misskey.i - .notifications(const INotificationsRequest( + child: Scaffold( + appBar: AppBar( + title: Text(S.of(context).notification), + bottom: TabBar( + tabs: [ + Tab(text: S.of(context).notificationAll), + Tab(text: S.of(context).notificationForMe), + Tab(text: S.of(context).notificationDirect), + ], + ), + ), + body: Padding( + padding: const EdgeInsets.only(left: 5.0, right: 5.0), + child: TabBarView( + children: [ + PushableListView( + initializeFuture: () async { + final localize = S.of(context); + final result = await misskey.i.notifications( + const INotificationsRequest( limit: 50, markAsRead: true, - )); - ref - .read(notesProvider(widget.account)) - .registerAll(result.map((e) => e.note).whereNotNull()); + ), + ); + ref + .read(notesWithProvider) + .registerAll(result.map((e) => e.note).whereNotNull()); - ref - .read(accountRepositoryProvider.notifier) - .readAllNotification(widget.account); - return result.toNotificationData(localize); - }, - nextFuture: (lastElement, _) async { - final localize = S.of(context); - final result = await misskey.i.notifications( - INotificationsRequest( - limit: 50, untilId: lastElement.id)); - ref - .read(notesProvider(widget.account)) - .registerAll(result.map((e) => e.note).whereNotNull()); - return result.toNotificationData(localize); - }, - itemBuilder: (context, notification) => Align( - alignment: Alignment.center, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 800), - child: NotificationItem( - notification: notification, - account: widget.account, - ), + await ref + .read(accountRepositoryProvider.notifier) + .readAllNotification(accountContext.postAccount); + return result.toNotificationData(localize); + }, + nextFuture: (lastElement, _) async { + final localize = S.of(context); + final result = await misskey.i.notifications( + INotificationsRequest( + limit: 50, + untilId: lastElement.id, ), + ); + ref + .read(notesWithProvider) + .registerAll(result.map((e) => e.note).whereNotNull()); + return result.toNotificationData(localize); + }, + itemBuilder: (context, notification) => Align( + alignment: Alignment.center, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: NotificationItem(notification: notification), ), ), - PushableListView( - initializeFuture: () async { - final notes = await ref - .read(misskeyProvider(widget.account)) - .notes - .mentions(const NotesMentionsRequest()); - ref.read(notesProvider(widget.account)).registerAll(notes); - return notes.toList(); - }, - nextFuture: (item, _) async { - final notes = await ref - .read(misskeyProvider(widget.account)) - .notes - .mentions(NotesMentionsRequest(untilId: item.id)); - ref.read(notesProvider(widget.account)).registerAll(notes); - return notes.toList(); - }, - itemBuilder: (context, note) { - return misskey_note.MisskeyNote(note: note); - }, - ), - PushableListView( - initializeFuture: () async { - final notes = await ref - .read(misskeyProvider(widget.account)) - .notes - .mentions(const NotesMentionsRequest( - visibility: NoteVisibility.specified)); - ref.read(notesProvider(widget.account)).registerAll(notes); - return notes.toList(); - }, - nextFuture: (item, _) async { - final notes = await ref - .read(misskeyProvider(widget.account)) - .notes - .mentions(NotesMentionsRequest( - untilId: item.id, - visibility: NoteVisibility.specified)); - ref.read(notesProvider(widget.account)).registerAll(notes); - return notes.toList(); - }, - itemBuilder: (context, note) { - return misskey_note.MisskeyNote(note: note); - }, - ), - ], - ), + ), + PushableListView( + initializeFuture: () async { + final notes = await ref + .read(misskeyPostContextProvider) + .notes + .mentions(const NotesMentionsRequest()); + ref.read(notesWithProvider).registerAll(notes); + return notes.toList(); + }, + nextFuture: (item, _) async { + final notes = await ref + .read(misskeyPostContextProvider) + .notes + .mentions(NotesMentionsRequest(untilId: item.id)); + ref.read(notesWithProvider).registerAll(notes); + return notes.toList(); + }, + itemBuilder: (context, note) { + return misskey_note.MisskeyNote(note: note); + }, + ), + PushableListView( + initializeFuture: () async { + final notes = + await ref.read(misskeyPostContextProvider).notes.mentions( + const NotesMentionsRequest( + visibility: NoteVisibility.specified, + ), + ); + ref.read(notesWithProvider).registerAll(notes); + return notes.toList(); + }, + nextFuture: (item, _) async { + final notes = + await ref.read(misskeyPostContextProvider).notes.mentions( + NotesMentionsRequest( + untilId: item.id, + visibility: NoteVisibility.specified, + ), + ); + ref.read(notesWithProvider).registerAll(notes); + return notes.toList(); + }, + itemBuilder: (context, note) { + return misskey_note.MisskeyNote(note: note); + }, + ), + ], ), ), ), @@ -143,31 +147,29 @@ class NotificationPageState extends ConsumerState { final showActionsProvider = StateProvider.autoDispose.family((ref, _) => true); -final followRequestsProvider = FutureProvider.autoDispose - .family, Account>((ref, account) async { +@Riverpod(dependencies: [misskeyPostContext]) +Future> followRequests(FollowRequestsRef ref) async { final response = await ref - .watch(misskeyProvider(account)) + .watch(misskeyPostContextProvider) .following .requests .list(const FollowingRequestsListRequest()); return response.toList(); -}); +} class NotificationItem extends ConsumerWidget { final NotificationData notification; - final Account account; const NotificationItem({ - super.key, required this.notification, - required this.account, + super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { final notification = this.notification; final showActions = ref.watch(showActionsProvider(notification)); - final followRequests = ref.watch(followRequestsProvider(account)); + final followRequests = ref.watch(followRequestsProvider); switch (notification) { case RenoteReactionNotificationData(): @@ -187,25 +189,28 @@ class NotificationItem extends ConsumerWidget { if (hasReaction && hasRenote) Expanded( child: SimpleMfmText( - S.of(context).renoteAndReactionsNotification( - notification.reactionUsers.first.$2?.name ?? - notification - .reactionUsers.first.$2?.username, - notification.renoteUsers.first?.name ?? - notification.renoteUsers.first?.username, - ), - emojis: Map.of( - notification.reactionUsers.first.$2?.emojis ?? - {}) - ..addAll(notification.renoteUsers.first?.emojis ?? - {})), + S.of(context).renoteAndReactionsNotification( + notification.reactionUsers.first.$2?.name ?? + notification + .reactionUsers.first.$2?.username, + notification.renoteUsers.first?.name ?? + notification.renoteUsers.first?.username, + ), + emojis: Map.of( + notification.reactionUsers.first.$2?.emojis ?? {}, + )..addAll( + notification.renoteUsers.first?.emojis ?? {}, + ), + ), ), if (hasReaction && !hasRenote) Expanded( child: SimpleMfmText( - S.of(context).reactionNotification(notification - .reactionUsers.first.$2?.name ?? - notification.reactionUsers.first.$2?.username), + S.of(context).reactionNotification( + notification.reactionUsers.first.$2?.name ?? + notification + .reactionUsers.first.$2?.username, + ), emojis: notification.reactionUsers.first.$2?.emojis ?? {}, ), @@ -213,11 +218,12 @@ class NotificationItem extends ConsumerWidget { if (hasRenote && !hasReaction) Expanded( child: SimpleMfmText( - S.of(context).renoteNotification( + S.of(context).renoteNotification( notification.renoteUsers.first?.name ?? - notification.renoteUsers.first?.username), - emojis: - notification.renoteUsers.first?.emojis ?? {}), + notification.renoteUsers.first?.username, + ), + emojis: notification.renoteUsers.first?.emojis ?? {}, + ), ), Text(notification.createdAt.differenceNow(context)), ], @@ -236,8 +242,10 @@ class NotificationItem extends ConsumerWidget { if (hasRenote) Container( decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).primaryColor)), + border: Border.all( + color: Theme.of(context).primaryColor, + ), + ), padding: const EdgeInsets.all(5), child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -248,7 +256,7 @@ class NotificationItem extends ConsumerWidget { children: [ for (final user in notification.renoteUsers.whereNotNull()) - UserListItem(user: user) + UserListItem(user: user), ], ), ], @@ -259,16 +267,19 @@ class NotificationItem extends ConsumerWidget { if (hasReaction) Container( decoration: BoxDecoration( - border: Border.all( - color: Theme.of(context).primaryColor)), + border: Border.all( + color: Theme.of(context).primaryColor, + ), + ), padding: const EdgeInsets.all(5), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Text(S.of(context).reactionUsersInNotification), - for (final reaction in notification.reactionUsers - .mapIndexed( - (index, element) => (index, element))) ...[ + for (final reaction + in notification.reactionUsers.mapIndexed( + (index, element) => (index, element), + )) ...[ if (reaction.$2.$1 != null && (reaction.$1 > 0 && notification @@ -281,8 +292,12 @@ class NotificationItem extends ConsumerWidget { emojiData: MisskeyEmojiData.fromEmojiName( emojiName: reaction.$2.$1!, repository: ref.read( - emojiRepositoryProvider( - AccountScope.of(context))), + emojiRepositoryProvider( + ref + .read(accountContextProvider) + .getAccount, + ), + ), emojiInfo: notification.note?.reactionEmojis, ), @@ -290,15 +305,16 @@ class NotificationItem extends ConsumerWidget { ), if (reaction.$2.$2 != null) Padding( - padding: const EdgeInsets.only(left: 20), - child: UserListItem(user: reaction.$2.$2!)), - ] + padding: const EdgeInsets.only(left: 20), + child: UserListItem(user: reaction.$2.$2!), + ), + ], ], ), - ) + ), ], ), - ) + ), ], ), ); @@ -309,12 +325,13 @@ class NotificationItem extends ConsumerWidget { child: Column( children: [ if (notification.note != null) - misskey_note.MisskeyNote(note: notification.note!) + misskey_note.MisskeyNote(note: notification.note!), ], ), ); case FollowNotificationData(): final user = notification.user; + final type = notification.type; return Padding( padding: @@ -351,12 +368,11 @@ class NotificationItem extends ConsumerWidget { flex: 5, fit: FlexFit.tight, child: ElevatedButton( - onPressed: () => handleFollowRequest( + onPressed: () async => handleFollowRequest( ref, - account: account, accept: true, userId: user.id, - ).expectFailure(context), + ), child: Text(S.of(context).accept), ), ), @@ -365,12 +381,11 @@ class NotificationItem extends ConsumerWidget { flex: 5, fit: FlexFit.tight, child: OutlinedButton( - onPressed: () => handleFollowRequest( + onPressed: () async => handleFollowRequest( ref, - account: account, accept: false, userId: user.id, - ).expectFailure(context), + ), child: Text(S.of(context).reject), ), ), @@ -383,6 +398,10 @@ class NotificationItem extends ConsumerWidget { }, orElse: () => Container(), ), + if (type is FollowRequestAccepted && type.message != null) + SimpleMfmText( + S.of(context).messageForFollower(type.message ?? ""), + ) ], ), ); @@ -407,7 +426,8 @@ class NotificationItem extends ConsumerWidget { child: Row( children: [ Expanded( - child: Text(S.of(context).finishedVotedNotification)), + child: Text(S.of(context).finishedVotedNotification), + ), Text(notification.createdAt.differenceNow(context)), ], ), @@ -445,9 +465,11 @@ class NotificationItem extends ConsumerWidget { child: Row( children: [ Expanded( - child: SimpleMfmText(S - .of(context) - .roleAssignedNotification(notification.role?.name ?? "")), + child: SimpleMfmText( + S + .of(context) + .roleAssignedNotification(notification.role?.name ?? ""), + ), ), ], ), @@ -457,21 +479,22 @@ class NotificationItem extends ConsumerWidget { Future handleFollowRequest( WidgetRef ref, { - required Account account, required bool accept, required String userId, }) async { - final misskey = ref.watch(misskeyProvider(account)); + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + final misskey = ref.watch(misskeyPostContextProvider); - if (accept) { - await misskey.following.requests - .accept(FollowingRequestsAcceptRequest(userId: userId)); - } else { - await misskey.following.requests - .reject(FollowingRequestsRejectRequest(userId: userId)); - } + if (accept) { + await misskey.following.requests + .accept(FollowingRequestsAcceptRequest(userId: userId)); + } else { + await misskey.following.requests + .reject(FollowingRequestsRejectRequest(userId: userId)); + } - ref.invalidate(followRequestsProvider(account)); - ref.read(showActionsProvider(notification).notifier).state = false; + ref.invalidate(followRequestsProvider); + ref.read(showActionsProvider(notification).notifier).state = false; + }); } } diff --git a/lib/view/notification_page/notification_page.g.dart b/lib/view/notification_page/notification_page.g.dart new file mode 100644 index 000000000..f1094e3f0 --- /dev/null +++ b/lib/view/notification_page/notification_page.g.dart @@ -0,0 +1,29 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'notification_page.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$followRequestsHash() => r'0b953c859590344d5bc8e0eced81484ffadb2f47'; + +/// See also [followRequests]. +@ProviderFor(followRequests) +final followRequestsProvider = + AutoDisposeFutureProvider>.internal( + followRequests, + name: r'followRequestsProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$followRequestsHash, + dependencies: [misskeyPostContextProvider], + allTransitiveDependencies: { + misskeyPostContextProvider, + ...?misskeyPostContextProvider.allTransitiveDependencies + }, +); + +typedef FollowRequestsRef = AutoDisposeFutureProviderRef>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/notification_page/notification_page_data.dart b/lib/view/notification_page/notification_page_data.dart index db5547d45..15a83fd7e 100644 --- a/lib/view/notification_page/notification_page_data.dart +++ b/lib/view/notification_page/notification_page_data.dart @@ -1,6 +1,6 @@ -import 'package:flutter/material.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:misskey_dart/misskey_dart.dart"; sealed class NotificationData { final String id; @@ -32,17 +32,19 @@ sealed class MentionQuoteNotificationDataType { class _Mention implements MentionQuoteNotificationDataType { @override - get name => (context) => S.of(context).mention; + String Function(BuildContext context) get name => + (context) => S.of(context).mention; } class _QuotedRenote implements MentionQuoteNotificationDataType { @override - get name => (context) => S.of(context).quotedRenote; + String Function(BuildContext context) get name => + (context) => S.of(context).quotedRenote; } class _Reply implements MentionQuoteNotificationDataType { @override - get name => (context) => ""; + String Function(BuildContext context) get name => (context) => ""; } class MentionQuoteNotificationData extends NotificationData { @@ -62,26 +64,31 @@ class MentionQuoteNotificationData extends NotificationData { sealed class FollowNotificationDataType { String Function(BuildContext, String) get name; static final follow = _Follow(); - static final followRequestAccepted = _FollowRequestAccepted(); + factory FollowNotificationDataType.followRequestAccepted(String? message) => + FollowRequestAccepted(message); static final receiveFollowRequest = _ReceiveFollowRequest(); } class _Follow implements FollowNotificationDataType { @override - get name => + String Function(BuildContext context, String userName) get name => (context, userName) => S.of(context).followedNotification(userName); } -class _FollowRequestAccepted implements FollowNotificationDataType { +class FollowRequestAccepted implements FollowNotificationDataType { + final String? message; + FollowRequestAccepted(this.message); @override - get name => (context, userName) => - S.of(context).followRequestAcceptedNotification(userName); + String Function(BuildContext context, String userName) get name => + (context, userName) => + S.of(context).followRequestAcceptedNotification(userName); } class _ReceiveFollowRequest implements FollowNotificationDataType { @override - get name => (context, userName) => - S.of(context).receiveFollowRequestNotification(userName); + String Function(BuildContext context, String userName) get name => + (context, userName) => + S.of(context).receiveFollowRequestNotification(userName); } class FollowNotificationData extends NotificationData { @@ -152,15 +159,17 @@ extension INotificationsResponseExtension on Iterable { }); if (!isSummarize) { - resultList.add(RenoteReactionNotificationData( + resultList.add( + RenoteReactionNotificationData( note: element.note, reactionUsers: [(element.reaction, element.user)], renoteUsers: [], createdAt: element.createdAt, - id: element.id)); + id: element.id, + ), + ); } - break; case NotificationType.renote: var isSummarize = false; resultList @@ -172,109 +181,169 @@ extension INotificationsResponseExtension on Iterable { }); if (!isSummarize) { - resultList.add(RenoteReactionNotificationData( + resultList.add( + RenoteReactionNotificationData( note: element.note?.renote, reactionUsers: [], renoteUsers: [element.user], createdAt: element.createdAt, - id: element.id)); + id: element.id, + ), + ); } - break; - case NotificationType.quote: - resultList.add(MentionQuoteNotificationData( + resultList.add( + MentionQuoteNotificationData( createdAt: element.createdAt, note: element.note, user: element.user, type: MentionQuoteNotificationDataType.quote, - id: element.id)); + id: element.id, + ), + ); - break; case NotificationType.mention: - resultList.add(MentionQuoteNotificationData( + resultList.add( + MentionQuoteNotificationData( createdAt: element.createdAt, note: element.note, user: element.user, type: MentionQuoteNotificationDataType.mention, - id: element.id)); + id: element.id, + ), + ); - break; case NotificationType.reply: - resultList.add(MentionQuoteNotificationData( + resultList.add( + MentionQuoteNotificationData( createdAt: element.createdAt, note: element.note, user: element.user, type: MentionQuoteNotificationDataType.reply, - id: element.id)); - break; + id: element.id, + ), + ); case NotificationType.follow: - resultList.add(FollowNotificationData( + resultList.add( + FollowNotificationData( user: element.user, createdAt: element.createdAt, type: FollowNotificationDataType.follow, - id: element.id)); + id: element.id, + ), + ); - break; case NotificationType.followRequestAccepted: - resultList.add(FollowNotificationData( + resultList.add( + FollowNotificationData( user: element.user, createdAt: element.createdAt, - type: FollowNotificationDataType.followRequestAccepted, - id: element.id)); - break; + type: FollowNotificationDataType.followRequestAccepted( + element.message), + id: element.id, + ), + ); case NotificationType.receiveFollowRequest: - resultList.add(FollowNotificationData( + resultList.add( + FollowNotificationData( user: element.user, createdAt: element.createdAt, type: FollowNotificationDataType.receiveFollowRequest, - id: element.id)); - break; + id: element.id, + ), + ); case NotificationType.achievementEarned: - resultList.add(SimpleNotificationData( + resultList.add( + SimpleNotificationData( text: "${localize.achievementEarnedNotification}[${element.achievement}]", createdAt: element.createdAt, - id: element.id)); - break; + id: element.id, + ), + ); case NotificationType.pollVote: - resultList.add(PollNotification( + resultList.add( + PollNotification( note: element.note, createdAt: element.createdAt, - id: element.id)); - break; + id: element.id, + ), + ); case NotificationType.pollEnded: - resultList.add(PollNotification( + resultList.add( + PollNotification( note: element.note, createdAt: element.createdAt, - id: element.id)); - break; + id: element.id, + ), + ); case NotificationType.test: - resultList.add(SimpleNotificationData( + resultList.add( + SimpleNotificationData( text: localize.testNotification, createdAt: element.createdAt, - id: element.id)); - break; + id: element.id, + ), + ); case NotificationType.note: - resultList.add(NoteNotification( + resultList.add( + NoteNotification( note: element.note, createdAt: element.createdAt, - id: element.id)); - break; + id: element.id, + ), + ); case NotificationType.roleAssigned: - resultList.add(RoleNotification( + resultList.add( + RoleNotification( role: element.role, createdAt: element.createdAt, - id: element.id)); - break; + id: element.id, + ), + ); + case NotificationType.app: + resultList.add( + SimpleNotificationData( + text: localize.appNotification, + createdAt: element.createdAt, + id: element.id, + ), + ); - default: + case NotificationType.groupInvited: + case NotificationType.reactionGrouped: + case NotificationType.renoteGrouped: break; + case NotificationType.exportCompleted: + resultList.add( + SimpleNotificationData( + text: localize.exportCompleted, + createdAt: element.createdAt, + id: element.id, + ), + ); + case NotificationType.login: + resultList.add( + SimpleNotificationData( + text: localize.someoneLogined, + createdAt: element.createdAt, + id: element.id, + ), + ); + case NotificationType.unknown: + resultList.add( + SimpleNotificationData( + text: localize.unknownNotification, + createdAt: element.createdAt, + id: element.id, + ), + ); } } diff --git a/lib/view/photo_edit_page/clip_mode.dart b/lib/view/photo_edit_page/clip_mode.dart index c7cf5f3a0..57117dab1 100644 --- a/lib/view/photo_edit_page/clip_mode.dart +++ b/lib/view/photo_edit_page/clip_mode.dart @@ -1,14 +1,14 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/misskey_notes/custom_emoji.dart'; -import 'package:miria/view/photo_edit_page/edited_photo_image.dart'; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/state_notifier/photo_edit_page/photo_edit_state_notifier.dart"; +import "package:miria/view/common/misskey_notes/custom_emoji.dart"; +import "package:miria/view/photo_edit_page/edited_photo_image.dart"; class ClipMode extends ConsumerStatefulWidget { final GlobalKey renderingGlobalKey; - const ClipMode({super.key, required this.renderingGlobalKey}); + const ClipMode({required this.renderingGlobalKey, super.key}); @override ConsumerState createState() => ClipModeState(); @@ -20,20 +20,27 @@ class ClipModeState extends ConsumerState { @override Widget build(BuildContext context) { - final clipMode = - ref.watch(photoEditProvider.select((value) => value.clipMode)); - final defaultSize = - ref.watch(photoEditProvider.select((value) => value.defaultSize)); - final cropOffset = - ref.watch(photoEditProvider.select((value) => value.cropOffset)); - final actualSize = - ref.watch(photoEditProvider.select((value) => value.actualSize)); - final cropSize = - ref.watch(photoEditProvider.select((value) => value.cropSize)); - final reactions = - ref.watch(photoEditProvider.select((value) => value.emojis)); - final selectedReaction = ref - .watch(photoEditProvider.select((value) => value.selectedEmojiIndex)); + final clipMode = ref.watch( + photoEditStateNotifierProvider.select((value) => value.clipMode), + ); + final defaultSize = ref.watch( + photoEditStateNotifierProvider.select((value) => value.defaultSize), + ); + final cropOffset = ref.watch( + photoEditStateNotifierProvider.select((value) => value.cropOffset), + ); + final actualSize = ref.watch( + photoEditStateNotifierProvider.select((value) => value.actualSize), + ); + final cropSize = ref.watch( + photoEditStateNotifierProvider.select((value) => value.cropSize), + ); + final reactions = ref + .watch(photoEditStateNotifierProvider.select((value) => value.emojis)); + final selectedReaction = ref.watch( + photoEditStateNotifierProvider + .select((value) => value.selectedEmojiIndex), + ); final ratio = defaultSize.width / actualSize.width; @@ -43,19 +50,20 @@ class ClipModeState extends ConsumerState { child: Listener( onPointerMove: selectedReaction == null ? null - : (detail) => - ref.read(photoEditProvider.notifier).reactionMove(detail), + : (detail) => ref + .read(photoEditStateNotifierProvider.notifier) + .reactionMove(detail), child: GestureDetector( behavior: HitTestBehavior.opaque, onScaleStart: selectedReaction == null ? null : (detail) => ref - .read(photoEditProvider.notifier) + .read(photoEditStateNotifierProvider.notifier) .reactionScaleStart(detail), onScaleUpdate: selectedReaction == null ? null : (detail) => ref - .read(photoEditProvider.notifier) + .read(photoEditStateNotifierProvider.notifier) .reactionScaleUpdate(detail), child: RepaintBoundary( key: widget.renderingGlobalKey, @@ -66,58 +74,69 @@ class ClipModeState extends ConsumerState { if (clipMode) ...[ // mask Positioned( - left: cropOffset.dx + basePadding * ratio, - top: cropOffset.dy + basePadding * ratio, - width: cropSize.width, - height: cropSize.height, - child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all( - color: Colors.white.withAlpha(150), - width: 2 * ratio)), - )), + left: cropOffset.dx + basePadding * ratio, + top: cropOffset.dy + basePadding * ratio, + width: cropSize.width, + height: cropSize.height, + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all( + color: Colors.white.withAlpha(150), + width: 2 * ratio, + ), + ), + ), + ), //left top-down Positioned( - left: basePadding * ratio, - top: basePadding * ratio, - width: cropOffset.dx, - height: defaultSize.height, - child: DecoratedBox( - decoration: - BoxDecoration(color: Colors.black87.withAlpha(150)), - )), + left: basePadding * ratio, + top: basePadding * ratio, + width: cropOffset.dx, + height: defaultSize.height, + child: DecoratedBox( + decoration: + BoxDecoration(color: Colors.black87.withAlpha(150)), + ), + ), //right top-down Positioned( - left: - cropOffset.dx + basePadding * ratio + cropSize.width, - top: basePadding * ratio, - width: defaultSize.width - cropSize.width - cropOffset.dx, - height: defaultSize.height, - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.black87.withAlpha(150)))), + left: cropOffset.dx + basePadding * ratio + cropSize.width, + top: basePadding * ratio, + width: defaultSize.width - cropSize.width - cropOffset.dx, + height: defaultSize.height, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.black87.withAlpha(150), + ), + ), + ), //left over crop Positioned( - left: basePadding * ratio + cropOffset.dx, - top: basePadding * ratio, - width: cropSize.width, - height: cropOffset.dy, - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.black87.withAlpha(150)))), + left: basePadding * ratio + cropOffset.dx, + top: basePadding * ratio, + width: cropSize.width, + height: cropOffset.dy, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.black87.withAlpha(150), + ), + ), + ), //left under crop Positioned( - left: basePadding * ratio + cropOffset.dx, - top: - basePadding * ratio + cropSize.height + cropOffset.dy, - width: cropSize.width, - height: - defaultSize.height - cropSize.height - cropOffset.dy, - child: DecoratedBox( - decoration: BoxDecoration( - color: Colors.black87.withAlpha(150)))), + left: basePadding * ratio + cropOffset.dx, + top: basePadding * ratio + cropSize.height + cropOffset.dy, + width: cropSize.width, + height: + defaultSize.height - cropSize.height - cropOffset.dy, + child: DecoratedBox( + decoration: BoxDecoration( + color: Colors.black87.withAlpha(150), + ), + ), + ), Positioned( left: cropOffset.dx - (iconSize / 2 - basePadding) * ratio, @@ -127,7 +146,7 @@ class ClipModeState extends ConsumerState { child: Listener( behavior: HitTestBehavior.translucent, onPointerMove: (detail) => ref - .read(photoEditProvider.notifier) + .read(photoEditStateNotifierProvider.notifier) .cropMoveLeftTop(detail), child: Icon(Icons.add, size: iconSize * ratio), ), @@ -140,7 +159,7 @@ class ClipModeState extends ConsumerState { child: Listener( behavior: HitTestBehavior.translucent, onPointerMove: (detail) => ref - .read(photoEditProvider.notifier) + .read(photoEditStateNotifierProvider.notifier) .cropMoveRightTop(detail), child: Icon(Icons.add, size: 40 * ratio), ), @@ -153,7 +172,7 @@ class ClipModeState extends ConsumerState { child: Listener( behavior: HitTestBehavior.translucent, onPointerMove: (detail) => ref - .read(photoEditProvider.notifier) + .read(photoEditStateNotifierProvider.notifier) .cropMoveLeftBottom(detail), child: Icon(Icons.add, size: 40 * ratio), ), @@ -168,7 +187,7 @@ class ClipModeState extends ConsumerState { child: Listener( behavior: HitTestBehavior.translucent, onPointerMove: (detail) => ref - .read(photoEditProvider.notifier) + .read(photoEditStateNotifierProvider.notifier) .cropMoveRightBottom(detail), child: Icon(Icons.add, size: 40 * ratio), ), @@ -195,13 +214,14 @@ class ClipModeState extends ConsumerState { height: reaction.$2.scale, child: GestureDetector( onTap: () => ref - .read(photoEditProvider.notifier) + .read(photoEditStateNotifierProvider.notifier) .selectReaction(reaction.$1), child: DecoratedBox( decoration: BoxDecoration( - border: reaction.$1 == selectedReaction - ? Border.all(color: Colors.white) - : null), + border: reaction.$1 == selectedReaction + ? Border.all(color: Colors.white) + : null, + ), child: SizedBox( width: reaction.$2.scale, height: reaction.$2.scale, @@ -218,7 +238,7 @@ class ClipModeState extends ConsumerState { ), ), ), - ) + ), ], ), ), diff --git a/lib/view/photo_edit_page/color_filter_image_preview.dart b/lib/view/photo_edit_page/color_filter_image_preview.dart index c7f0ac098..5985cf6aa 100644 --- a/lib/view/photo_edit_page/color_filter_image_preview.dart +++ b/lib/view/photo_edit_page/color_filter_image_preview.dart @@ -1,6 +1,6 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/state_notifier/photo_edit_page/photo_edit_state_notifier.dart"; class ColorFilterImagePreview extends ConsumerWidget { const ColorFilterImagePreview({super.key}); @@ -9,12 +9,16 @@ class ColorFilterImagePreview extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final previewImages = ref .watch( - photoEditProvider.select((value) => value.colorFilterPreviewImages)) + photoEditStateNotifierProvider + .select((value) => value.colorFilterPreviewImages), + ) .toList(); - final previewMode = - ref.watch(photoEditProvider.select((value) => value.colorFilterMode)); - final adaptive = - ref.watch(photoEditProvider.select((value) => value.adaptivePresets)); + final previewMode = ref.watch( + photoEditStateNotifierProvider.select((value) => value.colorFilterMode), + ); + final adaptive = ref.watch( + photoEditStateNotifierProvider.select((value) => value.adaptivePresets), + ); if (!previewMode) { return const SizedBox.shrink(); } @@ -26,15 +30,15 @@ class ColorFilterImagePreview extends ConsumerWidget { width: double.infinity, height: 100, child: ListView.builder( - key: const PageStorageKey('colorFilterImagePreview'), + key: const PageStorageKey("colorFilterImagePreview"), scrollDirection: Axis.horizontal, itemCount: previewImages.length, itemBuilder: (context, index) { final image = previewImages[index].image; if (image == null) return const SizedBox.shrink(); return GestureDetector( - onTap: () => ref - .read(photoEditProvider.notifier) + onTap: () async => ref + .read(photoEditStateNotifierProvider.notifier) .selectColorFilter(previewImages[index].name), child: DecoratedBox( decoration: adaptive.any((e) => e == previewImages[index].name) @@ -47,7 +51,7 @@ class ColorFilterImagePreview extends ConsumerWidget { child: Column( children: [ Expanded(child: Image.memory(image)), - Text(previewImages[index].name) + Text(previewImages[index].name), ], ), ), diff --git a/lib/view/photo_edit_page/edited_photo_image.dart b/lib/view/photo_edit_page/edited_photo_image.dart index 122675ae3..a764ea94f 100644 --- a/lib/view/photo_edit_page/edited_photo_image.dart +++ b/lib/view/photo_edit_page/edited_photo_image.dart @@ -1,26 +1,30 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/state_notifier/photo_edit_page/photo_edit_state_notifier.dart"; class EditedPhotoImage extends ConsumerWidget { const EditedPhotoImage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final image = - ref.watch(photoEditProvider.select((value) => value.editedImage)); + final image = ref.watch( + photoEditStateNotifierProvider.select((value) => value.editedImage), + ); - final defaultSize = - ref.watch(photoEditProvider.select((value) => value.defaultSize)); - final actualSize = - ref.watch(photoEditProvider.select((value) => value.actualSize)); + final defaultSize = ref.watch( + photoEditStateNotifierProvider.select((value) => value.defaultSize), + ); + final actualSize = ref.watch( + photoEditStateNotifierProvider.select((value) => value.actualSize), + ); if (image != null) { return Positioned.fill( - child: Padding( - padding: - EdgeInsets.all(10 * (defaultSize.width / actualSize.width)), - child: Image.memory(image))); + child: Padding( + padding: EdgeInsets.all(10 * (defaultSize.width / actualSize.width)), + child: Image.memory(image), + ), + ); } return const Positioned( child: SizedBox.shrink(), diff --git a/lib/view/photo_edit_page/license_confirm_dialog.dart b/lib/view/photo_edit_page/license_confirm_dialog.dart index 9e147b76d..127c78dc9 100644 --- a/lib/view/photo_edit_page/license_confirm_dialog.dart +++ b/lib/view/photo_edit_page/license_confirm_dialog.dart @@ -1,93 +1,74 @@ -import 'dart:math'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:miria/view/dialogs/simple_message_dialog.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:miria/view/dialogs/simple_message_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +part "license_confirm_dialog.g.dart"; -class LicenseConfirmDialog extends ConsumerStatefulWidget { - final String emoji; - final Account account; - - const LicenseConfirmDialog( - {super.key, required this.emoji, required this.account}); - - @override - ConsumerState createState() => - LicenseConfirmDialogState(); +@Riverpod(dependencies: [misskeyPostContext]) +Future _emoji(_EmojiRef ref, String emoji) async { + return await ref + .read(misskeyPostContextProvider) + .emoji(EmojiRequest(name: emoji)); } -class LicenseConfirmDialogState extends ConsumerState { - var isLoading = true; - Object? error; +@RoutePage() +class LicenseConfirmDialog extends ConsumerWidget implements AutoRouteWrapper { + final String emoji; + final Account account; - EmojiResponse? data; + const LicenseConfirmDialog({ + required this.emoji, + required this.account, + super.key, + }); @override - void didChangeDependencies() { - super.didChangeDependencies(); - - Future(() async { - try { - final response = await ref - .read(misskeyProvider(widget.account)) - .emoji(EmojiRequest(name: widget.emoji)); - if (!mounted) return; - setState(() { - isLoading = false; - data = response; - }); - } catch (e) { - if (!mounted) return; - setState(() { - error = e; - }); - } - }); - } + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: account, child: this); @override - Widget build(BuildContext context) { - if (error != null) { - return SimpleMessageDialog( - message: "${S.of(context).thrownError}\n$error"); - } - final data = this.data; - if (isLoading || data == null) { - return const Center(child: CircularProgressIndicator()); - } - - return AccountScope( - account: widget.account, - child: AlertDialog( - content: SizedBox( - child: SingleChildScrollView( - child: Column( - children: [ - Text( - S.of(context).customEmojiLicensedBy, - ), - MfmText( - mfmText: - data.license ?? S.of(context).customEmojiLicensedByNone) - ], + Widget build(BuildContext context, WidgetRef ref) { + final emojiResponse = ref.watch(_emojiProvider(emoji)); + return switch (emojiResponse) { + AsyncLoading() => const Center( + child: CircularProgressIndicator.adaptive(), + ), + AsyncError(:final error) => SimpleMessageDialog( + message: "${S.of(context).thrownError}\n$error", + ), + AsyncData(:final value) => AlertDialog( + content: SizedBox( + child: SingleChildScrollView( + child: Column( + children: [ + Text(S.of(context).customEmojiLicensedBy), + MfmText( + mfmText: value.license ?? + S.of(context).customEmojiLicensedByNone, + ), + ], + ), ), ), - ), - actions: [ - OutlinedButton( + actions: [ + OutlinedButton( onPressed: () => Navigator.of(context).pop(false), - child: Text(S.of(context).cancelEmojiChoosing)), - ElevatedButton( + child: Text(S.of(context).cancelEmojiChoosing), + ), + ElevatedButton( onPressed: () => Navigator.of(context).pop(true), - child: Text(S.of(context).doneEmojiChoosing)) - ], - ), - ); + child: Text(S.of(context).doneEmojiChoosing), + ), + ], + ), + }; } } diff --git a/lib/view/photo_edit_page/license_confirm_dialog.g.dart b/lib/view/photo_edit_page/license_confirm_dialog.g.dart new file mode 100644 index 000000000..5e60edf04 --- /dev/null +++ b/lib/view/photo_edit_page/license_confirm_dialog.g.dart @@ -0,0 +1,205 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'license_confirm_dialog.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$emojiHash() => r'28b9523a2b6115cce022c78dd796700ff24d3bac'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [_emoji]. +@ProviderFor(_emoji) +const _emojiProvider = _EmojiFamily(); + +/// See also [_emoji]. +class _EmojiFamily extends Family { + /// See also [_emoji]. + const _EmojiFamily(); + + static final Iterable _dependencies = [ + misskeyPostContextProvider + ]; + + static final Iterable _allTransitiveDependencies = + { + misskeyPostContextProvider, + ...?misskeyPostContextProvider.allTransitiveDependencies + }; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'_emojiProvider'; + + /// See also [_emoji]. + _EmojiProvider call( + String emoji, + ) { + return _EmojiProvider( + emoji, + ); + } + + @visibleForOverriding + @override + _EmojiProvider getProviderOverride( + covariant _EmojiProvider provider, + ) { + return call( + provider.emoji, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith( + FutureOr Function(_EmojiRef ref) create) { + return _$EmojiFamilyOverride(this, create); + } +} + +class _$EmojiFamilyOverride implements FamilyOverride { + _$EmojiFamilyOverride(this.overriddenFamily, this.create); + + final FutureOr Function(_EmojiRef ref) create; + + @override + final _EmojiFamily overriddenFamily; + + @override + _EmojiProvider getProviderOverride( + covariant _EmojiProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [_emoji]. +class _EmojiProvider extends AutoDisposeFutureProvider { + /// See also [_emoji]. + _EmojiProvider( + String emoji, + ) : this._internal( + (ref) => _emoji( + ref as _EmojiRef, + emoji, + ), + from: _emojiProvider, + name: r'_emojiProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$emojiHash, + dependencies: _EmojiFamily._dependencies, + allTransitiveDependencies: _EmojiFamily._allTransitiveDependencies, + emoji: emoji, + ); + + _EmojiProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.emoji, + }) : super.internal(); + + final String emoji; + + @override + Override overrideWith( + FutureOr Function(_EmojiRef ref) create, + ) { + return ProviderOverride( + origin: this, + override: _EmojiProvider._internal( + (ref) => create(ref as _EmojiRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + emoji: emoji, + ), + ); + } + + @override + (String,) get argument { + return (emoji,); + } + + @override + AutoDisposeFutureProviderElement createElement() { + return _EmojiProviderElement(this); + } + + _EmojiProvider _copyWith( + FutureOr Function(_EmojiRef ref) create, + ) { + return _EmojiProvider._internal( + (ref) => create(ref as _EmojiRef), + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + emoji: emoji, + ); + } + + @override + bool operator ==(Object other) { + return other is _EmojiProvider && other.emoji == emoji; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, emoji.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin _EmojiRef on AutoDisposeFutureProviderRef { + /// The parameter `emoji` of this provider. + String get emoji; +} + +class _EmojiProviderElement + extends AutoDisposeFutureProviderElement with _EmojiRef { + _EmojiProviderElement(super.provider); + + @override + String get emoji => (origin as _EmojiProvider).emoji; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/photo_edit_page/photo_edit_bottom_bar.dart b/lib/view/photo_edit_page/photo_edit_bottom_bar.dart index 1bbfed8e0..5c97b358e 100644 --- a/lib/view/photo_edit_page/photo_edit_bottom_bar.dart +++ b/lib/view/photo_edit_page/photo_edit_bottom_bar.dart @@ -1,19 +1,20 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/state_notifier/photo_edit_page/photo_edit_state_notifier.dart"; class PhotoEditBottomBar extends ConsumerWidget { const PhotoEditBottomBar({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final photoEdit = ref.read(photoEditProvider.notifier); + final photoEdit = ref.read(photoEditStateNotifierProvider.notifier); - final isClipMode = - ref.watch(photoEditProvider.select((value) => value.clipMode)); - final isColorFilterMode = - ref.watch(photoEditProvider.select((value) => value.colorFilterMode)); + final isClipMode = ref.watch( + photoEditStateNotifierProvider.select((value) => value.clipMode), + ); + final isColorFilterMode = ref.watch( + photoEditStateNotifierProvider.select((value) => value.colorFilterMode), + ); return BottomAppBar( child: Row( @@ -22,28 +23,31 @@ class PhotoEditBottomBar extends ConsumerWidget { children: [ DecoratedBox( decoration: BoxDecoration( - color: isClipMode ? Theme.of(context).primaryColorDark : null), + color: isClipMode ? Theme.of(context).primaryColorDark : null, + ), child: IconButton( - onPressed: () => photoEdit.crop(), - icon: const Icon(Icons.crop, color: Colors.white)), + onPressed: () async => photoEdit.crop(), + icon: const Icon(Icons.crop, color: Colors.white), + ), ), IconButton( - onPressed: () => photoEdit.rotate(), - icon: const Icon(Icons.refresh, color: Colors.white)), + onPressed: () async => photoEdit.rotate(), + icon: const Icon(Icons.refresh, color: Colors.white), + ), DecoratedBox( decoration: BoxDecoration( - color: isColorFilterMode - ? Theme.of(context).primaryColorDark - : null), + color: + isColorFilterMode ? Theme.of(context).primaryColorDark : null, + ), child: IconButton( - onPressed: () => photoEdit.colorFilter(), - icon: const Icon(Icons.palette_outlined, color: Colors.white)), + onPressed: () async => photoEdit.colorFilter(), + icon: const Icon(Icons.palette_outlined, color: Colors.white), + ), ), IconButton( - onPressed: () => - photoEdit.addReaction(AccountScope.of(context), context), - icon: - const Icon(Icons.add_reaction_outlined, color: Colors.white)), + onPressed: () async => photoEdit.addReaction(), + icon: const Icon(Icons.add_reaction_outlined, color: Colors.white), + ), ], ), ); diff --git a/lib/view/photo_edit_page/photo_edit_page.dart b/lib/view/photo_edit_page/photo_edit_page.dart index 9dbdb8255..68f9163cd 100644 --- a/lib/view/photo_edit_page/photo_edit_page.dart +++ b/lib/view/photo_edit_page/photo_edit_page.dart @@ -1,27 +1,26 @@ -import 'dart:typed_data'; +import "dart:typed_data"; -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/image_file.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/state_notifier/photo_edit_page/photo_edit_state_notifier.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; -import 'package:miria/view/photo_edit_page/clip_mode.dart'; -import 'package:miria/view/photo_edit_page/color_filter_image_preview.dart'; -import 'package:miria/view/photo_edit_page/photo_edit_bottom_bar.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/image_file.dart"; +import "package:miria/providers.dart"; +import "package:miria/state_notifier/photo_edit_page/photo_edit_state_notifier.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/dialogs/simple_confirm_dialog.dart"; +import "package:miria/view/photo_edit_page/clip_mode.dart"; +import "package:miria/view/photo_edit_page/color_filter_image_preview.dart"; +import "package:miria/view/photo_edit_page/photo_edit_bottom_bar.dart"; @RoutePage() -class PhotoEditPage extends ConsumerStatefulWidget { - final Account account; +class PhotoEditPage extends ConsumerStatefulWidget implements AutoRouteWrapper { + final AccountContext accountContext; final MisskeyPostFile file; final void Function(Uint8List) onSubmit; const PhotoEditPage({ - required this.account, + required this.accountContext, required this.file, required this.onSubmit, super.key, @@ -29,10 +28,15 @@ class PhotoEditPage extends ConsumerStatefulWidget { @override ConsumerState createState() => PhotoEditPageState(); + + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); } class PhotoEditPageState extends ConsumerState { - PhotoEditStateNotifier get photoEdit => ref.read(photoEditProvider.notifier); + PhotoEditStateNotifier get photoEdit => + ref.read(photoEditStateNotifierProvider.notifier); final renderingAreaKey = GlobalKey(); @@ -44,66 +48,70 @@ class PhotoEditPageState extends ConsumerState { @override Widget build(BuildContext context) { - return AccountScope( - account: widget.account, - child: PopScope( - canPop: false, - child: Scaffold( - appBar: AppBar( - title: Text(S.of(context).editPhoto), - leading: IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: const Icon(Icons.arrow_back_ios_new), - ), - actions: [ - IconButton( - onPressed: () async { - photoEdit.clearSelectMode(); - final confirm = await SimpleConfirmDialog.show( - context: context, - message: S.of(context).confirmSavingPhoto, - primary: S.of(context).doneEditingPhoto, - secondary: S.of(context).continueEditingPhoto); - - final result = - await photoEdit.createSaveData(renderingAreaKey); - if (result == null) return; - if (!mounted) return; - if (!mounted) return; - if (confirm == true) { - widget.onSubmit(result); - context.back(); - } - }, - icon: const Icon(Icons.save)) - ], + return PopScope( + canPop: false, + child: Scaffold( + appBar: AppBar( + title: Text(S.of(context).editPhoto), + leading: IconButton( + onPressed: () => Navigator.of(context).pop(), + icon: const Icon(Icons.arrow_back_ios_new), ), - body: Column( - mainAxisSize: MainAxisSize.max, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - child: LayoutBuilder(builder: (context, constraints) { + actions: [ + IconButton( + onPressed: () async { + await photoEdit.clearSelectMode(); + if (!context.mounted) return; + final confirm = await SimpleConfirmDialog.show( + context: context, + message: S.of(context).confirmSavingPhoto, + primary: S.of(context).doneEditingPhoto, + secondary: S.of(context).continueEditingPhoto, + ); + + final result = await photoEdit.createSaveData(renderingAreaKey); + if (result == null) return; + if (!mounted) return; + if (confirm == true) { + widget.onSubmit(result); + if (!context.mounted) return; + Navigator.of(context).pop(); + } + }, + icon: const Icon(Icons.save), + ), + ], + ), + body: Column( + mainAxisSize: MainAxisSize.max, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + child: LayoutBuilder( + builder: (context, constraints) { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { photoEdit.decideDrawArea( - Size(constraints.maxWidth, constraints.maxHeight)); + Size(constraints.maxWidth, constraints.maxHeight), + ); }); return SizedBox( - width: constraints.maxWidth, - height: constraints.maxHeight, - child: FittedBox( - fit: BoxFit.contain, - child: ClipMode( - renderingGlobalKey: renderingAreaKey, - ))); - }), + width: constraints.maxWidth, + height: constraints.maxHeight, + child: FittedBox( + fit: BoxFit.contain, + child: ClipMode( + renderingGlobalKey: renderingAreaKey, + ), + ), + ); + }, ), - const ColorFilterImagePreview(), - ], - ), - bottomNavigationBar: const PhotoEditBottomBar(), + ), + const ColorFilterImagePreview(), + ], ), + bottomNavigationBar: const PhotoEditBottomBar(), ), ); } diff --git a/lib/view/reaction_picker_dialog/reaction_picker_content.dart b/lib/view/reaction_picker_dialog/reaction_picker_content.dart index d301f9a8d..967ed7b69 100644 --- a/lib/view/reaction_picker_dialog/reaction_picker_content.dart +++ b/lib/view/reaction_picker_dialog/reaction_picker_content.dart @@ -1,26 +1,25 @@ -import 'dart:async'; - -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/repository/emoji_repository.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/custom_emoji.dart'; -import 'package:miria/view/dialogs/simple_message_dialog.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:visibility_detector/visibility_detector.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "dart:async"; + +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/emoji_repository.dart"; +import "package:miria/view/common/misskey_notes/custom_emoji.dart"; +import "package:miria/view/dialogs/simple_message_dialog.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:visibility_detector/visibility_detector.dart"; class ReactionPickerContent extends ConsumerStatefulWidget { final FutureOr Function(MisskeyEmojiData emoji) onTap; final bool isAcceptSensitive; const ReactionPickerContent({ - super.key, required this.onTap, required this.isAcceptSensitive, + super.key, }); @override @@ -30,8 +29,9 @@ class ReactionPickerContent extends ConsumerStatefulWidget { class ReactionPickerContentState extends ConsumerState { final categoryList = []; - EmojiRepository get emojiRepository => - ref.read(emojiRepositoryProvider(AccountScope.of(context))); + EmojiRepository get emojiRepository => ref.read( + emojiRepositoryProvider(ref.read(accountContextProvider).getAccount), + ); @override void didChangeDependencies() { @@ -39,12 +39,14 @@ class ReactionPickerContentState extends ConsumerState { categoryList ..clear() - ..addAll(emojiRepository.emoji - ?.map((e) => e.category) - .toSet() - .toList() - .whereNotNull() ?? - []); + ..addAll( + emojiRepository.emoji + ?.map((e) => e.category) + .toSet() + .toList() + .whereNotNull() ?? + [], + ); } @override @@ -73,20 +75,20 @@ class ReactionPickerContentState extends ConsumerState { crossAxisAlignment: WrapCrossAlignment.start, children: [ for (final emoji in (emojiRepository.emoji ?? []).where( - (element) => - element.category == categoryList[index])) + (element) => element.category == categoryList[index], + )) EmojiButton( emoji: emoji.emoji, onTap: widget.onTap, isAcceptSensitive: widget.isAcceptSensitive, - ) + ), ], ), ), - ) + ), ], ), - ) + ), ], ), ); @@ -100,11 +102,11 @@ class EmojiButton extends ConsumerStatefulWidget { final bool isAcceptSensitive; const EmojiButton({ - super.key, required this.emoji, required this.onTap, - this.isForceVisible = false, required this.isAcceptSensitive, + super.key, + this.isForceVisible = false, }); @override @@ -134,17 +136,19 @@ class EmojiButtonState extends ConsumerState { : const BoxDecoration(), child: ElevatedButton( style: ButtonStyle( - backgroundColor: MaterialStatePropertyAll(Colors.transparent), - padding: MaterialStatePropertyAll(EdgeInsets.all(5)), - elevation: MaterialStatePropertyAll(0), - minimumSize: MaterialStatePropertyAll(Size.zero), - overlayColor: MaterialStatePropertyAll(AppTheme.of(context).colorTheme.accentedBackground), + backgroundColor: const WidgetStatePropertyAll(Colors.transparent), + padding: const WidgetStatePropertyAll(EdgeInsets.all(5)), + elevation: const WidgetStatePropertyAll(0), + minimumSize: const WidgetStatePropertyAll(Size.zero), + overlayColor: WidgetStatePropertyAll( + AppTheme.of(context).colorTheme.accentedBackground, + ), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), onPressed: () async { if (!isVisibility) return; if (disabled) { - SimpleMessageDialog.show( + await SimpleMessageDialog.show( context, S.of(context).disabledUsingSensitiveCustomEmoji, ); @@ -172,49 +176,50 @@ class EmojiSearch extends ConsumerStatefulWidget { final bool isAcceptSensitive; const EmojiSearch({ - super.key, required this.onTap, required this.isAcceptSensitive, + super.key, }); @override - ConsumerState createState() => - EmojiSearchState(); + ConsumerState createState() => EmojiSearchState(); } class EmojiSearchState extends ConsumerState { final emojis = []; - EmojiRepository get emojiRepository => - ref.read(emojiRepositoryProvider(AccountScope.of(context))); + EmojiRepository get emojiRepository => ref.read( + emojiRepositoryProvider(ref.read(accountContextProvider).getAccount), + ); @override void didChangeDependencies() { super.didChangeDependencies(); - emojis.clear(); - emojis.addAll(emojiRepository.defaultEmojis().toList()); + emojis + ..clear() + ..addAll(emojiRepository.defaultEmojis().toList()); } @override Widget build(BuildContext context) { - return Column(children: [ - TextField( - decoration: const InputDecoration(prefixIcon: Icon(Icons.search)), - autofocus: true, - keyboardType: TextInputType.emailAddress, - onChanged: (value) { - Future(() async { - final result = await emojiRepository.searchEmojis(value); - if (!mounted) return; - setState(() { - emojis.clear(); - emojis.addAll(result); + return Column( + children: [ + TextField( + decoration: const InputDecoration(prefixIcon: Icon(Icons.search)), + autofocus: true, + onChanged: (value) { + Future(() async { + final result = await emojiRepository.searchEmojis(value); + if (!mounted) return; + setState(() { + emojis.clear(); + emojis.addAll(result); + }); }); - }); - }, - ), - const Padding(padding: EdgeInsets.only(top: 10)), - Align( + }, + ), + const Padding(padding: EdgeInsets.only(top: 10)), + Align( alignment: Alignment.topLeft, child: Wrap( spacing: 5, @@ -227,9 +232,11 @@ class EmojiSearchState extends ConsumerState { onTap: widget.onTap, isForceVisible: true, isAcceptSensitive: widget.isAcceptSensitive, - ) + ), ], - )) - ]); + ), + ), + ], + ); } } diff --git a/lib/view/reaction_picker_dialog/reaction_picker_dialog.dart b/lib/view/reaction_picker_dialog/reaction_picker_dialog.dart index c4b08cf05..b3ee29049 100644 --- a/lib/view/reaction_picker_dialog/reaction_picker_dialog.dart +++ b/lib/view/reaction_picker_dialog/reaction_picker_dialog.dart @@ -1,43 +1,37 @@ -import 'package:flutter/material.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/view/reaction_picker_dialog/reaction_picker_content.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/reaction_picker_dialog/reaction_picker_content.dart"; -class ReactionPickerDialog extends ConsumerStatefulWidget { +@RoutePage() +class ReactionPickerDialog extends ConsumerWidget implements AutoRouteWrapper { final Account account; final bool isAcceptSensitive; const ReactionPickerDialog({ - super.key, required this.account, required this.isAcceptSensitive, + super.key, }); @override - ConsumerState createState() => - _ReactionPickerDialogState(); -} - -class _ReactionPickerDialogState extends ConsumerState { - @override - void initState() { - super.initState(); - } + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: account, child: this); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return AlertDialog( contentPadding: const EdgeInsets.all(5), - content: AccountScope( - account: widget.account, - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.9, - height: MediaQuery.of(context).size.height * 0.9, - child: ReactionPickerContent( - isAcceptSensitive: widget.isAcceptSensitive, - onTap: (emoji) => Navigator.of(context).pop(emoji), - )), + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.9, + height: MediaQuery.of(context).size.height * 0.9, + child: ReactionPickerContent( + isAcceptSensitive: isAcceptSensitive, + onTap: (emoji) async => context.maybePop(emoji), + ), ), ); } diff --git a/lib/view/search_page/note_search.dart b/lib/view/search_page/note_search.dart index 1f2f6fe37..46f214df8 100644 --- a/lib/view/search_page/note_search.dart +++ b/lib/view/search_page/note_search.dart @@ -1,21 +1,18 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mfm_parser/mfm_parser.dart'; -import 'package:miria/model/note_search_condition.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:miria/view/settings_page/tab_settings_page/channel_select_dialog.dart'; -import 'package:miria/view/user_page/user_list_item.dart'; -import 'package:miria/view/user_select_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:mfm_parser/mfm_parser.dart"; +import "package:miria/model/note_search_condition.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:miria/view/user_page/user_list_item.dart"; +import "package:misskey_dart/misskey_dart.dart"; -final noteSearchProvider = - StateProvider.autoDispose((ref) => const NoteSearchCondition()); - -class NoteSearch extends ConsumerStatefulWidget { +class NoteSearch extends HookConsumerWidget { final NoteSearchCondition? initialCondition; final FocusNode? focusNode; @@ -26,37 +23,17 @@ class NoteSearch extends ConsumerStatefulWidget { }); @override - ConsumerState createState() => NoteSearchState(); -} - -class NoteSearchState extends ConsumerState { - var isDetail = false; - late final controller = TextEditingController( - text: widget.initialCondition?.query, - ); - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final initial = widget.initialCondition; - if (initial != null) { - Future(() { - ref.read(noteSearchProvider.notifier).state = initial; - }); - } - } + Widget build(BuildContext context, WidgetRef ref) { + final conditionController = + useTextEditingController(text: initialCondition?.query); + final searchQuery = useState(""); + final selectedUser = useState(initialCondition?.user); + final selectedChannel = useState(initialCondition?.channel); + final localOnly = useState(initialCondition?.localOnly ?? false); + final isDetail = useState(false); - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final condition = ref.watch(noteSearchProvider); - final selectedUser = condition.user; - final selectedChannel = condition.channel; + final selectedUserValue = selectedUser.value; + final selectedChannelValue = selectedChannel.value; return Column( children: [ @@ -66,33 +43,26 @@ class NoteSearchState extends ConsumerState { children: [ Expanded( child: TextField( - controller: controller, + controller: conditionController, decoration: const InputDecoration( prefixIcon: Icon(Icons.search), ), - focusNode: widget.focusNode, + focusNode: focusNode, autofocus: true, textInputAction: TextInputAction.done, - onSubmitted: (value) { - ref.read(noteSearchProvider.notifier).state = - condition.copyWith(query: value); - }, + onSubmitted: (value) => searchQuery.value = value, ), ), IconButton( - onPressed: () { - setState(() { - isDetail = !isDetail; - }); - }, - icon: isDetail + onPressed: () => isDetail.value = !isDetail.value, + icon: isDetail.value ? const Icon(Icons.keyboard_arrow_up) : const Icon(Icons.keyboard_arrow_down), ), ], ), ), - if (isDetail) + if (isDetail.value) SizedBox( width: double.infinity, child: Padding( @@ -117,38 +87,37 @@ class NoteSearchState extends ConsumerState { defaultVerticalAlignment: TableCellVerticalAlignment.middle, children: [ - TableRow(children: [ - Text(S.of(context).user), - Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisSize: MainAxisSize.max, - children: [ - Expanded( - child: selectedUser == null - ? Container() - : UserListItem(user: selectedUser), - ), - IconButton( + TableRow( + children: [ + Text(S.of(context).user), + Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + Expanded( + child: selectedUserValue == null + ? Container() + : UserListItem(user: selectedUserValue), + ), + IconButton( onPressed: () async { - final selected = await showDialog( - context: context, - builder: (context2) => UserSelectDialog( - account: AccountScope.of(context), + final selected = + await context.pushRoute( + UserSelectRoute( + accountContext: + ref.read(accountContextProvider), ), ); - - ref - .read(noteSearchProvider.notifier) - .state = condition.copyWith( - user: selected, - ); + selectedUser.value = selected; }, icon: - const Icon(Icons.keyboard_arrow_right)) - ], - ) - ]), + const Icon(Icons.keyboard_arrow_right), + ), + ], + ), + ], + ), TableRow( children: [ Text(S.of(context).channel), @@ -158,27 +127,21 @@ class NoteSearchState extends ConsumerState { mainAxisSize: MainAxisSize.max, children: [ Expanded( - child: selectedChannel == null + child: selectedChannelValue == null ? Container() - : Text(selectedChannel.name), + : Text(selectedChannelValue.name), ), IconButton( onPressed: () async { - final selected = - await showDialog( - context: context, - builder: (context2) => - ChannelSelectDialog( - account: AccountScope.of( - context, - ), + final selected = await context + .pushRoute( + ChannelSelectRoute( + account: ref + .read(accountContextProvider) + .postAccount, ), ); - ref - .read(noteSearchProvider.notifier) - .state = condition.copyWith( - channel: selected, - ); + selectedChannel.value = selected; }, icon: const Icon(Icons.keyboard_arrow_right), @@ -193,14 +156,9 @@ class NoteSearchState extends ConsumerState { Row( children: [ Checkbox( - value: condition.localOnly, - onChanged: (value) => ref - .read( - noteSearchProvider.notifier, - ) - .state = condition.copyWith( - localOnly: !condition.localOnly, - ), + value: localOnly.value, + onChanged: (value) => + localOnly.value = value ?? false, ), ], ), @@ -214,10 +172,15 @@ class NoteSearchState extends ConsumerState { ), ), ), - const Expanded( + Expanded( child: Padding( - padding: EdgeInsets.only(right: 10), - child: NoteSearchList(), + padding: const EdgeInsets.only(right: 10), + child: NoteSearchList( + query: searchQuery.value, + localOnly: localOnly.value, + channelId: selectedChannel.value?.id, + userId: selectedUser.value?.id, + ), ), ), ], @@ -226,65 +189,72 @@ class NoteSearchState extends ConsumerState { } class NoteSearchList extends ConsumerWidget { - const NoteSearchList({super.key}); + final String query; + final bool localOnly; + final String? channelId; + final String? userId; + + const NoteSearchList({ + required this.query, + required this.localOnly, + super.key, + this.channelId, + this.userId, + }); @override Widget build(BuildContext context, WidgetRef ref) { - final condition = ref.watch(noteSearchProvider); - final account = AccountScope.of(context); - final parsedSearchValue = const MfmParser().parse(condition.query ?? ""); + final parsedSearchValue = const MfmParser().parse(query); final isHashtagOnly = parsedSearchValue.length == 1 && parsedSearchValue[0] is MfmHashTag; - if (condition.isEmpty) { - return Container(); - } + if (query.isEmpty) return const SizedBox.shrink(); return PushableListView( - listKey: condition.hashCode, + listKey: Object.hash(query, localOnly, channelId, userId), initializeFuture: () async { final Iterable notes; if (isHashtagOnly) { - notes = await ref.read(misskeyProvider(account)).notes.searchByTag( + notes = await ref.read(misskeyGetContextProvider).notes.searchByTag( NotesSearchByTagRequest( tag: (parsedSearchValue[0] as MfmHashTag).hashTag, ), ); } else { - notes = await ref.read(misskeyProvider(account)).notes.search( + notes = await ref.read(misskeyGetContextProvider).notes.search( NotesSearchRequest( - query: condition.query ?? "", - userId: condition.user?.id, - channelId: condition.channel?.id, - host: condition.localOnly ? "." : null, + query: query, + userId: userId, + channelId: channelId, + host: localOnly ? "." : null, ), ); } - ref.read(notesProvider(account)).registerAll(notes); + ref.read(notesWithProvider).registerAll(notes); return notes.toList(); }, nextFuture: (lastItem, _) async { final Iterable notes; if (isHashtagOnly) { - notes = await ref.read(misskeyProvider(account)).notes.searchByTag( + notes = await ref.read(misskeyGetContextProvider).notes.searchByTag( NotesSearchByTagRequest( tag: (parsedSearchValue[0] as MfmHashTag).hashTag, untilId: lastItem.id, ), ); } else { - notes = await ref.read(misskeyProvider(account)).notes.search( + notes = await ref.read(misskeyGetContextProvider).notes.search( NotesSearchRequest( - query: condition.query ?? "", - userId: condition.user?.id, - channelId: condition.channel?.id, - host: condition.localOnly ? "." : null, + query: query, + userId: userId, + channelId: channelId, + host: localOnly ? "." : null, untilId: lastItem.id, ), ); } - ref.read(notesProvider(account)).registerAll(notes); + ref.read(notesWithProvider).registerAll(notes); return notes.toList(); }, itemBuilder: (context, item) { diff --git a/lib/view/search_page/search_page.dart b/lib/view/search_page/search_page.dart index 1f09f4cdc..0a550ad1a 100644 --- a/lib/view/search_page/search_page.dart +++ b/lib/view/search_page/search_page.dart @@ -1,14 +1,15 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/note_search_condition.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/view/search_page/note_search.dart'; -import 'package:miria/view/user_select_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/note_search_condition.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/search_page/note_search.dart"; +import "package:miria/view/user_select_dialog.dart"; +import "package:misskey_dart/misskey_dart.dart"; final noteSearchProvider = StateProvider.autoDispose((ref) => ""); final noteSearchUserProvider = StateProvider.autoDispose((ref) => null); @@ -18,91 +19,64 @@ final noteSearchChannelProvider = final noteSearchLocalOnlyProvider = StateProvider.autoDispose((ref) => false); @RoutePage() -class SearchPage extends ConsumerStatefulWidget { +class SearchPage extends HookConsumerWidget implements AutoRouteWrapper { final NoteSearchCondition? initialNoteSearchCondition; - final Account account; + final AccountContext accountContext; const SearchPage({ + required this.accountContext, super.key, this.initialNoteSearchCondition, - required this.account, }); @override - ConsumerState createState() => SearchPageState(); -} - -class SearchPageState extends ConsumerState { - late final List focusNodes; - int tabIndex = 0; - - @override - void initState() { - super.initState(); - focusNodes = [FocusNode(), FocusNode()]; - } + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); @override - void dispose() { - for (final focusNode in focusNodes) { - focusNode.dispose(); - } - super.dispose(); - } + Widget build(BuildContext context, WidgetRef ref) { + final focusNodes = [useFocusNode(), useFocusNode()]; + final tabController = useTabController(initialLength: 2); + final tabIndex = useState(0); + tabController.addListener(() { + if (tabController.index != tabIndex.value) { + focusNodes[tabController.index].requestFocus(); + tabIndex.value = tabController.index; + } + }); - @override - Widget build(BuildContext context) { - return DefaultTabController( - length: 2, - child: AccountScope( - account: widget.account, - child: Scaffold( - appBar: AppBar( - title: Text(S.of(context).search), - bottom: TabBar( - tabs: [ - Tab(text: S.of(context).note), - Tab(text: S.of(context).user), - ], - ), + return Scaffold( + appBar: AppBar( + title: Text(S.of(context).search), + bottom: TabBar( + controller: tabController, + tabs: [ + Tab(text: S.of(context).note), + Tab(text: S.of(context).user), + ], + ), + ), + body: TabBarView( + controller: tabController, + children: [ + NoteSearch( + initialCondition: initialNoteSearchCondition, + focusNode: focusNodes[0], ), - body: Builder( - builder: (context) { - final tabController = DefaultTabController.of(context); - tabController.addListener(() { - if (tabController.index != tabIndex) { - focusNodes[tabController.index].requestFocus(); - setState(() { - tabIndex = tabController.index; - }); - } - }); - return TabBarView( - controller: tabController, - children: [ - NoteSearch( - initialCondition: widget.initialNoteSearchCondition, - focusNode: focusNodes[0], - ), - Padding( - padding: - const EdgeInsets.only(left: 10, right: 10, top: 10), - child: UserSelectContent( - focusNode: focusNodes[1], - isDetail: true, - onSelected: (item) => context.pushRoute( - UserRoute( - userId: item.id, - account: widget.account, - ), - ), - ), - ), - ], - ); - }, + Padding( + padding: const EdgeInsets.only(left: 10, right: 10, top: 10), + child: UserSelectContent( + focusNode: focusNodes[1], + isDetail: true, + onSelected: (item) async => context.pushRoute( + UserRoute( + userId: item.id, + accountContext: ref.read(accountContextProvider), + ), + ), + ), ), - ), + ], ), ); } diff --git a/lib/view/server_detail_dialog.dart b/lib/view/server_detail_dialog.dart index 0cd784e61..b4486f1d7 100644 --- a/lib/view/server_detail_dialog.dart +++ b/lib/view/server_detail_dialog.dart @@ -1,138 +1,135 @@ -import 'dart:math'; +import "dart:async"; +import "dart:math"; -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:fl_chart/fl_chart.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/constants.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:collection/collection.dart"; +import "package:fl_chart/fl_chart.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/socket_timeline_repository.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/constants.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; +import "package:uuid/uuid.dart"; -class ServerDetailDialog extends ConsumerStatefulWidget { - //TODO: 本当はサーバー情報取るのにアカウントいらない... - final Account account; +part "server_detail_dialog.g.dart"; - const ServerDetailDialog({ - super.key, - required this.account, - }); +@Riverpod(dependencies: [misskeyGetContext]) +Future _onlineCounts(_OnlineCountsRef ref) async { + final onlineUserCountsResponse = + await ref.read(misskeyGetContextProvider).getOnlineUsersCount(); + return onlineUserCountsResponse.count; +} - @override - ConsumerState createState() => - ServerDetailDialogState(); +@Riverpod(dependencies: [misskeyGetContext]) +Future _totalMemories(_TotalMemoriesRef ref) async { + final serverInfoResponse = + await ref.read(misskeyGetContextProvider).serverInfo(); + return serverInfoResponse.mem.total; } -class ServerDetailDialogState extends ConsumerState { - SocketController? controller; - SocketController? queueController; - int? onlineUsers; - int? totalMemories; - int? ping; +@Riverpod(dependencies: [misskeyGetContext]) +Future _ping(_PingRef ref) async { + final sendDate = DateTime.now(); + final pingResponse = await ref.read(misskeyGetContextProvider).ping(); + + return pingResponse.pong - sendDate.millisecondsSinceEpoch; +} - List logged = []; - List queueLogged = []; +@RoutePage() +class ServerDetailDialog extends HookConsumerWidget + implements AutoRouteWrapper { + final AccountContext accountContext; + + const ServerDetailDialog({ + required this.accountContext, + super.key, + }); @override - void dispose() { - super.dispose(); - controller?.disconnect(); - queueController?.disconnect(); + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); + + String format(double value) { + return ((value * 10000).toInt() / 100).toString(); } @override - void didChangeDependencies() { - final misskey = ref.read(misskeyProvider(widget.account)); - super.didChangeDependencies(); - controller?.disconnect(); - controller = misskey.serverStatsLogStream( - (response) => setState(() { - logged.insertAll(0, response); - }), - (response) { - setState(() { - logged.add(response); - }); - }, - ); - queueController?.disconnect(); - queueController = misskey.queueStatsLogStream( - (response) => setState(() { - queueLogged.insertAll(0, response); - }), - (response) { - setState(() { - queueLogged.add(response); - }); - }, - ); - misskey.startStreaming(); - - Future(() async { - try { - final onlineUserCountsResponse = await ref - .read(misskeyProvider(widget.account)) - .getOnlineUsersCount(); + Widget build(BuildContext context, WidgetRef ref) { + final onlineUsers = ref.watch(_onlineCountsProvider).valueOrNull; + final totalMemories = ref.watch(_totalMemoriesProvider).valueOrNull; + final ping = ref.watch(_pingProvider).valueOrNull; - if (!mounted) return; - setState(() { - onlineUsers = onlineUserCountsResponse.count; - }); - } catch (e) { - //TODO - } - try { - final serverInfoResponse = - await ref.read(misskeyProvider(widget.account)).serverInfo(); - if (!mounted) return; - setState(() { - totalMemories = serverInfoResponse.mem.total; - }); - } catch (e) { - //TODO - } + final logged = useState([]); + final queueLogged = useState([]); - await refreshPing(); - }); - } + final currentStat = logged.value.lastOrNull; + final currentQueueStats = queueLogged.value.lastOrNull; + final queueId = useMemoized(() => const Uuid().v4()); + final statsId = useMemoized(() => const Uuid().v4()); - Future refreshPing() async { - try { - final sendDate = DateTime.now(); - final pingResponse = - await ref.read(misskeyProvider(widget.account)).ping(); + useEffect( + () { + final misskey = ref.read(misskeyGetContextProvider); + StreamSubscription? serverStats; + StreamSubscription? jobQueue; + StreamingController? streaming; + unawaited(() async { + streaming = await ref.read(misskeyStreamingProvider(misskey).future); + jobQueue = + streaming!.queueStatsLogStream(id: queueId).listen((response) { + final body = response.body; + if (body is! StatsChannelEvent) return; + final innerBody = body.body; + if (innerBody is! JobQueueResponse) return; + queueLogged.value = [...queueLogged.value, innerBody]; + }); - if (!mounted) return; - setState(() { - ping = pingResponse.pong - sendDate.millisecondsSinceEpoch; - }); - } catch (e) { - //TODO - } - } + serverStats = + streaming!.serverStatsLogStream(id: statsId).listen((response) { + final body = response.body; + if (body is! StatsChannelEvent) return; + final innerBody = body.body; + if (innerBody is! ServerMetricsResponse) return; + logged.value = [...logged.value, innerBody]; + }); + }()); - String format(double value) { - return ((value * 10000).toInt() / 100).toString(); - } + return () { + unawaited(() async { + await ( + streaming?.removeChannel(queueId) ?? Future.value(), + streaming?.removeChannel(statsId) ?? Future.value(), + jobQueue?.cancel() ?? Future.value(), + serverStats?.cancel() ?? Future.value(), + ).wait; + }()); + }; + }, + const [], + ); - @override - Widget build(BuildContext context) { - final currentStat = logged.lastOrNull; - final currentQueueStats = queueLogged.lastOrNull; return AlertDialog( title: Row( children: [ - Expanded(child: Text(widget.account.host)), + Expanded(child: Text(accountContext.getAccount.host)), IconButton( - onPressed: () { - Navigator.of(context).pop(); - context.pushRoute(FederationRoute( - account: widget.account, host: widget.account.host)); - }, - icon: const Icon(Icons.keyboard_arrow_right)) + onPressed: () async { + Navigator.of(context).pop(); + await context.pushRoute( + FederationRoute( + accountContext: ref.read(accountContextProvider), + host: accountContext.getAccount.host, + ), + ); + }, + icon: const Icon(Icons.keyboard_arrow_right), + ), ], ), content: SizedBox( @@ -169,23 +166,33 @@ class ServerDetailDialogState extends ConsumerState { children: [ Text(S.of(context).cpuUsageRate), if (currentStat != null) - Text.rich(TextSpan(children: [ - TextSpan( - text: ((currentStat.cpu * 10000).toInt() / 100) - .toString(), - style: - Theme.of(context).textTheme.headlineSmall), + Text.rich( TextSpan( - text: " %", - style: Theme.of(context).textTheme.bodyMedium) - ])), - if (logged.isNotEmpty) + children: [ + TextSpan( + text: + ((currentStat.cpu * 10000).toInt() / 100) + .toString(), + style: + Theme.of(context).textTheme.headlineSmall, + ), + TextSpan( + text: " %", + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + if (logged.value.isNotEmpty) Chart( - data: logged - .skip(max(0, logged.length - 41)) - .mapIndexed((index, element) => - FlSpot(index.toDouble(), element.cpu)) - .toList()) + data: logged.value + .skip(max(0, logged.value.length - 41)) + .mapIndexed( + (index, element) => + FlSpot(index.toDouble(), element.cpu), + ) + .toList(), + ), ], ), ), @@ -201,62 +208,72 @@ class ServerDetailDialogState extends ConsumerState { TextSpan( children: [ TextSpan( - text: format( - currentStat.mem.used / totalMemories!), - style: Theme.of(context) - .textTheme - .headlineSmall), + text: format( + currentStat.mem.used / totalMemories, + ), + style: + Theme.of(context).textTheme.headlineSmall, + ), TextSpan( - text: " %", - style: - Theme.of(context).textTheme.bodyMedium) + text: " %", + style: Theme.of(context).textTheme.bodyMedium, + ), ], ), ), - if (totalMemories != null && logged.isNotEmpty) + if (totalMemories != null && logged.value.isNotEmpty) Chart( - data: logged - .skip(max(0, logged.length - 41)) - .mapIndexed((index, element) => FlSpot( - index.toDouble(), - element.mem.used / totalMemories!)) - .toList()) + data: logged.value + .skip(max(0, logged.value.length - 41)) + .mapIndexed( + (index, element) => FlSpot( + index.toDouble(), + element.mem.used / totalMemories, + ), + ) + .toList(), + ), ], ), - ) + ), ], ), const Padding(padding: EdgeInsets.only(top: 10)), Row( children: [ Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Text(S.of(context).responseTime), if (ping != null) Text.rich( TextSpan( children: [ TextSpan( - text: ping.format(), - style: Theme.of(context) - .textTheme - .headlineSmall), + text: ping.format(), + style: + Theme.of(context).textTheme.headlineSmall, + ), TextSpan( text: " ${S.of(context).milliSeconds}", style: Theme.of(context).textTheme.bodyMedium, ), WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: IconButton( - onPressed: () => refreshPing(), - icon: const Icon(Icons.refresh))) + alignment: PlaceholderAlignment.middle, + child: IconButton( + onPressed: () => + ref.invalidate(_pingProvider), + icon: const Icon(Icons.refresh), + ), + ), ], ), - ) - ])), + ), + ], + ), + ), Expanded(child: Container()), ], ), @@ -308,7 +325,7 @@ class ServerDetailDialogState extends ConsumerState { ), ), ], - ) + ), ], const Padding(padding: EdgeInsets.only(top: 10)), Text(S.of(context).deliverQueue), @@ -358,7 +375,7 @@ class ServerDetailDialogState extends ConsumerState { ), ), ], - ) + ), ], ], ), @@ -371,7 +388,7 @@ class ServerDetailDialogState extends ConsumerState { class Chart extends StatelessWidget { final List data; - const Chart({super.key, required this.data}); + const Chart({required this.data, super.key}); @override Widget build(BuildContext context) { @@ -379,13 +396,13 @@ class Chart extends StatelessWidget { height: 100, child: LineChart( LineChartData( - gridData: FlGridData( + gridData: const FlGridData( drawHorizontalLine: false, drawVerticalLine: false, ), - titlesData: FlTitlesData(show: false), + titlesData: const FlTitlesData(show: false), borderData: FlBorderData(show: false), - lineTouchData: LineTouchData(enabled: false), + lineTouchData: const LineTouchData(enabled: false), minX: 0, maxX: 40, minY: 0, @@ -398,10 +415,11 @@ class Chart extends StatelessWidget { Theme.of(context).textTheme.bodyMedium?.color?.withAlpha(200), barWidth: 4, belowBarData: BarAreaData( - show: true, - color: Theme.of(context).textTheme.bodyMedium?.color), - dotData: FlDotData(show: false), - ) + show: true, + color: Theme.of(context).textTheme.bodyMedium?.color, + ), + dotData: const FlDotData(show: false), + ), ], ), ), diff --git a/lib/view/server_detail_dialog.g.dart b/lib/view/server_detail_dialog.g.dart new file mode 100644 index 000000000..4712797d8 --- /dev/null +++ b/lib/view/server_detail_dialog.g.dart @@ -0,0 +1,62 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'server_detail_dialog.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$onlineCountsHash() => r'9b9bdbde8ef6371fcbd1fc821ffa4dd8a0b3cc45'; + +/// See also [_onlineCounts]. +@ProviderFor(_onlineCounts) +final _onlineCountsProvider = AutoDisposeFutureProvider.internal( + _onlineCounts, + name: r'_onlineCountsProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$onlineCountsHash, + dependencies: [misskeyGetContextProvider], + allTransitiveDependencies: { + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies + }, +); + +typedef _OnlineCountsRef = AutoDisposeFutureProviderRef; +String _$totalMemoriesHash() => r'1da96291809626dde75f32632d9a7bc6ee1d8cb6'; + +/// See also [_totalMemories]. +@ProviderFor(_totalMemories) +final _totalMemoriesProvider = AutoDisposeFutureProvider.internal( + _totalMemories, + name: r'_totalMemoriesProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$totalMemoriesHash, + dependencies: [misskeyGetContextProvider], + allTransitiveDependencies: { + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies + }, +); + +typedef _TotalMemoriesRef = AutoDisposeFutureProviderRef; +String _$pingHash() => r'd6ff1de136a88b171fa0d694ba12d34e5e6701a7'; + +/// See also [_ping]. +@ProviderFor(_ping) +final _pingProvider = AutoDisposeFutureProvider.internal( + _ping, + name: r'_pingProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$pingHash, + dependencies: [misskeyGetContextProvider], + allTransitiveDependencies: { + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies + }, +); + +typedef _PingRef = AutoDisposeFutureProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/settings_page/account_settings_page/account_list.dart b/lib/view/settings_page/account_settings_page/account_list.dart index 698bfab59..a25e477f2 100644 --- a/lib/view/settings_page/account_settings_page/account_list.dart +++ b/lib/view/settings_page/account_settings_page/account_list.dart @@ -1,13 +1,14 @@ -import 'dart:io'; +import "dart:io"; -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/avatar_icon.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/account_repository.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/avatar_icon.dart"; @RoutePage() class AccountListPage extends ConsumerWidget { @@ -22,9 +23,7 @@ class AccountListPage extends ConsumerWidget { leading: Container(), actions: [ IconButton( - onPressed: () { - context.pushRoute(const LoginRoute()); - }, + onPressed: () async => await context.pushRoute(const LoginRoute()), icon: const Icon(Icons.add), ), ], @@ -60,10 +59,13 @@ class AccountListPage extends ConsumerWidget { child: Padding( padding: const EdgeInsets.all(10), child: ElevatedButton( - onPressed: () { - context.router - ..removeWhere((route) => true) - ..push(const SplashRoute()); + onPressed: () async { + final newState = ref.refresh(accountsProvider); + print(newState); + final router = context.router..removeWhere((route) => true); + WidgetsBinding.instance.addPostFrameCallback((_) async { + await router.push(const SplashRoute()); + }); }, child: Text(S.of(context).quitAccountSettings), ), @@ -95,6 +97,14 @@ class AccountListItem extends ConsumerWidget { trailing: Row( mainAxisSize: MainAxisSize.min, children: [ + IconButton( + icon: const Icon(Icons.settings), + onPressed: () { + context.pushRoute( + SeveralAccountGeneralSettingsRoute(account: account), + ); + }, + ), IconButton( icon: const Icon(Icons.delete), onPressed: () { diff --git a/lib/view/settings_page/app_info_page/app_info_page.dart b/lib/view/settings_page/app_info_page/app_info_page.dart index a579dfd01..886aa7b60 100644 --- a/lib/view/settings_page/app_info_page/app_info_page.dart +++ b/lib/view/settings_page/app_info_page/app_info_page.dart @@ -1,42 +1,33 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:package_info_plus/package_info_plus.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:package_info_plus/package_info_plus.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; -@RoutePage() -class AppInfoPage extends ConsumerStatefulWidget { - const AppInfoPage({super.key}); - - @override - ConsumerState createState() => AppInfoPageState(); -} +part "app_info_page.g.dart"; -class AppInfoPageState extends ConsumerState { - PackageInfo? packageInfo; +@riverpod +Future packageInfo(PackageInfoRef ref) async => + await PackageInfo.fromPlatform(); - @override - void didChangeDependencies() { - super.didChangeDependencies(); - Future(() async { - packageInfo = await PackageInfo.fromPlatform(); - if (!mounted) return; - setState(() {}); - }); - } +@RoutePage() +class AppInfoPage extends ConsumerWidget { + const AppInfoPage({super.key}); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final packageInfo = ref.watch(packageInfoProvider).valueOrNull; return Scaffold( appBar: AppBar(title: Text(S.of(context).aboutMiria)), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(10), - child: AccountScope( - account: Account.demoAccount("", null), + child: AccountContextScope.as( + account: ref.read(accountsProvider).first, child: Column( children: [ MfmText( diff --git a/lib/view/settings_page/app_info_page/app_info_page.g.dart b/lib/view/settings_page/app_info_page/app_info_page.g.dart new file mode 100644 index 000000000..49db85b71 --- /dev/null +++ b/lib/view/settings_page/app_info_page/app_info_page.g.dart @@ -0,0 +1,24 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'app_info_page.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$packageInfoHash() => r'896e08f7e06f989407a274ad02d557d62106c603'; + +/// See also [packageInfo]. +@ProviderFor(packageInfo) +final packageInfoProvider = AutoDisposeFutureProvider.internal( + packageInfo, + name: r'packageInfoProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$packageInfoHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef PackageInfoRef = AutoDisposeFutureProviderRef; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/settings_page/general_settings_page/general_settings_page.dart b/lib/view/settings_page/general_settings_page/general_settings_page.dart index 5bddbffa1..b71a61677 100644 --- a/lib/view/settings_page/general_settings_page/general_settings_page.dart +++ b/lib/view/settings_page/general_settings_page/general_settings_page.dart @@ -1,113 +1,114 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:miria/const.dart'; -import 'package:miria/model/general_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:miria/view/themes/built_in_color_themes.dart'; +import "dart:async"; + +import "package:auto_route/auto_route.dart"; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/const.dart"; +import "package:miria/model/general_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/themes/built_in_color_themes.dart"; @RoutePage() -class GeneralSettingsPage extends ConsumerStatefulWidget { +class GeneralSettingsPage extends HookConsumerWidget { const GeneralSettingsPage({super.key}); @override - ConsumerState createState() => - GeneralSettingsPageState(); -} + Widget build(BuildContext context, WidgetRef ref) { + final settings = ref.watch(generalSettingsRepositoryProvider).settings; -class GeneralSettingsPageState extends ConsumerState { - String lightModeTheme = ""; - String darkModeTheme = ""; - ThemeColorSystem colorSystem = ThemeColorSystem.system; - NSFWInherit nsfwInherit = NSFWInherit.inherit; - AutomaticPush automaticPush = AutomaticPush.none; - bool enableDirectReaction = false; - bool enableAnimatedMFM = true; - bool enableLongTextElipsed = false; - bool enableFavoritedRenoteElipsed = true; - TabPosition tabPosition = TabPosition.top; - double textScaleFactor = 1.0; - EmojiType emojiType = EmojiType.twemoji; - String defaultFontName = ""; - String serifFontName = ""; - String monospaceFontName = ""; - String cursiveFontName = ""; - String fantasyFontName = ""; - Languages language = Languages.jaJP; + final lightModeTheme = useState(settings.lightColorThemeId); + final darkModeTheme = useState(settings.darkColorThemeId); + final colorSystem = useState(settings.themeColorSystem); + final nsfwInherit = useState(settings.nsfwInherit); + final automaticPush = useState(settings.automaticPush); + final enableDirectReaction = useState(settings.enableDirectReaction); + final enableAnimatedMFM = useState(settings.enableAnimatedMFM); + final enableLongTextElipsed = useState(settings.enableLongTextElipsed); + final enableFavoritedRenoteElipsed = + useState(settings.enableFavoritedRenoteElipsed); + final tabPosition = useState(settings.tabPosition); + final textScaleFactor = useState(settings.textScaleFactor); + final emojiType = useState(settings.emojiType); + final defaultFontName = useState(settings.defaultFontName); + final serifFontName = useState(settings.serifFontName); + final monospaceFontName = useState(settings.monospaceFontName); + final cursiveFontName = useState(settings.cursiveFontName); + final fantasyFontName = useState(settings.fantasyFontName); + final language = useState(settings.languages); + final isDeckMode = useState(settings.isDeckMode); - @override - void initState() { - super.initState(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - final settings = ref.read(generalSettingsRepositoryProvider).settings; - setState(() { - lightModeTheme = settings.lightColorThemeId; - if (lightModeTheme.isEmpty) { - lightModeTheme = builtInColorThemes + useMemoized(() { + if (lightModeTheme.value.isEmpty) { + lightModeTheme.value = builtInColorThemes .where((element) => !element.isDarkTheme) .first .id; } - darkModeTheme = settings.darkColorThemeId; - if (darkModeTheme.isEmpty || - builtInColorThemes.every((element) => - !element.isDarkTheme || element.id != darkModeTheme)) { - darkModeTheme = + if (darkModeTheme.value.isEmpty || + builtInColorThemes.every( + (element) => + !element.isDarkTheme || element.id != darkModeTheme.value, + )) { + darkModeTheme.value = builtInColorThemes.where((element) => element.isDarkTheme).first.id; } - colorSystem = settings.themeColorSystem; - nsfwInherit = settings.nsfwInherit; - enableDirectReaction = settings.enableDirectReaction; - automaticPush = settings.automaticPush; - enableAnimatedMFM = settings.enableAnimatedMFM; - enableLongTextElipsed = settings.enableLongTextElipsed; - enableFavoritedRenoteElipsed = settings.enableFavoritedRenoteElipsed; - tabPosition = settings.tabPosition; - textScaleFactor = settings.textScaleFactor; - emojiType = settings.emojiType; - defaultFontName = settings.defaultFontName; - serifFontName = settings.serifFontName; - monospaceFontName = settings.monospaceFontName; - cursiveFontName = settings.cursiveFontName; - fantasyFontName = settings.fantasyFontName; - language = settings.languages; }); - } + final dependencies = [ + lightModeTheme.value, + darkModeTheme.value, + colorSystem.value, + nsfwInherit.value, + enableDirectReaction.value, + automaticPush.value, + enableAnimatedMFM.value, + enableFavoritedRenoteElipsed.value, + enableLongTextElipsed.value, + tabPosition.value, + emojiType.value, + textScaleFactor.value, + defaultFontName.value, + serifFontName.value, + monospaceFontName.value, + cursiveFontName.value, + fantasyFontName.value, + language.value, + isDeckMode.value, + ]; + final save = useCallback( + () async { + await ref.read(generalSettingsRepositoryProvider).update( + GeneralSettings( + lightColorThemeId: lightModeTheme.value, + darkColorThemeId: darkModeTheme.value, + themeColorSystem: colorSystem.value, + nsfwInherit: nsfwInherit.value, + enableDirectReaction: enableDirectReaction.value, + automaticPush: automaticPush.value, + enableAnimatedMFM: enableAnimatedMFM.value, + enableFavoritedRenoteElipsed: + enableFavoritedRenoteElipsed.value, + enableLongTextElipsed: enableLongTextElipsed.value, + tabPosition: tabPosition.value, + emojiType: emojiType.value, + textScaleFactor: textScaleFactor.value, + defaultFontName: defaultFontName.value, + serifFontName: serifFontName.value, + monospaceFontName: monospaceFontName.value, + cursiveFontName: cursiveFontName.value, + fantasyFontName: fantasyFontName.value, + languages: language.value, + isDeckMode: isDeckMode.value, + ), + ); + }, + dependencies, + ); - Future save() async { - ref.read(generalSettingsRepositoryProvider).update( - GeneralSettings( - lightColorThemeId: lightModeTheme, - darkColorThemeId: darkModeTheme, - themeColorSystem: colorSystem, - nsfwInherit: nsfwInherit, - enableDirectReaction: enableDirectReaction, - automaticPush: automaticPush, - enableAnimatedMFM: enableAnimatedMFM, - enableFavoritedRenoteElipsed: enableFavoritedRenoteElipsed, - enableLongTextElipsed: enableLongTextElipsed, - tabPosition: tabPosition, - emojiType: emojiType, - textScaleFactor: textScaleFactor, - defaultFontName: defaultFontName, - serifFontName: serifFontName, - monospaceFontName: monospaceFontName, - cursiveFontName: cursiveFontName, - fantasyFontName: fantasyFontName, - languages: language), - ); - } + useMemoized(() => unawaited(save()), dependencies); - @override - Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text(S.of(context).generalSettings)), body: SingleChildScrollView( @@ -141,13 +142,11 @@ class GeneralSettingsPageState extends ConsumerState { child: Text(element.displayName), ), ], - value: language, - onChanged: (value) => setState( - () { - language = value ?? Languages.jaJP; - save(); - }, - ), + value: language.value, + onChanged: (value) async { + language.value = value ?? Languages.jaJP; + await save(); + }, ), const Padding(padding: EdgeInsets.only(top: 10)), Text(S.of(context).displayOfSensitiveNotes), @@ -160,13 +159,9 @@ class GeneralSettingsPageState extends ConsumerState { child: Text(element.displayName(context)), ), ], - value: nsfwInherit, - onChanged: (value) => setState( - () { - nsfwInherit = value ?? NSFWInherit.inherit; - save(); - }, - ), + value: nsfwInherit.value, + onChanged: (value) async => + nsfwInherit.value = value ?? NSFWInherit.inherit, ), const Padding(padding: EdgeInsets.only(top: 10)), Text(S.of(context).infiniteScroll), @@ -179,40 +174,36 @@ class GeneralSettingsPageState extends ConsumerState { child: Text(element.displayName(context)), ), ], - value: automaticPush, - onChanged: (value) => setState( - () { - automaticPush = value ?? AutomaticPush.none; - save(); - }, - ), + value: automaticPush.value, + onChanged: (value) async => + automaticPush.value = value ?? AutomaticPush.none, + ), + const Text("デッキモード"), //TODO: localize + CheckboxListTile( + title: const Text("デッキモードにします。"), + value: isDeckMode.value, + onChanged: (value) => isDeckMode.value = value ?? false, ), const Padding(padding: EdgeInsets.only(top: 10)), Text(S.of(context).enableAnimatedMfm), CheckboxListTile( - value: enableAnimatedMFM, - onChanged: (value) => setState(() { - enableAnimatedMFM = value ?? true; - save(); - }), + value: enableAnimatedMFM.value, + onChanged: (value) => + enableAnimatedMFM.value = value ?? true, title: Text(S.of(context).enableAnimatedMfmDescription), ), const Padding(padding: EdgeInsets.only(top: 10)), Text(S.of(context).collapseNotes), CheckboxListTile( - value: enableFavoritedRenoteElipsed, - onChanged: (value) => setState(() { - enableFavoritedRenoteElipsed = value ?? true; - save(); - }), + value: enableFavoritedRenoteElipsed.value, + onChanged: (value) => + enableFavoritedRenoteElipsed.value = value ?? true, title: Text(S.of(context).collapseReactionedRenotes), ), CheckboxListTile( - value: enableLongTextElipsed, - onChanged: (value) => setState(() { - enableLongTextElipsed = value ?? true; - save(); - }), + value: enableLongTextElipsed.value, + onChanged: (value) async => + enableLongTextElipsed.value = value ?? true, title: Text(S.of(context).collapseLongNotes), ), const Padding(padding: EdgeInsets.only(top: 10)), @@ -230,13 +221,9 @@ class GeneralSettingsPageState extends ConsumerState { ), ), ], - value: tabPosition, - onChanged: (value) => setState( - () { - tabPosition = value ?? TabPosition.top; - save(); - }, - ), + value: tabPosition.value, + onChanged: (value) async => + tabPosition.value = value ?? TabPosition.top, ), ], ), @@ -265,13 +252,9 @@ class GeneralSettingsPageState extends ConsumerState { child: Text(S.of(context).themeIsh(element.name)), ), ], - value: lightModeTheme, - onChanged: (value) => setState( - () { - lightModeTheme = value ?? ""; - save(); - }, - ), + value: lightModeTheme.value, + onChanged: (value) async => + lightModeTheme.value = value ?? "", ), const Padding(padding: EdgeInsets.only(top: 10)), Text(S.of(context).themeForDarkMode), @@ -284,11 +267,8 @@ class GeneralSettingsPageState extends ConsumerState { child: Text(S.of(context).themeIsh(element.name)), ), ], - value: darkModeTheme, - onChanged: (value) => setState(() { - darkModeTheme = value ?? ""; - save(); - }), + value: darkModeTheme.value, + onChanged: (value) => darkModeTheme.value = value ?? "", ), const Padding(padding: EdgeInsets.only(top: 10)), Text(S.of(context).selectLightOrDarkMode), @@ -300,11 +280,9 @@ class GeneralSettingsPageState extends ConsumerState { child: Text(colorSystem.displayName(context)), ), ], - value: colorSystem, - onChanged: (value) => setState(() { - colorSystem = value ?? ThemeColorSystem.system; - save(); - }), + value: colorSystem.value, + onChanged: (value) => colorSystem.value = + value ?? ThemeColorSystem.system, ), ], ), @@ -323,16 +301,12 @@ class GeneralSettingsPageState extends ConsumerState { style: Theme.of(context).textTheme.titleLarge, ), CheckboxListTile( - value: enableDirectReaction, + value: enableDirectReaction.value, title: Text(S.of(context).emojiTapReaction), subtitle: Text(S.of(context).emojiTapReactionDescription), - onChanged: (value) { - setState(() { - enableDirectReaction = !enableDirectReaction; - save(); - }); - }, + onChanged: (value) => + enableDirectReaction.value = value ?? false, ), const Padding(padding: EdgeInsets.only(top: 10)), Text(S.of(context).emojiStyle), @@ -344,14 +318,10 @@ class GeneralSettingsPageState extends ConsumerState { child: Text(type.displayName(context)), ), ], - value: emojiType, + value: emojiType.value, isExpanded: true, - onChanged: (value) { - setState(() { - emojiType = value ?? EmojiType.twemoji; - save(); - }); - }, + onChanged: (value) => + emojiType.value = value ?? EmojiType.twemoji, ), ], ), @@ -370,18 +340,24 @@ class GeneralSettingsPageState extends ConsumerState { style: Theme.of(context).textTheme.titleSmall, ), Slider( - value: textScaleFactor, + value: textScaleFactor.value, min: 0.5, max: 1.5, divisions: 10, - label: "${(textScaleFactor * 100).toInt()}%", + label: "${(textScaleFactor.value * 100).toInt()}%", onChanged: (value) { - setState(() { - textScaleFactor = value; - save(); - }); + textScaleFactor.value = value; }, ), + Center( + child: ElevatedButton( + onPressed: (settings.textScaleFactor == + textScaleFactor.value) + ? null + : save, + child: const Text("変更"), + ), + ), const Padding(padding: EdgeInsets.only(top: 10)), Text( S.of(context).fontStandard, @@ -396,19 +372,16 @@ class GeneralSettingsPageState extends ConsumerState { font.actualName.isEmpty ? S.of(context).systemFont : font.displayName, - // style: GoogleFonts.asMap()[font.actualName] - // ?.call(), ), - ) + ), ], value: choosableFonts.firstWhereOrNull( - (e) => e.actualName == defaultFontName) ?? + (e) => e.actualName == defaultFontName.value, + ) ?? choosableFonts.first, isExpanded: true, - onChanged: (item) => setState(() { - defaultFontName = item?.actualName ?? ""; - save(); - }), + onChanged: (item) => + defaultFontName.value = item?.actualName ?? "", ), const Padding(padding: EdgeInsets.only(top: 10)), Text( @@ -424,20 +397,16 @@ class GeneralSettingsPageState extends ConsumerState { font.actualName.isEmpty ? S.of(context).systemFont : font.displayName, - // style: GoogleFonts.asMap()[font.actualName] - // ?.call() ?? - // AppTheme.of(context).serifStyle, ), ), ], value: choosableFonts.firstWhereOrNull( - (e) => e.actualName == serifFontName) ?? + (e) => e.actualName == serifFontName.value, + ) ?? choosableFonts.first, isExpanded: true, - onChanged: (item) => setState(() { - serifFontName = item?.actualName ?? ""; - save(); - }), + onChanged: (item) => + serifFontName.value = item?.actualName ?? "", ), const Padding(padding: EdgeInsets.only(top: 10)), Text( @@ -453,18 +422,16 @@ class GeneralSettingsPageState extends ConsumerState { font.actualName.isEmpty ? S.of(context).systemFont : font.displayName, - // style: GoogleFonts.asMap()[font.actualName] - // ?.call() ?? - // AppTheme.of(context).monospaceStyle, ), ), ], value: choosableFonts.firstWhereOrNull( - (e) => e.actualName == monospaceFontName) ?? + (e) => e.actualName == monospaceFontName.value, + ) ?? choosableFonts.first, isExpanded: true, - onChanged: (item) => setState( - () => monospaceFontName = item?.actualName ?? ""), + onChanged: (item) => + monospaceFontName.value = item?.actualName ?? "", ), const Padding(padding: EdgeInsets.only(top: 10)), Text( @@ -480,20 +447,16 @@ class GeneralSettingsPageState extends ConsumerState { font.actualName.isEmpty ? S.of(context).systemFont : font.displayName, - // style: GoogleFonts.asMap()[font.actualName] - // ?.call() ?? - // AppTheme.of(context).cursiveStyle, ), - ) + ), ], value: choosableFonts.firstWhereOrNull( - (e) => e.actualName == cursiveFontName) ?? + (e) => e.actualName == cursiveFontName.value, + ) ?? choosableFonts.first, isExpanded: true, - onChanged: (item) => setState(() { - cursiveFontName = item?.actualName ?? ""; - save(); - }), + onChanged: (item) => + cursiveFontName.value = item?.actualName ?? "", ), const Padding(padding: EdgeInsets.only(top: 10)), Text( @@ -509,20 +472,16 @@ class GeneralSettingsPageState extends ConsumerState { font.actualName.isEmpty ? S.of(context).systemFont : font.displayName, - // style: GoogleFonts.asMap()[font.actualName] - // ?.call() ?? - // AppTheme.of(context).fantasyStyle, ), - ) + ), ], value: choosableFonts.firstWhereOrNull( - (e) => e.actualName == fantasyFontName) ?? + (e) => e.actualName == fantasyFontName.value, + ) ?? choosableFonts.first, isExpanded: true, - onChanged: (item) => setState(() { - fantasyFontName = item?.actualName ?? ""; - save(); - }), + onChanged: (item) => + fantasyFontName.value = item?.actualName ?? "", ), ], ), diff --git a/lib/view/settings_page/import_export_page/folder_select_dialog.dart b/lib/view/settings_page/import_export_page/folder_select_dialog.dart index c7fb24c3c..266c8c8f5 100644 --- a/lib/view/settings_page/import_export_page/folder_select_dialog.dart +++ b/lib/view/settings_page/import_export_page/folder_select_dialog.dart @@ -1,11 +1,12 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/futable_list_builder.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/futable_list_builder.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; class FolderResult { const FolderResult(this.folder); @@ -13,16 +14,17 @@ class FolderResult { final DriveFolder? folder; } +@RoutePage() class FolderSelectDialog extends ConsumerStatefulWidget { final Account account; final List? fileShowTarget; final String confirmationText; const FolderSelectDialog({ - super.key, required this.account, required this.fileShowTarget, required this.confirmationText, + super.key, }); @override @@ -52,7 +54,7 @@ class FolderSelectDialogState extends ConsumerState { ), Expanded(child: Text(path.map((e) => e.name).join("/"))), ], - ) + ), ], ), content: SizedBox( @@ -65,6 +67,7 @@ class FolderSelectDialogState extends ConsumerState { shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), showAd: false, + hideIsEmpty: true, initializeFuture: () async { final misskey = ref.read(misskeyProvider(widget.account)); final response = await misskey.drive.folders.folders( @@ -125,7 +128,7 @@ class FolderSelectDialogState extends ConsumerState { Expanded(child: Text(item.name)), ], ), - ) + ), ], ), ), @@ -136,7 +139,7 @@ class FolderSelectDialogState extends ConsumerState { Navigator.of(context).pop(FolderResult(path.lastOrNull)); }, child: Text(widget.confirmationText), - ) + ), ], ); } diff --git a/lib/view/settings_page/import_export_page/import_export_page.dart b/lib/view/settings_page/import_export_page/import_export_page.dart index 8b23cae1c..a3bdf6c01 100644 --- a/lib/view/settings_page/import_export_page/import_export_page.dart +++ b/lib/view/settings_page/import_export_page/import_export_page.dart @@ -1,11 +1,11 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/dialogs/simple_message_dialog.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/error_dialog_handler.dart"; +import "package:miria/view/dialogs/simple_message_dialog.dart"; @RoutePage() class ImportExportPage extends ConsumerStatefulWidget { @@ -29,99 +29,105 @@ class ImportExportPageState extends ConsumerState { body: Padding( padding: const EdgeInsets.only(left: 10, right: 10), child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S.of(context).importSettings, - style: Theme.of(context).textTheme.titleLarge, - ), - Text(S.of(context).importSettingsDescription), - Row( - children: [ - Expanded( - child: DropdownButton( - isExpanded: true, - items: [ - for (final account in accounts) - DropdownMenuItem( - value: account, - child: Text(account.acct.toString()), - ), - ], - value: selectedImportAccount, - onChanged: (Account? value) { - setState(() { - selectedImportAccount = value; - }); - }, - ), - ), - ElevatedButton( - onPressed: () async { - final account = selectedImportAccount; - if (account == null) { - await SimpleMessageDialog.show( - context, - S.of(context).pleaseSelectAccount, - ); - return; - } - await ref - .read(importExportRepository) - .import(context, account) - .expectFailure(context); + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).settingsFileManagement, + style: Theme.of(context).textTheme.titleLarge, + ), + Text(S.of(context).importAndExportSettingsDescription), + Text( + S.of(context).importSettings, + style: Theme.of(context).textTheme.titleLarge, + ), + Text(S.of(context).importSettingsDescription), + Row( + children: [ + Expanded( + child: DropdownButton( + isExpanded: true, + items: [ + for (final account in accounts) + DropdownMenuItem( + value: account, + child: Text(account.acct.toString()), + ), + ], + value: selectedImportAccount, + onChanged: (value) { + setState(() { + selectedImportAccount = value; + }); }, - child: Text(S.of(context).select), ), - ], - ), - const Padding(padding: EdgeInsets.only(top: 30)), - Text( - S.of(context).exportSettings, - style: Theme.of(context).textTheme.titleLarge, - ), - Text(S.of(context).exportSettingsDescription1), - Text(S.of(context).exportSettingsDescription2), - Row( - children: [ - Expanded( - child: DropdownButton( - isExpanded: true, - items: [ - for (final account in accounts) - DropdownMenuItem( - value: account, - child: Text(account.acct.toString()), - ), - ], - value: selectedExportAccount, - onChanged: (Account? value) { - setState(() { - selectedExportAccount = value; - }); - }, - ), + ), + ElevatedButton( + onPressed: () async { + final account = selectedImportAccount; + if (account == null) { + await SimpleMessageDialog.show( + context, + S.of(context).pleaseSelectAccount, + ); + return; + } + await ref + .read(importExportRepositoryProvider) + .import(context, account) + .expectFailure(context); + }, + child: Text(S.of(context).select), + ), + ], + ), + const Padding(padding: EdgeInsets.only(top: 30)), + Text( + S.of(context).exportSettings, + style: Theme.of(context).textTheme.titleLarge, + ), + Text(S.of(context).exportSettingsDescription), + Row( + children: [ + Expanded( + child: DropdownButton( + isExpanded: true, + items: [ + for (final account in accounts) + DropdownMenuItem( + value: account, + child: Text(account.acct.toString()), + ), + ], + value: selectedExportAccount, + onChanged: (value) { + setState(() { + selectedExportAccount = value; + }); + }, ), - ElevatedButton( - onPressed: () { - final account = selectedExportAccount; - if (account == null) { - SimpleMessageDialog.show( - context, - S.of(context).pleaseSelectAccountToExportSettings, - ); - return; - } - ref - .read(importExportRepository) - .export(context, account) - .expectFailure(context); - }, - child: Text(S.of(context).save)), - ], - ), - ]), + ), + ElevatedButton( + onPressed: () async { + final account = selectedExportAccount; + if (account == null) { + await SimpleMessageDialog.show( + context, + S.of(context).pleaseSelectAccountToExportSettings, + ); + return; + } + await ref + .read(importExportRepositoryProvider) + .export(context, account) + .expectFailure(context); + }, + child: Text(S.of(context).save), + ), + ], + ), + ], + ), ), ); } diff --git a/lib/view/settings_page/settings_page.dart b/lib/view/settings_page/settings_page.dart index 34f878e11..dc6d63336 100644 --- a/lib/view/settings_page/settings_page.dart +++ b/lib/view/settings_page/settings_page.dart @@ -1,7 +1,7 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:miria/router/app_router.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:miria/router/app_router.dart"; @RoutePage() class SettingsPage extends StatelessWidget { @@ -17,32 +17,32 @@ class SettingsPage extends StatelessWidget { title: Text(S.of(context).generalSettings), leading: const Icon(Icons.settings), trailing: const Icon(Icons.chevron_right), - onTap: () => context.pushRoute(const GeneralSettingsRoute()), + onTap: () async => context.pushRoute(const GeneralSettingsRoute()), ), ListTile( title: Text(S.of(context).accountSettings), leading: const Icon(Icons.account_circle), trailing: const Icon(Icons.chevron_right), - onTap: () => context.pushRoute(const AccountListRoute()), + onTap: () async => context.pushRoute(const AccountListRoute()), ), ListTile( title: Text(S.of(context).tabSettings), leading: const Icon(Icons.tab), trailing: const Icon(Icons.chevron_right), - onTap: () => context.pushRoute(const TabSettingsListRoute()), + onTap: () async => context.pushRoute(const TabSettingsListRoute()), ), ListTile( title: Text(S.of(context).settingsImportAndExport), leading: const Icon(Icons.import_export), trailing: const Icon(Icons.chevron_right), - onTap: () => context.pushRoute(const ImportExportRoute()), + onTap: () async => context.pushRoute(const ImportExportRoute()), ), ListTile( title: Text(S.of(context).aboutMiria), leading: const Icon(Icons.info), trailing: const Icon(Icons.chevron_right), - onTap: () => context.pushRoute(const AppInfoRoute()), - ) + onTap: () async => context.pushRoute(const AppInfoRoute()), + ), ], ), ); diff --git a/lib/view/settings_page/tab_settings_page/antenna_select_dialog.dart b/lib/view/settings_page/tab_settings_page/antenna_select_dialog.dart index aa9fc5607..241f48ecd 100644 --- a/lib/view/settings_page/tab_settings_page/antenna_select_dialog.dart +++ b/lib/view/settings_page/tab_settings_page/antenna_select_dialog.dart @@ -1,69 +1,67 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; -class AntennaSelectDialog extends ConsumerStatefulWidget { - final Account account; +part "antenna_select_dialog.g.dart"; - const AntennaSelectDialog({super.key, required this.account}); +@Riverpod(dependencies: [misskeyGetContext]) +Future> _antennas(_AntennasRef ref) async => + (await ref.read(misskeyGetContextProvider).antennas.list()).toList(); - @override - ConsumerState createState() => - AntennaSelectDialogState(); -} +@RoutePage() +class AntennaSelectDialog extends ConsumerWidget implements AutoRouteWrapper { + final Account account; -class AntennaSelectDialogState extends ConsumerState { - final antennas = []; + const AntennaSelectDialog({required this.account, super.key}); @override - void didChangeDependencies() { - super.didChangeDependencies(); - Future(() async { - final myAntennas = - await ref.read(misskeyProvider(widget.account)).antennas.list(); - antennas - ..clear() - ..addAll(myAntennas); - if (!mounted) return; - setState(() {}); - }); - } + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: account, child: this); @override - Widget build(BuildContext context) { - return AccountScope( - account: widget.account, - child: AlertDialog( - title: Text(S.of(context).selectAntenna), - content: SizedBox( - width: MediaQuery.of(context).size.width * 0.8, - height: MediaQuery.of(context).size.height * 0.8, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S.of(context).antenna, - style: Theme.of(context).textTheme.titleMedium, - ), - ListView.builder( + Widget build(BuildContext context, WidgetRef ref) { + final antennas = ref.watch(_antennasProvider); + + return AlertDialog( + title: Text(S.of(context).selectAntenna), + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: MediaQuery.of(context).size.height * 0.8, + child: SingleChildScrollView( + child: switch (antennas) { + AsyncLoading() => const Center( + child: CircularProgressIndicator.adaptive(), + ), + AsyncError(:final error, :final stackTrace) => + ErrorDetail(error: error, stackTrace: stackTrace), + AsyncData(:final value) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).antenna, + style: Theme.of(context).textTheme.titleMedium, + ), + ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemCount: antennas.length, + itemCount: value.length, itemBuilder: (context, index) { return ListTile( - onTap: () { - Navigator.of(context).pop(antennas[index]); - }, - title: Text(antennas[index].name)); - }), - ], - ), - ), + onTap: () => Navigator.of(context).pop(value[index]), + title: Text(value[index].name), + ); + }, + ), + ], + ), + }, ), ), ); diff --git a/lib/view/settings_page/tab_settings_page/antenna_select_dialog.g.dart b/lib/view/settings_page/tab_settings_page/antenna_select_dialog.g.dart new file mode 100644 index 000000000..809383fdb --- /dev/null +++ b/lib/view/settings_page/tab_settings_page/antenna_select_dialog.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'antenna_select_dialog.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$antennasHash() => r'2f5f4fc30eb5ccde122a407b099fb35066746e53'; + +/// See also [_antennas]. +@ProviderFor(_antennas) +final _antennasProvider = AutoDisposeFutureProvider>.internal( + _antennas, + name: r'_antennasProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$antennasHash, + dependencies: [misskeyGetContextProvider], + allTransitiveDependencies: { + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies + }, +); + +typedef _AntennasRef = AutoDisposeFutureProviderRef>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/settings_page/tab_settings_page/channel_select_dialog.dart b/lib/view/settings_page/tab_settings_page/channel_select_dialog.dart index 9a8520735..82c2bc9d3 100644 --- a/lib/view/settings_page/tab_settings_page/channel_select_dialog.dart +++ b/lib/view/settings_page/tab_settings_page/channel_select_dialog.dart @@ -1,16 +1,23 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/view/channels_page/channel_favorited.dart'; -import 'package:miria/view/channels_page/channel_followed.dart'; -import 'package:miria/view/channels_page/channel_search.dart'; -import 'package:miria/view/channels_page/channel_trend.dart'; -import 'package:miria/view/common/account_scope.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:miria/model/account.dart"; +import "package:miria/view/channels_page/channel_favorited.dart"; +import "package:miria/view/channels_page/channel_followed.dart"; +import "package:miria/view/channels_page/channel_search.dart"; +import "package:miria/view/channels_page/channel_trend.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class ChannelSelectDialog extends StatelessWidget { +@RoutePage() +class ChannelSelectDialog extends StatelessWidget implements AutoRouteWrapper { final Account account; - const ChannelSelectDialog({super.key, required this.account}); + const ChannelSelectDialog({required this.account, super.key}); + + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: account, child: this); @override Widget build(BuildContext context) { @@ -46,40 +53,37 @@ class ChannelSelectDialog extends StatelessWidget { ), ), Expanded( - child: AccountScope( - account: account, - child: TabBarView( - children: [ - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: ChannelSearch( - onChannelSelected: (channel) => - Navigator.of(context).pop(channel), - ), + child: TabBarView( + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: ChannelSearch( + onChannelSelected: (channel) async => + context.maybePop(channel), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: ChannelTrend( - onChannelSelected: (channel) => - Navigator.of(context).pop(channel), - ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: ChannelTrend( + onChannelSelected: (channel) async => + context.maybePop(channel), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: ChannelFavorited( - onChannelSelected: (channel) => - Navigator.of(context).pop(channel), - ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: ChannelFavorited( + onChannelSelected: (channel) async => + context.maybePop(channel), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 10), - child: ChannelFollowed( - onChannelSelected: (channel) => - Navigator.of(context).pop(channel), - ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: ChannelFollowed( + onChannelSelected: (channel) async => + context.maybePop(channel), ), - ], - ), + ), + ], ), ), ], diff --git a/lib/view/settings_page/tab_settings_page/icon_select_dialog.dart b/lib/view/settings_page/tab_settings_page/icon_select_dialog.dart index e4c84d335..0fabaae8e 100644 --- a/lib/view/settings_page/tab_settings_page/icon_select_dialog.dart +++ b/lib/view/settings_page/tab_settings_page/icon_select_dialog.dart @@ -1,9 +1,9 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/tab_icon.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/reaction_picker_dialog/reaction_picker_content.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/tab_icon.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/reaction_picker_dialog/reaction_picker_content.dart"; class IconSelectDialog extends StatelessWidget { final icons = [ @@ -76,57 +76,62 @@ class IconSelectDialog extends StatelessWidget { final Account account; - IconSelectDialog({super.key, required this.account}); + IconSelectDialog({required this.account, super.key}); @override Widget build(BuildContext context) { return AlertDialog( - title: Text(S.of(context).selectIcon), - content: DefaultTabController( - length: 2, - child: SizedBox( - width: MediaQuery.of(context).size.width * 0.8, - height: MediaQuery.of(context).size.width * 0.8, - child: Column( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 5), - child: DecoratedBox( - decoration: - BoxDecoration(color: Theme.of(context).primaryColor), - child: TabBar( - tabs: [ - Tab(text: S.of(context).standardIcon), - Tab(text: S.of(context).emojiIcon), + title: Text(S.of(context).selectIcon), + content: DefaultTabController( + length: 2, + child: SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: MediaQuery.of(context).size.width * 0.8, + child: Column( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 5), + child: DecoratedBox( + decoration: + BoxDecoration(color: Theme.of(context).primaryColor), + child: TabBar( + tabs: [ + Tab(text: S.of(context).standardIcon), + Tab(text: S.of(context).emojiIcon), + ], + ), + ), + ), + Expanded( + child: TabBarView( + children: [ + SingleChildScrollView( + child: Wrap( + children: [ + for (final icon in icons) + IconButton( + onPressed: () => Navigator.of(context) + .pop(TabIcon(codePoint: icon.codePoint)), + icon: Icon(icon), + ), ], ), ), - ), - Expanded( - child: TabBarView(children: [ - SingleChildScrollView( - child: Wrap( - children: [ - for (final icon in icons) - IconButton( - onPressed: () => Navigator.of(context) - .pop(TabIcon(codePoint: icon.codePoint)), - icon: Icon(icon)), - ], - ), + AccountContextScope.as( + account: account, + child: ReactionPickerContent( + isAcceptSensitive: true, + onTap: (emoji) => Navigator.of(context) + .pop(TabIcon(customEmojiName: emoji.baseName)), ), - AccountScope( - account: account, - child: ReactionPickerContent( - isAcceptSensitive: true, - onTap: (emoji) => Navigator.of(context) - .pop(TabIcon(customEmojiName: emoji.baseName)), - ), - ) - ]), - ) - ], + ), + ], + ), ), - ))); + ], + ), + ), + ), + ); } } diff --git a/lib/view/settings_page/tab_settings_page/role_select_dialog.dart b/lib/view/settings_page/tab_settings_page/role_select_dialog.dart index 737c84506..f1414763f 100644 --- a/lib/view/settings_page/tab_settings_page/role_select_dialog.dart +++ b/lib/view/settings_page/tab_settings_page/role_select_dialog.dart @@ -1,67 +1,67 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; -class RoleSelectDialog extends ConsumerStatefulWidget { - final Account account; +part "role_select_dialog.g.dart"; - const RoleSelectDialog({super.key, required this.account}); +@Riverpod(dependencies: [misskeyGetContext]) +Future> _roles(_RolesRef ref) async => + (await ref.read(misskeyGetContextProvider).roles.list()).toList(); - @override - ConsumerState createState() => - RoleSelectDialogState(); -} +@RoutePage() +class RoleSelectDialog extends ConsumerWidget implements AutoRouteWrapper { + final Account account; -class RoleSelectDialogState extends ConsumerState { - final roles = []; + const RoleSelectDialog({required this.account, super.key}); @override - void didChangeDependencies() { - super.didChangeDependencies(); - Future(() async { - final rolesList = - await ref.read(misskeyProvider(widget.account)).roles.list(); - roles - ..clear() - ..addAll(rolesList); - setState(() {}); - }); - } + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: account, child: this); @override - Widget build(BuildContext context) { - return AccountScope( - account: widget.account, - child: AlertDialog( - title: Text(S.of(context).selectRole), - content: SizedBox( - width: MediaQuery.of(context).size.width * 0.8, - height: MediaQuery.of(context).size.height * 0.8, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S.of(context).role, - style: Theme.of(context).textTheme.titleMedium, - ), - ListView.builder( + Widget build(BuildContext context, WidgetRef ref) { + final roles = ref.watch(_rolesProvider); + + return AlertDialog( + title: Text(S.of(context).selectRole), + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: MediaQuery.of(context).size.height * 0.8, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).role, + style: Theme.of(context).textTheme.titleMedium, + ), + switch (roles) { + AsyncLoading() => + const Center(child: CircularProgressIndicator.adaptive()), + AsyncError(:final error, :final stackTrace) => + ErrorDetail(error: error, stackTrace: stackTrace), + AsyncData(:final value) => ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemCount: roles.length, + itemCount: value.length, itemBuilder: (context, index) { return ListTile( - onTap: () { - Navigator.of(context).pop(roles[index]); - }, - title: Text(roles[index].name)); - }), - ], - ), + onTap: () { + Navigator.of(context).pop(value[index]); + }, + title: Text(value[index].name), + ); + }, + ), + }, + ], ), ), ), diff --git a/lib/view/settings_page/tab_settings_page/role_select_dialog.g.dart b/lib/view/settings_page/tab_settings_page/role_select_dialog.g.dart new file mode 100644 index 000000000..3b1f2048f --- /dev/null +++ b/lib/view/settings_page/tab_settings_page/role_select_dialog.g.dart @@ -0,0 +1,28 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'role_select_dialog.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$rolesHash() => r'13862ac2ff4ab8eb509cccb37ed5bacd591602de'; + +/// See also [_roles]. +@ProviderFor(_roles) +final _rolesProvider = + AutoDisposeFutureProvider>.internal( + _roles, + name: r'_rolesProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$rolesHash, + dependencies: [misskeyGetContextProvider], + allTransitiveDependencies: { + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies + }, +); + +typedef _RolesRef = AutoDisposeFutureProviderRef>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/settings_page/tab_settings_page/tab_settings_list_page.dart b/lib/view/settings_page/tab_settings_page/tab_settings_list_page.dart index af1753de6..f5fc4ae52 100644 --- a/lib/view/settings_page/tab_settings_page/tab_settings_list_page.dart +++ b/lib/view/settings_page/tab_settings_page/tab_settings_list_page.dart @@ -1,14 +1,14 @@ -import 'dart:io'; +import "dart:io"; -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/tab_setting.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/tab_icon_view.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/tab_setting.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/tab_icon_view.dart"; @RoutePage() class TabSettingsListPage extends ConsumerWidget { @@ -29,9 +29,7 @@ class TabSettingsListPage extends ConsumerWidget { title: Text(S.of(context).tabSettings), actions: [ IconButton( - onPressed: () { - context.pushRoute(TabSettingsRoute()); - }, + onPressed: () async => await context.pushRoute(TabSettingsRoute()), icon: const Icon(Icons.add), ), ], @@ -65,13 +63,13 @@ class TabSettingsListPage extends ConsumerWidget { ); } }, - onReorder: (oldIndex, newIndex) { + onReorder: (oldIndex, newIndex) async { if (oldIndex < newIndex) { newIndex -= 1; } final item = tabSettings.removeAt(oldIndex); tabSettings.insert(newIndex, item); - ref.read(tabSettingsRepositoryProvider).save(tabSettings); + await ref.read(tabSettingsRepositoryProvider).save(tabSettings); }, ), ), @@ -80,10 +78,9 @@ class TabSettingsListPage extends ConsumerWidget { child: Padding( padding: const EdgeInsets.all(10), child: ElevatedButton( - onPressed: () { - context.router - ..removeWhere((route) => true) - ..push(const SplashRoute()); + onPressed: () async { + context.router.removeWhere((route) => true); + await context.router.push(const SplashRoute()); }, child: Text(S.of(context).apply), ), @@ -97,9 +94,9 @@ class TabSettingsListPage extends ConsumerWidget { class TabSettingsListItem extends ConsumerWidget { const TabSettingsListItem({ - super.key, required this.tabSetting, required this.index, + super.key, }); final TabSetting tabSetting; @@ -109,7 +106,7 @@ class TabSettingsListItem extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final account = ref.watch(accountProvider(tabSetting.acct)); return ListTile( - leading: AccountScope( + leading: AccountContextScope.as( account: account, child: TabIconView(icon: tabSetting.icon), ), @@ -118,7 +115,7 @@ class TabSettingsListItem extends ConsumerWidget { "${tabSetting.tabType.displayName(context)} / ${tabSetting.acct}", ), trailing: const Icon(Icons.drag_handle), - onTap: () => context.pushRoute(TabSettingsRoute(tabIndex: index)), + onTap: () async => context.pushRoute(TabSettingsRoute(tabIndex: index)), ); } } diff --git a/lib/view/settings_page/tab_settings_page/tab_settings_page.dart b/lib/view/settings_page/tab_settings_page/tab_settings_page.dart index bdbe7d6f7..66d5b3db2 100644 --- a/lib/view/settings_page/tab_settings_page/tab_settings_page.dart +++ b/lib/view/settings_page/tab_settings_page/tab_settings_page.dart @@ -1,154 +1,144 @@ -import 'package:auto_route/annotations.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/extensions/users_lists_show_response_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/tab_icon.dart'; -import 'package:miria/model/tab_setting.dart'; -import 'package:miria/model/tab_type.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/dialogs/simple_message_dialog.dart'; -import 'package:miria/view/common/tab_icon_view.dart'; -import 'package:miria/view/settings_page/tab_settings_page/role_select_dialog.dart'; -import 'package:miria/view/settings_page/tab_settings_page/antenna_select_dialog.dart'; -import 'package:miria/view/settings_page/tab_settings_page/channel_select_dialog.dart'; -import 'package:miria/view/settings_page/tab_settings_page/icon_select_dialog.dart'; -import 'package:miria/view/settings_page/tab_settings_page/user_list_select_dialog.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "dart:async"; + +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/users_lists_show_response_extension.dart"; +import "package:miria/hooks/use_async.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/tab_icon.dart"; +import "package:miria/model/tab_setting.dart"; +import "package:miria/model/tab_type.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/tab_icon_view.dart"; +import "package:miria/view/dialogs/simple_message_dialog.dart"; +import "package:miria/view/settings_page/tab_settings_page/icon_select_dialog.dart"; +import "package:misskey_dart/misskey_dart.dart"; @RoutePage() -class TabSettingsPage extends ConsumerStatefulWidget { +class TabSettingsPage extends HookConsumerWidget { const TabSettingsPage({super.key, this.tabIndex}); final int? tabIndex; @override - ConsumerState createState() => - TabSettingsAddDialogState(); -} + Widget build(BuildContext context, WidgetRef ref) { + final initialTabSetting = tabIndex != null + ? ref + .read(tabSettingsRepositoryProvider) + .tabSettings + .toList()[tabIndex!] + : null; -class TabSettingsAddDialogState extends ConsumerState { - late Account? selectedAccount = ref.read(accountsProvider).first; - TabType? selectedTabType = TabType.localTimeline; - RolesListResponse? selectedRole; - CommunityChannel? selectedChannel; - UsersList? selectedUserList; - Antenna? selectedAntenna; - TextEditingController nameController = TextEditingController(); - TabIcon? selectedIcon; - bool renoteDisplay = true; - bool isSubscribe = true; - bool isMediaOnly = false; - bool isIncludeReply = false; + final selectedAccount = useState( + initialTabSetting != null + ? ref.read(accountProvider(initialTabSetting.acct)) + : ref.read(accountsProvider).first, + ); - bool get availableIncludeReply => - selectedTabType == TabType.localTimeline || - selectedTabType == TabType.hybridTimeline; + final isTabTypeAvailable = useCallback( + (tabType) => switch (tabType) { + TabType.localTimeline => + selectedAccount.value?.i.policies.ltlAvailable ?? false, + TabType.globalTimeline => + selectedAccount.value?.i.policies.gtlAvailable ?? false, + _ => true, + }, + [selectedAccount.value], + ); - bool isTabTypeAvailable(TabType tabType) { - return switch (tabType) { - TabType.localTimeline => - selectedAccount?.i.policies.ltlAvailable ?? false, - TabType.globalTimeline => - selectedAccount?.i.policies.gtlAvailable ?? false, - _ => true, - }; - } + final selectedTabType = useState( + initialTabSetting != null && isTabTypeAvailable(initialTabSetting.tabType) + ? initialTabSetting.tabType + : TabType.localTimeline, + ); - @override - void didChangeDependencies() { - super.didChangeDependencies(); + final selectedRole = useState(null); + final selectedChannel = useState(null); + final selectedUserList = useState(null); + final selectedAntenna = useState(null); + + final nameController = useTextEditingController( + text: initialTabSetting != null + ? initialTabSetting.name ?? + initialTabSetting.tabType.displayName(context) + : "", + ); + + final availableIncludeReply = + selectedTabType.value == TabType.localTimeline || + selectedTabType.value == TabType.hybridTimeline; + + final selectedIcon = useState(initialTabSetting?.icon); + final renoteDisplay = useState(initialTabSetting?.renoteDisplay ?? true); + final isSubscribe = useState(initialTabSetting?.isSubscribe ?? true); + final isMediaOnly = useState(initialTabSetting?.isMediaOnly ?? false); + final isIncludeReply = + useState(initialTabSetting?.isIncludeReplies ?? false); + + final initialize = useAsync(() async { + if (initialTabSetting == null) return; + + final roleId = initialTabSetting.roleId; + final channelId = initialTabSetting.channelId; + final listId = initialTabSetting.listId; + final antennaId = initialTabSetting.antennaId; - final tab = widget.tabIndex; - if (tab != null) { - final tabSetting = - ref.read(tabSettingsRepositoryProvider).tabSettings.toList()[tab]; - selectedAccount = ref.read(accountProvider(tabSetting.acct)); - selectedTabType = - isTabTypeAvailable(tabSetting.tabType) ? tabSetting.tabType : null; - final roleId = tabSetting.roleId; - final channelId = tabSetting.channelId; - final listId = tabSetting.listId; - final antennaId = tabSetting.antennaId; - nameController.text = - tabSetting.name ?? tabSetting.tabType.displayName(context); - selectedIcon = tabSetting.icon; - renoteDisplay = tabSetting.renoteDisplay; - isSubscribe = tabSetting.isSubscribe; - isMediaOnly = tabSetting.isMediaOnly; - isIncludeReply = tabSetting.isIncludeReplies; if (roleId != null) { - Future(() async { - selectedRole = await ref - .read(misskeyProvider(selectedAccount!)) - .roles - .show(RolesShowRequest(roleId: roleId)); - setState(() {}); - }); + selectedRole.value = await ref + .read(misskeyProvider(selectedAccount.value!)) + .roles + .show(RolesShowRequest(roleId: roleId)); } if (channelId != null) { - Future(() async { - selectedChannel = await ref - .read(misskeyProvider(selectedAccount!)) - .channels - .show(ChannelsShowRequest(channelId: channelId)); - if (!mounted) return; - setState(() {}); - }); + selectedChannel.value = await ref + .read(misskeyProvider(selectedAccount.value!)) + .channels + .show(ChannelsShowRequest(channelId: channelId)); } if (listId != null) { - Future(() async { - final response = await ref - .read(misskeyProvider(selectedAccount!)) - .users - .list - .show(UsersListsShowRequest(listId: listId)); - selectedUserList = response.toUsersList(); - if (!mounted) return; - setState(() {}); - }); + selectedUserList.value = (await ref + .read(misskeyProvider(selectedAccount.value!)) + .users + .list + .show(UsersListsShowRequest(listId: listId))) + .toUsersList(); } if (antennaId != null) { - Future(() async { - selectedAntenna = await ref - .read(misskeyProvider(selectedAccount!)) - .antennas - .show(AntennasShowRequest(antennaId: antennaId)); - if (!mounted) return; - setState(() {}); - }); + selectedAntenna.value = await ref + .read(misskeyProvider(selectedAccount.value!)) + .antennas + .show(AntennasShowRequest(antennaId: antennaId)); } - } - } - - @override - void dispose() { - nameController.dispose(); - super.dispose(); - } + }); + useMemoized(() => unawaited(initialize.execute())); - @override - Widget build(BuildContext context) { final accounts = ref.watch(accountsProvider); + return Scaffold( appBar: AppBar( title: Text(S.of(context).tabSettings), actions: [ - if (widget.tabIndex != null) + if (tabIndex != null) IconButton( - onPressed: () { - ref.read(tabSettingsRepositoryProvider).save(ref - .read(tabSettingsRepositoryProvider) - .tabSettings - .toList() - ..removeAt(widget.tabIndex!)); + onPressed: () async { + await ref.read(tabSettingsRepositoryProvider).save( + ref + .read(tabSettingsRepositoryProvider) + .tabSettings + .toList() + ..removeAt(tabIndex!), + ); - if (!mounted) return; - Navigator.of(context).pop(); - }, - icon: const Icon(Icons.delete_outline_outlined)) + if (!context.mounted) return; + Navigator.of(context).pop(); + }, + icon: const Icon(Icons.delete_outline_outlined), + ), ], ), body: SingleChildScrollView( @@ -169,22 +159,20 @@ class TabSettingsAddDialogState extends ConsumerState { ), ], onChanged: (value) { - final tabType = selectedTabType; - setState(() { - selectedAccount = value; - selectedTabType = - tabType != null && isTabTypeAvailable(tabType) - ? tabType - : null; - selectedAntenna = null; - selectedUserList = null; - selectedChannel = null; - if (selectedIcon?.customEmojiName != null) { - selectedIcon = null; - } - }); + final tabType = selectedTabType.value; + selectedAccount.value = value; + selectedTabType.value = + tabType != null && isTabTypeAvailable(tabType) + ? tabType + : null; + selectedAntenna.value = null; + selectedUserList.value = null; + selectedChannel.value = null; + if (selectedIcon.value?.customEmojiName != null) { + selectedIcon.value = null; + } }, - value: selectedAccount, + value: selectedAccount.value, ), const Padding(padding: EdgeInsets.all(10)), Text(S.of(context).tabType), @@ -198,104 +186,112 @@ class TabSettingsAddDialogState extends ConsumerState { ), ], onChanged: (value) { - setState(() { - selectedTabType = value; - }); + selectedTabType.value = value; }, - value: selectedTabType, + value: selectedTabType.value, ), const Padding(padding: EdgeInsets.all(10)), - if (selectedTabType == TabType.roleTimeline) ...[ + if (selectedTabType.value == TabType.roleTimeline) ...[ Text(S.of(context).roleTimeline), - Row( - children: [ - Expanded(child: Text(selectedRole?.name ?? "")), - IconButton( - onPressed: () async { - final selected = selectedAccount; - if (selected == null) return; + switch (initialize.value) { + AsyncData() => Row( + children: [ + Expanded(child: Text(selectedRole.value?.name ?? "")), + IconButton( + onPressed: () async { + final selected = selectedAccount.value; + if (selected == null) return; - selectedRole = await showDialog( - context: context, - builder: (context) => - RoleSelectDialog(account: selected)); - setState(() { + selectedRole.value = + await context.pushRoute( + RoleSelectRoute(account: selected), + ); nameController.text = - selectedRole?.name ?? nameController.text; - }); - }, - icon: const Icon(Icons.navigate_next)) - ], - ) + selectedRole.value?.name ?? nameController.text; + }, + icon: const Icon(Icons.navigate_next), + ), + ], + ), + _ => const CircularProgressIndicator.adaptive(), + }, ], - if (selectedTabType == TabType.channel) ...[ + if (selectedTabType.value == TabType.channel) ...[ Text(S.of(context).channel), - Row( - children: [ - Expanded(child: Text(selectedChannel?.name ?? "")), - IconButton( - onPressed: () async { - final selected = selectedAccount; - if (selected == null) return; + switch (initialize.value) { + AsyncData() => Row( + children: [ + Expanded( + child: Text(selectedChannel.value?.name ?? ""), + ), + IconButton( + onPressed: () async { + final selected = selectedAccount.value; + if (selected == null) return; - selectedChannel = await showDialog( - context: context, - builder: (context) => - ChannelSelectDialog(account: selected)); - setState(() { - nameController.text = - selectedChannel?.name ?? nameController.text; - }); - }, - icon: const Icon(Icons.navigate_next)) - ], - ) + selectedChannel.value = await context.pushRoute( + ChannelSelectRoute(account: selected), + ); + nameController.text = selectedChannel.value?.name ?? + nameController.text; + }, + icon: const Icon(Icons.navigate_next), + ), + ], + ), + _ => const CircularProgressIndicator.adaptive(), + }, ], - if (selectedTabType == TabType.userList) ...[ + if (selectedTabType.value == TabType.userList) ...[ Text(S.of(context).list), - Row( - children: [ - Expanded(child: Text(selectedUserList?.name ?? "")), - IconButton( - onPressed: () async { - final selected = selectedAccount; - if (selected == null) return; + switch (initialize.value) { + AsyncData() => Row( + children: [ + Expanded( + child: Text(selectedUserList.value?.name ?? ""), + ), + IconButton( + onPressed: () async { + final selected = selectedAccount.value; + if (selected == null) return; - selectedUserList = await showDialog( - context: context, - builder: (context) => - UserListSelectDialog(account: selected)); - setState(() { + selectedUserList.value = await context.pushRoute( + UserListSelectRoute(account: selected), + ); nameController.text = - selectedUserList?.name ?? nameController.text; - }); - }, - icon: const Icon(Icons.navigate_next)) - ], - ) + selectedUserList.value?.name ?? + nameController.text; + }, + icon: const Icon(Icons.navigate_next), + ), + ], + ), + _ => const CircularProgressIndicator.adaptive(), + }, ], - if (selectedTabType == TabType.antenna) ...[ + if (selectedTabType.value == TabType.antenna) ...[ Text(S.of(context).antenna), Row( children: [ - Expanded(child: Text(selectedAntenna?.name ?? "")), - IconButton( - onPressed: () async { - final selected = selectedAccount; - if (selected == null) return; + Expanded(child: Text(selectedAntenna.value?.name ?? "")), + switch (initialize.value) { + AsyncData() => IconButton( + onPressed: () async { + final selected = selectedAccount.value; + if (selected == null) return; - selectedAntenna = await showDialog( - context: context, - builder: (context) => - AntennaSelectDialog(account: selected)); - setState(() { - nameController.text = - selectedAntenna?.name ?? nameController.text; - }); - }, - icon: const Icon(Icons.navigate_next)) + selectedAntenna.value = await context.pushRoute( + AntennaSelectRoute(account: selected), + ); + nameController.text = selectedAntenna.value?.name ?? + nameController.text; + }, + icon: const Icon(Icons.navigate_next), + ), + _ => const CircularProgressIndicator.adaptive(), + }, ], - ) + ), ], const Padding(padding: EdgeInsets.all(10)), Text(S.of(context).tabName), @@ -308,98 +304,102 @@ class TabSettingsAddDialogState extends ConsumerState { Row( children: [ Expanded( - child: selectedAccount == null - ? Container() - : AccountScope( - account: selectedAccount!, - child: SizedBox( - height: 32, - child: TabIconView( - icon: selectedIcon, - size: IconTheme.of(context).size), - ))), + child: selectedAccount.value == null + ? Container() + : AccountContextScope.as( + account: selectedAccount.value!, + child: SizedBox( + height: 32, + child: TabIconView( + icon: selectedIcon.value, + size: IconTheme.of(context).size, + ), + ), + ), + ), IconButton( - onPressed: () async { - if (selectedAccount == null) return; - selectedIcon = await showDialog( - context: context, - builder: (context) => IconSelectDialog( - account: selectedAccount!, - )); - setState(() {}); - }, - icon: const Icon(Icons.navigate_next)) + onPressed: () async { + if (selectedAccount.value == null) return; + selectedIcon.value = await showDialog( + context: context, + builder: (context) => IconSelectDialog( + account: selectedAccount.value!, + ), + ); + }, + icon: const Icon(Icons.navigate_next), + ), ], ), CheckboxListTile( title: Text(S.of(context).displayRenotes), - value: renoteDisplay, + value: renoteDisplay.value, onChanged: (value) => - setState(() => renoteDisplay = !renoteDisplay), + renoteDisplay.value = !renoteDisplay.value, ), if (availableIncludeReply) CheckboxListTile( title: Text(S.of(context).includeReplies), subtitle: Text(S.of(context).includeRepliesAvailability), - value: isIncludeReply, - enabled: !isMediaOnly, - onChanged: (value) => setState(() { - isIncludeReply = !isIncludeReply; + value: isIncludeReply.value, + enabled: !isMediaOnly.value, + onChanged: (value) { + isIncludeReply.value = !isIncludeReply.value; if (value ?? false) { - isMediaOnly = false; + isMediaOnly.value = false; } - }), + }, ), CheckboxListTile( title: Text(S.of(context).mediaOnly), - value: isMediaOnly, - enabled: !isIncludeReply, - onChanged: (value) => setState(() { - isMediaOnly = !isMediaOnly; + value: isMediaOnly.value, + enabled: !isIncludeReply.value, + onChanged: (value) { + isMediaOnly.value = !isMediaOnly.value; if (value ?? false) { - isIncludeReply = false; + isIncludeReply.value = false; } - }), + }, ), CheckboxListTile( title: Text(S.of(context).subscribeNotes), subtitle: Text(S.of(context).subscribeNotesDescription), - value: isSubscribe, - onChanged: (value) => - setState(() => isSubscribe = !isSubscribe), + value: isSubscribe.value, + onChanged: (value) => isSubscribe.value = !isSubscribe.value, ), Center( child: ElevatedButton( onPressed: () async { - final account = selectedAccount; + final account = selectedAccount.value; if (account == null) { - SimpleMessageDialog.show( + await SimpleMessageDialog.show( context, S.of(context).pleaseSelectAccount, ); return; } - final tabType = selectedTabType; + final tabType = selectedTabType.value; if (tabType == null) { - SimpleMessageDialog.show( + await SimpleMessageDialog.show( context, S.of(context).pleaseSelectTabType, ); return; } - final icon = selectedIcon; + final icon = selectedIcon.value; if (icon == null) { - SimpleMessageDialog.show( + await SimpleMessageDialog.show( context, S.of(context).pleaseSelectIcon, ); return; } - if (tabType == TabType.channel && selectedChannel == null) { - SimpleMessageDialog.show( + if (tabType == TabType.channel && + selectedChannel.value == null) { + await SimpleMessageDialog.show( context, S.of(context).pleaseSelectChannel, ); @@ -407,24 +407,25 @@ class TabSettingsAddDialogState extends ConsumerState { } if (tabType == TabType.userList && - selectedUserList == null) { - SimpleMessageDialog.show( + selectedUserList.value == null) { + await SimpleMessageDialog.show( context, S.of(context).pleaseSelectList, ); return; } - if (tabType == TabType.antenna && selectedAntenna == null) { - SimpleMessageDialog.show( + if (tabType == TabType.antenna && + selectedAntenna.value == null) { + await SimpleMessageDialog.show( context, S.of(context).pleaseSelectAntenna, ); return; } if (tabType == TabType.roleTimeline && - selectedRole == null) { - SimpleMessageDialog.show( + selectedRole.value == null) { + await SimpleMessageDialog.show( context, S.of(context).pleaseSelectRole, ); @@ -440,30 +441,30 @@ class TabSettingsAddDialogState extends ConsumerState { tabType: tabType, name: nameController.text, acct: account.acct, - roleId: selectedRole?.id, - channelId: selectedChannel?.id, - listId: selectedUserList?.id, - antennaId: selectedAntenna?.id, - renoteDisplay: renoteDisplay, - isSubscribe: isSubscribe, - isIncludeReplies: isIncludeReply, - isMediaOnly: isMediaOnly, + roleId: selectedRole.value?.id, + channelId: selectedChannel.value?.id, + listId: selectedUserList.value?.id, + antennaId: selectedAntenna.value?.id, + renoteDisplay: renoteDisplay.value, + isSubscribe: isSubscribe.value, + isIncludeReplies: isIncludeReply.value, + isMediaOnly: isMediaOnly.value, ); - if (widget.tabIndex == null) { + if (tabIndex == null) { await ref .read(tabSettingsRepositoryProvider) .save([...list, newTabSetting]); } else { - list[widget.tabIndex!] = newTabSetting; + list[tabIndex!] = newTabSetting; await ref.read(tabSettingsRepositoryProvider).save(list); } - if (!mounted) return; + if (!context.mounted) return; Navigator.of(context).pop(); }, child: Text(S.of(context).done), ), - ) + ), ], ), ), diff --git a/lib/view/settings_page/tab_settings_page/user_list_select_dialog.dart b/lib/view/settings_page/tab_settings_page/user_list_select_dialog.dart index 082eb446c..91487275a 100644 --- a/lib/view/settings_page/tab_settings_page/user_list_select_dialog.dart +++ b/lib/view/settings_page/tab_settings_page/user_list_select_dialog.dart @@ -1,69 +1,69 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; -class UserListSelectDialog extends ConsumerStatefulWidget { - final Account account; +part "user_list_select_dialog.g.dart"; - const UserListSelectDialog({super.key, required this.account}); +@Riverpod(dependencies: [misskeyGetContext]) +Future> _usersList(_UsersListRef ref) async => + (await ref.read(misskeyGetContextProvider).users.list.list()).toList(); - @override - ConsumerState createState() => - UserListSelectDialogState(); -} +@RoutePage() +class UserListSelectDialog extends ConsumerWidget implements AutoRouteWrapper { + final Account account; -class UserListSelectDialogState extends ConsumerState { - final userLists = []; + const UserListSelectDialog({required this.account, super.key}); @override - void didChangeDependencies() { - super.didChangeDependencies(); - Future(() async { - final myLists = - await ref.read(misskeyProvider(widget.account)).users.list.list(); - userLists - ..clear() - ..addAll(myLists); - if (!mounted) return; - setState(() {}); - }); - } + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: account, child: this); @override - Widget build(BuildContext context) { - return AccountScope( - account: widget.account, - child: AlertDialog( - title: Text(S.of(context).selectList), - content: SizedBox( - width: MediaQuery.of(context).size.width * 0.8, - height: MediaQuery.of(context).size.height * 0.8, - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S.of(context).list, - style: Theme.of(context).textTheme.titleMedium, - ), - ListView.builder( + Widget build(BuildContext context, WidgetRef ref) { + final usersList = ref.watch(_usersListProvider); + + return AlertDialog( + title: Text(S.of(context).selectList), + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: MediaQuery.of(context).size.height * 0.8, + child: SingleChildScrollView( + child: switch (usersList) { + AsyncLoading() => const Center( + child: CircularProgressIndicator.adaptive(), + ), + AsyncError(:final error, :final stackTrace) => + ErrorDetail(error: error, stackTrace: stackTrace), + AsyncData(:final value) => Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).list, + style: Theme.of(context).textTheme.titleMedium, + ), + ListView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemCount: userLists.length, + itemCount: value.length, itemBuilder: (context, index) { return ListTile( - onTap: () { - Navigator.of(context).pop(userLists[index]); - }, - title: Text(userLists[index].name ?? "")); - }), - ], - ), - ), + onTap: () { + Navigator.of(context).pop(value[index]); + }, + title: Text(value[index].name ?? ""), + ); + }, + ), + ], + ), + }, ), ), ); diff --git a/lib/view/settings_page/tab_settings_page/user_list_select_dialog.g.dart b/lib/view/settings_page/tab_settings_page/user_list_select_dialog.g.dart new file mode 100644 index 000000000..95457ec84 --- /dev/null +++ b/lib/view/settings_page/tab_settings_page/user_list_select_dialog.g.dart @@ -0,0 +1,27 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_list_select_dialog.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$usersListHash() => r'a1c8ccd94f9b25bed8160a8a4eb27b1678661ece'; + +/// See also [_usersList]. +@ProviderFor(_usersList) +final _usersListProvider = AutoDisposeFutureProvider>.internal( + _usersList, + name: r'_usersListProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') ? null : _$usersListHash, + dependencies: [misskeyGetContextProvider], + allTransitiveDependencies: { + misskeyGetContextProvider, + ...?misskeyGetContextProvider.allTransitiveDependencies + }, +); + +typedef _UsersListRef = AutoDisposeFutureProviderRef>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/several_account_settings_page/cache_management_page/cache_management_page.dart b/lib/view/several_account_settings_page/cache_management_page/cache_management_page.dart index 5d27723d0..8c2d20fbc 100644 --- a/lib/view/several_account_settings_page/cache_management_page/cache_management_page.dart +++ b/lib/view/several_account_settings_page/cache_management_page/cache_management_page.dart @@ -1,16 +1,16 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/account_settings.dart'; -import 'package:miria/providers.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/account_settings.dart"; +import "package:miria/providers.dart"; @RoutePage() class CacheManagementPage extends ConsumerStatefulWidget { final Account account; - const CacheManagementPage({super.key, required this.account}); + const CacheManagementPage({required this.account, super.key}); @override ConsumerState createState() => @@ -52,14 +52,16 @@ class CacheManagementPageState extends ConsumerState { ), ]; Future save() async { - await ref.read(accountSettingsRepositoryProvider).save(ref - .read(accountSettingsRepositoryProvider) - .fromAccount(widget.account) - .copyWith( - iCacheStrategy: iCacheStrategy, - metaChacheStrategy: metaCacheStrategy, - emojiCacheStrategy: emojisCacheStrategy, - )); + await ref.read(accountSettingsRepositoryProvider).save( + ref + .read(accountSettingsRepositoryProvider) + .fromAccount(widget.account) + .copyWith( + iCacheStrategy: iCacheStrategy, + metaChacheStrategy: metaCacheStrategy, + emojiCacheStrategy: emojisCacheStrategy, + ), + ); } @override @@ -68,54 +70,55 @@ class CacheManagementPageState extends ConsumerState { appBar: AppBar(title: Text(S.of(context).cacheSettings)), body: SingleChildScrollView( child: Padding( - padding: const EdgeInsets.all(10), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - S.of(context).userCache, - style: Theme.of(context).textTheme.titleMedium, - ), - DropdownButton( - items: buildCacheStrategyItems, - value: iCacheStrategy, - isExpanded: true, - onChanged: (value) => setState(() { - iCacheStrategy = value; - save(); - }), - ), - const Padding(padding: EdgeInsets.only(top: 10)), - Text( - S.of(context).emojiCache, - style: Theme.of(context).textTheme.titleMedium, - ), - DropdownButton( - items: buildCacheStrategyItems, - value: emojisCacheStrategy, - isExpanded: true, - onChanged: (value) => setState(() { - emojisCacheStrategy = value; - save(); - }), - ), - const Padding(padding: EdgeInsets.only(top: 10)), - Text( - S.of(context).serverCache, - style: Theme.of(context).textTheme.titleMedium, - ), - DropdownButton( - items: buildCacheStrategyItems, - value: metaCacheStrategy, - isExpanded: true, - onChanged: (value) => setState(() { - metaCacheStrategy = value; - save(); - }), - ), - ], - )), + padding: const EdgeInsets.all(10), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).userCache, + style: Theme.of(context).textTheme.titleMedium, + ), + DropdownButton( + items: buildCacheStrategyItems, + value: iCacheStrategy, + isExpanded: true, + onChanged: (value) => setState(() { + iCacheStrategy = value; + save(); + }), + ), + const Padding(padding: EdgeInsets.only(top: 10)), + Text( + S.of(context).emojiCache, + style: Theme.of(context).textTheme.titleMedium, + ), + DropdownButton( + items: buildCacheStrategyItems, + value: emojisCacheStrategy, + isExpanded: true, + onChanged: (value) => setState(() { + emojisCacheStrategy = value; + save(); + }), + ), + const Padding(padding: EdgeInsets.only(top: 10)), + Text( + S.of(context).serverCache, + style: Theme.of(context).textTheme.titleMedium, + ), + DropdownButton( + items: buildCacheStrategyItems, + value: metaCacheStrategy, + isExpanded: true, + onChanged: (value) => setState(() { + metaCacheStrategy = value; + save(); + }), + ), + ], + ), + ), ), ); } diff --git a/lib/view/several_account_settings_page/instance_mute_page/instance_mute_page.dart b/lib/view/several_account_settings_page/instance_mute_page/instance_mute_page.dart index 843d490c4..36e86e7d5 100644 --- a/lib/view/several_account_settings_page/instance_mute_page/instance_mute_page.dart +++ b/lib/view/several_account_settings_page/instance_mute_page/instance_mute_page.dart @@ -1,61 +1,86 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/futurable.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; -@RoutePage() -class InstanceMutePage extends ConsumerStatefulWidget { - final Account account; - - const InstanceMutePage({super.key, required this.account}); +part "instance_mute_page.g.dart"; +@Riverpod(dependencies: [misskeyPostContext]) +class InstanceMutePageNotifier extends _$InstanceMutePageNotifier { @override - ConsumerState createState() => - InstanceMutePageState(); -} + Future<(List, AsyncValue?)> build() async { + return ( + (await ref.read(misskeyPostContextProvider).i.i()).mutedInstances, + null + ); + } -class InstanceMutePageState extends ConsumerState { - final controller = TextEditingController(); + Future save(String text) async { + final beforeState = await future; + state = AsyncData((beforeState.$1, const AsyncLoading())); - @override - void dispose() { - super.dispose(); - controller.dispose(); + final mutedInstances = + text.split("\n").whereNot((element) => element.trim().isEmpty).toList(); + state = AsyncData( + ( + beforeState.$1, + await ref.read(dialogStateNotifierProvider.notifier).guard( + () async { + await ref + .read(misskeyPostContextProvider) + .i + .update(IUpdateRequest(mutedInstances: mutedInstances)); + await ref.read(appRouterProvider).maybePop(); + }, + ) + ), + ); } +} - Future save() async { - final text = controller.text; +@RoutePage() +class InstanceMutePage extends HookConsumerWidget implements AutoRouteWrapper { + final Account account; - final List mutedInstances = - text.split("\n").whereNot((element) => element.trim().isEmpty).toList(); + const InstanceMutePage({required this.account, super.key}); - await ref - .read(misskeyProvider(widget.account)) - .i - .update(IUpdateRequest(mutedInstances: mutedInstances)); - if (!mounted) return; - Navigator.of(context).pop(); - } + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: account, child: this); @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final controller = useTextEditingController(); + final state = ref.watch(instanceMutePageNotifierProvider); + + ref.listen( + instanceMutePageNotifierProvider + .select((value) => value.valueOrNull?.$1), (_, next) { + if (next == null) return; + controller.text = next.join("\n"); + }); + return Scaffold( appBar: AppBar(title: Text(S.of(context).instanceMute)), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(10), - child: CommonFuture( - future: ref.read(misskeyProvider(widget.account)).i.i(), - futureFinished: (data) { - controller.text = data.mutedInstances.join("\n"); - }, - complete: (context, data) { - return Column( + child: switch (state) { + AsyncLoading() => const Center( + child: CircularProgressIndicator.adaptive(), + ), + AsyncError(:final error, :final stackTrace) => + ErrorDetail(error: error, stackTrace: stackTrace), + AsyncValue() => Column( crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.max, children: [ @@ -83,14 +108,15 @@ class InstanceMutePageState extends ConsumerState { style: Theme.of(context).textTheme.bodySmall, ), ElevatedButton.icon( - onPressed: save, + onPressed: () async => ref + .read(instanceMutePageNotifierProvider.notifier) + .save(controller.text), icon: const Icon(Icons.save), label: Text(S.of(context).save), ), ], - ); - }, - ), + ), + }, ), ), ); diff --git a/lib/view/several_account_settings_page/instance_mute_page/instance_mute_page.g.dart b/lib/view/several_account_settings_page/instance_mute_page/instance_mute_page.g.dart new file mode 100644 index 000000000..637862275 --- /dev/null +++ b/lib/view/several_account_settings_page/instance_mute_page/instance_mute_page.g.dart @@ -0,0 +1,31 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'instance_mute_page.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$instanceMutePageNotifierHash() => + r'66d63bb0c86f7bec0da2078bfcaff5f43a86d50f'; + +/// See also [InstanceMutePageNotifier]. +@ProviderFor(InstanceMutePageNotifier) +final instanceMutePageNotifierProvider = AutoDisposeAsyncNotifierProvider< + InstanceMutePageNotifier, (List, AsyncValue?)>.internal( + InstanceMutePageNotifier.new, + name: r'instanceMutePageNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$instanceMutePageNotifierHash, + dependencies: [misskeyPostContextProvider], + allTransitiveDependencies: { + misskeyPostContextProvider, + ...?misskeyPostContextProvider.allTransitiveDependencies + }, +); + +typedef _$InstanceMutePageNotifier + = AutoDisposeAsyncNotifier<(List, AsyncValue?)>; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/several_account_settings_page/reaction_deck_page/add_reactions_dialog.dart b/lib/view/several_account_settings_page/reaction_deck_page/add_reactions_dialog.dart index b40ee6267..60d8c5d8a 100644 --- a/lib/view/several_account_settings_page/reaction_deck_page/add_reactions_dialog.dart +++ b/lib/view/several_account_settings_page/reaction_deck_page/add_reactions_dialog.dart @@ -1,14 +1,16 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:json5/json5.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:url_launcher/url_launcher.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:json5/json5.dart"; +import "package:miria/model/account.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:url_launcher/url_launcher.dart"; -class AddReactionsDialog extends StatefulWidget { +class AddReactionsDialog extends HookConsumerWidget { const AddReactionsDialog({ - super.key, required this.account, + super.key, this.domain = "system", }); @@ -16,29 +18,17 @@ class AddReactionsDialog extends StatefulWidget { final String domain; @override - State createState() => _AddReactionsDialogState(); -} - -class _AddReactionsDialogState extends State { - final controller = TextEditingController(); - final formKey = GlobalKey(); - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } + Widget build(BuildContext context, WidgetRef ref) { + final formKey = useState(GlobalKey()); - @override - Widget build(BuildContext context) { - final host = widget.account.host; + final host = account.host; final uri = Uri( scheme: "https", host: host, pathSegments: [ "registry", "value", - widget.domain, + domain, "client", "base", "reactions", @@ -48,7 +38,7 @@ class _AddReactionsDialogState extends State { title: Text(S.of(context).bulkAddReactions), scrollable: true, content: Form( - key: formKey, + key: formKey.value, child: Column( children: [ Column( @@ -64,7 +54,7 @@ class _AddReactionsDialogState extends State { children: [ Text(S.of(context).bulkAddReactionsDescription2), TextButton( - onPressed: () => launchUrl( + onPressed: () async => launchUrl( uri, mode: LaunchMode.externalApplication, ), @@ -98,8 +88,7 @@ class _AddReactionsDialogState extends State { return S.of(context).pleaseInput; } try { - final emojiNames = JSON5.parse(value) as List; - emojiNames.map((name) => name as String); + (JSON5.parse(value) as List).map((name) => name as String); } catch (e) { return S.of(context).invalidInput; } @@ -107,7 +96,7 @@ class _AddReactionsDialogState extends State { }, autovalidateMode: AutovalidateMode.onUserInteraction, onSaved: (value) { - if (formKey.currentState!.validate()) { + if (formKey.value.currentState!.validate()) { final emojiNames = JSON5.parse(value!) as List; Navigator.of(context) .pop(emojiNames.map((name) => name as String).toList()); @@ -115,7 +104,7 @@ class _AddReactionsDialogState extends State { }, ), ElevatedButton( - onPressed: () => formKey.currentState?.save.call(), + onPressed: () => formKey.value.currentState?.save(), child: Text(S.of(context).done), ), ], diff --git a/lib/view/several_account_settings_page/reaction_deck_page/reaction_deck_page.dart b/lib/view/several_account_settings_page/reaction_deck_page/reaction_deck_page.dart index d90bd4fed..2ae06bbd7 100644 --- a/lib/view/several_account_settings_page/reaction_deck_page/reaction_deck_page.dart +++ b/lib/view/several_account_settings_page/reaction_deck_page/reaction_deck_page.dart @@ -1,19 +1,20 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:json5/json5.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/misskey_notes/custom_emoji.dart'; -import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; -import 'package:miria/view/reaction_picker_dialog/reaction_picker_dialog.dart'; -import 'package:miria/view/several_account_settings_page/reaction_deck_page/add_reactions_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:reorderables/reorderables.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:json5/json5.dart"; +import "package:miria/log.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/misskey_notes/custom_emoji.dart"; +import "package:miria/view/dialogs/simple_confirm_dialog.dart"; +import "package:miria/view/several_account_settings_page/reaction_deck_page/add_reactions_dialog.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:reorderables/reorderables.dart"; enum ReactionDeckPageMenuType { addMany, copy, clear } @@ -21,7 +22,7 @@ enum ReactionDeckPageMenuType { addMany, copy, clear } class ReactionDeckPage extends ConsumerStatefulWidget { final Account account; - const ReactionDeckPage({super.key, required this.account}); + const ReactionDeckPage({required this.account, super.key}); @override ConsumerState createState() => @@ -47,7 +48,8 @@ class ReactionDeckPageState extends ConsumerState { reactions ..clear() ..addAll( - ref.read(emojiRepositoryProvider(widget.account)).defaultEmojis()); + ref.read(emojiRepositoryProvider(widget.account)).defaultEmojis(), + ); } @override @@ -78,7 +80,7 @@ class ReactionDeckPageState extends ConsumerState { child: Text(S.of(context).copy), ), ], - ) + ), ], ), body: Padding( @@ -91,41 +93,42 @@ class ReactionDeckPageState extends ConsumerState { child: Padding( padding: const EdgeInsets.all(10), child: ReorderableWrap( - scrollPhysics: const NeverScrollableScrollPhysics(), - spacing: 5, - runSpacing: 5, - children: [ - for (final reaction in reactions) - GestureDetector( - onTap: () { - setState(() { - reactions.remove(reaction); - save(); - }); - }, - child: CustomEmoji( - emojiData: reaction, - fontSizeRatio: 2, - isAttachTooltip: false, - ), - ) - ], - onReorder: (int oldIndex, int newIndex) { - setState(() { - final element = reactions.removeAt(oldIndex); - reactions.insert(newIndex, element); - save(); - }); - }), + scrollPhysics: const NeverScrollableScrollPhysics(), + spacing: 5, + runSpacing: 5, + children: [ + for (final reaction in reactions) + GestureDetector( + onTap: () { + setState(() { + reactions.remove(reaction); + save(); + }); + }, + child: CustomEmoji( + emojiData: reaction, + fontSizeRatio: 2, + isAttachTooltip: false, + ), + ), + ], + onReorder: (oldIndex, newIndex) { + setState(() { + final element = reactions.removeAt(oldIndex); + reactions.insert(newIndex, element); + save(); + }); + }, + ), ), ), Row( children: [ IconButton( onPressed: () async { - final reaction = await showDialog( - context: context, - builder: (context) => ReactionPickerDialog( + final reaction = + await context.pushRoute( + ReactionPickerRoute( account: widget.account, isAcceptSensitive: true, ), @@ -148,7 +151,7 @@ class ReactionDeckPageState extends ConsumerState { child: Text(S.of(context).editReactionDeckDescription), ), ], - ) + ), ], ), ), @@ -167,13 +170,13 @@ class ReactionDeckPageState extends ConsumerState { ), ); - print(reactions); + logger.info(reactions); } catch (e) { final endpoints = await ref.read(misskeyProvider(widget.account)).endpoints(); final domain = endpoints.contains("i/registry/scopes-with-domain") ? "@" : "system"; - if (!mounted) return; + if (!context.mounted) return; final emojiNames = await showDialog>( context: context, builder: (context) => AddReactionsDialog( @@ -204,8 +207,8 @@ class ReactionDeckPageState extends ConsumerState { } } - void copyReactions({required BuildContext context}) { - Clipboard.setData( + Future copyReactions({required BuildContext context}) async { + await Clipboard.setData( ClipboardData( text: JSON5.stringify( reactions @@ -221,8 +224,12 @@ class ReactionDeckPageState extends ConsumerState { ), ), ); + if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(S.of(context).doneCopy), duration: const Duration(seconds: 1)), + SnackBar( + content: Text(S.of(context).doneCopy), + duration: const Duration(seconds: 1), + ), ); } diff --git a/lib/view/several_account_settings_page/several_account_general_settings_page/several_account_general_settings_page.dart b/lib/view/several_account_settings_page/several_account_general_settings_page/several_account_general_settings_page.dart index e59410a34..c3f9d5750 100644 --- a/lib/view/several_account_settings_page/several_account_general_settings_page/several_account_general_settings_page.dart +++ b/lib/view/several_account_settings_page/several_account_general_settings_page/several_account_general_settings_page.dart @@ -1,24 +1,24 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mfm/mfm.dart'; -import 'package:miria/extensions/note_visibility_extension.dart'; -import 'package:miria/extensions/reaction_acceptance_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/account_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:mfm/mfm.dart"; +import "package:miria/extensions/note_visibility_extension.dart"; +import "package:miria/extensions/reaction_acceptance_extension.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/account_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:misskey_dart/misskey_dart.dart"; @RoutePage() class SeveralAccountGeneralSettingsPage extends ConsumerStatefulWidget { final Account account; const SeveralAccountGeneralSettingsPage({ - super.key, required this.account, + super.key, }); @override @@ -41,9 +41,11 @@ class SeveralAccountGeneralSettingsPageState final loadedSettings = ref .read(accountSettingsRepositoryProvider) .accountSettings - .firstWhereOrNull((element) => - element.userId == widget.account.userId && - element.host == widget.account.host); + .firstWhereOrNull( + (element) => + element.userId == widget.account.userId && + element.host == widget.account.host, + ); if (loadedSettings != null) { accountSettings = loadedSettings; if (!mounted) return; @@ -73,7 +75,7 @@ class SeveralAccountGeneralSettingsPageState @override Widget build(BuildContext context) { - return AccountScope( + return AccountContextScope.as( account: widget.account, child: Scaffold( appBar: AppBar( @@ -107,23 +109,23 @@ class SeveralAccountGeneralSettingsPageState const Padding(padding: EdgeInsets.only(top: 10)), Text(S.of(context).noteVisibility), DropdownButton( - items: [ - for (final noteVisibility - in NoteVisibility.values) - DropdownMenuItem( - value: noteVisibility, - child: - Text(noteVisibility.displayName(context)), - ), - ], - value: defaultNoteVisibility, - onChanged: (value) { - setState(() { - defaultNoteVisibility = - value ?? NoteVisibility.public; - save(); - }); - }), + items: [ + for (final noteVisibility in NoteVisibility.values) + DropdownMenuItem( + value: noteVisibility, + child: + Text(noteVisibility.displayName(context)), + ), + ], + value: defaultNoteVisibility, + onChanged: (value) { + setState(() { + defaultNoteVisibility = + value ?? NoteVisibility.public; + save(); + }); + }, + ), const Padding(padding: EdgeInsets.only(top: 10)), CheckboxListTile( value: defaultIsLocalOnly, @@ -140,25 +142,24 @@ class SeveralAccountGeneralSettingsPageState const Padding(padding: EdgeInsets.only(top: 10)), Text(S.of(context).reactionAcceptance), DropdownButton( - items: [ + items: [ + DropdownMenuItem( + child: Text(S.of(context).reactionAcceptanceAll), + ), + for (final acceptance in ReactionAcceptance.values) DropdownMenuItem( - child: - Text(S.of(context).reactionAcceptanceAll), + value: acceptance, + child: Text(acceptance.displayName(context)), ), - for (final acceptance - in ReactionAcceptance.values) - DropdownMenuItem( - value: acceptance, - child: Text(acceptance.displayName(context)), - ), - ], - value: defaultReactionAppearance, - onChanged: (value) { - setState(() { - defaultReactionAppearance = value; - save(); - }); - }), + ], + value: defaultReactionAppearance, + onChanged: (value) { + setState(() { + defaultReactionAppearance = value; + save(); + }); + }, + ), const Padding(padding: EdgeInsets.only(top: 10)), Text(S.of(context).ad), CheckboxListTile( @@ -176,7 +177,7 @@ class SeveralAccountGeneralSettingsPageState ), ), ), - ) + ), ], ), ), diff --git a/lib/view/several_account_settings_page/several_account_settings_page.dart b/lib/view/several_account_settings_page/several_account_settings_page.dart index c8a858f38..9b7b1248c 100644 --- a/lib/view/several_account_settings_page/several_account_settings_page.dart +++ b/lib/view/several_account_settings_page/several_account_settings_page.dart @@ -1,15 +1,15 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/several_account_settings_page/word_mute_page/word_mute_page.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:miria/model/account.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/several_account_settings_page/word_mute_page/word_mute_page.dart"; @RoutePage() class SeveralAccountSettingsPage extends StatelessWidget { final Account account; - const SeveralAccountSettingsPage({super.key, required this.account}); + const SeveralAccountSettingsPage({required this.account, super.key}); @override Widget build(BuildContext context) { @@ -22,56 +22,52 @@ class SeveralAccountSettingsPage extends StatelessWidget { body: ListView( children: [ ListTile( - onTap: () { - context.pushRoute( - SeveralAccountGeneralSettingsRoute(account: account)); - }, + onTap: () async => await context.pushRoute( + SeveralAccountGeneralSettingsRoute(account: account), + ), title: Text(S.of(context).generalSettings), leading: const Icon(Icons.settings), trailing: const Icon(Icons.chevron_right), ), ListTile( - onTap: () { - context.pushRoute(ReactionDeckRoute(account: account)); - }, + onTap: () async => + await context.pushRoute(ReactionDeckRoute(account: account)), title: Text(S.of(context).reactionDeck), leading: const Icon(Icons.favorite), trailing: const Icon(Icons.chevron_right), ), ListTile( - onTap: () { - context.pushRoute( - WordMuteRoute(account: account, muteType: MuteType.soft)); - }, + onTap: () async => await context.pushRoute( + WordMuteRoute(account: account, muteType: MuteType.soft), + ), title: Text(S.of(context).wordMute), leading: const Icon(Icons.comments_disabled), trailing: const Icon(Icons.chevron_right), ), ListTile( - onTap: () { - context.pushRoute( - WordMuteRoute(account: account, muteType: MuteType.hard)); - }, + onTap: () async => await context.pushRoute( + WordMuteRoute(account: account, muteType: MuteType.hard), + ), title: Text(S.of(context).hardWordMute), leading: const Icon(Icons.comments_disabled), trailing: const Icon(Icons.chevron_right), ), ListTile( - onTap: () { - context.pushRoute(InstanceMuteRoute(account: account)); - }, + onTap: () async => await context.pushRoute( + InstanceMuteRoute(account: account), + ), title: Text(S.of(context).instanceMute), leading: const Icon(Icons.comments_disabled), trailing: const Icon(Icons.chevron_right), ), ListTile( - onTap: () { - context.pushRoute(CacheManagementRoute(account: account)); - }, + onTap: () async => await context.pushRoute( + CacheManagementRoute(account: account), + ), title: Text(S.of(context).cacheSettings), leading: const Icon(Icons.cached), trailing: const Icon(Icons.chevron_right), - ) + ), ], ), ); diff --git a/lib/view/several_account_settings_page/soft_mute_page/soft_mute_page.dart b/lib/view/several_account_settings_page/soft_mute_page/soft_mute_page.dart deleted file mode 100644 index e3ab24169..000000000 --- a/lib/view/several_account_settings_page/soft_mute_page/soft_mute_page.dart +++ /dev/null @@ -1,42 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/view/common/futurable.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; - -class SoftMutePage extends ConsumerStatefulWidget { - const SoftMutePage({super.key}); - - @override - ConsumerState createState() => SoftMuteState(); -} - -class SoftMuteState extends ConsumerState { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text(S.of(context).wordMute)), - body: SingleChildScrollView( - child: CommonFuture( - future: () async { - return []; - }(), - complete: (context, data) { - return Row( - children: [ - Card(child: Text(S.of(context).hideConditionalNotes)), - const TextField( - maxLines: null, - minLines: 5, - ), - Text( - S.of(context).muteSettingDescription, - style: Theme.of(context).textTheme.bodySmall, - ), - ], - ); - }, - ), - ), - ); - } -} diff --git a/lib/view/several_account_settings_page/word_mute_page/word_mute_page.dart b/lib/view/several_account_settings_page/word_mute_page/word_mute_page.dart index 348c06abc..cb9b7da06 100644 --- a/lib/view/several_account_settings_page/word_mute_page/word_mute_page.dart +++ b/lib/view/several_account_settings_page/word_mute_page/word_mute_page.dart @@ -1,38 +1,31 @@ -import 'package:auto_route/annotations.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/futurable.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/futurable.dart"; +import "package:misskey_dart/misskey_dart.dart"; enum MuteType { soft, hard } @RoutePage() -class WordMutePage extends ConsumerStatefulWidget { +class WordMutePage extends HookConsumerWidget implements AutoRouteWrapper { final Account account; final MuteType muteType; const WordMutePage({ - super.key, required this.account, required this.muteType, + super.key, }); @override - ConsumerState createState() => WordMutePageState(); -} - -class WordMutePageState extends ConsumerState { - final controller = TextEditingController(); - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: account, child: this); String muteValueString(List? wordMutes) { if (wordMutes == null) return ""; @@ -49,10 +42,8 @@ class WordMutePageState extends ConsumerState { .join("\n"); } - Future save() async { - final text = controller.text; - - final List wordMutes = + Future save(String text, BuildContext context, WidgetRef ref) async { + final wordMutes = text.split("\n").whereNot((element) => element.trim().isEmpty).map((e) { if (e.startsWith("/")) { return MuteWord(regExp: e); @@ -61,33 +52,37 @@ class WordMutePageState extends ConsumerState { } }).toList(); - await ref.read(misskeyProvider(widget.account)).i.update( + await ref.read(misskeyGetContextProvider).i.update( IUpdateRequest( - mutedWords: widget.muteType == MuteType.soft ? wordMutes : null, - hardMutedWords: widget.muteType == MuteType.hard ? wordMutes : null, + mutedWords: muteType == MuteType.soft ? wordMutes : null, + hardMutedWords: muteType == MuteType.hard ? wordMutes : null, ), ); - if (!mounted) return; - Navigator.of(context).pop(); + if (!context.mounted) return; + await context.maybePop(); } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { + final controller = useTextEditingController(); + return Scaffold( appBar: AppBar( - title: Text(switch (widget.muteType) { - MuteType.soft => S.of(context).wordMute, - MuteType.hard => S.of(context).hardWordMute, - }), + title: Text( + switch (muteType) { + MuteType.soft => S.of(context).wordMute, + MuteType.hard => S.of(context).hardWordMute, + }, + ), ), body: SingleChildScrollView( child: Padding( padding: const EdgeInsets.all(10), child: CommonFuture( - future: ref.read(misskeyProvider(widget.account)).i.i(), + future: ref.read(misskeyPostContextProvider).i.i(), futureFinished: (data) { controller.text = muteValueString( - widget.muteType == MuteType.soft + muteType == MuteType.soft ? data.mutedWords : data.hardMutedWords, ); @@ -109,7 +104,7 @@ class WordMutePageState extends ConsumerState { style: Theme.of(context).textTheme.bodySmall, ), ElevatedButton.icon( - onPressed: save, + onPressed: () => save(controller.text, context, ref), icon: const Icon(Icons.save), label: Text(S.of(context).save), ), diff --git a/lib/view/share_extension_page/share_extension_page.dart b/lib/view/share_extension_page/share_extension_page.dart index 7680c0abb..e7e110768 100644 --- a/lib/view/share_extension_page/share_extension_page.dart +++ b/lib/view/share_extension_page/share_extension_page.dart @@ -1,15 +1,16 @@ -import 'dart:convert'; +import "dart:convert"; -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:freezed_annotation/freezed_annotation.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:shared_preference_app_group/shared_preference_app_group.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/account_repository.dart"; +import "package:miria/router/app_router.dart"; +import "package:shared_preference_app_group/shared_preference_app_group.dart"; -part 'share_extension_page.freezed.dart'; -part 'share_extension_page.g.dart'; +part "share_extension_page.freezed.dart"; +part "share_extension_page.g.dart"; @freezed class ShareExtensionData with _$ShareExtensionData { @@ -58,19 +59,23 @@ class ShareExtensionPageState extends ConsumerState { try { await ref.read(accountRepositoryProvider.notifier).load(); final json = jsonDecode( - await SharedPreferenceAppGroup.get("ShareKey") as String? ?? ""); + await SharedPreferenceAppGroup.get("ShareKey") as String? ?? "", + ); await SharedPreferenceAppGroup.setString("ShareKey", ""); final sharedData = ShareExtensionData.fromJson(json as Map); if (ref.read(accountsProvider).length >= 2) { if (!mounted) return; - context.replaceRoute(SharingAccountSelectRoute( + await context.replaceRoute( + SharingAccountSelectRoute( sharingText: sharedData.text.join("\n"), - filePath: sharedData.files.map((e) => e.path).toList())); + filePath: sharedData.files.map((e) => e.path).toList(), + ), + ); } else { if (!mounted) return; - context.replaceRoute( + await context.replaceRoute( NoteCreateRoute( initialAccount: ref.read(accountsProvider)[0], initialText: sharedData.text.join("\n"), @@ -96,7 +101,7 @@ class ShareExtensionPageState extends ConsumerState { child: SizedBox( width: 100, height: 100, - child: CircularProgressIndicator(), + child: CircularProgressIndicator.adaptive(), ), ), ); diff --git a/lib/view/share_extension_page/share_extension_page.freezed.dart b/lib/view/share_extension_page/share_extension_page.freezed.dart index 1242d6351..3adad0db0 100644 --- a/lib/view/share_extension_page/share_extension_page.freezed.dart +++ b/lib/view/share_extension_page/share_extension_page.freezed.dart @@ -12,7 +12,7 @@ part of 'share_extension_page.dart'; T _$identity(T value) => value; final _privateConstructorUsedError = UnsupportedError( - 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#custom-getters-and-methods'); + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); ShareExtensionData _$ShareExtensionDataFromJson(Map json) { return _ShareExtensionData.fromJson(json); @@ -23,8 +23,12 @@ mixin _$ShareExtensionData { List get text => throw _privateConstructorUsedError; List get files => throw _privateConstructorUsedError; + /// Serializes this ShareExtensionData to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of ShareExtensionData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $ShareExtensionDataCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -48,6 +52,8 @@ class _$ShareExtensionDataCopyWithImpl<$Res, $Val extends ShareExtensionData> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of ShareExtensionData + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -86,6 +92,8 @@ class __$$ShareExtensionDataImplCopyWithImpl<$Res> $Res Function(_$ShareExtensionDataImpl) _then) : super(_value, _then); + /// Create a copy of ShareExtensionData + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -147,14 +155,16 @@ class _$ShareExtensionDataImpl implements _ShareExtensionData { const DeepCollectionEquality().equals(other._files, _files)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash( runtimeType, const DeepCollectionEquality().hash(_text), const DeepCollectionEquality().hash(_files)); - @JsonKey(ignore: true) + /// Create a copy of ShareExtensionData + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$ShareExtensionDataImplCopyWith<_$ShareExtensionDataImpl> get copyWith => @@ -181,8 +191,11 @@ abstract class _ShareExtensionData implements ShareExtensionData { List get text; @override List get files; + + /// Create a copy of ShareExtensionData + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$ShareExtensionDataImplCopyWith<_$ShareExtensionDataImpl> get copyWith => throw _privateConstructorUsedError; } @@ -196,8 +209,12 @@ mixin _$SharedFiles { String get path => throw _privateConstructorUsedError; int get type => throw _privateConstructorUsedError; + /// Serializes this SharedFiles to a JSON map. Map toJson() => throw _privateConstructorUsedError; - @JsonKey(ignore: true) + + /// Create a copy of SharedFiles + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) $SharedFilesCopyWith get copyWith => throw _privateConstructorUsedError; } @@ -221,6 +238,8 @@ class _$SharedFilesCopyWithImpl<$Res, $Val extends SharedFiles> // ignore: unused_field final $Res Function($Val) _then; + /// Create a copy of SharedFiles + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -259,6 +278,8 @@ class __$$SharedFilesImplCopyWithImpl<$Res> _$SharedFilesImpl _value, $Res Function(_$SharedFilesImpl) _then) : super(_value, _then); + /// Create a copy of SharedFiles + /// with the given fields replaced by the non-null parameter values. @pragma('vm:prefer-inline') @override $Res call({ @@ -305,11 +326,13 @@ class _$SharedFilesImpl implements _SharedFiles { (identical(other.type, type) || other.type == type)); } - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) @override int get hashCode => Object.hash(runtimeType, path, type); - @JsonKey(ignore: true) + /// Create a copy of SharedFiles + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) @override @pragma('vm:prefer-inline') _$$SharedFilesImplCopyWith<_$SharedFilesImpl> get copyWith => @@ -334,8 +357,11 @@ abstract class _SharedFiles implements SharedFiles { String get path; @override int get type; + + /// Create a copy of SharedFiles + /// with the given fields replaced by the non-null parameter values. @override - @JsonKey(ignore: true) + @JsonKey(includeFromJson: false, includeToJson: false) _$$SharedFilesImplCopyWith<_$SharedFilesImpl> get copyWith => throw _privateConstructorUsedError; } diff --git a/lib/view/share_extension_page/share_extension_page.g.dart b/lib/view/share_extension_page/share_extension_page.g.dart index 146f5dba1..a8d858add 100644 --- a/lib/view/share_extension_page/share_extension_page.g.dart +++ b/lib/view/share_extension_page/share_extension_page.g.dart @@ -25,7 +25,7 @@ Map _$$ShareExtensionDataImplToJson( _$SharedFilesImpl _$$SharedFilesImplFromJson(Map json) => _$SharedFilesImpl( path: json['path'] as String, - type: json['type'] as int, + type: (json['type'] as num).toInt(), ); Map _$$SharedFilesImplToJson(_$SharedFilesImpl instance) => diff --git a/lib/view/sharing_account_select_page/account_select_page.dart b/lib/view/sharing_account_select_page/account_select_page.dart index 0ac734b4e..b5ca44eeb 100644 --- a/lib/view/sharing_account_select_page/account_select_page.dart +++ b/lib/view/sharing_account_select_page/account_select_page.dart @@ -1,11 +1,11 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/avatar_icon.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/avatar_icon.dart"; @RoutePage() class SharingAccountSelectPage extends ConsumerWidget { @@ -31,7 +31,7 @@ class SharingAccountSelectPage extends ConsumerWidget { .loadFromLocalCache(); } if (!context.mounted) return; - context.replaceRoute( + await context.replaceRoute( NoteCreateRoute( initialAccount: account, initialText: sharingText, @@ -41,8 +41,10 @@ class SharingAccountSelectPage extends ConsumerWidget { ); }, leading: AvatarIcon(user: account.i), - title: Text(account.i.name ?? account.i.username, - style: Theme.of(context).textTheme.titleMedium), + title: Text( + account.i.name ?? account.i.username, + style: Theme.of(context).textTheme.titleMedium, + ), subtitle: Text( account.acct.toString(), style: Theme.of(context).textTheme.bodySmall, diff --git a/lib/view/splash_page/splash_page.dart b/lib/view/splash_page/splash_page.dart index 49a9b4963..9d5bc215a 100644 --- a/lib/view/splash_page/splash_page.dart +++ b/lib/view/splash_page/splash_page.dart @@ -1,13 +1,14 @@ -import 'dart:io'; +import "dart:io"; -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/licenses.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:receive_sharing_intent/receive_sharing_intent.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/licenses.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/account_repository.dart"; +import "package:miria/router/app_router.dart"; +import "package:receive_sharing_intent/receive_sharing_intent.dart"; @RoutePage() class SplashPage extends ConsumerStatefulWidget { @@ -31,7 +32,6 @@ class SplashPageState extends ConsumerState { for (final account in ref.read(accountsProvider)) { await ref.read(emojiRepositoryProvider(account)).loadFromLocalCache(); - ref.read(mainStreamRepositoryProvider(account)).connect(); } if (_isFirst) { @@ -43,7 +43,8 @@ class SplashPageState extends ConsumerState { } LicenseRegistry.addLicense( - () => Stream.fromIterable(miriaInheritedLicenses)); + () => Stream.fromIterable(miriaInheritedLicenses), + ); } _isFirst = false; @@ -53,57 +54,61 @@ class SplashPageState extends ConsumerState { Widget build(BuildContext context) { return Scaffold( body: FutureBuilder( - future: initialize(), - builder: (context, snapshot) { - if (snapshot.connectionState == ConnectionState.done) { - final accounts = ref.read(accountsProvider); - final isSigned = accounts.isNotEmpty; - final hasTabSetting = ref - .read(tabSettingsRepositoryProvider) - .tabSettings - .isNotEmpty; + future: initialize(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.done) { + final accounts = ref.read(accountsProvider); + final isSigned = accounts.isNotEmpty; + final hasTabSetting = + ref.read(tabSettingsRepositoryProvider).tabSettings.isNotEmpty; - if (isSigned && hasTabSetting) { - context.replaceRoute(TimeLineRoute( - initialTabSetting: ref - .read(tabSettingsRepositoryProvider) - .tabSettings - .first)); - if (initialSharingMedias.isNotEmpty || - initialSharingText.isNotEmpty) { - if (accounts.length == 1) { - context.pushRoute(NoteCreateRoute( + if (isSigned && hasTabSetting) { + context.replaceRoute( + TimeLineRoute( + initialTabSetting: + ref.read(tabSettingsRepositoryProvider).tabSettings.first, + ), + ); + if (initialSharingMedias.isNotEmpty || + initialSharingText.isNotEmpty) { + if (accounts.length == 1) { + context.pushRoute( + NoteCreateRoute( initialMediaFiles: initialSharingMedias, initialText: initialSharingText, initialAccount: accounts.first, - )); - } else { - context.pushRoute(SharingAccountSelectRoute( + ), + ); + } else { + context.pushRoute( + SharingAccountSelectRoute( filePath: initialSharingMedias, sharingText: initialSharingText, - )); - } + ), + ); } - } else if (isSigned && !hasTabSetting) { - // KeyChainに保存したデータだけアンインストールしても残るので - // この状況が発生する - Future(() async { - for (final account in accounts) { - await ref - .read(accountRepositoryProvider.notifier) - .remove(account); - } - if (!mounted) return; + } + } else if (isSigned && !hasTabSetting) { + // KeyChainに保存したデータだけアンインストールしても残るので + // この状況が発生する + Future(() async { + for (final account in accounts) { + await ref + .read(accountRepositoryProvider.notifier) + .remove(account); + } + if (!mounted) return; - context.replaceRoute(const LoginRoute()); - }); - } else { context.replaceRoute(const LoginRoute()); - } + }); + } else { + context.replaceRoute(const LoginRoute()); } + } - return const Center(child: CircularProgressIndicator()); - }), + return const Center(child: CircularProgressIndicator.adaptive()); + }, + ), ); } } diff --git a/lib/view/themes/app_theme.dart b/lib/view/themes/app_theme.dart index fffc8e0df..84cf5f224 100644 --- a/lib/view/themes/app_theme.dart +++ b/lib/view/themes/app_theme.dart @@ -1,13 +1,14 @@ -import 'package:flutter/material.dart'; -import 'package:miria/model/color_theme.dart'; +import "package:flutter/material.dart"; +import "package:miria/model/color_theme.dart"; +import "package:miria/model/general_settings.dart"; class AppTheme extends InheritedWidget { final AppThemeData themeData; const AppTheme({ - super.key, required super.child, required this.themeData, + super.key, }); @override @@ -46,6 +47,7 @@ class AppThemeData { final List renoteDashPattern; final Color currentDisplayTabColor; final Color buttonBackground; + final Languages languages; const AppThemeData({ required this.colorTheme, @@ -69,5 +71,6 @@ class AppThemeData { required this.renoteDashPattern, required this.currentDisplayTabColor, required this.buttonBackground, + required this.languages, }); } diff --git a/lib/view/themes/app_theme_scope.dart b/lib/view/themes/app_theme_scope.dart index 1b2f7f153..e03d21826 100644 --- a/lib/view/themes/app_theme_scope.dart +++ b/lib/view/themes/app_theme_scope.dart @@ -1,19 +1,19 @@ -import 'package:collection/collection.dart'; -import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:miria/extensions/color_extension.dart'; -import 'package:miria/model/color_theme.dart'; -import 'package:miria/model/general_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:miria/view/themes/built_in_color_themes.dart'; +import "package:collection/collection.dart"; +import "package:flutter/foundation.dart"; +import "package:flutter/material.dart"; +import "package:google_fonts/google_fonts.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/color_extension.dart"; +import "package:miria/model/color_theme.dart"; +import "package:miria/model/general_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:miria/view/themes/built_in_color_themes.dart"; class AppThemeScope extends ConsumerStatefulWidget { final Widget child; - const AppThemeScope({super.key, required this.child}); + const AppThemeScope({required this.child, super.key}); @override ConsumerState createState() => AppThemeScopeState(); @@ -27,24 +27,25 @@ class AppThemeScopeState extends ConsumerState { required String monospaceFontName, required String cursiveFontName, required String fantasyFontName, + required Languages languages, }) { return AppThemeData( colorTheme: theme, isDarkMode: theme.isDarkTheme, noteTextStyle: const InputDecoration(), reactionButtonStyle: ElevatedButton.styleFrom( - padding: const EdgeInsets.all(5), - elevation: 0, - minimumSize: const Size(0, 0), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - visualDensity: const VisualDensity(horizontal: 0, vertical: 0), - shape: - RoundedRectangleBorder(borderRadius: BorderRadius.circular(5))), + padding: const EdgeInsets.all(5), + elevation: 0, + minimumSize: const Size(0, 0), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + visualDensity: const VisualDensity(horizontal: 0, vertical: 0), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(5)), + ), linkStyle: TextStyle(color: theme.link), hashtagStyle: TextStyle(color: theme.hashtag), mentionStyle: TextStyle(color: theme.mention), - serifStyle: resolveFontFamilySerif(serifFontName), - monospaceStyle: resolveFontFamilyMonospace(monospaceFontName), + serifStyle: resolveFontFamilySerif(serifFontName, languages), + monospaceStyle: resolveFontFamilyMonospace(monospaceFontName, languages), cursiveStyle: cursiveFontName.isNotEmpty ? (fromGoogleFont(cursiveFontName) ?? const TextStyle()) : const TextStyle(), @@ -62,76 +63,127 @@ class AppThemeScopeState extends ConsumerState { currentDisplayTabColor: theme.isDarkTheme ? theme.primaryDarken : theme.primaryLighten, unicodeEmojiStyle: resolveUnicodeEmojiStyle(), + languages: languages, ); } - String resolveFontFamilyName(String defaultFontName) { + dynamic resolveFontFamilyName(String defaultFontName, Languages languages) { if (defaultFontName.isNotEmpty) { return defaultFontName; } if (defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS) { return "SF Pro Text"; + } else if (defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.windows) { + if (languages == Languages.jaJP || languages == Languages.jaOJ) { + return "Noto Sans JP"; + } else if (languages == Languages.zhCN) { + return "Noto Sans SC"; + } else { + return "Noto Sans"; + } + } else { + if (languages == Languages.jaJP || languages == Languages.jaOJ) { + return "Noto Sans CJK JP"; + } else if (languages == Languages.zhCN) { + return "Noto Sans CJK SC"; + } else { + return "Noto Sans"; + } } - if (defaultTargetPlatform == TargetPlatform.linux) { - return "Noto Sans CJK JP"; - } - - return "KosugiMaru"; } - List resolveFontFamilyFallback(String defaultFontName) { - if (defaultTargetPlatform == TargetPlatform.windows || - defaultTargetPlatform == TargetPlatform.linux) { - return [ - if (defaultFontName.isNotEmpty) resolveFontFamilyName(""), - "Noto Sans CJK JP", - "KosugiMaru", - "BIZ UDPGothic" - ]; - } + List resolveFontFamilyFallback( + String defaultFontName, Languages languages) { if (defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS) { return [ - if (defaultFontName.isNotEmpty) resolveFontFamilyName(""), + if (defaultFontName.isNotEmpty) resolveFontFamilyName("", languages), "Hiragino Maru Gothic ProN", "Apple Color Emoji", ]; + } else { + return [ + if (defaultFontName.isNotEmpty) resolveFontFamilyName("", languages), + "Noto Color Emoji", + ]; } - return []; } - TextStyle resolveFontFamilySerif(String defaultFontName) { + TextStyle resolveFontFamilySerif(String serifFontName, Languages languages) { + final String? fontName; final fallback = []; if (defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS) { - fallback.addAll(["Hiragino Mincho ProN", "Apple Color Emoji"]); + fontName = "Hiragino Mincho ProN"; + fallback.addAll(const [ + "Apple Color Emoji", + ]); } else { - fallback.addAll(["Noto Serif CJK JP", "Noto Serif", "Droid Serif"]); + if (defaultTargetPlatform == TargetPlatform.android || + defaultTargetPlatform == TargetPlatform.windows) { + if (languages == Languages.jaJP || languages == Languages.jaOJ) { + fontName = "Noto Serif JP"; + } else if (languages == Languages.zhCN) { + fontName = "Noto Serif SC"; + } else { + fontName = "Noto Serif"; + } + } else { + if (languages == Languages.jaJP || languages == Languages.jaOJ) { + fontName = "Noto Serif CJK JP"; + } else if (languages == Languages.zhCN) { + fontName = "Noto Serif CJK SC"; + } else { + fontName = "Noto Serif"; + } + } + fallback.addAll(const [ + "Noto Color Emoji", + ]); } - return (defaultFontName.isEmpty - ? const TextStyle() - : (fromGoogleFont(defaultFontName) ?? const TextStyle())) + return (serifFontName.isNotEmpty + ? (fromGoogleFont(serifFontName) ?? TextStyle(fontFamily: fontName)) + : TextStyle(fontFamily: fontName)) .copyWith(fontFamilyFallback: fallback); } - TextStyle resolveFontFamilyMonospace(String monospaceFontName) { + TextStyle resolveFontFamilyMonospace( + String monospaceFontName, Languages languages) { final String? fontName; final fallback = []; if (defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS) { - fontName = "Monaco"; - fallback.addAll(const ["Apple Color Emoji", "Hiragino Maru Gothic ProN"]); - } else if (defaultTargetPlatform == TargetPlatform.windows) { - fontName = "Consolas"; - fallback.addAll(const ["Segoe UI Emoji", "Noto Color Emoji", "Meiryo"]); - } else if (defaultTargetPlatform == TargetPlatform.android) { - fontName = "Droid Sans Mono"; - fallback.addAll(const ["Noto Color Emoji", "Noto Sans JP"]); + if (defaultTargetPlatform == TargetPlatform.iOS) { + fontName = "Menlo"; + } else { + fontName = "Monaco"; + } + fallback.addAll(const [ + "Apple Color Emoji", + "Hiragino Maru Gothic ProN", + ]); } else { - fontName = null; + if (defaultTargetPlatform == TargetPlatform.android) { + fontName = "Droid Sans Mono"; + } else if (defaultTargetPlatform == TargetPlatform.windows) { + fontName = "Consolas"; + } else { + if (languages == Languages.jaJP || languages == Languages.jaOJ) { + fontName = "Noto Sans Mono CJK JP"; + } else if (languages == Languages.zhCN) { + fontName = "Noto Sans Mono CJK SC"; + } else { + fontName = "Noto Sans"; + } + } + fallback.addAll(const [ + "Noto Color Emoji", + "Noto Mono", + ]); } return (monospaceFontName.isNotEmpty ? (fromGoogleFont(monospaceFontName) ?? @@ -144,24 +196,19 @@ class AppThemeScopeState extends ConsumerState { if (defaultTargetPlatform == TargetPlatform.iOS || defaultTargetPlatform == TargetPlatform.macOS) { return const TextStyle( - fontFamily: "Apple Color Emoji", - fontFamilyFallback: [ - "Apple Color Emoji", - "Hiragino Maru Gothic ProN" - ]); - } - if (defaultTargetPlatform == TargetPlatform.windows) { - return const TextStyle( - fontFamily: "Segoe UI Emoji", - fontFamilyFallback: ["Segoe UI Emoji", "Noto Color Emoji", "Meiryo"]); - } - if (defaultTargetPlatform == TargetPlatform.android || - defaultTargetPlatform == TargetPlatform.linux) { + fontFamily: "Apple Color Emoji", + fontFamilyFallback: [ + "Hiragino Maru Gothic ProN", + ], + ); + } else { return const TextStyle( - fontFamily: "Noto Color Emoji", - fontFamilyFallback: ["Noto Color Emoji", "Noto Sans JP"]); + fontFamily: "Noto Color Emoji", + fontFamilyFallback: [ + "Noto Sans", + ], + ); } - return const TextStyle(); } TextTheme applyGoogleFont(TextTheme textTheme, String? fontName) { @@ -172,7 +219,7 @@ class AppThemeScopeState extends ConsumerState { TextStyle? fromGoogleFont(String? fontName) { return fontName != null && - fontName.isNotEmpty == true && + fontName.isNotEmpty && GoogleFonts.asMap().containsKey(fontName) ? GoogleFonts.getFont(fontName) : null; @@ -182,22 +229,28 @@ class AppThemeScopeState extends ConsumerState { required BuildContext context, required ColorTheme theme, required String defaultFontName, + required Languages languages, }) { final textThemePre = applyGoogleFont( - Theme.of(context).textTheme.merge((theme.isDarkTheme - ? ThemeData.dark() - : ThemeData.light()) - .textTheme - .apply( - fontFamily: resolveFontFamilyName(defaultFontName), - fontFamilyFallback: resolveFontFamilyFallback(defaultFontName), - bodyColor: theme.foreground)), - defaultFontName); + Theme.of(context).textTheme.merge( + (theme.isDarkTheme ? ThemeData.dark() : ThemeData.light()) + .textTheme + .apply( + fontFamily: resolveFontFamilyName(defaultFontName, languages), + fontFamilyFallback: + resolveFontFamilyFallback(defaultFontName, languages), + bodyColor: theme.foreground, + ), + ), + defaultFontName, + ); final textTheme = textThemePre.copyWith( - bodySmall: textThemePre.bodySmall?.copyWith( - color: theme.isDarkTheme - ? theme.foreground.darken(0.1) - : theme.foreground.lighten(0.1))); + bodySmall: textThemePre.bodySmall?.copyWith( + color: theme.isDarkTheme + ? theme.foreground.darken(0.1) + : theme.foreground.lighten(0.1), + ), + ); final themeData = ThemeData( colorScheme: ColorScheme.fromSeed( @@ -224,7 +277,7 @@ class AppThemeScopeState extends ConsumerState { listTileTheme: ListTileThemeData(iconColor: theme.foreground), scaffoldBackgroundColor: theme.panel, tabBarTheme: TabBarTheme( - overlayColor: MaterialStatePropertyAll(theme.primary), + overlayColor: WidgetStatePropertyAll(theme.primary), labelColor: Colors.white, labelStyle: textTheme.titleSmall, unselectedLabelStyle: @@ -236,27 +289,24 @@ class AppThemeScopeState extends ConsumerState { textTheme: textTheme, iconTheme: IconThemeData(color: theme.foreground), elevatedButtonTheme: ElevatedButtonThemeData( - style: ButtonStyle( - textStyle: MaterialStatePropertyAll( - textTheme.bodyMedium?.copyWith( - inherit: false, - color: Colors.white, - ), - ), - backgroundColor: MaterialStatePropertyAll(theme.primary), - foregroundColor: const MaterialStatePropertyAll(Colors.white), - elevation: const MaterialStatePropertyAll(0), - shape: MaterialStatePropertyAll( - RoundedRectangleBorder(borderRadius: BorderRadius.circular(100)), + style: ElevatedButton.styleFrom( + textStyle: textTheme.bodyMedium?.copyWith( + inherit: false, + color: Colors.white, ), + backgroundColor: theme.primary, + foregroundColor: Colors.white, + elevation: 0, + shape: + RoundedRectangleBorder(borderRadius: BorderRadius.circular(100)), visualDensity: const VisualDensity(horizontal: 0, vertical: 0), tapTargetSize: MaterialTapTargetSize.padded, ), ), outlinedButtonTheme: OutlinedButtonThemeData( style: ButtonStyle( - foregroundColor: MaterialStatePropertyAll(theme.primary), - shape: MaterialStatePropertyAll( + foregroundColor: WidgetStatePropertyAll(theme.primary), + shape: WidgetStatePropertyAll( RoundedRectangleBorder(borderRadius: BorderRadius.circular(100)), ), visualDensity: const VisualDensity(horizontal: 0, vertical: 0), @@ -265,8 +315,8 @@ class AppThemeScopeState extends ConsumerState { ), textButtonTheme: TextButtonThemeData( style: ButtonStyle( - iconColor: MaterialStatePropertyAll(theme.primary), - foregroundColor: MaterialStatePropertyAll(theme.primary), + iconColor: WidgetStatePropertyAll(theme.primary), + foregroundColor: WidgetStatePropertyAll(theme.primary), ), ), dividerTheme: DividerThemeData(color: theme.divider), @@ -301,12 +351,12 @@ class AppThemeScopeState extends ConsumerState { isDense: true, ), checkboxTheme: CheckboxThemeData( - fillColor: MaterialStateProperty.resolveWith( + fillColor: WidgetStateProperty.resolveWith( (states) { - if (states.contains(MaterialState.disabled)) { + if (states.contains(WidgetState.disabled)) { return null; } - if (states.contains(MaterialState.selected)) { + if (states.contains(WidgetState.selected)) { return theme.primary; } return null; @@ -337,6 +387,11 @@ class AppThemeScopeState extends ConsumerState { valueIndicatorColor: theme.panel, valueIndicatorShape: const RectangularSliderValueIndicatorShape(), ), + textSelectionTheme: TextSelectionThemeData( + cursorColor: theme.primary, + selectionColor: theme.accentedBackground, + selectionHandleColor: theme.primary, + ), ); return themeData; @@ -344,26 +399,46 @@ class AppThemeScopeState extends ConsumerState { @override Widget build(BuildContext context) { - final colorSystem = ref.watch(generalSettingsRepositoryProvider - .select((value) => value.settings.themeColorSystem)); - final lightTheme = ref.watch(generalSettingsRepositoryProvider - .select((value) => value.settings.lightColorThemeId)); - final darkTheme = ref.watch(generalSettingsRepositoryProvider - .select((value) => value.settings.darkColorThemeId)); + final colorSystem = ref.watch( + generalSettingsRepositoryProvider + .select((value) => value.settings.themeColorSystem), + ); + final lightTheme = ref.watch( + generalSettingsRepositoryProvider + .select((value) => value.settings.lightColorThemeId), + ); + final darkTheme = ref.watch( + generalSettingsRepositoryProvider + .select((value) => value.settings.darkColorThemeId), + ); final textScaleFactor = ref.watch( generalSettingsRepositoryProvider .select((value) => value.settings.textScaleFactor), ); - final defaultFontName = ref.watch(generalSettingsRepositoryProvider - .select((value) => value.settings.defaultFontName)); - final serifFontName = ref.watch(generalSettingsRepositoryProvider - .select((value) => value.settings.serifFontName)); - final monospaceFontName = ref.watch(generalSettingsRepositoryProvider - .select((value) => value.settings.monospaceFontName)); - final cursiveFontName = ref.watch(generalSettingsRepositoryProvider - .select((value) => value.settings.cursiveFontName)); - final fantasyFontName = ref.watch(generalSettingsRepositoryProvider - .select((value) => value.settings.fantasyFontName)); + final defaultFontName = ref.watch( + generalSettingsRepositoryProvider + .select((value) => value.settings.defaultFontName), + ); + final serifFontName = ref.watch( + generalSettingsRepositoryProvider + .select((value) => value.settings.serifFontName), + ); + final monospaceFontName = ref.watch( + generalSettingsRepositoryProvider + .select((value) => value.settings.monospaceFontName), + ); + final cursiveFontName = ref.watch( + generalSettingsRepositoryProvider + .select((value) => value.settings.cursiveFontName), + ); + final fantasyFontName = ref.watch( + generalSettingsRepositoryProvider + .select((value) => value.settings.fantasyFontName), + ); + final languages = ref.watch( + generalSettingsRepositoryProvider + .select((value) => value.settings.languages), + ); final bool isDark; if (colorSystem == ThemeColorSystem.system) { @@ -375,9 +450,11 @@ class AppThemeScopeState extends ConsumerState { isDark = false; } - final foundColorTheme = builtInColorThemes.firstWhereOrNull((e) => - e.isDarkTheme == isDark && - e.id == (isDark ? darkTheme : lightTheme)) ?? + final foundColorTheme = builtInColorThemes.firstWhereOrNull( + (e) => + e.isDarkTheme == isDark && + e.id == (isDark ? darkTheme : lightTheme), + ) ?? builtInColorThemes .firstWhere((element) => element.isDarkTheme == isDark); @@ -386,15 +463,18 @@ class AppThemeScopeState extends ConsumerState { context: context, theme: foundColorTheme, defaultFontName: defaultFontName, + languages: languages, ), child: AppTheme( themeData: buildDarkAppThemeData( - context: context, - theme: foundColorTheme, - serifFontName: serifFontName, - monospaceFontName: monospaceFontName, - cursiveFontName: cursiveFontName, - fantasyFontName: fantasyFontName), + context: context, + theme: foundColorTheme, + serifFontName: serifFontName, + monospaceFontName: monospaceFontName, + cursiveFontName: cursiveFontName, + fantasyFontName: fantasyFontName, + languages: languages, + ), child: MediaQuery( data: MediaQuery.of(context).copyWith( alwaysUse24HourFormat: true, diff --git a/lib/view/themes/built_in_color_themes.dart b/lib/view/themes/built_in_color_themes.dart index 98019083c..fb9da4857 100644 --- a/lib/view/themes/built_in_color_themes.dart +++ b/lib/view/themes/built_in_color_themes.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; -import 'package:miria/model/color_theme.dart'; +import "package:flutter/material.dart"; +import "package:miria/model/color_theme.dart"; const builtInColorThemes = [ ColorTheme( @@ -400,5 +400,5 @@ const builtInColorThemes = [ buttonGradateB: Color(0xff007aa4), panel: Color(0xff1d2d30), panelBackground: Color(0xff23363a), - ) + ), ]; diff --git a/lib/view/time_line_page/misskey_time_line.dart b/lib/view/time_line_page/misskey_time_line.dart index b21c4374f..b154909fc 100644 --- a/lib/view/time_line_page/misskey_time_line.dart +++ b/lib/view/time_line_page/misskey_time_line.dart @@ -1,173 +1,148 @@ -import 'dart:math'; - -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/model/general_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/repository/time_line_repository.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/timeline_listview.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; - -class MisskeyTimeline extends ConsumerStatefulWidget { - final ChangeNotifierProvider timeLineRepositoryProvider; +import "dart:async"; +import "dart:math"; + +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/log.dart"; +import "package:miria/model/general_settings.dart"; +import "package:miria/model/tab_setting.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/time_line_repository.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/timeline_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; + +class MisskeyTimeline extends HookConsumerWidget { + final TabSetting tabSetting; final TimelineScrollController controller; MisskeyTimeline({ + required this.tabSetting, super.key, TimelineScrollController? controller, - required this.timeLineRepositoryProvider, }) : controller = controller ?? TimelineScrollController(); @override - ConsumerState createState() => MisskeyTimelineState(); -} - -class MisskeyTimelineState extends ConsumerState { - List showingNotes = []; - late final TimelineScrollController scrollController = widget.controller; - bool isScrolling = false; - late TimelineRepository timelineRepository = - ref.read(widget.timeLineRepositoryProvider); - bool contextAccessed = false; - - bool isInitStated = false; - bool isDownDirectionLoading = false; - bool isLastLoaded = false; - - Future downDirectionLoad() async { - if (isDownDirectionLoading) return; - Future(() async { - try { - if (!mounted) return; - setState(() { - isDownDirectionLoading = true; + Widget build(BuildContext context, WidgetRef ref) { + final timelineRepository = ref.read(timelineProvider(tabSetting)); + final isDownDirectionLoading = useState(false); + final isLastLoaded = useState(false); + + useEffect( + () { + timelineRepository.startTimeLine(); + return () => timelineRepository.disconnect(); + }, + [tabSetting], + ); + + useMemoized( + () { + isDownDirectionLoading.value = false; + isLastLoaded.value = false; + }, + [tabSetting], + ); + + final downDirectionLoad = useCallback( + () { + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (isDownDirectionLoading.value) return; + try { + isDownDirectionLoading.value = true; + final result = await timelineRepository.previousLoad(); + isDownDirectionLoading.value = false; + isLastLoaded.value = result == 0; + } catch (e) { + isDownDirectionLoading.value = false; + rethrow; + } }); - final result = await timelineRepository.previousLoad(); - if (!mounted) return; - setState(() { - isDownDirectionLoading = false; - isLastLoaded = result == 0; - }); - } catch (e) { - if (mounted) { - setState(() { - isDownDirectionLoading = false; - }); - } - rethrow; - } - }); - } + }, + [isDownDirectionLoading], + ); - @override - void didUpdateWidget(covariant MisskeyTimeline oldWidget) { - super.didUpdateWidget(oldWidget); - contextAccessed = true; - if (oldWidget.timeLineRepositoryProvider != - widget.timeLineRepositoryProvider) { - ref.read(oldWidget.timeLineRepositoryProvider).disconnect(); - ref.read(widget.timeLineRepositoryProvider).startTimeLine(); - timelineRepository = ref.read(widget.timeLineRepositoryProvider); - isDownDirectionLoading = false; - isLastLoaded = false; + if (controller.positions.isNotEmpty) { + controller.scrollToTop(); } - } - - @override - void initState() { - super.initState(); - if (isInitStated) return; - Future(() { - ref.read(widget.timeLineRepositoryProvider).startTimeLine(); - }); - } + final repository = ref.watch(timelineProvider(tabSetting)); - @override - void dispose() { - super.dispose(); - if (contextAccessed) timelineRepository.disconnect(); - } + return Padding( + padding: const EdgeInsets.only(right: 10), + child: TimelineListView.builder( + reverse: true, + controller: controller, + itemCount: + repository.newerNotes.length + repository.olderNotes.length + 1, + itemBuilder: (context, index) { + // final corecctedIndex = index - 5; + final correctedNewer = [ + if (timelineRepository.olderNotes.isNotEmpty) + ...timelineRepository.olderNotes + .slice(0, min(5, timelineRepository.olderNotes.length)) + .reversed, + ...timelineRepository.newerNotes, + ]; + final correctedOlder = [ + if (timelineRepository.olderNotes.length > 5) + ...timelineRepository.olderNotes + .slice(5, timelineRepository.olderNotes.length), + ]; + + if (index > 0) { + if ((index - 1) >= correctedNewer.length) { + return null; + } - @override - Widget build(BuildContext context) { - if (scrollController.positions.isNotEmpty) { - scrollController.scrollToTop(); - } - final repository = ref.watch(widget.timeLineRepositoryProvider); + return NoteWrapper( + targetNote: correctedNewer[index - 1], + timeline: timelineRepository, + ); + } - return Padding( - padding: const EdgeInsets.only(right: 10), - child: TimelineListView.builder( - reverse: true, - controller: scrollController, - itemCount: - repository.newerNotes.length + repository.olderNotes.length + 1, - itemBuilder: (BuildContext context, int index) { - // final corecctedIndex = index - 5; - final correctedNewer = [ - if (timelineRepository.olderNotes.isNotEmpty) - ...timelineRepository.olderNotes - .slice(0, min(5, timelineRepository.olderNotes.length)) - .reversed, - ...timelineRepository.newerNotes, - ]; - final correctedOlder = [ - if (timelineRepository.olderNotes.length > 5) - ...timelineRepository.olderNotes - .slice(5, timelineRepository.olderNotes.length) - ]; - - if (index > 0) { - if ((index - 1) >= correctedNewer.length) { - return null; - } - - return NoteWrapper( - targetNote: correctedNewer[index - 1], - timeline: timelineRepository, - ); + if (-index == correctedOlder.length) { + if (isLastLoaded.value) { + return const SizedBox.shrink(); } - if (-index == correctedOlder.length) { - if (isLastLoaded) { - return const SizedBox.shrink(); - } - - if (isDownDirectionLoading && - repository.newerNotes.length + repository.olderNotes.length != - 0) { - return const Padding( - padding: EdgeInsets.only(top: 10, bottom: 10), - child: Center(child: CircularProgressIndicator())); - } - - if (ref.read(generalSettingsRepositoryProvider - .select((value) => value.settings.automaticPush)) == - AutomaticPush.automatic) { - downDirectionLoad(); - } - - return Center( - child: IconButton( - onPressed: downDirectionLoad.expectFailure(context), - icon: const Icon(Icons.keyboard_arrow_down), - )); + if (isDownDirectionLoading.value && + repository.newerNotes.length + repository.olderNotes.length != + 0) { + return const Padding( + padding: EdgeInsets.only(top: 10, bottom: 10), + child: Center(child: CircularProgressIndicator.adaptive()), + ); } - if (-index >= correctedOlder.length) { - return null; + if (ref.read( + generalSettingsRepositoryProvider + .select((value) => value.settings.automaticPush), + ) == + AutomaticPush.automatic) { + unawaited(downDirectionLoad()); } - return NoteWrapper( - targetNote: correctedOlder[-index], - timeline: timelineRepository, + return Center( + child: IconButton( + onPressed: downDirectionLoad, + icon: const Icon(Icons.keyboard_arrow_down), + ), ); - }, - )); + } + + if (-index >= correctedOlder.length) { + return null; + } + + return NoteWrapper( + targetNote: correctedOlder[-index], + timeline: timelineRepository, + ); + }, + ), + ); } } @@ -176,9 +151,9 @@ class NoteWrapper extends ConsumerStatefulWidget { final TimelineRepository timeline; const NoteWrapper({ - super.key, required this.targetNote, required this.timeline, + super.key, }); @override @@ -190,17 +165,21 @@ class NoteWrapperState extends ConsumerState { void didChangeDependencies() { super.didChangeDependencies(); if (widget.targetNote.renoteId != null && widget.targetNote.text == null) { - widget.timeline.subscribe(SubscribeItem( - noteId: widget.targetNote.renoteId!, - renoteId: null, - replyId: null, - )); + widget.timeline.subscribe( + SubscribeItem( + noteId: widget.targetNote.renoteId!, + renoteId: null, + replyId: null, + ), + ); } else { - widget.timeline.subscribe(SubscribeItem( - noteId: widget.targetNote.id, - renoteId: widget.targetNote.renoteId, - replyId: widget.targetNote.replyId, - )); + widget.timeline.subscribe( + SubscribeItem( + noteId: widget.targetNote.id, + renoteId: widget.targetNote.renoteId, + replyId: widget.targetNote.replyId, + ), + ); } } @@ -212,12 +191,15 @@ class NoteWrapperState extends ConsumerState { @override Widget build(BuildContext context) { - final note = ref.watch(notesProvider(AccountScope.of(context)) - .select((note) => note.notes[widget.targetNote.id])); + final note = ref.watch( + notesWithProvider.select((note) => note.notes[widget.targetNote.id]), + ); if (note == null) { - print("note was not found. ${widget.targetNote}"); + logger.info("note was not found. ${widget.targetNote}"); return MisskeyNote( - note: widget.targetNote, key: ValueKey(widget.targetNote.id)); + note: widget.targetNote, + key: ValueKey(widget.targetNote.id), + ); } return MisskeyNote(note: note, key: ValueKey(note.id)); } diff --git a/lib/view/time_line_page/nyanpuppu.dart b/lib/view/time_line_page/nyanpuppu.dart index fbf0fcf04..8ab28d9bd 100644 --- a/lib/view/time_line_page/nyanpuppu.dart +++ b/lib/view/time_line_page/nyanpuppu.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; -import 'package:miria/view/common/misskey_notes/network_image.dart'; +import "package:flutter/material.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; // blobs in community discord server final _discordOfficialEmojiList = [ @@ -16,7 +16,7 @@ final _discordOfficialEmojiList = [ // meowmoji 2 "https://cdn.discordapp.com/emojis/380080817214324739.png?size=32", - "https://cdn.discordapp.com/emojis/391358218971906050.png?size=32" + "https://cdn.discordapp.com/emojis/391358218971906050.png?size=32", //mewomoji 3 ]..shuffle(); diff --git a/lib/view/time_line_page/time_line_page.dart b/lib/view/time_line_page/time_line_page.dart index 6948f14e8..09727011f 100644 --- a/lib/view/time_line_page/time_line_page.dart +++ b/lib/view/time_line_page/time_line_page.dart @@ -1,34 +1,37 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/model/general_settings.dart'; -import 'package:miria/model/tab_setting.dart'; -import 'package:miria/model/tab_type.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/repository/socket_timeline_repository.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/channel_dialog.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/error_detail.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/server_detail_dialog.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:miria/view/common/common_drawer.dart'; -import 'package:miria/view/common/notification_icon.dart'; -import 'package:miria/view/common/tab_icon_view.dart'; -import 'package:miria/view/common/timeline_listview.dart'; -import 'package:miria/view/time_line_page/misskey_time_line.dart'; -import 'package:miria/view/time_line_page/nyanpuppu.dart'; -import 'package:miria/view/time_line_page/timeline_emoji.dart'; -import 'package:miria/view/time_line_page/timeline_note.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "dart:async"; + +import "package:auto_route/auto_route.dart"; +import "package:collection/collection.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/general_settings.dart"; +import "package:miria/model/tab_setting.dart"; +import "package:miria/model/tab_type.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/socket_timeline_repository.dart"; +import "package:miria/repository/time_line_repository.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/common_drawer.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:miria/view/common/error_dialog_handler.dart"; +import "package:miria/view/common/notification_icon.dart"; +import "package:miria/view/common/tab_icon_view.dart"; +import "package:miria/view/common/timeline_listview.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:miria/view/time_line_page/misskey_time_line.dart"; +import "package:miria/view/time_line_page/nyanpuppu.dart"; +import "package:miria/view/time_line_page/timeline_emoji.dart"; +import "package:miria/view/time_line_page/timeline_note.dart"; +import "package:miria/view/time_line_page/timeline_tablet_ui.dart"; +import "package:misskey_dart/misskey_dart.dart"; @RoutePage() class TimeLinePage extends ConsumerStatefulWidget { final TabSetting initialTabSetting; - const TimeLinePage({super.key, required this.initialTabSetting}); + const TimeLinePage({required this.initialTabSetting, super.key}); @override ConsumerState createState() => TimeLinePageState(); @@ -42,6 +45,8 @@ class TimeLinePageState extends ConsumerState { final GlobalKey scaffoldKey = GlobalKey(); + TimelineRepository? timelineRepository; + @override void initState() { tabSettings = ref.read( @@ -97,26 +102,21 @@ class TimeLinePageState extends ConsumerState { } void reload() { - ref.read(currentTabSetting.timelineProvider).moveToOlder(); + ref.read(timelineProvider(currentTabSetting)).moveToOlder(); scrollControllers[currentIndex].forceScrollToTop(); } void changeTab(int index) { - final tabSetting = tabSettings[index]; - if ([TabType.globalTimeline, TabType.homeTimeline, TabType.hybridTimeline] - .contains(tabSetting.tabType)) { - ref.read(tabSetting.timelineProvider).moveToOlder(); - } setState(() { currentIndex = index; }); } - void noteCreateRoute() { + Future noteCreateRoute() async { CommunityChannel? channel; if (currentTabSetting.channelId != null) { final Note? note; - final timeline = ref.read(currentTabSetting.timelineProvider); + final timeline = ref.read(timelineProvider(currentTabSetting)); if (timeline.olderNotes.isNotEmpty) { note = timeline.olderNotes.first; } else if (timeline.newerNotes.isNotEmpty) { @@ -145,11 +145,13 @@ class TimeLinePageState extends ConsumerState { final sendText = ref.read(timelineNoteProvider).text; ref.read(timelineNoteProvider).text = ""; final account = ref.read(accountProvider(currentTabSetting.acct)); - context.pushRoute(NoteCreateRoute( - channel: channel, - initialText: sendText, - initialAccount: account, - )); + await context.pushRoute( + NoteCreateRoute( + channel: channel, + initialText: sendText, + initialAccount: account, + ), + ); } Widget buildAppbar() { @@ -165,7 +167,7 @@ class TimeLinePageState extends ConsumerState { color: tabSetting == currentTabSetting ? AppTheme.of(context).currentDisplayTabColor : Colors.transparent, - child: AccountScope( + child: AccountContextScope.as( account: account, child: IconButton( icon: TabIconView( @@ -185,22 +187,29 @@ class TimeLinePageState extends ConsumerState { ), ), actions: [ - AccountScope( + AccountContextScope.as( account: account, child: const NotificationIcon(), ), ], leading: IconButton( - onPressed: () => scaffoldKey.currentState?.openDrawer(), - icon: const Icon(Icons.menu)), + onPressed: () => scaffoldKey.currentState?.openDrawer(), + icon: const Icon(Icons.menu), + ), ); } @override Widget build(BuildContext context) { - final socketTimelineBase = ref.watch(currentTabSetting.timelineProvider); - final socketTimeline = socketTimelineBase is SocketTimelineRepository - ? socketTimelineBase + final deckMode = ref.watch( + generalSettingsRepositoryProvider + .select((value) => value.settings.isDeckMode), + ); + if (deckMode) return const TimelineTablet(); + + timelineRepository = ref.watch(timelineProvider(currentTabSetting)); + final socketTimeline = timelineRepository is SocketTimelineRepository + ? timelineRepository as SocketTimelineRepository? : null; tabSettings = ref.watch( tabSettingsRepositoryProvider.select((repo) => repo.tabSettings.toList()), @@ -210,10 +219,11 @@ class TimeLinePageState extends ConsumerState { return Scaffold( key: scaffoldKey, appBar: PreferredSize( - preferredSize: const Size.fromHeight(0), - child: AppBar( - automaticallyImplyLeading: false, - )), + preferredSize: const Size.fromHeight(0), + child: AppBar( + automaticallyImplyLeading: false, + ), + ), body: SafeArea( child: Column( children: [ @@ -247,12 +257,11 @@ class TimeLinePageState extends ConsumerState { const Nyanpuppu(), if (currentTabSetting.tabType == TabType.channel) IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => ChannelDialog( - channelId: currentTabSetting.channelId ?? "", + onPressed: () async { + await context.pushRoute( + ChannelDescriptionRoute( account: account, + channelId: currentTabSetting.channelId ?? "", ), ); }, @@ -261,10 +270,10 @@ class TimeLinePageState extends ConsumerState { else if (currentTabSetting.tabType == TabType.userList) IconButton( icon: const Icon(Icons.info_outline), - onPressed: () { - context.pushRoute( + onPressed: () async { + await context.pushRoute( UsersListDetailRoute( - account: account, + accountContext: AccountContext.as(account), listId: currentTabSetting.listId!, ), ); @@ -278,11 +287,10 @@ class TimeLinePageState extends ConsumerState { ].contains(currentTabSetting.tabType)) ...[ AnnoucementInfo(tabSetting: currentTabSetting), IconButton( - onPressed: () { - showDialog( - context: context, - builder: (context) => ServerDetailDialog( - account: account, + onPressed: () async { + await context.pushRoute( + ServerDetailRoute( + accountContext: AccountContext.as(account), ), ); }, @@ -293,24 +301,21 @@ class TimeLinePageState extends ConsumerState { padding: EdgeInsets.only(right: 5), ), IconButton( - onPressed: () => ref - .read( - currentTabSetting.tabType - .timelineProvider(currentTabSetting), - ) + onPressed: () async => ref + .read(timelineProvider(currentTabSetting)) .reconnect(), icon: socketTimeline != null && socketTimeline.isReconnecting - ? const CircularProgressIndicator() + ? const CircularProgressIndicator.adaptive() : const Icon(Icons.refresh), - ) + ), ], ), ), if (socketTimeline?.isLoading == true) const Padding( padding: EdgeInsets.only(top: 10), - child: Center(child: CircularProgressIndicator()), + child: Center(child: CircularProgressIndicator.adaptive()), ), if (socketTimeline?.error != null) ErrorDetail( @@ -325,7 +330,7 @@ class TimeLinePageState extends ConsumerState { itemBuilder: (_, index) { final tabSetting = tabSettings[index]; final account = ref.watch(accountProvider(tabSetting.acct)); - return AccountScope( + return AccountContextScope.as( account: account, child: Column( mainAxisAlignment: MainAxisAlignment.start, @@ -336,8 +341,7 @@ class TimeLinePageState extends ConsumerState { Expanded( child: MisskeyTimeline( controller: scrollControllers[index], - timeLineRepositoryProvider: - tabSetting.tabType.timelineProvider(tabSetting), + tabSetting: tabSetting, ), ), const TimelineEmoji(), @@ -356,8 +360,20 @@ class TimeLinePageState extends ConsumerState { // : null, child: Row( children: [ - const Expanded( - child: TimelineNoteField(), + Expanded( + child: Focus( + onKeyEvent: (node, event) { + if (event is KeyDownEvent) { + if (event.logicalKey == LogicalKeyboardKey.enter && + HardwareKeyboard.instance.isControlPressed) { + note().expectFailure(context); + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + }, + child: const TimelineNoteField(), + ), ), IconButton( onPressed: note.expectFailure(context), @@ -366,7 +382,7 @@ class TimeLinePageState extends ConsumerState { IconButton( onPressed: noteCreateRoute, icon: const Icon(Icons.keyboard_arrow_right), - ) + ), ], ), ), @@ -382,9 +398,7 @@ class TimeLinePageState extends ConsumerState { ), resizeToAvoidBottomInset: true, drawerEnableOpenDragGesture: true, - drawer: CommonDrawer( - initialOpenAcct: currentTabSetting.acct, - ), + drawer: CommonDrawer(initialOpenAcct: currentTabSetting.acct), ); } } @@ -392,7 +406,7 @@ class TimeLinePageState extends ConsumerState { class BannerArea extends ConsumerWidget { final TabSetting tabSetting; - const BannerArea({super.key, required this.tabSetting}); + const BannerArea({required this.tabSetting, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -402,9 +416,11 @@ class BannerArea extends ConsumerWidget { ); // ダイアログの実装が大変なので(状態管理とか)いったんバナーと一緒に扱う - final bannerDatas = bannerAnnouncement.where((element) => - element.display == AnnouncementDisplayType.banner || - element.display == AnnouncementDisplayType.dialog); + final bannerDatas = bannerAnnouncement.where( + (element) => + element.display == AnnouncementDisplayType.banner || + element.display == AnnouncementDisplayType.dialog, + ); if (bannerDatas.isEmpty) return const SizedBox.shrink(); @@ -444,11 +460,13 @@ class BannerArea extends ConsumerWidget { class AnnoucementInfo extends ConsumerWidget { final TabSetting tabSetting; - const AnnoucementInfo({super.key, required this.tabSetting}); + const AnnoucementInfo({required this.tabSetting, super.key}); - void announcementsRoute(BuildContext context, WidgetRef ref) { + Future announcementsRoute(BuildContext context, WidgetRef ref) async { final account = ref.read(accountProvider(tabSetting.acct)); - context.pushRoute(AnnouncementRoute(account: account)); + await context.pushRoute( + AnnouncementRoute(accountContext: AccountContext.as(account)), + ); } @override @@ -460,27 +478,32 @@ class AnnoucementInfo extends ConsumerWidget { if (hasUnread) { return IconButton( - onPressed: () => announcementsRoute(context, ref), - icon: Stack(children: [ + onPressed: () async => announcementsRoute(context, ref), + icon: Stack( + children: [ const Icon(Icons.campaign), Transform.translate( - offset: const Offset(12, 12), - child: SizedBox( - width: 14, - height: 14, - child: Container( - decoration: BoxDecoration( - border: Border.all(color: Colors.white, width: 1.5), - borderRadius: BorderRadius.circular(20), - color: Theme.of(context).primaryColor, - ), + offset: const Offset(12, 12), + child: SizedBox( + width: 14, + height: 14, + child: Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.white, width: 1.5), + borderRadius: BorderRadius.circular(20), + color: Theme.of(context).primaryColor, ), - )), - ])); + ), + ), + ), + ], + ), + ); } else { return IconButton( - onPressed: () => announcementsRoute(context, ref), - icon: const Icon(Icons.campaign)); + onPressed: () async => announcementsRoute(context, ref), + icon: const Icon(Icons.campaign), + ); } } } @@ -488,7 +511,7 @@ class AnnoucementInfo extends ConsumerWidget { class AnnouncementIcon extends StatelessWidget { final AnnouncementIconType iconType; - const AnnouncementIcon({super.key, required this.iconType}); + const AnnouncementIcon({required this.iconType, super.key}); @override Widget build(BuildContext context) { diff --git a/lib/view/time_line_page/timeline_emoji.dart b/lib/view/time_line_page/timeline_emoji.dart index 560cd15b8..63e8f7dde 100644 --- a/lib/view/time_line_page/timeline_emoji.dart +++ b/lib/view/time_line_page/timeline_emoji.dart @@ -1,7 +1,7 @@ -import 'package:flutter/material.dart'; -import 'package:miria/view/common/note_create/input_completation.dart'; -import 'package:miria/view/time_line_page/timeline_note.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/view/common/note_create/input_completation.dart"; +import "package:miria/view/time_line_page/timeline_note.dart"; class TimelineEmoji extends ConsumerWidget { const TimelineEmoji({super.key}); @@ -9,7 +9,8 @@ class TimelineEmoji extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return InputComplement( - controller: ref.read(timelineNoteProvider), - focusNode: timelineFocusNode); + controller: ref.read(timelineNoteProvider), + focusNode: timelineFocusNode, + ); } } diff --git a/lib/view/time_line_page/timeline_note.dart b/lib/view/time_line_page/timeline_note.dart index 62b4686db..6beb1b06b 100644 --- a/lib/view/time_line_page/timeline_note.dart +++ b/lib/view/time_line_page/timeline_note.dart @@ -1,6 +1,6 @@ -import 'package:flutter/material.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/view/themes/app_theme.dart"; final timelineNoteProvider = ChangeNotifierProvider.autoDispose((ref) => TextEditingController()); @@ -8,22 +8,11 @@ final timelineNoteProvider = final timelineFocusNode = ChangeNotifierProvider.autoDispose((ref) => FocusNode()); -class TimelineNoteField extends ConsumerStatefulWidget { +class TimelineNoteField extends ConsumerWidget { const TimelineNoteField({super.key}); @override - ConsumerState createState() => - TimelineNoteFieldState(); -} - -class TimelineNoteFieldState extends ConsumerState { - @override - void initState() { - super.initState(); - } - - @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { final noteStyle = AppTheme.of(context).noteTextStyle; return Padding( padding: const EdgeInsets.only(left: 8.0, right: 8.0), diff --git a/lib/view/time_line_page/timeline_scroll_controller.dart b/lib/view/time_line_page/timeline_scroll_controller.dart index e69de29bb..8b1378917 100644 --- a/lib/view/time_line_page/timeline_scroll_controller.dart +++ b/lib/view/time_line_page/timeline_scroll_controller.dart @@ -0,0 +1 @@ + diff --git a/lib/view/time_line_page/timeline_tablet_ui.dart b/lib/view/time_line_page/timeline_tablet_ui.dart new file mode 100644 index 000000000..d35919bd7 --- /dev/null +++ b/lib/view/time_line_page/timeline_tablet_ui.dart @@ -0,0 +1,394 @@ +import "dart:async"; + +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/hooks/use_async.dart"; +import "package:miria/model/tab_setting.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/socket_timeline_repository.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/avatar_icon.dart"; +import "package:miria/view/common/common_drawer.dart"; +import "package:miria/view/common/misskey_notes/local_only_icon.dart"; +import "package:miria/view/common/timeline_listview.dart"; +import "package:miria/view/note_create_page/note_create_setting_top.dart"; +import "package:miria/view/note_create_page/note_visibility_dialog.dart"; +import "package:miria/view/note_create_page/reaction_acceptance_dialog.dart"; +import "package:miria/view/time_line_page/misskey_time_line.dart"; +import "package:misskey_dart/misskey_dart.dart"; + +class TimelineTablet extends HookConsumerWidget { + const TimelineTablet({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final tabSettings = ref.read( + tabSettingsRepositoryProvider.select((repo) => repo.tabSettings), + ); + + return Scaffold( + drawer: const TimelineTabletDrawer(), + body: MediaQuery( + data: MediaQuery.of(context).copyWith( + devicePixelRatio: MediaQuery.of(context).devicePixelRatio * 0.5, + ), + child: SafeArea( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + children: [ + const TimelineTabletDrawer(), + const SizedBox(width: 15), + for (final tabSetting in tabSettings) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 2), + child: DecoratedBox( + decoration: BoxDecoration( + border: Border( + right: + BorderSide(color: Theme.of(context).primaryColor), + ), + ), + child: SizedBox( + width: 300, + child: Padding( + padding: const EdgeInsets.only(right: 4), + child: Timeline(tabSetting: tabSetting), + ), + ), + ), + ), + ], + ), + ), + ), + ), + ); + } +} + +class TimelineTabletDrawer extends ConsumerWidget { + const TimelineTabletDrawer({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final firstAccount = + ref.watch(accountsProvider.select((value) => value.first.acct)); + return CommonDrawer( + initialOpenAcct: firstAccount, + allOpen: true, + ); + } +} + +class Timeline extends HookConsumerWidget { + final TabSetting tabSetting; + + const Timeline({ + required this.tabSetting, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final account = ref.watch(accountProvider(tabSetting.acct)); + final scrollController = useMemoized(() => TimelineScrollController()); + useEffect(() => scrollController.dispose, []); + + return AccountContextScope.as( + account: account, + child: Column( + children: [ + const SizedBox(height: 5), + SectionHeader( + tabSetting: tabSetting, + scrollController: scrollController, + ), + TabTextField(tabSetting: tabSetting), + Expanded( + child: MisskeyTimeline( + tabSetting: tabSetting, + controller: scrollController, + ), + ), + ], + ), + ); + } +} + +class SectionHeader extends ConsumerWidget { + final TabSetting tabSetting; + final TimelineScrollController scrollController; + const SectionHeader({ + required this.tabSetting, + required this.scrollController, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final faviconUrl = ref.watch( + accountProvider(ref.read(accountContextProvider).getAccount.acct) + .select((value) => value.meta?.iconUrl), + ); + final socketTimelineBase = ref.watch(timelineProvider(tabSetting)); + final socketTimeline = socketTimelineBase is SocketTimelineRepository + ? socketTimelineBase + : null; + + return Row( + children: [ + Image.network( + faviconUrl?.toString() ?? + "https://${ref.read(accountContextProvider).getAccount.host}/favicon.ico", + height: 28, + ), + AvatarIcon( + user: ref.read(accountContextProvider).getAccount.i, + height: 28, + ), + Expanded( + child: GestureDetector( + onTap: () => scrollController.forceScrollToTop(), + child: Text(tabSetting.name ?? tabSetting.tabType.name), + ), + ), + if (socketTimeline != null) + IconButton( + icon: const Icon(Icons.refresh), + onPressed: () async => socketTimeline.reconnect(), + ), + ], + ); + } +} + +class TabTextField extends HookConsumerWidget { + final TabSetting tabSetting; + + const TabTextField({ + required this.tabSetting, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final controller = useTextEditingController(); + final account = ref.watch(accountContextProvider).postAccount; + final settings = + ref.read(accountSettingsRepositoryProvider).fromAccount(account); + final reactionAppearance = useState(settings.defaultReactionAcceptance); + final localOnly = useState(settings.defaultIsLocalOnly); + final visibility = useState(settings.defaultNoteVisibility); + + final note = useHandledFuture(() async { + await ref.read(misskeyPostContextProvider).notes.create( + NotesCreateRequest( + text: controller.text, + localOnly: localOnly.value, + visibility: visibility.value, + reactionAcceptance: reactionAppearance.value, + channelId: tabSetting.channelId, + ), + ); + controller.text = ""; + }); + + return Column( + children: [ + Row( + children: [ + const Expanded(child: SizedBox.shrink()), + IconButton( + onPressed: () async { + final result = await showModalBottomSheet( + context: context, + builder: (context) => NoteVisibilityDialog( + account: ref.read(accountContextProvider).postAccount, + ), + ); + if (result != null) visibility.value = result; + }, + icon: Icon(resolveVisibilityIcon(visibility.value)), + ), + IconButton( + onPressed: () => localOnly.value = !localOnly.value, + icon: localOnly.value + ? const LocalOnlyIcon() + : const Icon(Icons.rocket), + ), + IconButton( + onPressed: () async { + final result = await showModalBottomSheet( + context: context, + builder: (context) => const ReactionAcceptanceDialog(), + ); + reactionAppearance.value = result; + }, + icon: AcceptanceIcon(acceptance: reactionAppearance.value), + ), + IconButton( + onPressed: note.executeOrNull, + icon: const Icon(Icons.send), + ), + ], + ), + Focus( + onKeyEvent: (node, event) { + if (event is KeyDownEvent) { + if (event.logicalKey == LogicalKeyboardKey.enter && + (HardwareKeyboard.instance.isControlPressed || + HardwareKeyboard.instance.isMetaPressed)) { + unawaited(note.execute()); + return KeyEventResult.handled; + } + } + return KeyEventResult.ignored; + }, + child: EmojiInputComplement(controller: controller), + ), + ], + ); + } +} + +class EmojiInputComplement extends HookWidget { + final TextEditingController controller; + + const EmojiInputComplement({ + required this.controller, + super.key, + }); + + @override + Widget build(BuildContext context) { + final overlayEntry = useState(null); + final layerLink = useMemoized(() => LayerLink()); + final options = useMemoized( + () => [ + "apple", + "banana", + "grape", + "orange", + "pineapple", + "strawberry", + "watermelon", + ], + ); + + final hideOverlay = useCallback( + () { + overlayEntry.value?.remove(); + overlayEntry.value = null; + }, + [overlayEntry], + ); + + final selectOption = useCallback( + (option) { + final text = controller.text; + final selection = controller.selection; + final newText = + text.replaceRange(selection.start, selection.end, option); + + controller.value = controller.value.copyWith( + text: newText, + selection: + TextSelection.collapsed(offset: selection.start + option.length), + ); + + hideOverlay(); + }, + [controller, hideOverlay], + ); + + final getCursorOffset = useCallback( + () { + final textPainter = TextPainter( + text: TextSpan( + text: controller.text, + style: DefaultTextStyle.of(context).style, + ), + textDirection: TextDirection.ltr, + )..layout(); + final caretOffset = + textPainter.getOffsetForCaret(controller.selection.base, Rect.zero); + return caretOffset.translate(0, textPainter.height); + }, + [controller], + ); + + final createOverlayEntry = useCallback( + () { + final renderBox = context.findRenderObject() as RenderBox?; + if (renderBox == null) return null; + final size = renderBox.size; + final cursorOffset = getCursorOffset(); + + return OverlayEntry( + builder: (context) => Positioned( + width: size.width, + child: CompositedTransformFollower( + link: layerLink, + showWhenUnlinked: false, + offset: cursorOffset, + child: Material( + elevation: 4.0, + child: ListView( + padding: EdgeInsets.zero, + shrinkWrap: true, + children: [ + for (final option in options) + ListTile( + title: Text(option), + onTap: () => selectOption(option), + ), + ], + ), + ), + ), + ), + ); + }, + [context, layerLink, options, getCursorOffset, selectOption], + ); + + final showOverlay = useCallback( + () { + if (overlayEntry.value != null) { + overlayEntry.value!.remove(); + } + final entry = createOverlayEntry(); + if (entry == null) return; + overlayEntry.value = entry; + Overlay.of(context).insert(entry); + }, + [createOverlayEntry, overlayEntry], + ); + + useEffect( + () { + void onTextChanged() { + if (controller.text.contains("@")) { + showOverlay(); + } else { + hideOverlay(); + } + } + + controller.addListener(onTextChanged); + + return () => controller.removeListener(onTextChanged); + }, + [controller], + ); + + return TextField( + controller: controller, + maxLines: 2, + ); + } +} diff --git a/lib/view/user_page/antenna_modal_sheet.dart b/lib/view/user_page/antenna_modal_sheet.dart index 8b7d14e41..0437e68db 100644 --- a/lib/view/user_page/antenna_modal_sheet.dart +++ b/lib/view/user_page/antenna_modal_sheet.dart @@ -1,29 +1,34 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/extensions/user_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/antenna_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/antenna_page/antenna_settings_dialog.dart'; -import 'package:miria/view/common/error_detail.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/user_extension.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/antenna_settings.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/antenna_page/antennas_notifier.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class AntennaModalSheet extends ConsumerWidget { +@RoutePage() +class AntennaModalSheet extends ConsumerWidget implements AutoRouteWrapper { const AntennaModalSheet({ - super.key, required this.account, required this.user, + super.key, }); final Account account; final User user; + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: account, child: this); + @override Widget build(BuildContext context, WidgetRef ref) { - final misskey = ref.watch(misskeyProvider(account)); - final antennas = ref.watch(antennasNotifierProvider(misskey)); + final antennas = ref.watch(antennasNotifierProvider); return antennas.when( data: (antennas) { @@ -43,17 +48,16 @@ class AntennaModalSheet extends ConsumerWidget { } if (value) { await ref - .read(antennasNotifierProvider(misskey).notifier) + .read(antennasNotifierProvider.notifier) .updateAntenna( antenna.id, AntennaSettings.fromAntenna(antenna).copyWith( users: [...antenna.users, user.acct], ), - ) - .expectFailure(context); + ); } else { await ref - .read(antennasNotifierProvider(misskey).notifier) + .read(antennasNotifierProvider.notifier) .updateAntenna( antenna.id, AntennaSettings.fromAntenna(antenna).copyWith( @@ -61,8 +65,7 @@ class AntennaModalSheet extends ConsumerWidget { .where((acct) => acct != user.acct) .toList(), ), - ) - .expectFailure(context); + ); } }, title: Text(antenna.name), @@ -72,9 +75,8 @@ class AntennaModalSheet extends ConsumerWidget { leading: const Icon(Icons.add), title: Text(S.of(context).createAntenna), onTap: () async { - final settings = await showDialog( - context: context, - builder: (context) => AntennaSettingsDialog( + final settings = await context.pushRoute( + AntennaSettingsRoute( title: Text(S.of(context).create), initialSettings: const AntennaSettings( src: AntennaSource.users, @@ -83,12 +85,10 @@ class AntennaModalSheet extends ConsumerWidget { ), ); if (!context.mounted) return; - if (settings != null) { - await ref - .read(antennasNotifierProvider(misskey).notifier) - .create(settings) - .expectFailure(context); - } + if (settings == null) return; + await ref + .read(antennasNotifierProvider.notifier) + .create(settings); }, ); } @@ -96,7 +96,7 @@ class AntennaModalSheet extends ConsumerWidget { ); }, error: (e, st) => Center(child: ErrorDetail(error: e, stackTrace: st)), - loading: () => const Center(child: CircularProgressIndicator()), + loading: () => const Center(child: CircularProgressIndicator.adaptive()), ); } } diff --git a/lib/view/user_page/update_memo_dialog.dart b/lib/view/user_page/update_memo_dialog.dart index e3b1eb043..c766cc33d 100644 --- a/lib/view/user_page/update_memo_dialog.dart +++ b/lib/view/user_page/update_memo_dialog.dart @@ -1,52 +1,43 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/hooks/use_async.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/sending_elevated_button.dart"; +import "package:miria/view/user_page/user_info_notifier.dart"; -class UpdateMemoDialog extends ConsumerStatefulWidget { - final Account account; +@RoutePage() +class UpdateMemoDialog extends HookConsumerWidget implements AutoRouteWrapper { + final AccountContext accountContext; final String initialMemo; final String userId; const UpdateMemoDialog({ - super.key, - required this.account, + required this.accountContext, required this.initialMemo, required this.userId, + super.key, }); @override - ConsumerState createState() => - UpdateMemoDialogState(); -} - -class UpdateMemoDialogState extends ConsumerState { - final controller = TextEditingController(); - - Future memoSave() async { - await ref.read(misskeyProvider(widget.account)).users.updateMemo( - UsersUpdateMemoRequest(userId: widget.userId, memo: controller.text)); - if (!mounted) return; - Navigator.of(context).pop(controller.text); - } - - @override - void initState() { - super.initState(); - controller.text = widget.initialMemo; - } + Widget wrappedRoute(BuildContext context) => AccountContextScope( + context: accountContext, + child: this, + ); @override - void dispose() { - controller.dispose(); - super.dispose(); - } + Widget build(BuildContext context, WidgetRef ref) { + final controller = useTextEditingController(text: initialMemo); + final updateMemo = useAsync(() async { + await ref + .read(userInfoNotifierProxyProvider(userId)) + .updateMemo(controller.text); + await ref.read(appRouterProvider).maybePop(); + }); - @override - Widget build(BuildContext context) { return AlertDialog( title: Text(S.of(context).memo), content: TextField( @@ -58,15 +49,16 @@ class UpdateMemoDialogState extends ConsumerState { ), actions: [ OutlinedButton( - onPressed: () { - Navigator.of(context).pop(); - }, + onPressed: () async => context.maybePop(), child: Text(S.of(context).cancel), ), - ElevatedButton( - onPressed: memoSave.expectFailure(context), - child: Text(S.of(context).save), - ), + switch (updateMemo.value) { + AsyncLoading() => const SendingElevatedButton(), + _ => ElevatedButton( + onPressed: () async => updateMemo.execute(), + child: Text(S.of(context).save), + ), + }, ], ); } diff --git a/lib/view/user_page/user_clips.dart b/lib/view/user_page/user_clips.dart index 70e386fe8..a133636d9 100644 --- a/lib/view/user_page/user_clips.dart +++ b/lib/view/user_page/user_clips.dart @@ -1,29 +1,28 @@ -import 'package:flutter/material.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/clip_item.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/clip_item.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; class UserClips extends ConsumerWidget { final String userId; - const UserClips({super.key, required this.userId}); + const UserClips({required this.userId, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return PushableListView( initializeFuture: () async { final response = await ref - .read(misskeyProvider(AccountScope.of(context))) + .read(misskeyGetContextProvider) .users .clips(UsersClipsRequest(userId: userId)); return response.toList(); }, nextFuture: (lastItem, _) async { final response = await ref - .read(misskeyProvider(AccountScope.of(context))) + .read(misskeyGetContextProvider) .users .clips(UsersClipsRequest(userId: userId, untilId: lastItem.id)); return response.toList(); diff --git a/lib/view/user_page/user_control_dialog.dart b/lib/view/user_page/user_control_dialog.dart index d7e952426..9c6b6f4f5 100644 --- a/lib/view/user_page/user_control_dialog.dart +++ b/lib/view/user_page/user_control_dialog.dart @@ -1,20 +1,22 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/extensions/user_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/note_search_condition.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/common/misskey_notes/abuse_dialog.dart'; -import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; -import 'package:miria/view/user_page/antenna_modal_sheet.dart'; -import 'package:miria/view/user_page/users_list_modal_sheet.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "dart:async"; + +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter/services.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/user_extension.dart"; +import "package:miria/hooks/use_async.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/note_search_condition.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/state_notifier/common/misskey_notes/misskey_note_notifier.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/user_page/user_info_notifier.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:url_launcher/url_launcher.dart"; enum UserControl { createMute, @@ -25,206 +27,153 @@ enum UserControl { deleteBlock, } -class UserControlDialog extends ConsumerStatefulWidget { +@RoutePage() +class UserControlDialog extends HookConsumerWidget implements AutoRouteWrapper { final Account account; final UserDetailed response; const UserControlDialog({ - super.key, required this.account, required this.response, + super.key, }); @override - ConsumerState createState() => - UserControlDialogState(); -} + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: account, child: this); -class UserControlDialogState extends ConsumerState { - Future addToList() async { - return showModalBottomSheet( - context: context, - builder: (context) => UsersListModalSheet( - account: widget.account, - user: widget.response, - ), - ); - } + @override + Widget build(BuildContext context, WidgetRef ref) { + final provider = userInfoNotifierProxyProvider(response.id); - Future addToAntenna() async { - return showModalBottomSheet( - context: context, - builder: (context) => AntennaModalSheet( - account: widget.account, - user: widget.response, - ), + final createBlocking = useAsync(ref.read(provider).createBlocking); + final deleteBlocking = useAsync(ref.read(provider).deleteBlocking); + final createRenoteMute = useAsync(ref.read(provider).createRenoteMute); + final deleteRenoteMute = useAsync(ref.read(provider).deleteRenoteMute); + final createMute = useAsync(ref.read(provider).createMute); + final deleteMute = useAsync(ref.read(provider).deleteMute); + final copyName = useAsync(() async { + await Clipboard.setData( + ClipboardData(text: response.name ?? response.username), + ); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).doneCopy), + duration: const Duration(seconds: 1), + ), + ); + Navigator.of(context).pop(); + }); + final copyScreenName = useAsync(() async { + await Clipboard.setData(ClipboardData(text: response.acct)); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).doneCopy), + duration: const Duration(seconds: 1), + ), + ); + Navigator.of(context).pop(); + }); + final copyLinks = useAsync(() async { + await Clipboard.setData( + ClipboardData( + text: Uri( + scheme: "https", + host: account.host, + path: response.acct, + ).toString(), + ), + ); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).doneCopy), + duration: const Duration(seconds: 1), + ), + ); + Navigator.of(context).pop(); + }); + final openBrowserAsRemote = useAsync(() async { + final uri = response.uri ?? response.url; + if (uri == null) return; + await launchUrl(uri, mode: LaunchMode.externalApplication); + if (!context.mounted) return; + Navigator.of(context).pop(); + }); + final openUserInOtherAccount = useAsync( + () async => ref + .read(misskeyNoteNotifierProvider.notifier) + .openUserInOtherAccount(response), ); - } - Future getExpire() async { - return await showDialog( - context: context, builder: (context) => const ExpireSelectDialog()); - } + final isLoading = [ + createBlocking.value, + deleteBlocking.value, + createRenoteMute.value, + deleteRenoteMute.value, + createMute.value, + deleteMute.value, + ].where((e) => e is AsyncData || e is AsyncLoading).isNotEmpty; - Future renoteMuteCreate() async { - await ref - .read(misskeyProvider(widget.account)) - .renoteMute - .create(RenoteMuteCreateRequest(userId: widget.response.id)); - if (!mounted) return; - Navigator.of(context).pop(UserControl.createRenoteMute); - } - - Future renoteMuteDelete() async { - await ref - .read(misskeyProvider(widget.account)) - .renoteMute - .delete(RenoteMuteDeleteRequest(userId: widget.response.id)); - if (!mounted) return; - Navigator.of(context).pop(UserControl.deleteRenoteMute); - } - - Future muteCreate() async { - final expires = await getExpire(); - if (expires == null) return; - final expiresDate = expires == Expire.indefinite - ? null - : DateTime.now().add(expires.expires!); - await ref.read(misskeyProvider(widget.account)).mute.create( - MuteCreateRequest(userId: widget.response.id, expiresAt: expiresDate)); - if (!mounted) return; - Navigator.of(context).pop(UserControl.createMute); - } - - Future muteDelete() async { - await ref - .read(misskeyProvider(widget.account)) - .mute - .delete(MuteDeleteRequest(userId: widget.response.id)); - if (!mounted) return; - Navigator.of(context).pop(UserControl.deleteMute); - } - - Future blockingCreate() async { - if (await SimpleConfirmDialog.show( - context: context, - message: S.of(context).confirmCreateBlock, - primary: S.of(context).createBlock, - secondary: S.of(context).cancel, - ) != - true) { - return; + if (isLoading) { + return const Center(child: CircularProgressIndicator.adaptive()); } - await ref - .read(misskeyProvider(widget.account)) - .blocking - .create(BlockCreateRequest(userId: widget.response.id)); - if (!mounted) return; - Navigator.of(context).pop(UserControl.createBlock); - } - - Future blockingDelete() async { - await ref - .read(misskeyProvider(widget.account)) - .blocking - .delete(BlockDeleteRequest(userId: widget.response.id)); - if (!mounted) return; - Navigator.of(context).pop(UserControl.deleteBlock); - } - - @override - Widget build(BuildContext context) { - final user = widget.response; + final user = response; return ListView( children: [ ListTile( leading: const Icon(Icons.copy), title: Text(S.of(context).copyName), - onTap: () { - Clipboard.setData( - ClipboardData( - text: widget.response.name ?? widget.response.username, - ), - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(S.of(context).doneCopy), duration: const Duration(seconds: 1)), - ); - Navigator.of(context).pop(); - }, + onTap: copyName.executeOrNull, ), ListTile( leading: const Icon(Icons.alternate_email), title: Text(S.of(context).copyUserScreenName), - onTap: () { - Clipboard.setData(ClipboardData(text: widget.response.acct)); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(S.of(context).doneCopy), duration: const Duration(seconds: 1)), - ); - Navigator.of(context).pop(); - }, + onTap: copyScreenName.executeOrNull, ), ListTile( leading: const Icon(Icons.link), title: Text(S.of(context).copyLinks), - onTap: () { - Clipboard.setData( - ClipboardData( - text: Uri( - scheme: "https", - host: widget.account.host, - path: widget.response.acct, - ).toString(), - ), - ); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(S.of(context).doneCopy), duration: const Duration(seconds: 1)), - ); - Navigator.of(context).pop(); - }, + onTap: copyLinks.executeOrNull, ), ListTile( leading: const Icon(Icons.open_in_browser), title: Text(S.of(context).openBrowsers), - onTap: () { - launchUrl( + onTap: () async { + await launchUrl( Uri( scheme: "https", - host: widget.account.host, - path: widget.response.acct, + host: account.host, + path: response.acct, ), - mode: LaunchMode.inAppWebView, + mode: LaunchMode.externalApplication, ); + if (!context.mounted) return; Navigator.of(context).pop(); }, ), - if (widget.response.host != null) + if (response.host != null) ListTile( leading: const Icon(Icons.rocket_launch), title: Text(S.of(context).openBrowsersAsRemote), - onTap: () { - final uri = widget.response.uri ?? widget.response.url; - if (uri == null) return; - launchUrl(uri, mode: LaunchMode.inAppWebView); - Navigator.of(context).pop(); - }, + onTap: openBrowserAsRemote.executeOrNull, ), ListTile( leading: const Icon(Icons.open_in_new), title: Text(S.of(context).openInAnotherAccount), - onTap: () => ref - .read(misskeyNoteNotifierProvider(widget.account).notifier) - .openUserInOtherAccount(context, user) - .expectFailure(context), + onTap: openUserInOtherAccount.executeOrNull, ), ListTile( leading: const Icon(Icons.search), title: Text(S.of(context).searchNote), - onTap: () => context.pushRoute( + onTap: () async => context.pushRoute( SearchRoute( - account: widget.account, + accountContext: ref.read(accountContextProvider), initialNoteSearchCondition: NoteSearchCondition( - user: widget.response, + user: response, ), ), ), @@ -232,75 +181,70 @@ class UserControlDialogState extends ConsumerState { ListTile( leading: const Icon(Icons.list), title: Text(S.of(context).addToList), - onTap: addToList, + onTap: () async => context + .pushRoute(UsersListModalRoute(account: account, user: response)), ), ListTile( leading: const Icon(Icons.settings_input_antenna), title: Text(S.of(context).addToAntenna), - onTap: addToAntenna, + onTap: () async => context + .pushRoute(AntennaModalRoute(account: account, user: user)), ), if (user is UserDetailedNotMeWithRelations) ...[ if (user.isRenoteMuted) ListTile( leading: const Icon(Icons.repeat_rounded), title: Text(S.of(context).deleteRenoteMute), - onTap: renoteMuteDelete.expectFailure(context), + onTap: deleteRenoteMute.executeOrNull, ) else ListTile( leading: const Icon(Icons.repeat_rounded), title: Text(S.of(context).createRenoteMute), - onTap: renoteMuteCreate.expectFailure(context), + onTap: createRenoteMute.executeOrNull, ), if (user.isMuted) ListTile( leading: const Icon(Icons.visibility), title: Text(S.of(context).deleteMute), - onTap: muteDelete.expectFailure(context), + onTap: deleteMute.executeOrNull, ) else ListTile( leading: const Icon(Icons.visibility_off), title: Text(S.of(context).createMute), - onTap: muteCreate.expectFailure(context), + onTap: createMute.executeOrNull, ), if (user.isBlocking) ListTile( leading: const Icon(Icons.block), title: Text(S.of(context).deleteBlock), - onTap: blockingDelete.expectFailure(context), + onTap: deleteBlocking.executeOrNull, ) else ListTile( leading: const Icon(Icons.block), title: Text(S.of(context).createBlock), - onTap: blockingCreate.expectFailure(context), + onTap: createBlocking.executeOrNull, ), ListTile( leading: const Icon(Icons.report), title: Text(S.of(context).reportAbuse), - onTap: () { - Navigator.of(context).pop(); - showDialog( - context: context, - builder: (context) => AbuseDialog( - account: widget.account, - targetUser: widget.response)); + onTap: () async { + await ( + context.maybePop(), + context.pushRoute( + AbuseRoute(account: account, targetUser: response), + ) + ).wait; }, - ) + ), ], ], ); } } -class ExpireSelectDialog extends StatefulWidget { - const ExpireSelectDialog({super.key}); - - @override - State createState() => ExpireSelectDialogState(); -} - enum Expire { indefinite(null), minutes_10(Duration(minutes: 10)), @@ -323,32 +267,34 @@ enum Expire { } } -class ExpireSelectDialogState extends State { - Expire? selectedExpire = Expire.indefinite; +@RoutePage() +class ExpireSelectDialog extends HookWidget { + const ExpireSelectDialog({super.key}); @override Widget build(BuildContext context) { + final selectedExpire = useState(Expire.indefinite); + return AlertDialog( title: Text(S.of(context).selectDuration), - content: Container( - child: DropdownButton( - items: [ - for (final value in Expire.values) - DropdownMenuItem( - value: value, - child: Text(value.displayName(context)), - ) - ], - onChanged: (value) => setState(() => selectedExpire = value), - value: selectedExpire, - ), + content: DropdownButton( + items: [ + for (final value in Expire.values) + DropdownMenuItem( + value: value, + child: Text(value.displayName(context)), + ), + ], + onChanged: (value) => selectedExpire.value = value, + value: selectedExpire.value, ), actions: [ ElevatedButton( - onPressed: () { - Navigator.of(context).pop(selectedExpire); - }, - child: Text(S.of(context).done)) + onPressed: () { + Navigator.of(context).pop(selectedExpire.value); + }, + child: Text(S.of(context).done), + ), ], ); } diff --git a/lib/view/user_page/user_detail.dart b/lib/view/user_page/user_detail.dart index 840f0fe3c..cb18c02a6 100644 --- a/lib/view/user_page/user_detail.dart +++ b/lib/view/user_page/user_detail.dart @@ -1,221 +1,51 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:confetti/confetti.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/extensions/date_time_extension.dart'; -import 'package:miria/extensions/user_extension.dart'; -import 'package:miria/extensions/string_extensions.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/avatar_icon.dart'; -import 'package:miria/view/common/constants.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/misskey_notes/network_image.dart'; -import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:miria/view/user_page/update_memo_dialog.dart'; -import 'package:miria/view/user_page/user_control_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:confetti/confetti.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/date_time_extension.dart"; +import "package:miria/extensions/string_extensions.dart"; +import "package:miria/extensions/user_extension.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/avatar_icon.dart"; +import "package:miria/view/common/constants.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:miria/view/user_page/user_info_notifier.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class UserDetail extends ConsumerStatefulWidget { - final Account account; - final Account? controlAccount; +class UserDetail extends ConsumerWidget { final UserDetailed response; const UserDetail({ - super.key, required this.response, - required this.account, - required this.controlAccount, + super.key, }); - @override - ConsumerState createState() => UserDetailState(); -} - -class UserDetailState extends ConsumerState { - late UserDetailed response; - bool isFollowEditing = false; - String memo = ""; - - Future followCreate() async { - if (isFollowEditing) return; - - final user = response; - if (user is! UserDetailedNotMeWithRelations) { - return; - } - - setState(() { - isFollowEditing = true; - }); - try { - await ref - .read(misskeyProvider(AccountScope.of(context))) - .following - .create(FollowingCreateRequest(userId: user.id)); - if (!mounted) return; - final requiresFollowRequest = user.isLocked && !user.isFollowed; - setState(() { - isFollowEditing = false; - response = user.copyWith( - isFollowing: !requiresFollowRequest, - hasPendingFollowRequestFromYou: requiresFollowRequest, - ); - }); - } catch (e) { - if (!mounted) return; - setState(() { - isFollowEditing = false; - }); - rethrow; - } - } - - Future followDelete() async { - if (isFollowEditing) return; - - final user = response; - if (user is! UserDetailedNotMeWithRelations) { - return; - } - - final account = AccountScope.of(context); - if (await SimpleConfirmDialog.show( - context: context, - message: S.of(context).confirmUnfollow, - primary: S.of(context).deleteFollow, - secondary: S.of(context).cancel, - ) != - true) { - return; - } - setState(() { - isFollowEditing = true; - }); - try { - await ref - .read(misskeyProvider(account)) - .following - .delete(FollowingDeleteRequest(userId: user.id)); - if (!mounted) return; - setState(() { - isFollowEditing = false; - response = user.copyWith(isFollowing: false); - }); - } catch (e) { - if (!mounted) return; - setState(() { - isFollowEditing = false; - }); - rethrow; - } - } + Widget buildContent(BuildContext context, WidgetRef ref) { + final response = this.response; + // final isFollowEditing = ref.watch( + // userInfoNotifierProvider(response.id) + // .select((value) => value.value?.follow is AsyncLoading), + // ); + final notifier = ref.read(userInfoNotifierProxyProvider(response.id)); + final memo = response.memo ?? ""; - Future followRequestCancel() async { - if (isFollowEditing) return; - - final user = response; - if (user is! UserDetailedNotMeWithRelations) { - return; - } + final isSameAccount = ref.read(accountContextProvider).isSame; - setState(() { - isFollowEditing = true; - }); - try { - await ref - .read(misskeyProvider(AccountScope.of(context))) - .following - .requests - .cancel(FollowingRequestsCancelRequest(userId: user.id)); - if (!mounted) return; - setState(() { - isFollowEditing = false; - response = user.copyWith(hasPendingFollowRequestFromYou: false); - }); - } catch (e) { - if (!mounted) return; - setState(() { - isFollowEditing = false; - }); - rethrow; - } - } - - Future userControl() async { - final result = await showModalBottomSheet( - context: context, - builder: (context) => UserControlDialog( - account: widget.account, - response: response, - ), - ); - if (result == null) return; - - final user = response; - if (user is! UserDetailedNotMeWithRelations) { - return; - } - - switch (result) { - case UserControl.createMute: - setState(() { - response = user.copyWith(isMuted: true); - }); - break; - case UserControl.deleteMute: - setState(() { - response = user.copyWith(isMuted: false); - }); - break; - case UserControl.createRenoteMute: - setState(() { - response = user.copyWith(isRenoteMuted: true); - }); - break; - case UserControl.deleteRenoteMute: - setState(() { - response = user.copyWith(isRenoteMuted: false); - }); - break; - case UserControl.createBlock: - setState(() { - response = user.copyWith(isBlocking: true); - }); - break; - case UserControl.deleteBlock: - setState(() { - response = user.copyWith(isBlocking: false); - }); - break; - } - } - - @override - void initState() { - super.initState(); - response = widget.response; - memo = response.memo ?? ""; - } - - Widget buildContent() { - final user = response; - - return Column(children: [ - if (widget.controlAccount == null) - Padding( + return Column( + children: [ + if (isSameAccount) + Padding( padding: const EdgeInsets.only(right: 10), child: Row( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, children: [ - if (user is UserDetailedNotMeWithRelations) + if (response is UserDetailedNotMeWithRelations) Expanded( child: Align( alignment: Alignment.centerRight, @@ -224,28 +54,28 @@ class UserDetailState extends ConsumerState { child: Wrap( crossAxisAlignment: WrapCrossAlignment.center, children: [ - if (user.isRenoteMuted) + if (response.isRenoteMuted) Card( child: Padding( padding: const EdgeInsets.all(10), child: Text(S.of(context).renoteMuting), ), ), - if (user.isMuted) + if (response.isMuted) Card( child: Padding( padding: const EdgeInsets.all(10), child: Text(S.of(context).muting), ), ), - if (user.isBlocking) + if (response.isBlocking) Card( child: Padding( padding: const EdgeInsets.all(10), child: Text(S.of(context).blocking), ), ), - if (user.isFollowed) + if (response.isFollowed) Padding( padding: const EdgeInsets.only(right: 8.0), child: Card( @@ -255,32 +85,29 @@ class UserDetailState extends ConsumerState { ), ), ), - if (!isFollowEditing) - if (user.isFollowing) - ElevatedButton( - onPressed: - followDelete.expectFailure(context), - child: Text(S.of(context).unfollow), - ) - else if (user.hasPendingFollowRequestFromYou) - ElevatedButton( - onPressed: followRequestCancel - .expectFailure(context), - child: Text( - S.of(context).followRequestPending, - ), - ) - else - OutlinedButton( - onPressed: - followCreate.expectFailure(context), - child: Text( - user.isLocked - ? S.of(context).followRequest - : S.of(context).createFollow, - ), - ) + //if (!isFollowEditing) + if (response.isFollowing) + ElevatedButton( + onPressed: notifier.deleteFollow, + child: Text(S.of(context).unfollow), + ) + else if (response.hasPendingFollowRequestFromYou) + ElevatedButton( + onPressed: notifier.cancelFollowRequest, + child: Text( + S.of(context).followRequestPending, + ), + ) else + OutlinedButton( + onPressed: notifier.createFollow, + child: Text( + response.isLocked + ? S.of(context).followRequest + : S.of(context).createFollow, + ), + ), + /*else Align( alignment: Alignment.centerRight, child: TextButton.icon( @@ -300,7 +127,7 @@ class UserDetailState extends ConsumerState { ), label: Text(S.of(context).refreshing), ), - ), + ),*/ ], ), ), @@ -311,265 +138,320 @@ class UserDetailState extends ConsumerState { Align( alignment: Alignment.center, child: IconButton( - onPressed: userControl, + onPressed: () async => await context.pushRoute( + UserControlRoute( + account: ref.read(accountContextProvider).postAccount, + response: response, + ), + ), icon: const Icon(Icons.more_vert), ), - ) + ), ], - )), - const Divider(), - Padding( - padding: const EdgeInsets.only(left: 10, right: 10, top: 12), - child: Column(children: [ - Row( + ), + ), + const Divider(), + Padding( + padding: const EdgeInsets.only(left: 10, right: 10, top: 12), + child: Column( children: [ - AvatarIcon( - user: response, - height: 80, - ), - Expanded( - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - MfmText( - mfmText: response.name ?? response.username, - style: Theme.of(context).textTheme.headlineSmall, - emoji: response.emojis, - ), - Text( - response.acct, - style: Theme.of(context).textTheme.bodyLarge, - ), - ], + Row( + children: [ + AvatarIcon( + user: response, + height: 80, ), - ), - ), - ], - ), - const Padding(padding: EdgeInsets.only(top: 5)), - if (widget.controlAccount == null) - Card( - child: Padding( - padding: const EdgeInsets.all(10), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Expanded( - child: Text( - memo.isNotEmpty ? memo : S.of(context).memoDescription, - style: memo.isNotEmpty - ? null - : Theme.of(context).inputDecorationTheme.hintStyle, + Expanded( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SimpleMfmText( + response.name ?? response.username, + style: Theme.of(context).textTheme.headlineSmall, + emojis: response.emojis, + ), + Text( + response.acct, + style: Theme.of(context).textTheme.bodyLarge, + ), + ], ), ), - IconButton( - onPressed: () async { - final result = await showDialog( - context: context, - builder: (context) => UpdateMemoDialog( - account: widget.account, - initialMemo: memo, - userId: response.id, - )); - if (result != null) { - setState(() { - memo = result; - }); - } - }, - icon: const Icon(Icons.edit)), - ], - ), + ), + ], ), - ), - const Padding(padding: EdgeInsets.only(top: 5)), - Wrap( - spacing: 5, - runSpacing: 5, - children: [ - for (final role in response.roles ?? []) RoleChip(role: role), - ], - ), - const Padding(padding: EdgeInsets.only(top: 5)), - if (response.host != null) - Card( - child: Padding( - padding: const EdgeInsets.all(10.0), - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( + const Padding(padding: EdgeInsets.only(top: 5)), + if (isSameAccount) + Card( + child: Padding( + padding: const EdgeInsets.all(10), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - const Icon(Icons.warning_amber_rounded), - Text(S.of(context).remoteUserCaution), + Expanded( + child: Text( + memo.isNotEmpty + ? memo + : S.of(context).memoDescription, + style: memo.isNotEmpty + ? null + : Theme.of(context) + .inputDecorationTheme + .hintStyle, + ), + ), + IconButton( + onPressed: () async => await context.pushRoute( + UpdateMemoRoute( + accountContext: ref.read(accountContextProvider), + initialMemo: memo, + userId: response.id, + ), + ), + icon: const Icon(Icons.edit), + ), ], ), - GestureDetector( - onTap: () => context.pushRoute(FederationRoute( - account: AccountScope.of(context), - host: response.host!)), - child: Text( - S.of(context).showServerInformation, - style: AppTheme.of(context).linkStyle, - ), - ), - ], + ), ), + const Padding(padding: EdgeInsets.only(top: 5)), + Wrap( + spacing: 5, + runSpacing: 5, + children: [ + for (final role in response.roles ?? []) RoleChip(role: role), + ], ), - ), - Align( - alignment: Alignment.center, - child: MfmText( - mfmText: response.description ?? "", - emoji: response.emojis, - ), - ), - const Padding(padding: EdgeInsets.only(top: 20)), - Table( - columnWidths: const { - 1: FlexColumnWidth(1), - 2: FlexColumnWidth(1), - }, - children: [ - TableRow(children: [ - TableCell( - child: Text( - S.of(context).location, - textAlign: TextAlign.center, + if (response is UserDetailedNotMeWithRelations && + response.followedMessage != null && + response.isFollowing) + Card( + child: SimpleMfmText( + S + .of(context) + .messageForFollower(response.followedMessage ?? ""), ), ), - TableCell(child: Text(response.location ?? "")) - ]), - TableRow(children: [ - TableCell( - child: Text( - S.of(context).registeredDate, - textAlign: TextAlign.center, + const Padding(padding: EdgeInsets.only(top: 5)), + if (response.host != null) + Card( + child: Padding( + padding: const EdgeInsets.all(10.0), + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon(Icons.warning_amber_rounded), + Text(S.of(context).remoteUserCaution), + ], + ), + GestureDetector( + onTap: () async => context.pushRoute( + FederationRoute( + accountContext: ref.read(accountContextProvider), + host: response.host!, + ), + ), + child: Text( + S.of(context).showServerInformation, + style: AppTheme.of(context).linkStyle, + ), + ), + ], + ), ), ), - TableCell( - child: Text(response.createdAt.format(context)), - ), //FIXME - ]), - TableRow(children: [ - TableCell( - child: Text( - S.of(context).birthday, - textAlign: TextAlign.center, - ), + Align( + alignment: Alignment.center, + child: MfmText( + mfmText: response.description ?? "", + emoji: response.emojis, ), - TableCell(child: Text(response.birthday?.format(context) ?? "")) - ]) - ], - ), - const Padding(padding: EdgeInsets.only(top: 20)), - if (response.fields?.isNotEmpty == true) ...[ - Table( - columnWidths: const { - 1: FlexColumnWidth(2), - 2: FlexColumnWidth(3), - }, - children: [ - for (final field in response.fields ?? []) - TableRow(children: [ - TableCell( - child: MfmText( - mfmText: field.name, - emoji: response.emojis, + ), + const Padding(padding: EdgeInsets.only(top: 20)), + Table( + columnWidths: const { + 1: FlexColumnWidth(1), + 2: FlexColumnWidth(1), + }, + children: [ + TableRow( + children: [ + TableCell( + child: Text( + S.of(context).location, + textAlign: TextAlign.center, + ), ), - ), - TableCell( - child: MfmText( - mfmText: field.value, - emoji: response.emojis, - )), - ]) - ], - ), - const Padding(padding: EdgeInsets.only(top: 20)), - ], - Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - Column( - children: [ - Text(response.notesCount.format(), - style: Theme.of(context).textTheme.titleMedium), - Text( - S.of(context).note, - style: Theme.of(context).textTheme.bodyMedium, - ) - ], - ), - if (widget.response.isFollowingVisibleForMe) - InkWell( - onTap: () => context.pushRoute(UserFolloweeRoute( - userId: response.id, account: AccountScope.of(context))), - child: Column( - children: [ - Text(response.followingCount.format(), - style: Theme.of(context).textTheme.titleMedium), - Text( - S.of(context).follow, - style: Theme.of(context).textTheme.bodyMedium, - ) - ], - ), + TableCell(child: Text(response.location ?? "")), + ], + ), + TableRow( + children: [ + TableCell( + child: Text( + S.of(context).registeredDate, + textAlign: TextAlign.center, + ), + ), + TableCell( + child: Text(response.createdAt.format(context)), + ), //FIXME + ], + ), + TableRow( + children: [ + TableCell( + child: Text( + S.of(context).birthday, + textAlign: TextAlign.center, + ), + ), + TableCell( + child: Text(response.birthday?.format(context) ?? ""), + ), + ], + ), + ], ), - if (widget.response.isFollowersVisibleForMe) - InkWell( - onTap: () => context.pushRoute(UserFollowerRoute( - userId: response.id, account: AccountScope.of(context))), - child: Column( - mainAxisSize: MainAxisSize.max, + const Padding(padding: EdgeInsets.only(top: 20)), + if (response.fields?.isNotEmpty == true) ...[ + Table( + columnWidths: const { + 1: FlexColumnWidth(2), + 2: FlexColumnWidth(3), + }, children: [ - Text(response.followersCount.format(), - style: Theme.of(context).textTheme.titleMedium), - Text( - S.of(context).follower, - style: Theme.of(context).textTheme.bodyMedium, - ) + for (final field in response.fields ?? []) + TableRow( + children: [ + TableCell( + child: MfmText( + mfmText: field.name, + emoji: response.emojis, + ), + ), + TableCell( + child: MfmText( + mfmText: field.value, + emoji: response.emojis, + ), + ), + ], + ), ], ), + const Padding(padding: EdgeInsets.only(top: 20)), + ], + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + Text( + response.notesCount.format(), + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + S.of(context).note, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + if (response.isFollowingVisibleForMe) + InkWell( + onTap: () async => context.pushRoute( + UserFolloweeRoute( + userId: response.id, + accountContext: ref.read(accountContextProvider), + ), + ), + child: Column( + children: [ + Text( + response.followingCount.format(), + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + S.of(context).follow, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + if (response.isFollowersVisibleForMe) + InkWell( + onTap: () async => context.pushRoute( + UserFollowerRoute( + userId: response.id, + accountContext: ref.read(accountContextProvider), + ), + ), + child: Column( + mainAxisSize: MainAxisSize.max, + children: [ + Text( + response.followersCount.format(), + style: Theme.of(context).textTheme.titleMedium, + ), + Text( + S.of(context).follower, + style: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ), + ], ), - ]), - ]), - ), - ]); + ], + ), + ), + ], + ); } @override - Widget build(BuildContext context) { + Widget build(BuildContext context, WidgetRef ref) { return CustomScrollView( slivers: [ SliverToBoxAdapter( - child: BirthdayConfetti( - response: widget.response, - child:Column(children: [ + child: BirthdayConfetti( + response: response, + child: Column( + children: [ if (response.bannerUrl != null) Image.network(response.bannerUrl.toString()), Align( - alignment: Alignment.center, - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 800), - child: buildContent())), - const Padding(padding: EdgeInsets.only(top: 20)) - ]))), + alignment: Alignment.center, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 800), + child: buildContent(context, ref), + ), + ), + const Padding(padding: EdgeInsets.only(top: 20)), + ], + ), + ), + ), if (response.pinnedNotes != null) SliverPadding( padding: const EdgeInsets.only(right: 10), sliver: SliverList.builder( itemCount: response.pinnedNotes!.length, - itemBuilder: (context, index) => - MisskeyNote( - note: response.pinnedNotes![index], - loginAs: widget.controlAccount) - )) - ]); + itemBuilder: (context, index) => MisskeyNote( + note: response.pinnedNotes![index], + ), + ), + ), + ], + ); } } @@ -578,9 +460,9 @@ class BirthdayConfetti extends StatefulWidget { final Widget child; const BirthdayConfetti({ - super.key, required this.response, required this.child, + super.key, }); @override @@ -609,10 +491,11 @@ class BirthdayConfettiState extends State { if (now.month == widget.response.birthday?.month && now.day == widget.response.birthday?.day) { return ConfettiWidget( - confettiController: confettiController, - blastDirection: 0, - numberOfParticles: 40, - child: widget.child); + confettiController: confettiController, + blastDirection: 0, + numberOfParticles: 40, + child: widget.child, + ); } return widget.child; @@ -620,13 +503,12 @@ class BirthdayConfettiState extends State { } class RoleChip extends ConsumerWidget { - const RoleChip({super.key, required this.role}); + const RoleChip({required this.role, super.key}); final UserRole role; @override Widget build(BuildContext context, WidgetRef ref) { - final account = AccountScope.of(context); final textStyle = Theme.of(context).textTheme.bodyMedium; final height = MediaQuery.textScalerOf(context) .scale((textStyle?.fontSize ?? 14) * (textStyle?.height ?? 1)); @@ -635,13 +517,16 @@ class RoleChip extends ConsumerWidget { child: GestureDetector( onTap: () async { final response = await ref - .read(misskeyProvider(account)) + .read(misskeyGetContextProvider) .roles .show(RolesShowRequest(roleId: role.id)); if (response.isPublic && response.isExplorable) { if (!context.mounted) return; - context.pushRoute( - ExploreRoleUsersRoute(item: response, account: account), + await context.pushRoute( + ExploreRoleUsersRoute( + item: response, + accountContext: ref.read(accountContextProvider), + ), ); } }, diff --git a/lib/view/user_page/user_followee.dart b/lib/view/user_page/user_followee.dart index 660942394..f435b4275 100644 --- a/lib/view/user_page/user_followee.dart +++ b/lib/view/user_page/user_followee.dart @@ -1,51 +1,52 @@ -import 'package:auto_route/annotations.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:miria/view/user_page/user_list_item.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:miria/view/user_page/user_list_item.dart"; +import "package:misskey_dart/misskey_dart.dart"; @RoutePage() -class UserFolloweePage extends ConsumerWidget { +class UserFolloweePage extends ConsumerWidget implements AutoRouteWrapper { final String userId; - final Account account; + final AccountContext accountContext; const UserFolloweePage({ - super.key, required this.userId, - required this.account, + required this.accountContext, + super.key, }); + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); @override Widget build(BuildContext context, WidgetRef ref) { - return AccountScope( - account: account, - child: Scaffold( - appBar: AppBar(title: Text(S.of(context).follow)), - body: PushableListView( - initializeFuture: () async { - final response = await ref - .read(misskeyProvider(account)) - .users - .following(UsersFollowingRequest(userId: userId)); - return response.toList(); - }, - nextFuture: (lastItem, _) async { - final response = await ref - .read(misskeyProvider(account)) - .users - .following(UsersFollowingRequest( - userId: userId, untilId: lastItem.id)); - return response.toList(); - }, - itemBuilder: (context, item) => UserListItem( - user: item.followee!, - isDetail: true, - ), + return Scaffold( + appBar: AppBar(title: Text(S.of(context).follow)), + body: PushableListView( + initializeFuture: () async { + final response = await ref + .read(misskeyGetContextProvider) + .users + .following(UsersFollowingRequest(userId: userId)); + return response.toList(); + }, + nextFuture: (lastItem, _) async { + final response = + await ref.read(misskeyGetContextProvider).users.following( + UsersFollowingRequest( + userId: userId, + untilId: lastItem.id, + ), + ); + return response.toList(); + }, + itemBuilder: (context, item) => UserListItem( + user: item.followee!, + isDetail: true, ), ), ); diff --git a/lib/view/user_page/user_follower.dart b/lib/view/user_page/user_follower.dart index 1c38ec1b1..c43204a53 100644 --- a/lib/view/user_page/user_follower.dart +++ b/lib/view/user_page/user_follower.dart @@ -1,51 +1,53 @@ -import 'package:auto_route/annotations.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:miria/view/user_page/user_list_item.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:miria/view/user_page/user_list_item.dart"; +import "package:misskey_dart/misskey_dart.dart"; @RoutePage() -class UserFollowerPage extends ConsumerWidget { +class UserFollowerPage extends ConsumerWidget implements AutoRouteWrapper { final String userId; - final Account account; + final AccountContext accountContext; const UserFollowerPage({ - super.key, required this.userId, - required this.account, + required this.accountContext, + super.key, }); + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); + @override Widget build(BuildContext context, WidgetRef ref) { - return AccountScope( - account: account, - child: Scaffold( - appBar: AppBar(title: Text(S.of(context).follower)), - body: PushableListView( - initializeFuture: () async { - final response = await ref - .read(misskeyProvider(account)) - .users - .followers(UsersFollowersRequest(userId: userId)); - return response.toList(); - }, - nextFuture: (lastItem, _) async { - final response = await ref - .read(misskeyProvider(account)) - .users - .followers(UsersFollowersRequest( - userId: userId, untilId: lastItem.id)); - return response.toList(); - }, - itemBuilder: (context, item) => UserListItem( - user: item.follower!, - isDetail: true, - ), + return Scaffold( + appBar: AppBar(title: Text(S.of(context).follower)), + body: PushableListView( + initializeFuture: () async { + final response = await ref + .read(misskeyGetContextProvider) + .users + .followers(UsersFollowersRequest(userId: userId)); + return response.toList(); + }, + nextFuture: (lastItem, _) async { + final response = + await ref.read(misskeyGetContextProvider).users.followers( + UsersFollowersRequest( + userId: userId, + untilId: lastItem.id, + ), + ); + return response.toList(); + }, + itemBuilder: (context, item) => UserListItem( + user: item.follower!, + isDetail: true, ), ), ); diff --git a/lib/view/user_page/user_info_notifier.dart b/lib/view/user_page/user_info_notifier.dart new file mode 100644 index 000000000..78be30fe8 --- /dev/null +++ b/lib/view/user_page/user_info_notifier.dart @@ -0,0 +1,330 @@ +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:freezed_annotation/freezed_annotation.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/note_repository.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/dialog/dialog_state.dart"; +import "package:miria/view/user_page/user_control_dialog.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; + +part "user_info_notifier.freezed.dart"; +part "user_info_notifier.g.dart"; + +@freezed +class UserInfo with _$UserInfo { + const factory UserInfo({ + required String userId, + required UserDetailed response, + String? remoteUserId, + UserDetailed? remoteResponse, + MetaResponse? metaResponse, + }) = _UserInfo; + + const UserInfo._(); +} + +// Riverpod 3.0ではこのようなことをする必要がないはず +// でもまだ https://github.com/rrousselGit/riverpod/issues/767 の機能がないことに加え、 +// https://github.com/rrousselGit/riverpod/issues/2383 のようなこともあるので、 +// UserInfoNotifierが直接accountContextにdependenciesを設定したり、引数のデフォルトにしたりすることが現状できない。 +@Riverpod(dependencies: [accountContext]) +Raw userInfoNotifierProxy( + UserInfoNotifierProxyRef ref, + String userId, +) { + return ref.read( + userInfoNotifierProvider( + userId: userId, + context: ref.read(accountContextProvider), + ).notifier, + ); +} + +@Riverpod(dependencies: [accountContext]) +AsyncValue userInfoProxy(UserInfoProxyRef ref, String userId) { + return ref.watch( + userInfoNotifierProvider( + userId: userId, + context: ref.read(accountContextProvider), + ), + ); +} + +@Riverpod() +class UserInfoNotifier extends _$UserInfoNotifier { + DialogStateNotifier get _dialog => + ref.read(dialogStateNotifierProvider.notifier); + + Misskey get _getMisskey => ref.read(misskeyProvider(context.getAccount)); + Misskey get _postMisskey => ref.read(misskeyProvider(context.postAccount)); + NoteRepository get _noteRepo => ref.read(notesProvider(context.getAccount)); + + @override + Future build({ + required String userId, + required AccountContext context, + }) async { + final localResponse = + await _getMisskey.users.show(UsersShowRequest(userId: userId)); + _noteRepo.registerAll(localResponse.pinnedNotes ?? []); + + final remoteHost = localResponse.host; + final localOnlyState = AsyncData( + UserInfo(userId: userId, response: localResponse), + ); + state = localOnlyState; + if (remoteHost == null) { + return localOnlyState.value; + } + + try { + final meta = + await ref.read(misskeyWithoutAccountProvider(remoteHost)).meta(); + final remoteResponse = await ref + .read(misskeyWithoutAccountProvider(remoteHost)) + .users + .showByName( + UsersShowByUserNameRequest(userName: localResponse.username), + ); + + await ref + .read( + emojiRepositoryProvider(Account.demoAccount(remoteHost, meta)), + ) + .loadFromSourceIfNeed(); + + ref + .read(notesProvider(Account.demoAccount(remoteHost, meta))) + .registerAll(remoteResponse.pinnedNotes ?? []); + + return UserInfo( + userId: userId, + response: localResponse, + remoteUserId: remoteResponse.id, + remoteResponse: remoteResponse, + metaResponse: meta, + ); + } catch (e) { + return localOnlyState.value; + } + } + + Future> updateMemo(String text) async { + return await _dialog.guard(() async { + await _postMisskey.users.updateMemo( + UsersUpdateMemoRequest(userId: userId, memo: text), + ); + + final before = await future; + state = AsyncData( + before.copyWith( + //TODO: こういう使い方するならAPIの結果をsealed classにしてあげたい + response: switch (before.response) { + UserDetailedNotMe(:final copyWith) => copyWith(memo: text), + UserDetailedNotMeWithRelations(:final copyWith) => + copyWith(memo: text), + MeDetailed(:final copyWith) => copyWith(memo: text), + UserDetailed() => before.response, + }, + ), + ); + }); + } + + Future> createFollow() async { + return await _dialog.guard(() async { + await _postMisskey.following + .create(FollowingCreateRequest(userId: userId)); + + final before = await future; + final response = before.response; + if (response is! UserDetailedNotMeWithRelations) { + return; + } + + final requiresFollowRequest = response.isLocked && !response.isFollowed; + state = AsyncData( + before.copyWith( + response: response.copyWith( + isFollowing: !requiresFollowRequest, + hasPendingFollowRequestFromYou: requiresFollowRequest, + ), + ), + ); + }); + } + + Future?> deleteFollow() async { + final confirm = await _dialog.showDialog( + message: (context) => S.of(context).confirmUnfollow, + actions: (context) => [S.of(context).deleteFollow, S.of(context).cancel], + ); + if (confirm == 1) return null; + + return await _dialog.guard(() async { + await _postMisskey.following + .delete(FollowingDeleteRequest(userId: userId)); + + final before = await future; + final response = before.response; + if (response is! UserDetailedNotMeWithRelations) { + return; + } + + state = AsyncData( + before.copyWith(response: response.copyWith(isFollowing: false)), + ); + }); + } + + Future> cancelFollowRequest() async { + return await _dialog.guard(() async { + await _postMisskey.following.requests + .cancel(FollowingRequestsCancelRequest(userId: userId)); + final before = await future; + final response = before.response; + if (response is! UserDetailedNotMeWithRelations) { + return; + } + + state = AsyncData( + before.copyWith( + response: response.copyWith(hasPendingFollowRequestFromYou: false), + ), + ); + }); + } + + /// ミュートする + Future?> createMute() async { + final expires = await ref.read(appRouterProvider).push( + const ExpireSelectRoute(), + ); + if (expires == null) return null; + final expiresDate = expires == Expire.indefinite + ? null + : DateTime.now().add(expires.expires!); + + return await _dialog.guard(() async { + await _postMisskey.mute.create( + MuteCreateRequest( + userId: userId, + expiresAt: expiresDate, + ), + ); + + final before = await future; + final response = before.response; + if (response is! UserDetailedNotMeWithRelations) { + return; + } + + state = AsyncData( + before.copyWith(response: response.copyWith(isMuted: true)), + ); + await ref.read(appRouterProvider).maybePop(); + }); + } + + /// ミュートを解除する + Future?> deleteMute() async { + return await _dialog.guard(() async { + await _postMisskey.mute.delete(MuteDeleteRequest(userId: userId)); + final before = await future; + final response = before.response; + if (response is! UserDetailedNotMeWithRelations) { + return; + } + + state = AsyncData( + before.copyWith(response: response.copyWith(isMuted: false)), + ); + await ref.read(appRouterProvider).maybePop(); + }); + } + + /// Renoteをミュートする + Future> createRenoteMute() async { + return await _dialog.guard(() async { + await _postMisskey.renoteMute.create( + RenoteMuteCreateRequest(userId: userId), + ); + final before = await future; + final response = before.response; + if (response is! UserDetailedNotMeWithRelations) { + return; + } + + state = AsyncData( + before.copyWith(response: response.copyWith(isRenoteMuted: true)), + ); + await ref.read(appRouterProvider).maybePop(); + }); + } + + /// Renoteのミュートを解除する + Future> deleteRenoteMute() async { + return await _dialog.guard(() async { + await _postMisskey.renoteMute.delete( + RenoteMuteDeleteRequest(userId: userId), + ); + final before = await future; + final response = before.response; + if (response is! UserDetailedNotMeWithRelations) { + return; + } + + state = AsyncData( + before.copyWith(response: response.copyWith(isRenoteMuted: false)), + ); + await ref.read(appRouterProvider).maybePop(); + }); + } + + /// ブロックする + Future?> createBlocking() async { + final confirm = await _dialog.showDialog( + message: (context) => S.of(context).confirmCreateBlock, + actions: (context) => [ + S.of(context).createBlock, + S.of(context).cancel, + ], + ); + if (confirm == 1) return null; + + return await _dialog.guard(() async { + await _postMisskey.blocking.create(BlockCreateRequest(userId: userId)); + + final before = await future; + final response = before.response; + if (response is! UserDetailedNotMeWithRelations) { + return; + } + + state = AsyncData( + before.copyWith(response: response.copyWith(isBlocking: true)), + ); + await ref.read(appRouterProvider).maybePop(); + }); + } + + /// ブロックを解除する + Future> deleteBlocking() async { + return await _dialog.guard(() async { + await _postMisskey.blocking.delete(BlockDeleteRequest(userId: userId)); + + final before = await future; + final response = before.response; + if (response is! UserDetailedNotMeWithRelations) { + return; + } + + state = AsyncData( + before.copyWith(response: response.copyWith(isBlocking: false)), + ); + await ref.read(appRouterProvider).maybePop(); + }); + } +} diff --git a/lib/view/user_page/user_info_notifier.freezed.dart b/lib/view/user_page/user_info_notifier.freezed.dart new file mode 100644 index 000000000..1011b65ae --- /dev/null +++ b/lib/view/user_page/user_info_notifier.freezed.dart @@ -0,0 +1,252 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'user_info_notifier.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +/// @nodoc +mixin _$UserInfo { + String get userId => throw _privateConstructorUsedError; + UserDetailed get response => throw _privateConstructorUsedError; + String? get remoteUserId => throw _privateConstructorUsedError; + UserDetailed? get remoteResponse => throw _privateConstructorUsedError; + MetaResponse? get metaResponse => throw _privateConstructorUsedError; + + /// Create a copy of UserInfo + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserInfoCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserInfoCopyWith<$Res> { + factory $UserInfoCopyWith(UserInfo value, $Res Function(UserInfo) then) = + _$UserInfoCopyWithImpl<$Res, UserInfo>; + @useResult + $Res call( + {String userId, + UserDetailed response, + String? remoteUserId, + UserDetailed? remoteResponse, + MetaResponse? metaResponse}); + + $MetaResponseCopyWith<$Res>? get metaResponse; +} + +/// @nodoc +class _$UserInfoCopyWithImpl<$Res, $Val extends UserInfo> + implements $UserInfoCopyWith<$Res> { + _$UserInfoCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UserInfo + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? userId = null, + Object? response = null, + Object? remoteUserId = freezed, + Object? remoteResponse = freezed, + Object? metaResponse = freezed, + }) { + return _then(_value.copyWith( + userId: null == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as String, + response: null == response + ? _value.response + : response // ignore: cast_nullable_to_non_nullable + as UserDetailed, + remoteUserId: freezed == remoteUserId + ? _value.remoteUserId + : remoteUserId // ignore: cast_nullable_to_non_nullable + as String?, + remoteResponse: freezed == remoteResponse + ? _value.remoteResponse + : remoteResponse // ignore: cast_nullable_to_non_nullable + as UserDetailed?, + metaResponse: freezed == metaResponse + ? _value.metaResponse + : metaResponse // ignore: cast_nullable_to_non_nullable + as MetaResponse?, + ) as $Val); + } + + /// Create a copy of UserInfo + /// with the given fields replaced by the non-null parameter values. + @override + @pragma('vm:prefer-inline') + $MetaResponseCopyWith<$Res>? get metaResponse { + if (_value.metaResponse == null) { + return null; + } + + return $MetaResponseCopyWith<$Res>(_value.metaResponse!, (value) { + return _then(_value.copyWith(metaResponse: value) as $Val); + }); + } +} + +/// @nodoc +abstract class _$$UserInfoImplCopyWith<$Res> + implements $UserInfoCopyWith<$Res> { + factory _$$UserInfoImplCopyWith( + _$UserInfoImpl value, $Res Function(_$UserInfoImpl) then) = + __$$UserInfoImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String userId, + UserDetailed response, + String? remoteUserId, + UserDetailed? remoteResponse, + MetaResponse? metaResponse}); + + @override + $MetaResponseCopyWith<$Res>? get metaResponse; +} + +/// @nodoc +class __$$UserInfoImplCopyWithImpl<$Res> + extends _$UserInfoCopyWithImpl<$Res, _$UserInfoImpl> + implements _$$UserInfoImplCopyWith<$Res> { + __$$UserInfoImplCopyWithImpl( + _$UserInfoImpl _value, $Res Function(_$UserInfoImpl) _then) + : super(_value, _then); + + /// Create a copy of UserInfo + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? userId = null, + Object? response = null, + Object? remoteUserId = freezed, + Object? remoteResponse = freezed, + Object? metaResponse = freezed, + }) { + return _then(_$UserInfoImpl( + userId: null == userId + ? _value.userId + : userId // ignore: cast_nullable_to_non_nullable + as String, + response: null == response + ? _value.response + : response // ignore: cast_nullable_to_non_nullable + as UserDetailed, + remoteUserId: freezed == remoteUserId + ? _value.remoteUserId + : remoteUserId // ignore: cast_nullable_to_non_nullable + as String?, + remoteResponse: freezed == remoteResponse + ? _value.remoteResponse + : remoteResponse // ignore: cast_nullable_to_non_nullable + as UserDetailed?, + metaResponse: freezed == metaResponse + ? _value.metaResponse + : metaResponse // ignore: cast_nullable_to_non_nullable + as MetaResponse?, + )); + } +} + +/// @nodoc + +class _$UserInfoImpl extends _UserInfo { + const _$UserInfoImpl( + {required this.userId, + required this.response, + this.remoteUserId, + this.remoteResponse, + this.metaResponse}) + : super._(); + + @override + final String userId; + @override + final UserDetailed response; + @override + final String? remoteUserId; + @override + final UserDetailed? remoteResponse; + @override + final MetaResponse? metaResponse; + + @override + String toString() { + return 'UserInfo(userId: $userId, response: $response, remoteUserId: $remoteUserId, remoteResponse: $remoteResponse, metaResponse: $metaResponse)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserInfoImpl && + (identical(other.userId, userId) || other.userId == userId) && + (identical(other.response, response) || + other.response == response) && + (identical(other.remoteUserId, remoteUserId) || + other.remoteUserId == remoteUserId) && + (identical(other.remoteResponse, remoteResponse) || + other.remoteResponse == remoteResponse) && + (identical(other.metaResponse, metaResponse) || + other.metaResponse == metaResponse)); + } + + @override + int get hashCode => Object.hash(runtimeType, userId, response, remoteUserId, + remoteResponse, metaResponse); + + /// Create a copy of UserInfo + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserInfoImplCopyWith<_$UserInfoImpl> get copyWith => + __$$UserInfoImplCopyWithImpl<_$UserInfoImpl>(this, _$identity); +} + +abstract class _UserInfo extends UserInfo { + const factory _UserInfo( + {required final String userId, + required final UserDetailed response, + final String? remoteUserId, + final UserDetailed? remoteResponse, + final MetaResponse? metaResponse}) = _$UserInfoImpl; + const _UserInfo._() : super._(); + + @override + String get userId; + @override + UserDetailed get response; + @override + String? get remoteUserId; + @override + UserDetailed? get remoteResponse; + @override + MetaResponse? get metaResponse; + + /// Create a copy of UserInfo + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserInfoImplCopyWith<_$UserInfoImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/view/user_page/user_info_notifier.g.dart b/lib/view/user_page/user_info_notifier.g.dart new file mode 100644 index 000000000..796682d30 --- /dev/null +++ b/lib/view/user_page/user_info_notifier.g.dart @@ -0,0 +1,604 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'user_info_notifier.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$userInfoNotifierProxyHash() => + r'45c3a25fbd146b289be6e8929752418ed1adf59a'; + +/// Copied from Dart SDK +class _SystemHash { + _SystemHash._(); + + static int combine(int hash, int value) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + value); + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); + return hash ^ (hash >> 6); + } + + static int finish(int hash) { + // ignore: parameter_assignments + hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); + // ignore: parameter_assignments + hash = hash ^ (hash >> 11); + return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); + } +} + +/// See also [userInfoNotifierProxy]. +@ProviderFor(userInfoNotifierProxy) +const userInfoNotifierProxyProvider = UserInfoNotifierProxyFamily(); + +/// See also [userInfoNotifierProxy]. +class UserInfoNotifierProxyFamily extends Family { + /// See also [userInfoNotifierProxy]. + const UserInfoNotifierProxyFamily(); + + static final Iterable _dependencies = [ + accountContextProvider + ]; + + static final Iterable _allTransitiveDependencies = + { + accountContextProvider, + ...?accountContextProvider.allTransitiveDependencies + }; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'userInfoNotifierProxyProvider'; + + /// See also [userInfoNotifierProxy]. + UserInfoNotifierProxyProvider call( + String userId, + ) { + return UserInfoNotifierProxyProvider( + userId, + ); + } + + @visibleForOverriding + @override + UserInfoNotifierProxyProvider getProviderOverride( + covariant UserInfoNotifierProxyProvider provider, + ) { + return call( + provider.userId, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith( + Raw Function(UserInfoNotifierProxyRef ref) create) { + return _$UserInfoNotifierProxyFamilyOverride(this, create); + } +} + +class _$UserInfoNotifierProxyFamilyOverride implements FamilyOverride { + _$UserInfoNotifierProxyFamilyOverride(this.overriddenFamily, this.create); + + final Raw Function(UserInfoNotifierProxyRef ref) create; + + @override + final UserInfoNotifierProxyFamily overriddenFamily; + + @override + UserInfoNotifierProxyProvider getProviderOverride( + covariant UserInfoNotifierProxyProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [userInfoNotifierProxy]. +class UserInfoNotifierProxyProvider + extends AutoDisposeProvider> { + /// See also [userInfoNotifierProxy]. + UserInfoNotifierProxyProvider( + String userId, + ) : this._internal( + (ref) => userInfoNotifierProxy( + ref as UserInfoNotifierProxyRef, + userId, + ), + from: userInfoNotifierProxyProvider, + name: r'userInfoNotifierProxyProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$userInfoNotifierProxyHash, + dependencies: UserInfoNotifierProxyFamily._dependencies, + allTransitiveDependencies: + UserInfoNotifierProxyFamily._allTransitiveDependencies, + userId: userId, + ); + + UserInfoNotifierProxyProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.userId, + }) : super.internal(); + + final String userId; + + @override + Override overrideWith( + Raw Function(UserInfoNotifierProxyRef ref) create, + ) { + return ProviderOverride( + origin: this, + override: UserInfoNotifierProxyProvider._internal( + (ref) => create(ref as UserInfoNotifierProxyRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + userId: userId, + ), + ); + } + + @override + (String,) get argument { + return (userId,); + } + + @override + AutoDisposeProviderElement> createElement() { + return _UserInfoNotifierProxyProviderElement(this); + } + + UserInfoNotifierProxyProvider _copyWith( + Raw Function(UserInfoNotifierProxyRef ref) create, + ) { + return UserInfoNotifierProxyProvider._internal( + (ref) => create(ref as UserInfoNotifierProxyRef), + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + userId: userId, + ); + } + + @override + bool operator ==(Object other) { + return other is UserInfoNotifierProxyProvider && other.userId == userId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, userId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin UserInfoNotifierProxyRef + on AutoDisposeProviderRef> { + /// The parameter `userId` of this provider. + String get userId; +} + +class _UserInfoNotifierProxyProviderElement + extends AutoDisposeProviderElement> + with UserInfoNotifierProxyRef { + _UserInfoNotifierProxyProviderElement(super.provider); + + @override + String get userId => (origin as UserInfoNotifierProxyProvider).userId; +} + +String _$userInfoProxyHash() => r'f05bda4353cb2f3c6459b1548021ad68fb66bd5e'; + +/// See also [userInfoProxy]. +@ProviderFor(userInfoProxy) +const userInfoProxyProvider = UserInfoProxyFamily(); + +/// See also [userInfoProxy]. +class UserInfoProxyFamily extends Family { + /// See also [userInfoProxy]. + const UserInfoProxyFamily(); + + static final Iterable _dependencies = [ + accountContextProvider + ]; + + static final Iterable _allTransitiveDependencies = + { + accountContextProvider, + ...?accountContextProvider.allTransitiveDependencies + }; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'userInfoProxyProvider'; + + /// See also [userInfoProxy]. + UserInfoProxyProvider call( + String userId, + ) { + return UserInfoProxyProvider( + userId, + ); + } + + @visibleForOverriding + @override + UserInfoProxyProvider getProviderOverride( + covariant UserInfoProxyProvider provider, + ) { + return call( + provider.userId, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith( + AsyncValue Function(UserInfoProxyRef ref) create) { + return _$UserInfoProxyFamilyOverride(this, create); + } +} + +class _$UserInfoProxyFamilyOverride implements FamilyOverride { + _$UserInfoProxyFamilyOverride(this.overriddenFamily, this.create); + + final AsyncValue Function(UserInfoProxyRef ref) create; + + @override + final UserInfoProxyFamily overriddenFamily; + + @override + UserInfoProxyProvider getProviderOverride( + covariant UserInfoProxyProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [userInfoProxy]. +class UserInfoProxyProvider extends AutoDisposeProvider> { + /// See also [userInfoProxy]. + UserInfoProxyProvider( + String userId, + ) : this._internal( + (ref) => userInfoProxy( + ref as UserInfoProxyRef, + userId, + ), + from: userInfoProxyProvider, + name: r'userInfoProxyProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$userInfoProxyHash, + dependencies: UserInfoProxyFamily._dependencies, + allTransitiveDependencies: + UserInfoProxyFamily._allTransitiveDependencies, + userId: userId, + ); + + UserInfoProxyProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.userId, + }) : super.internal(); + + final String userId; + + @override + Override overrideWith( + AsyncValue Function(UserInfoProxyRef ref) create, + ) { + return ProviderOverride( + origin: this, + override: UserInfoProxyProvider._internal( + (ref) => create(ref as UserInfoProxyRef), + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + userId: userId, + ), + ); + } + + @override + (String,) get argument { + return (userId,); + } + + @override + AutoDisposeProviderElement> createElement() { + return _UserInfoProxyProviderElement(this); + } + + UserInfoProxyProvider _copyWith( + AsyncValue Function(UserInfoProxyRef ref) create, + ) { + return UserInfoProxyProvider._internal( + (ref) => create(ref as UserInfoProxyRef), + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + userId: userId, + ); + } + + @override + bool operator ==(Object other) { + return other is UserInfoProxyProvider && other.userId == userId; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, userId.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin UserInfoProxyRef on AutoDisposeProviderRef> { + /// The parameter `userId` of this provider. + String get userId; +} + +class _UserInfoProxyProviderElement + extends AutoDisposeProviderElement> + with UserInfoProxyRef { + _UserInfoProxyProviderElement(super.provider); + + @override + String get userId => (origin as UserInfoProxyProvider).userId; +} + +String _$userInfoNotifierHash() => r'cd97bc5433d147b8881446c0431fa623f0d2319e'; + +abstract class _$UserInfoNotifier + extends BuildlessAutoDisposeAsyncNotifier { + late final String userId; + late final AccountContext context; + + FutureOr build({ + required String userId, + required AccountContext context, + }); +} + +/// See also [UserInfoNotifier]. +@ProviderFor(UserInfoNotifier) +const userInfoNotifierProvider = UserInfoNotifierFamily(); + +/// See also [UserInfoNotifier]. +class UserInfoNotifierFamily extends Family { + /// See also [UserInfoNotifier]. + const UserInfoNotifierFamily(); + + static const Iterable? _dependencies = null; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'userInfoNotifierProvider'; + + /// See also [UserInfoNotifier]. + UserInfoNotifierProvider call({ + required String userId, + required AccountContext context, + }) { + return UserInfoNotifierProvider( + userId: userId, + context: context, + ); + } + + @visibleForOverriding + @override + UserInfoNotifierProvider getProviderOverride( + covariant UserInfoNotifierProvider provider, + ) { + return call( + userId: provider.userId, + context: provider.context, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(UserInfoNotifier Function() create) { + return _$UserInfoNotifierFamilyOverride(this, create); + } +} + +class _$UserInfoNotifierFamilyOverride implements FamilyOverride { + _$UserInfoNotifierFamilyOverride(this.overriddenFamily, this.create); + + final UserInfoNotifier Function() create; + + @override + final UserInfoNotifierFamily overriddenFamily; + + @override + UserInfoNotifierProvider getProviderOverride( + covariant UserInfoNotifierProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [UserInfoNotifier]. +class UserInfoNotifierProvider + extends AutoDisposeAsyncNotifierProviderImpl { + /// See also [UserInfoNotifier]. + UserInfoNotifierProvider({ + required String userId, + required AccountContext context, + }) : this._internal( + () => UserInfoNotifier() + ..userId = userId + ..context = context, + from: userInfoNotifierProvider, + name: r'userInfoNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$userInfoNotifierHash, + dependencies: UserInfoNotifierFamily._dependencies, + allTransitiveDependencies: + UserInfoNotifierFamily._allTransitiveDependencies, + userId: userId, + context: context, + ); + + UserInfoNotifierProvider._internal( + super.create, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.userId, + required this.context, + }) : super.internal(); + + final String userId; + final AccountContext context; + + @override + FutureOr runNotifierBuild( + covariant UserInfoNotifier notifier, + ) { + return notifier.build( + userId: userId, + context: context, + ); + } + + @override + Override overrideWith(UserInfoNotifier Function() create) { + return ProviderOverride( + origin: this, + override: UserInfoNotifierProvider._internal( + () => create() + ..userId = userId + ..context = context, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + userId: userId, + context: context, + ), + ); + } + + @override + ({ + String userId, + AccountContext context, + }) get argument { + return ( + userId: userId, + context: context, + ); + } + + @override + AutoDisposeAsyncNotifierProviderElement + createElement() { + return _UserInfoNotifierProviderElement(this); + } + + UserInfoNotifierProvider _copyWith( + UserInfoNotifier Function() create, + ) { + return UserInfoNotifierProvider._internal( + () => create() + ..userId = userId + ..context = context, + name: name, + dependencies: dependencies, + allTransitiveDependencies: allTransitiveDependencies, + debugGetCreateSourceHash: debugGetCreateSourceHash, + from: from, + userId: userId, + context: context, + ); + } + + @override + bool operator ==(Object other) { + return other is UserInfoNotifierProvider && + other.userId == userId && + other.context == context; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, userId.hashCode); + hash = _SystemHash.combine(hash, context.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin UserInfoNotifierRef on AutoDisposeAsyncNotifierProviderRef { + /// The parameter `userId` of this provider. + String get userId; + + /// The parameter `context` of this provider. + AccountContext get context; +} + +class _UserInfoNotifierProviderElement + extends AutoDisposeAsyncNotifierProviderElement + with UserInfoNotifierRef { + _UserInfoNotifierProviderElement(super.provider); + + @override + String get userId => (origin as UserInfoNotifierProvider).userId; + @override + AccountContext get context => (origin as UserInfoNotifierProvider).context; +} +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/user_page/user_list_item.dart b/lib/view/user_page/user_list_item.dart index 6b9d0dd98..d5d9b9f40 100644 --- a/lib/view/user_page/user_list_item.dart +++ b/lib/view/user_page/user_list_item.dart @@ -1,12 +1,12 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/avatar_icon.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/avatar_icon.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:misskey_dart/misskey_dart.dart"; class UserListItem extends ConsumerWidget { final User user; @@ -15,8 +15,8 @@ class UserListItem extends ConsumerWidget { final void Function()? onTap; const UserListItem({ - super.key, required this.user, + super.key, this.onTap, this.isDetail = false, }); @@ -25,8 +25,12 @@ class UserListItem extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { return InkWell( onTap: onTap ?? - () => context.pushRoute( - UserRoute(userId: user.id, account: AccountScope.of(context))), + () async => context.pushRoute( + UserRoute( + userId: user.id, + accountContext: ref.read(accountContextProvider), + ), + ), child: Padding( padding: const EdgeInsets.all(10), child: Row( @@ -89,11 +93,11 @@ class UserListItem extends ConsumerWidget { mfmText: (user as UserDetailed).description ?? "", emoji: user.emojis, maxLines: 5, - ) + ), ], ), ), - ) + ), ], ), ), diff --git a/lib/view/user_page/user_misskey_page.dart b/lib/view/user_page/user_misskey_page.dart index 0fe632a72..df79f9d85 100644 --- a/lib/view/user_page/user_misskey_page.dart +++ b/lib/view/user_page/user_misskey_page.dart @@ -1,43 +1,50 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/src/widgets/framework.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:url_launcher/url_launcher.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; class UserMisskeyPage extends ConsumerWidget { final String userId; - const UserMisskeyPage({super.key, required this.userId}); + const UserMisskeyPage({required this.userId, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return PushableListView( initializeFuture: () async { final response = await ref - .read(misskeyProvider(AccountScope.of(context))) + .read(misskeyGetContextProvider) .users .pages(UsersPagesRequest(userId: userId)); return response.toList(); }, nextFuture: (item, _) async { final response = await ref - .read(misskeyProvider(AccountScope.of(context))) + .read(misskeyGetContextProvider) .users .pages(UsersPagesRequest(userId: userId, untilId: item.id)); return response.toList(); }, itemBuilder: (context, page) { return ListTile( - title: Text(page.title), - onTap: () { - context.pushRoute(MisskeyRouteRoute( - account: AccountScope.of(context), page: page)); - }, + title: MfmText( + mfmText: page.title, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.bold), + ), + subtitle: MfmText(mfmText: page.summary ?? ""), + onTap: () async => context.pushRoute( + MisskeyRouteRoute( + accountContext: ref.read(accountContextProvider), + page: page, + ), + ), ); }, ); diff --git a/lib/view/user_page/user_notes.dart b/lib/view/user_page/user_notes.dart index a66aaf10c..1ae064318 100644 --- a/lib/view/user_page/user_notes.dart +++ b/lib/view/user_page/user_notes.dart @@ -1,46 +1,32 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:miria/view/user_page/user_page.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/date_time_picker.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:miria/view/user_page/user_info_notifier.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class UserNotes extends ConsumerStatefulWidget { +class UserNotes extends HookConsumerWidget { final String userId; final String? remoteUserId; - final Account? actualAccount; const UserNotes({ - super.key, required this.userId, + super.key, this.remoteUserId, - this.actualAccount, - }) : assert((remoteUserId == null) == (actualAccount == null)); + }); @override - ConsumerState createState() => UserNotesState(); -} - -class UserNotesState extends ConsumerState { - Misskey get misskey => ref.read(misskeyProvider(AccountScope.of(context))); - - bool isFileOnly = false; - bool withReply = false; - bool renote = true; - bool highlight = false; - DateTime? untilDate; + Widget build(BuildContext context, WidgetRef ref) { + final isFileOnly = useState(false); + final withReply = useState(false); + final renote = useState(true); + final highlight = useState(false); + final untilDate = useState(null); - @override - void didChangeDependencies() { - super.didChangeDependencies(); - } - - @override - Widget build(BuildContext context) { return Column( children: [ Padding( @@ -52,32 +38,35 @@ class UserNotesState extends ConsumerState { child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: ToggleButtons( - isSelected: [withReply, isFileOnly, renote, highlight], + isSelected: [ + withReply.value, + isFileOnly.value, + renote.value, + highlight.value, + ], onPressed: (value) { - setState(() { - switch (value) { - case 0: - withReply = !withReply; - if (withReply) { - isFileOnly = false; - } - highlight = false; - case 1: - isFileOnly = !isFileOnly; - if (isFileOnly) { - withReply = false; - } - highlight = false; - case 2: - renote = !renote; - highlight = false; - case 3: - withReply = false; - isFileOnly = false; - renote = false; - highlight = true; - } - }); + switch (value) { + case 0: + withReply.value = !withReply.value; + if (withReply.value) { + isFileOnly.value = false; + } + highlight.value = false; + case 1: + isFileOnly.value = !isFileOnly.value; + if (isFileOnly.value) { + withReply.value = false; + } + highlight.value = false; + case 2: + renote.value = !renote.value; + highlight.value = false; + case 3: + withReply.value = false; + isFileOnly.value = false; + renote.value = false; + highlight.value = true; + } }, children: [ Padding( @@ -103,20 +92,24 @@ class UserNotesState extends ConsumerState { ), IconButton( onPressed: () async { - final userInfo = ref.read(userInfoProvider(widget.userId)); - final firstDate = widget.actualAccount == null - ? userInfo?.response?.createdAt - : userInfo?.remoteResponse?.createdAt; + final userInfo = ref.read( + userInfoProxyProvider(userId) + .select((value) => value.requireValue), + ); + final firstDate = ref.read(accountContextProvider).isSame + ? userInfo.response.createdAt + : userInfo.remoteResponse?.createdAt; - final result = await showDatePicker( + final result = await showDateTimePicker( context: context, - initialDate: untilDate ?? DateTime.now(), - helpText: S.of(context).showNotesBeforeThisDate, + initialDate: untilDate.value ?? DateTime.now(), firstDate: firstDate ?? DateTime.now(), lastDate: DateTime.now(), + datePickerHelpText: S.of(context).showNotesBeforeThisDate, + timePickerHelpText: S.of(context).showNotesBeforeThisTime, ); if (result != null) { - untilDate = DateTime( + untilDate.value = DateTime( result.year, result.month, result.day, @@ -126,7 +119,6 @@ class UserNotesState extends ConsumerState { 999, ); } - setState(() {}); }, icon: const Icon(Icons.date_range), ), @@ -136,73 +128,80 @@ class UserNotesState extends ConsumerState { Expanded( child: PushableListView( listKey: Object.hashAll( - [isFileOnly, withReply, renote, untilDate, highlight]), - additionalErrorInfo: highlight + [ + isFileOnly.value, + withReply.value, + renote.value, + untilDate.value, + highlight.value, + ], + ), + additionalErrorInfo: highlight.value ? (context, e) => Text(S.of(context).userHighlightAvailability) : null, initializeFuture: () async { final Iterable notes; - if (highlight) { - notes = await misskey.users.featuredNotes( - UsersFeaturedNotesRequest( - userId: widget.remoteUserId ?? widget.userId), - ); + if (highlight.value) { + notes = await ref + .read(misskeyGetContextProvider) + .users + .featuredNotes( + UsersFeaturedNotesRequest( + userId: remoteUserId ?? userId, + ), + ); } else { - notes = await misskey.users.notes( - UsersNotesRequest( - userId: widget.remoteUserId ?? widget.userId, - withFiles: isFileOnly, - // 後方互換性のため - includeReplies: withReply, - includeMyRenotes: renote, - withReplies: withReply, - withRenotes: renote, - withChannelNotes: true, - untilDate: untilDate, - ), - ); + notes = await ref.read(misskeyGetContextProvider).users.notes( + UsersNotesRequest( + userId: remoteUserId ?? userId, + withFiles: isFileOnly.value, + // 後方互換性のため + includeReplies: withReply.value, + includeMyRenotes: renote.value, + withReplies: withReply.value, + withRenotes: renote.value, + withChannelNotes: true, + untilDate: untilDate.value, + ), + ); } - if (!mounted) return []; - ref - .read(notesProvider(AccountScope.of(context))) - .registerAll(notes); + if (!context.mounted) return []; + ref.read(notesWithProvider).registerAll(notes); return notes.toList(); }, nextFuture: (lastElement, _) async { final Iterable notes; - if (highlight) { - notes = await misskey.users.featuredNotes( - UsersFeaturedNotesRequest( - userId: widget.remoteUserId ?? widget.userId, - untilId: lastElement.id, - ), - ); + if (highlight.value) { + notes = await ref + .read(misskeyGetContextProvider) + .users + .featuredNotes( + UsersFeaturedNotesRequest( + userId: remoteUserId ?? userId, + untilId: lastElement.id, + ), + ); } else { - notes = await misskey.users.notes( - UsersNotesRequest( - userId: widget.remoteUserId ?? widget.userId, - untilId: lastElement.id, - withFiles: isFileOnly, - includeReplies: withReply, - includeMyRenotes: renote, - withReplies: withReply, - withRenotes: renote, - withChannelNotes: true, - untilDate: untilDate, - ), - ); + notes = await ref.read(misskeyGetContextProvider).users.notes( + UsersNotesRequest( + userId: remoteUserId ?? userId, + untilId: lastElement.id, + withFiles: isFileOnly.value, + includeReplies: withReply.value, + includeMyRenotes: renote.value, + withReplies: withReply.value, + withRenotes: renote.value, + withChannelNotes: true, + untilDate: untilDate.value, + ), + ); } - if (!mounted) return []; - ref - .read(notesProvider(AccountScope.of(context))) - .registerAll(notes); + if (!context.mounted) return []; + ref.read(notesWithProvider).registerAll(notes); return notes.toList(); }, itemBuilder: (context, element) { - return MisskeyNote( - note: element, - loginAs: widget.actualAccount, - ); + return MisskeyNote(note: element); }, ), ), diff --git a/lib/view/user_page/user_page.dart b/lib/view/user_page/user_page.dart index b708de1c8..714bc60f8 100644 --- a/lib/view/user_page/user_page.dart +++ b/lib/view/user_page/user_page.dart @@ -1,277 +1,197 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/error_detail.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:miria/view/user_page/user_clips.dart'; -import 'package:miria/view/user_page/user_detail.dart'; -import 'package:miria/view/user_page/user_misskey_page.dart'; -import 'package:miria/view/user_page/user_notes.dart'; -import 'package:miria/view/user_page/user_plays.dart'; -import 'package:miria/view/user_page/user_reactions.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:miria/view/user_page/user_clips.dart"; +import "package:miria/view/user_page/user_detail.dart"; +import "package:miria/view/user_page/user_info_notifier.dart"; +import "package:miria/view/user_page/user_misskey_page.dart"; +import "package:miria/view/user_page/user_notes.dart"; +import "package:miria/view/user_page/user_plays.dart"; +import "package:miria/view/user_page/user_reactions.dart"; -class UserInfo { +@RoutePage() +class UserPage extends HookConsumerWidget implements AutoRouteWrapper { final String userId; - final UserDetailed? response; - final String? remoteUserId; - final UserDetailed? remoteResponse; - final MetaResponse? metaResponse; - - const UserInfo({ + final AccountContext accountContext; + const UserPage({ required this.userId, - required this.response, - required this.remoteUserId, - required this.remoteResponse, - required this.metaResponse, + required this.accountContext, + super.key, }); -} - -final userInfoProvider = StateProvider.family.autoDispose(( - ref, - userId, -) => - null); - -@RoutePage() -class UserPage extends ConsumerStatefulWidget { - final String userId; - final Account account; - const UserPage({super.key, required this.userId, required this.account}); @override - ConsumerState createState() => UserPageState(); -} + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); -class UserPageState extends ConsumerState { @override - Widget build(BuildContext context) { - final userInfo = ref.watch(userInfoProvider(widget.userId)); - final isReactionAvailable = userInfo?.response?.publicReactions == true || - (userInfo?.response?.host == null && - userInfo?.response?.username == widget.account.userId); + Widget build(BuildContext context, WidgetRef ref) { + final userInfo = ref.watch(userInfoProxyProvider(userId)).valueOrNull; + + final isReactionAvailable = userInfo?.response.publicReactions == true || + (userInfo?.response.host == null && + userInfo?.response.username == accountContext.postAccount.userId); final isRemoteUser = - userInfo?.response?.host != null && userInfo?.remoteResponse != null; - return AccountScope( - account: widget.account, - child: DefaultTabController( - length: 5 + (isReactionAvailable ? 1 : 0) + (isRemoteUser ? 2 : 0), - child: Scaffold( - appBar: AppBar( - title: SimpleMfmText( - userInfo?.response?.name ?? userInfo?.response?.username ?? "", - emojis: userInfo?.response?.emojis ?? {}, - ), - actions: const [], - bottom: TabBar( - tabs: [ - if (!isRemoteUser) ...[ - Tab(text: S.of(context).userInfomation), - Tab(text: S.of(context).userNotes), - ] else ...[ - Tab(text: S.of(context).userInfomationLocal), - Tab(text: S.of(context).userInfomationRemote), - Tab(text: S.of(context).userNotesLocal), - Tab(text: S.of(context).userNotesRemote), - ], - Tab(text: S.of(context).clip), - if (isReactionAvailable) Tab(text: S.of(context).userReactions), - Tab(text: S.of(context).userPages), - Tab(text: S.of(context).userPlays), + userInfo?.response.host != null && userInfo?.remoteResponse != null; + + return DefaultTabController( + length: 5 + (isReactionAvailable ? 1 : 0) + (isRemoteUser ? 2 : 0), + child: Scaffold( + appBar: AppBar( + title: SimpleMfmText( + userInfo?.response.name ?? userInfo?.response.username ?? "", + emojis: userInfo?.response.emojis ?? {}, + ), + actions: const [], + bottom: TabBar( + tabs: [ + if (!isRemoteUser) ...[ + Tab(text: S.of(context).userInfomation), + Tab(text: S.of(context).userNotes), + ] else ...[ + Tab(text: S.of(context).userInfomationLocal), + Tab(text: S.of(context).userInfomationRemote), + Tab(text: S.of(context).userNotesLocal), + Tab(text: S.of(context).userNotesRemote), ], - isScrollable: true, - tabAlignment: TabAlignment.center, - ), + Tab(text: S.of(context).clip), + if (isReactionAvailable) Tab(text: S.of(context).userReactions), + Tab(text: S.of(context).userPages), + Tab(text: S.of(context).userPlays), + ], + isScrollable: true, + tabAlignment: TabAlignment.center, ), - body: Column( - children: [ - Expanded( - child: TabBarView( - children: [ - UserDetailTab(userId: widget.userId), - if (isRemoteUser) - AccountScope( - account: Account.demoAccount( - userInfo!.response!.host!, userInfo.metaResponse!), - child: UserDetail( - response: userInfo.remoteResponse!, - account: Account.demoAccount( - userInfo.response!.host!, - userInfo.metaResponse!), - controlAccount: widget.account, + ), + body: Column( + children: [ + Expanded( + child: TabBarView( + children: [ + UserDetailTab(userId: userId), + if (isRemoteUser) + AccountContextScope( + context: AccountContext( + getAccount: Account.demoAccount( + userInfo!.response.host!, + userInfo.metaResponse, ), + postAccount: + ref.read(accountContextProvider).postAccount, ), - Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: UserNotes( - userId: widget.userId, - ), + child: UserDetail(response: userInfo.remoteResponse!), + ), + Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: UserNotes( + userId: userId, ), - if (isRemoteUser) - AccountScope( - account: Account.demoAccount( - userInfo!.response!.host!, userInfo.metaResponse!), - child: Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: UserNotes( - userId: widget.userId, - remoteUserId: userInfo.remoteResponse!.id, - actualAccount: widget.account, - ), + ), + if (isRemoteUser) + AccountContextScope( + context: AccountContext( + getAccount: Account.demoAccount( + userInfo!.response.host!, + userInfo.metaResponse, + ), + postAccount: + ref.read(accountContextProvider).postAccount, + ), + child: Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: UserNotes( + userId: userId, + remoteUserId: userInfo.remoteResponse!.id, ), ), + ), + Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: UserClips(userId: userId), + ), + if (isReactionAvailable) Padding( padding: const EdgeInsets.only(left: 10, right: 10), - child: UserClips( - userId: widget.userId, - ), + child: UserReactions(userId: userId), ), - if (isReactionAvailable) - Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: UserReactions(userId: widget.userId), - ), - // ページ - if (isRemoteUser) - AccountScope( - account: Account.demoAccount( - userInfo!.response!.host!, userInfo.metaResponse!), - child: Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: UserMisskeyPage( - userId: userInfo.remoteResponse!.id), + // ページ + if (isRemoteUser) + AccountContextScope( + context: AccountContext( + getAccount: Account.demoAccount( + userInfo!.response.host!, + userInfo.metaResponse, + ), + postAccount: + ref.read(accountContextProvider).postAccount, + ), + child: Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: UserMisskeyPage( + userId: userInfo.remoteResponse!.id, ), - ) - else - Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: UserMisskeyPage(userId: widget.userId)), + ), + ) + else + Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: UserMisskeyPage(userId: userId), + ), - // Play - if (isRemoteUser) - AccountScope( - account: Account.demoAccount( - userInfo!.response!.host!, userInfo.metaResponse!), - child: Padding( - padding: const EdgeInsets.only(left: 10, right: 10), - child: UserPlays(userId: userInfo.remoteResponse!.id), + // Play + if (isRemoteUser) + AccountContextScope( + context: AccountContext( + getAccount: Account.demoAccount( + userInfo!.response.host!, + userInfo.metaResponse, ), - ) - else - Padding( + postAccount: + ref.read(accountContextProvider).postAccount, + ), + child: Padding( padding: const EdgeInsets.only(left: 10, right: 10), - child: UserPlays(userId: widget.userId), + child: UserPlays(userId: userInfo.remoteResponse!.id), ), - ], - ), + ) + else + Padding( + padding: const EdgeInsets.only(left: 10, right: 10), + child: UserPlays(userId: userId), + ), + ], ), - ], - ), + ), + ], ), ), ); } } -class UserDetailTab extends ConsumerStatefulWidget { +class UserDetailTab extends ConsumerWidget { final String userId; - const UserDetailTab({super.key, required this.userId}); - - @override - ConsumerState createState() => UserDetailTabState(); -} - -class UserDetailTabState extends ConsumerState { - UserDetailed? response; - UserDetailed? remoteResponse; - (Object?, StackTrace)? error; - - @override - void initState() { - super.initState(); - } - - @override - void didChangeDependencies() { - super.didChangeDependencies(); - Future(() async { - try { - final account = AccountScope.of(context); - response = await ref - .read(misskeyProvider(AccountScope.of(context))) - .users - .show(UsersShowRequest(userId: widget.userId)); - ref - .read(notesProvider(account)) - .registerAll(response?.pinnedNotes ?? []); - ref.read(userInfoProvider(widget.userId).notifier).state = UserInfo( - userId: widget.userId, - response: response, - remoteUserId: null, - remoteResponse: null, - metaResponse: null, - ); - - final remoteHost = response?.host; - if (remoteHost != null) { - final meta = - await ref.read(misskeyWithoutAccountProvider(remoteHost)).meta(); - final remoteResponse = await ref - .read(misskeyProvider(Account.demoAccount(remoteHost, meta))) - .users - .showByName( - UsersShowByUserNameRequest(userName: response!.username), - ); - - await ref - .read(emojiRepositoryProvider( - Account.demoAccount(remoteHost, meta))) - .loadFromSourceIfNeed(); - - ref - .read(notesProvider(Account.demoAccount(remoteHost, meta))) - .registerAll(remoteResponse.pinnedNotes ?? []); - ref.read(userInfoProvider(widget.userId).notifier).state = UserInfo( - userId: widget.userId, - response: response, - remoteUserId: remoteResponse.id, - remoteResponse: remoteResponse, - metaResponse: meta, - ); - } - } catch (e, s) { - if (!mounted) return; - setState(() { - error = (e, s); - }); - } - }); - } + const UserDetailTab({required this.userId, super.key}); @override - Widget build(BuildContext context) { - if (response != null) { - return UserDetail( - response: response!, - account: AccountScope.of(context), - controlAccount: null, - ); - } - if (error != null) { - return ErrorDetail( - error: error?.$1, - stackTrace: error?.$2, - ); - } - - return const Center( - child: CircularProgressIndicator(), - ); + Widget build(BuildContext context, WidgetRef ref) { + final userDetail = ref.watch(userInfoProxyProvider(userId)); + + return switch (userDetail) { + AsyncLoading() => + const Center(child: CircularProgressIndicator.adaptive()), + AsyncError(:final error, :final stackTrace) => ErrorDetail( + error: error, + stackTrace: stackTrace, + ), + AsyncData(:final value) => UserDetail(response: value.response) + }; } } diff --git a/lib/view/user_page/user_plays.dart b/lib/view/user_page/user_plays.dart index aef7d074a..d07c1a69b 100644 --- a/lib/view/user_page/user_plays.dart +++ b/lib/view/user_page/user_plays.dart @@ -1,31 +1,30 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/mfm_text.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:url_launcher/url_launcher.dart'; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:url_launcher/url_launcher.dart"; class UserPlays extends ConsumerWidget { final String userId; - const UserPlays({super.key, required this.userId}); + const UserPlays({required this.userId, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return PushableListView( initializeFuture: () async { final response = await ref - .read(misskeyProvider(AccountScope.of(context))) + .read(misskeyGetContextProvider) .users .flashs(UsersFlashsRequest(userId: userId)); return response.toList(); }, nextFuture: (item, _) async { final response = await ref - .read(misskeyProvider(AccountScope.of(context))) + .read(misskeyGetContextProvider) .users .flashs(UsersFlashsRequest(userId: userId, untilId: item.id)); return response.toList(); @@ -33,19 +32,22 @@ class UserPlays extends ConsumerWidget { itemBuilder: (context, play) { return ListTile( title: MfmText( - mfmText: play.title, - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(fontWeight: FontWeight.bold)), + mfmText: play.title, + style: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.bold), + ), subtitle: MfmText(mfmText: play.summary), - onTap: () { - launchUrl( - Uri( - scheme: "https", - host: AccountScope.of(context).host, - pathSegments: ["play", play.id]), - mode: LaunchMode.externalApplication); + onTap: () async { + await launchUrl( + Uri( + scheme: "https", + host: ref.read(accountContextProvider).getAccount.host, + pathSegments: ["play", play.id], + ), + mode: LaunchMode.externalApplication, + ); }, ); }, diff --git a/lib/view/user_page/user_reactions.dart b/lib/view/user_page/user_reactions.dart index 629fc30f9..546c129bf 100644 --- a/lib/view/user_page/user_reactions.dart +++ b/lib/view/user_page/user_reactions.dart @@ -1,48 +1,39 @@ -import 'package:flutter/material.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/custom_emoji.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/view/themes/app_theme.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/misskey_notes/custom_emoji.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:miria/view/themes/app_theme.dart"; +import "package:misskey_dart/misskey_dart.dart"; class UserReactions extends ConsumerWidget { final String userId; - const UserReactions({super.key, required this.userId}); + const UserReactions({required this.userId, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final account = AccountScope.of(context); return PushableListView( initializeFuture: () async { final response = await ref - .read(misskeyProvider(account)) + .read(misskeyGetContextProvider) .users .reactions(UsersReactionsRequest(userId: userId)); - ref - .read(notesProvider(account)) - .registerAll(response.map((e) => e.note)); + ref.read(notesWithProvider).registerAll(response.map((e) => e.note)); return response.toList(); }, nextFuture: (lastItem, _) async { - final response = await ref - .read(misskeyProvider(account)) - .users - .reactions( - UsersReactionsRequest(userId: userId, untilId: lastItem.id)); - ref - .read(notesProvider(account)) - .registerAll(response.map((e) => e.note)); + final response = + await ref.read(misskeyGetContextProvider).users.reactions( + UsersReactionsRequest(userId: userId, untilId: lastItem.id), + ); + ref.read(notesWithProvider).registerAll(response.map((e) => e.note)); return response.toList(); }, - itemBuilder: (context, item) { - return UserReaction(response: item); - }, + itemBuilder: (context, item) => UserReaction(response: item), ); } } @@ -50,7 +41,7 @@ class UserReactions extends ConsumerWidget { class UserReaction extends ConsumerWidget { final UsersReactionsResponse response; - const UserReaction({super.key, required this.response}); + const UserReaction({required this.response, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { @@ -68,29 +59,35 @@ class UserReaction extends ConsumerWidget { children: [ DecoratedBox( decoration: BoxDecoration( - color: AppTheme.of(context).colorTheme.accentedBackground, - borderRadius: const BorderRadius.only( - topLeft: Radius.circular(10), - topRight: Radius.circular(10), - )), + color: AppTheme.of(context).colorTheme.accentedBackground, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(10), + topRight: Radius.circular(10), + ), + ), child: Align( alignment: Alignment.centerLeft, child: Padding( padding: const EdgeInsets.only(left: 10, top: 3, bottom: 3), child: CustomEmoji( emojiData: MisskeyEmojiData.fromEmojiName( - emojiName: response.type, - emojiInfo: response.note.reactionEmojis, - repository: ref.read( - emojiRepositoryProvider(AccountScope.of(context)))), + emojiName: response.type, + emojiInfo: response.note.reactionEmojis, + repository: ref.read( + emojiRepositoryProvider( + ref.read(accountContextProvider).getAccount, + ), + ), + ), fontSizeRatio: 2, ), ), ), ), Padding( - padding: const EdgeInsets.only(right: 10, top: 10, bottom: 10), - child: MisskeyNote(note: response.note)), + padding: const EdgeInsets.only(right: 10, top: 10, bottom: 10), + child: MisskeyNote(note: response.note), + ), const Padding(padding: EdgeInsets.all(5)), ], ), diff --git a/lib/view/user_page/users_list_modal_sheet.dart b/lib/view/user_page/users_list_modal_sheet.dart index 3174f2e5f..c5fa823bf 100644 --- a/lib/view/user_page/users_list_modal_sheet.dart +++ b/lib/view/user_page/users_list_modal_sheet.dart @@ -1,28 +1,33 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/users_list_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/error_detail.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/users_list_page/users_list_settings_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/users_list_settings.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/state_notifier/user_list_page/users_lists_notifier.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class UsersListModalSheet extends ConsumerWidget { +@RoutePage() +class UsersListModalSheet extends ConsumerWidget implements AutoRouteWrapper { const UsersListModalSheet({ - super.key, required this.account, required this.user, + super.key, }); final Account account; final User user; + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope.as(account: account, child: this); + @override Widget build(BuildContext context, WidgetRef ref) { - final misskey = ref.watch(misskeyProvider(account)); - final lists = ref.watch(usersListsNotifierProvider(misskey)); + final lists = ref.watch(usersListsNotifierProvider); return lists.when( data: (lists) { @@ -39,14 +44,12 @@ class UsersListModalSheet extends ConsumerWidget { } if (value) { await ref - .read(usersListsNotifierProvider(misskey).notifier) - .push(list.id, user) - .expectFailure(context); + .read(usersListsNotifierProvider.notifier) + .push(list.id, user); } else { await ref - .read(usersListsNotifierProvider(misskey).notifier) - .pull(list.id, user) - .expectFailure(context); + .read(usersListsNotifierProvider.notifier) + .pull(list.id, user); } }, title: Text(list.name ?? ""), @@ -56,19 +59,14 @@ class UsersListModalSheet extends ConsumerWidget { leading: const Icon(Icons.add), title: Text(S.of(context).createList), onTap: () async { - final settings = await showDialog( - context: context, - builder: (context) => UsersListSettingsDialog( - title: Text(S.of(context).create), - ), + final settings = await context.pushRoute( + UsersListSettingsRoute(title: Text(S.of(context).create)), ); if (!context.mounted) return; - if (settings != null) { - await ref - .read(usersListsNotifierProvider(misskey).notifier) - .create(settings) - .expectFailure(context); - } + if (settings == null) return; + await ref + .read(usersListsNotifierProvider.notifier) + .create(settings); }, ); } @@ -76,7 +74,7 @@ class UsersListModalSheet extends ConsumerWidget { ); }, error: (e, st) => Center(child: ErrorDetail(error: e, stackTrace: st)), - loading: () => const Center(child: CircularProgressIndicator()), + loading: () => const Center(child: CircularProgressIndicator.adaptive()), ); } } diff --git a/lib/view/user_select_dialog.dart b/lib/view/user_select_dialog.dart index 0328f7439..15ace18b1 100644 --- a/lib/view/user_select_dialog.dart +++ b/lib/view/user_select_dialog.dart @@ -1,68 +1,55 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/extensions/origin_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:miria/view/user_page/user_list_item.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/origin_extension.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:miria/view/user_page/user_list_item.dart"; +import "package:misskey_dart/misskey_dart.dart"; -class UserSelectDialog extends StatelessWidget { - final Account account; +@RoutePage() +class UserSelectDialog extends StatelessWidget implements AutoRouteWrapper { + final AccountContext accountContext; - const UserSelectDialog({super.key, required this.account}); + const UserSelectDialog({required this.accountContext, super.key}); + + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); @override Widget build(BuildContext context) { - return AccountScope( - account: account, - child: AlertDialog( - content: SizedBox( - width: MediaQuery.of(context).size.width * 0.8, - height: MediaQuery.of(context).size.height * 0.8, - child: UserSelectContent( - onSelected: (item) => Navigator.of(context).pop(item), - ), + return AlertDialog( + content: SizedBox( + width: MediaQuery.of(context).size.width * 0.8, + height: MediaQuery.of(context).size.height * 0.8, + child: UserSelectContent( + onSelected: (item) async => context.maybePop(item), ), ), ); } } -class UserSelectContent extends ConsumerStatefulWidget { +class UserSelectContent extends HookConsumerWidget { final void Function(User) onSelected; final FocusNode? focusNode; final bool isDetail; const UserSelectContent({ - super.key, required this.onSelected, + super.key, this.focusNode, this.isDetail = false, }); @override - ConsumerState createState() => - UserSelectContentState(); -} - -final usersSelectDialogQueryProvider = StateProvider.autoDispose((ref) => ""); -final usersSelectDialogOriginProvider = - StateProvider.autoDispose((ref) => Origin.combined); - -class UserSelectContentState extends ConsumerState { - final queryController = TextEditingController(); - - @override - void dispose() { - queryController.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - final origin = ref.watch(usersSelectDialogOriginProvider); + Widget build(BuildContext context, WidgetRef ref) { + final queryController = useTextEditingController(); + final origin = useState(Origin.combined); + final searchQuery = useState(""); return Column( mainAxisSize: MainAxisSize.max, crossAxisAlignment: CrossAxisAlignment.start, @@ -70,31 +57,24 @@ class UserSelectContentState extends ConsumerState { children: [ TextField( controller: queryController, - focusNode: widget.focusNode, + focusNode: focusNode, autofocus: true, decoration: const InputDecoration(prefixIcon: Icon(Icons.search)), - onSubmitted: (value) { - ref.read(usersSelectDialogQueryProvider.notifier).state = value; - }, + onSubmitted: (value) => searchQuery.value = value, ), const Padding(padding: EdgeInsets.only(bottom: 10)), LayoutBuilder( builder: (context, constraints) { return ToggleButtons( isSelected: [ - for (final element in Origin.values) element == origin + for (final element in Origin.values) element == origin.value, ], constraints: BoxConstraints.expand( - width: constraints.maxWidth / Origin.values.length - - Theme.of(context) - .toggleButtonsTheme - .borderWidth! - .toInt() * - Origin.values.length), - onPressed: (index) { - ref.read(usersSelectDialogOriginProvider.notifier).state = - Origin.values[index]; - }, + width: constraints.maxWidth / Origin.values.length - + Theme.of(context).toggleButtonsTheme.borderWidth!.toInt() * + Origin.values.length, + ), + onPressed: (index) => origin.value = Origin.values[index], children: [ for (final element in Origin.values) Padding( @@ -107,10 +87,12 @@ class UserSelectContentState extends ConsumerState { ), Expanded( child: UsersSelectContentList( - onSelected: widget.onSelected, - isDetail: widget.isDetail, + onSelected: onSelected, + isDetail: isDetail, + query: searchQuery.value, + origin: origin.value, ), - ) + ), ], ); } @@ -118,51 +100,50 @@ class UserSelectContentState extends ConsumerState { class UsersSelectContentList extends ConsumerWidget { const UsersSelectContentList({ - super.key, required this.onSelected, required this.isDetail, + required this.query, + required this.origin, + super.key, }); final void Function(User) onSelected; final bool isDetail; + final String query; + final Origin origin; @override Widget build(BuildContext context, WidgetRef ref) { - final query = ref.watch(usersSelectDialogQueryProvider); - final origin = ref.watch(usersSelectDialogOriginProvider); - return PushableListView( - listKey: ObjectKey(Object.hashAll([ - query, - origin, - ])), + listKey: ObjectKey(Object.hashAll([query, origin])), initializeFuture: () async { if (query.isEmpty) { final response = await ref - .read(misskeyProvider(AccountScope.of(context))) + .read(misskeyGetContextProvider) .users - .getFrequentlyRepliedUsers(UsersGetFrequentlyRepliedUsersRequest( - userId: AccountScope.of(context).i.id)); + .getFrequentlyRepliedUsers( + UsersGetFrequentlyRepliedUsersRequest( + userId: ref.read(accountContextProvider).getAccount.i.id, + ), + ); return response.map((e) => e.user).toList(); } final response = await ref - .read(misskeyProvider(AccountScope.of(context))) + .read(misskeyGetContextProvider) .users .search(UsersSearchRequest(query: query, origin: origin)); return response.toList(); }, nextFuture: (lastItem, length) async { - if (query.isEmpty) { - return []; - } - final response = await ref - .read(misskeyProvider(AccountScope.of(context))) - .users - .search(UsersSearchRequest( - query: query, - origin: origin, - offset: length, - )); + if (query.isEmpty) return []; + + final response = await ref.read(misskeyGetContextProvider).users.search( + UsersSearchRequest( + query: query, + origin: origin, + offset: length, + ), + ); return response.toList(); }, itemBuilder: (context2, item) => UserListItem( diff --git a/lib/view/users_list_page/users_list_detail_page.dart b/lib/view/users_list_page/users_list_detail_page.dart index a5e46555e..bb7926d7c 100644 --- a/lib/view/users_list_page/users_list_detail_page.dart +++ b/lib/view/users_list_page/users_list_detail_page.dart @@ -1,19 +1,17 @@ -import 'package:auto_route/annotations.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/extensions/users_lists_show_response_extension.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/users_list_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/error_detail.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; -import 'package:miria/view/user_page/user_list_item.dart'; -import 'package:miria/view/user_select_dialog.dart'; -import 'package:miria/view/users_list_page/users_list_settings_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/users_lists_show_response_extension.dart"; +import "package:miria/model/users_list_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/error_detail.dart"; +import "package:miria/view/common/error_dialog_handler.dart"; +import "package:miria/view/dialogs/simple_confirm_dialog.dart"; +import "package:miria/view/user_page/user_list_item.dart"; +import "package:misskey_dart/misskey_dart.dart"; final _usersListNotifierProvider = AutoDisposeAsyncNotifierProviderFamily< _UsersListNotifier, UsersList, (Misskey, String)>(_UsersListNotifier.new); @@ -98,19 +96,23 @@ class _UsersListUsers } @RoutePage() -class UsersListDetailPage extends ConsumerWidget { +class UsersListDetailPage extends ConsumerWidget implements AutoRouteWrapper { const UsersListDetailPage({ - super.key, - required this.account, + required this.accountContext, required this.listId, + super.key, }); - final Account account; + final AccountContext accountContext; final String listId; + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); + @override Widget build(BuildContext context, WidgetRef ref) { - final misskey = ref.watch(misskeyProvider(account)); + final misskey = ref.watch(misskeyGetContextProvider); final arg = (misskey, listId); final list = ref.watch(_usersListNotifierProvider(arg)); final users = ref.watch(_usersListUsersProvider(arg)); @@ -123,20 +125,19 @@ class UsersListDetailPage extends ConsumerWidget { IconButton( icon: const Icon(Icons.settings), onPressed: () async { - final settings = await showDialog( - context: context, - builder: (context) => UsersListSettingsDialog( + final settings = await context.pushRoute( + UsersListSettingsRoute( title: Text(S.of(context).edit), initialSettings: UsersListSettings.fromUsersList(list), ), ); if (!context.mounted) return; - if (settings != null) { - ref - .read(_usersListNotifierProvider(arg).notifier) - .updateList(settings) - .expectFailure(context); - } + if (settings == null) return; + + await ref + .read(_usersListNotifierProvider(arg).notifier) + .updateList(settings) + .expectFailure(context); }, ), ], @@ -147,80 +148,78 @@ class UsersListDetailPage extends ConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 10), child: users.when( data: (users) { - return AccountScope( - account: account, - child: Column( - children: [ - ListTile( - title: Text(S.of(context).members), - subtitle: Text( - S.of(context).listCapacity( - users.length, - account.i.policies.userEachUserListsLimit, - ), - ), - trailing: ElevatedButton( - child: Text(S.of(context).addUser), - onPressed: () async { - final user = await showDialog( - context: context, - builder: (context) => - UserSelectDialog(account: account), - ); - if (user == null) { - return; - } - if (!context.mounted) return; - await ref - .read(_usersListUsersProvider(arg).notifier) - .push(user) - .expectFailure(context); - }, - ), + return Column( + children: [ + ListTile( + title: Text(S.of(context).members), + subtitle: Text( + S.of(context).listCapacity( + users.length, + accountContext + .postAccount.i.policies.userEachUserListsLimit, + ), + ), + trailing: ElevatedButton( + child: Text(S.of(context).addUser), + onPressed: () async { + final user = await context.pushRoute( + UserSelectRoute( + accountContext: accountContext, + ), + ); + if (user == null) return; + if (!context.mounted) return; + await ref + .read(_usersListUsersProvider(arg).notifier) + .push(user) + .expectFailure(context); + }, ), - const Divider(), - Expanded( - child: ListView.builder( - itemCount: users.length, - itemBuilder: (context, index) { - final user = users[index]; - return Row( - children: [ - Expanded( - child: UserListItem(user: user), - ), - IconButton( - icon: const Icon(Icons.close), - onPressed: () async { - final result = await SimpleConfirmDialog.show( - context: context, - message: S.of(context).confirmRemoveUser, - primary: S.of(context).removeUser, - secondary: S.of(context).cancel, - ); - if (!context.mounted) return; - if (result ?? false) { - await ref - .read( - _usersListUsersProvider(arg).notifier, - ) - .pull(user) - .expectFailure(context); - } - }, - ), - ], - ); - }, - ), + ), + const Divider(), + Expanded( + child: ListView.builder( + itemCount: users.length, + itemBuilder: (context, index) { + final user = users[index]; + return Row( + children: [ + Expanded( + child: UserListItem(user: user), + ), + IconButton( + icon: const Icon(Icons.close), + onPressed: () async { + final result = await SimpleConfirmDialog.show( + context: context, + message: S.of(context).confirmRemoveUser, + primary: S.of(context).removeUser, + secondary: S.of(context).cancel, + ); + if (!context.mounted) return; + if (result ?? false) { + await ref + .read( + _usersListUsersProvider(arg).notifier, + ) + .pull(user) + .expectFailure(context); + } + }, + ), + ], + ); + }, ), - ], - ), + ), + ], ); }, error: (e, st) => Center(child: ErrorDetail(error: e, stackTrace: st)), - loading: () => const Center(child: CircularProgressIndicator()), + loading: () => const Center( + child: CircularProgressIndicator.adaptive(), + ), ), ), ); diff --git a/lib/view/users_list_page/users_list_page.dart b/lib/view/users_list_page/users_list_page.dart index 0831db554..3fdf32564 100644 --- a/lib/view/users_list_page/users_list_page.dart +++ b/lib/view/users_list_page/users_list_page.dart @@ -1,26 +1,27 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/users_list_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/error_detail.dart'; -import 'package:miria/view/common/error_dialog_handler.dart'; -import 'package:miria/view/dialogs/simple_confirm_dialog.dart'; -import 'package:miria/view/users_list_page/users_list_settings_dialog.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/users_list_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/state_notifier/user_list_page/users_lists_notifier.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/error_detail.dart"; @RoutePage() -class UsersListPage extends ConsumerWidget { - final Account account; +class UsersListPage extends ConsumerWidget implements AutoRouteWrapper { + final AccountContext accountContext; - const UsersListPage(this.account, {super.key}); + const UsersListPage(this.accountContext, {super.key}); + + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); @override Widget build(BuildContext context, WidgetRef ref) { - final misskey = ref.watch(misskeyProvider(account)); - final list = ref.watch(usersListsNotifierProvider(misskey)); + final list = ref.watch(usersListsNotifierProvider); return Scaffold( appBar: AppBar( @@ -29,19 +30,13 @@ class UsersListPage extends ConsumerWidget { IconButton( icon: const Icon(Icons.add), onPressed: () async { - final settings = await showDialog( - context: context, - builder: (context) => UsersListSettingsDialog( - title: Text(S.of(context).create), - ), + final settings = await context.pushRoute( + UsersListSettingsRoute(title: Text(S.of(context).create)), ); - if (!context.mounted) return; - if (settings != null) { - ref - .read(usersListsNotifierProvider(misskey).notifier) - .create(settings) - .expectFailure(context); - } + if (settings == null) return; + await ref + .read(usersListsNotifierProvider.notifier) + .create(settings); }, ), ], @@ -58,27 +53,13 @@ class UsersListPage extends ConsumerWidget { title: Text(list.name ?? ""), trailing: IconButton( icon: const Icon(Icons.delete), - onPressed: () async { - final result = await SimpleConfirmDialog.show( - context: context, - message: S.of(context).confirmDeleteList, - primary: S.of(context).doDeleting, - secondary: S.of(context).cancel, - ); - if (!context.mounted) return; - if (result ?? false) { - await ref - .read( - usersListsNotifierProvider(misskey).notifier, - ) - .delete(list.id) - .expectFailure(context); - } - }, + onPressed: () async => ref + .read(usersListsNotifierProvider.notifier) + .delete(list.id), ), - onTap: () => context.pushRoute( + onTap: () async => context.pushRoute( UsersListTimelineRoute( - account: account, + accountContext: ref.read(accountContextProvider), list: list, ), ), @@ -88,7 +69,9 @@ class UsersListPage extends ConsumerWidget { }, error: (e, st) => Center(child: ErrorDetail(error: e, stackTrace: st)), - loading: () => const Center(child: CircularProgressIndicator()), + loading: () => const Center( + child: CircularProgressIndicator.adaptive(), + ), ), ), ); diff --git a/lib/view/users_list_page/users_list_settings_dialog.dart b/lib/view/users_list_page/users_list_settings_dialog.dart index 84f7e0552..da0eaf6a9 100644 --- a/lib/view/users_list_page/users_list_settings_dialog.dart +++ b/lib/view/users_list_page/users_list_settings_dialog.dart @@ -1,22 +1,19 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/users_list_settings.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_hooks/flutter_hooks.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/users_list_settings.dart"; +import "package:riverpod_annotation/riverpod_annotation.dart"; -final _formKeyProvider = Provider.autoDispose((ref) => GlobalKey()); +part "users_list_settings_dialog.g.dart"; -final _initialSettingsProvider = Provider.autoDispose( - (ref) => throw UnimplementedError(), -); +@Riverpod(dependencies: []) +UsersListSettings _initialSettings(_InitialSettingsRef ref) => + throw UnimplementedError(); -final _usersListSettingsNotifierProvider = - NotifierProvider.autoDispose<_UsersListSettingsNotifier, UsersListSettings>( - _UsersListSettingsNotifier.new, - dependencies: [_initialSettingsProvider], -); - -class _UsersListSettingsNotifier - extends AutoDisposeNotifier { +@Riverpod(dependencies: [_initialSettings]) +class _UsersListSettingsNotifier extends _$UsersListSettingsNotifier { @override UsersListSettings build() { return ref.watch(_initialSettingsProvider); @@ -35,7 +32,9 @@ class _UsersListSettingsNotifier } } -class UsersListSettingsDialog extends StatelessWidget { +@RoutePage() +class UsersListSettingsDialog extends HookConsumerWidget + implements AutoRouteWrapper { const UsersListSettingsDialog({ super.key, this.title, @@ -46,72 +45,66 @@ class UsersListSettingsDialog extends StatelessWidget { final UsersListSettings initialSettings; @override - Widget build(BuildContext context) { - return AlertDialog( - title: title, - content: ProviderScope( + Widget wrappedRoute(BuildContext context) => ProviderScope( overrides: [ _initialSettingsProvider.overrideWithValue(initialSettings), ], - child: const UsersListSettingsForm(), - ), - ); - } -} - -class UsersListSettingsForm extends ConsumerWidget { - const UsersListSettingsForm({super.key}); + child: this, + ); @override Widget build(BuildContext context, WidgetRef ref) { - final formKey = ref.watch(_formKeyProvider); + final formKey = useState(GlobalKey()); final initialSettings = ref.watch(_initialSettingsProvider); final settings = ref.watch(_usersListSettingsNotifierProvider); - return Form( - key: formKey, - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextFormField( - initialValue: initialSettings.name, - maxLength: 100, - decoration: InputDecoration( - labelText: S.of(context).listName, - contentPadding: const EdgeInsets.fromLTRB(12, 24, 12, 16), + return AlertDialog( + title: title, + content: Form( + key: formKey.value, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextFormField( + initialValue: initialSettings.name, + maxLength: 100, + decoration: InputDecoration( + labelText: S.of(context).listName, + contentPadding: const EdgeInsets.fromLTRB(12, 24, 12, 16), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return S.of(context).pleaseInput; + } + return null; + }, + onSaved: ref + .read(_usersListSettingsNotifierProvider.notifier) + .updateName, ), - validator: (value) { - if (value == null || value.isEmpty) { - return S.of(context).pleaseInput; - } - return null; - }, - onSaved: ref - .read(_usersListSettingsNotifierProvider.notifier) - .updateName, - ), - CheckboxListTile( - title: Text(S.of(context).public), - value: settings.isPublic, - onChanged: ref - .read(_usersListSettingsNotifierProvider.notifier) - .updateIsPublic, - ), - ElevatedButton( - child: Text(S.of(context).done), - onPressed: () { - if (formKey.currentState!.validate()) { - formKey.currentState!.save(); - final settings = ref.read(_usersListSettingsNotifierProvider); - if (settings == initialSettings) { - Navigator.of(context).pop(); - } else { - Navigator.of(context).pop(settings); + CheckboxListTile( + title: Text(S.of(context).public), + value: settings.isPublic, + onChanged: ref + .read(_usersListSettingsNotifierProvider.notifier) + .updateIsPublic, + ), + ElevatedButton( + child: Text(S.of(context).done), + onPressed: () { + if (formKey.value.currentState!.validate()) { + formKey.value.currentState!.save(); + final settings = ref.read(_usersListSettingsNotifierProvider); + if (settings == initialSettings) { + Navigator.of(context).pop(); + } else { + Navigator.of(context).pop(settings); + } } - } - }, - ), - ], + }, + ), + ], + ), ), ); } diff --git a/lib/view/users_list_page/users_list_settings_dialog.g.dart b/lib/view/users_list_page/users_list_settings_dialog.g.dart new file mode 100644 index 000000000..6f1faccaf --- /dev/null +++ b/lib/view/users_list_page/users_list_settings_dialog.g.dart @@ -0,0 +1,46 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'users_list_settings_dialog.dart'; + +// ************************************************************************** +// RiverpodGenerator +// ************************************************************************** + +String _$initialSettingsHash() => r'dfac19098b98ae3956aa22d651861d1133c68289'; + +/// See also [_initialSettings]. +@ProviderFor(_initialSettings) +final _initialSettingsProvider = + AutoDisposeProvider.internal( + _initialSettings, + name: r'_initialSettingsProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$initialSettingsHash, + dependencies: const [], + allTransitiveDependencies: const {}, +); + +typedef _InitialSettingsRef = AutoDisposeProviderRef; +String _$usersListSettingsNotifierHash() => + r'6e354936966b2a1d0e0c4dca6d41e74ff0ccb243'; + +/// See also [_UsersListSettingsNotifier]. +@ProviderFor(_UsersListSettingsNotifier) +final _usersListSettingsNotifierProvider = AutoDisposeNotifierProvider< + _UsersListSettingsNotifier, UsersListSettings>.internal( + _UsersListSettingsNotifier.new, + name: r'_usersListSettingsNotifierProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$usersListSettingsNotifierHash, + dependencies: [_initialSettingsProvider], + allTransitiveDependencies: { + _initialSettingsProvider, + ...?_initialSettingsProvider.allTransitiveDependencies + }, +); + +typedef _$UsersListSettingsNotifier = AutoDisposeNotifier; +// ignore_for_file: type=lint +// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, inference_failure_on_uninitialized_variable, inference_failure_on_function_return_type, inference_failure_on_untyped_parameter, deprecated_member_use_from_same_package diff --git a/lib/view/users_list_page/users_list_timeline.dart b/lib/view/users_list_page/users_list_timeline.dart index 804515bef..4bde345f8 100644 --- a/lib/view/users_list_page/users_list_timeline.dart +++ b/lib/view/users_list_page/users_list_timeline.dart @@ -1,35 +1,32 @@ -import 'package:flutter/widgets.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/pushable_listview.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:flutter/widgets.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/pushable_listview.dart"; +import "package:misskey_dart/misskey_dart.dart"; class UsersListTimeline extends ConsumerWidget { final String listId; - const UsersListTimeline({super.key, required this.listId}); + const UsersListTimeline({required this.listId, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final account = AccountScope.of(context); return PushableListView( initializeFuture: () async { final response = await ref - .read(misskeyProvider(account)) + .read(misskeyGetContextProvider) .notes .userListTimeline(UserListTimelineRequest(listId: listId)); - ref.read(notesProvider(account)).registerAll(response); + ref.read(notesWithProvider).registerAll(response); return response.toList(); }, nextFuture: (lastItem, _) async { - final response = await ref - .read(misskeyProvider(account)) - .notes - .userListTimeline( - UserListTimelineRequest(listId: listId, untilId: lastItem.id)); - ref.read(notesProvider(account)).registerAll(response); + final response = + await ref.read(misskeyGetContextProvider).notes.userListTimeline( + UserListTimelineRequest(listId: listId, untilId: lastItem.id), + ); + ref.read(notesWithProvider).registerAll(response); return response.toList(); }, itemBuilder: (context, item) { diff --git a/lib/view/users_list_page/users_list_timeline_page.dart b/lib/view/users_list_page/users_list_timeline_page.dart index 88d873dfb..2d0b6227c 100644 --- a/lib/view/users_list_page/users_list_timeline_page.dart +++ b/lib/view/users_list_page/users_list_timeline_page.dart @@ -1,18 +1,22 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/users_list_page/users_list_timeline.dart'; -import 'package:misskey_dart/misskey_dart.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/users_list_page/users_list_timeline.dart"; +import "package:misskey_dart/misskey_dart.dart"; @RoutePage() -class UsersListTimelinePage extends ConsumerWidget { - final Account account; +class UsersListTimelinePage extends ConsumerWidget implements AutoRouteWrapper { + final AccountContext accountContext; final UsersList list; - const UsersListTimelinePage(this.account, this.list, {super.key}); + const UsersListTimelinePage(this.accountContext, this.list, {super.key}); + + @override + Widget wrappedRoute(BuildContext context) => + AccountContextScope(context: accountContext, child: this); @override Widget build(BuildContext context, WidgetRef ref) { @@ -22,21 +26,18 @@ class UsersListTimelinePage extends ConsumerWidget { actions: [ IconButton( icon: const Icon(Icons.info_outline), - onPressed: () => context.pushRoute( + onPressed: () async => context.pushRoute( UsersListDetailRoute( - account: account, + accountContext: accountContext, listId: list.id, ), ), ), ], ), - body: AccountScope( - account: account, - child: Padding( - padding: const EdgeInsets.only(right: 10), - child: UsersListTimeline(listId: list.id), - ), + body: Padding( + padding: const EdgeInsets.only(right: 10), + child: UsersListTimeline(listId: list.id), ), ); } diff --git a/linux/my_application.cc b/linux/my_application.cc index 764b7a2d7..65fb98834 100644 --- a/linux/my_application.cc +++ b/linux/my_application.cc @@ -49,8 +49,8 @@ static void my_application_activate(GApplication* application) { g_autoptr(FlDartProject) project = fl_dart_project_new(); gtk_window_set_icon_from_file(window, g_strconcat(fl_dart_project_get_assets_path(project), "/assets/images/icon.png", NULL), NULL); - gtk_window_set_default_size(window, 1280, 720); - gtk_widget_realize(GTK_WIDGET(window)); + gtk_window_set_default_size(window, 400, 700); + gtk_widget_show(GTK_WIDGET(window)); fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index a90d38315..83e14241c 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -13,13 +13,13 @@ import media_kit_libs_macos_video import media_kit_video import package_info_plus import path_provider_foundation -import screen_brightness_macos import screen_retriever import share_plus import shared_preferences_foundation -import sqflite +import sqflite_darwin import url_launcher_macos import wakelock_plus +import webview_flutter_wkwebview import window_manager func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { @@ -31,12 +31,12 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { MediaKitVideoPlugin.register(with: registry.registrar(forPlugin: "MediaKitVideoPlugin")) FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) - ScreenBrightnessMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenBrightnessMacosPlugin")) ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) WakelockPlusMacosPlugin.register(with: registry.registrar(forPlugin: "WakelockPlusMacosPlugin")) + FLTWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "FLTWebViewFlutterPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 31f3fc7e5..a50cdd8fc 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -6,9 +6,6 @@ PODS: - flutter_secure_storage_macos (6.1.1): - FlutterMacOS - FlutterMacOS (1.0.0) - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - image_editor_common (1.0.0): - Flutter - FlutterMacOS @@ -32,13 +29,16 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqflite (0.0.2): + - sqflite_darwin (0.0.4): + - Flutter - FlutterMacOS - - FMDB (>= 2.7.5) - url_launcher_macos (0.0.1): - FlutterMacOS - wakelock_plus (0.0.1): - FlutterMacOS + - webview_flutter_wkwebview (0.0.1): + - Flutter + - FlutterMacOS - window_manager (0.2.0): - FlutterMacOS @@ -57,15 +57,12 @@ DEPENDENCIES: - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `Flutter/ephemeral/.symlinks/plugins/sqflite/macos`) + - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) + - webview_flutter_wkwebview (from `Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin`) - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) -SPEC REPOS: - trunk: - - FMDB - EXTERNAL SOURCES: device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos @@ -95,34 +92,36 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin - sqflite: - :path: Flutter/ephemeral/.symlinks/plugins/sqflite/macos + sqflite_darwin: + :path: Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos wakelock_plus: :path: Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos + webview_flutter_wkwebview: + :path: Flutter/ephemeral/.symlinks/plugins/webview_flutter_wkwebview/darwin window_manager: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f + device_info_plus: ce1b7762849d3ec103d0e0517299f2db7ad60720 flutter_image_compress_macos: c26c3c13ea0f28ae6dea4e139b3292e7729f99f1 - flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea + flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9 FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a image_editor_common: 1b11f59fad8909bafcdaa0f31cc9373425b58600 media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 media_kit_native_event_loop: 81fd5b45192b72f8b5b69eaf5b540f45777eb8d5 media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5 - package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce - path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c + package_info_plus: f5790acc797bf17c3e959e9d6cf162cc68ff7523 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 - share_plus: 76dd39142738f7a68dd57b05093b5e8193f220f7 - shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 - sqflite: a5789cceda41d54d23f31d6de539d65bb14100ea - url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 + share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13 + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 + webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 PODFILE CHECKSUM: 76079db0da3234e173fb67f3880ef7f6fc358307 diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib index 80e867a4e..98d886f27 100644 --- a/macos/Runner/Base.lproj/MainMenu.xib +++ b/macos/Runner/Base.lproj/MainMenu.xib @@ -332,10 +332,10 @@ - + - + diff --git a/pubspec.lock b/pubspec.lock index 0edbab40b..3af88c6bb 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,34 +5,47 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: ae92f5d747aee634b87f89d9946000c2de774be1d6ac3e58268224348cd0101a + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "61.0.0" + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" analyzer: dependency: transitive description: name: analyzer - sha256: ea3d8652bda62982addfd92fdc2d0214e5f82e43325104990d4f4c4a2a313562 + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + url: "https://pub.dev" + source: hosted + version: "6.7.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "0.11.3" archive: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.6.1" args: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.6.0" async: dependency: transitive description: @@ -45,18 +58,18 @@ packages: dependency: "direct main" description: name: auto_route - sha256: "9a2579b115cc42eb3c787c86deae2ffc4128dc6f1af976580e7f8e4bb7f803c6" + sha256: a9001a90539ca3effc168f7e1029a5885c7326b9032c09ac895e303c1d137704 url: "https://pub.dev" source: hosted - version: "6.4.0" + version: "8.3.0" auto_route_generator: dependency: "direct dev" description: name: auto_route_generator - sha256: e8056df4ff40700ce80b1fc7889cf62bfbb5250225f5fb2ea55d1f1271010a60 + sha256: a21d7a936c917488653c972f62d884d8adcf8c5d37acc7cd24da33cf784546c0 url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "8.1.0" badges: dependency: "direct main" description: @@ -93,10 +106,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "0343061a33da9c5810b2d6cee51945127d8f4c060b7fbdd9d54917f0a3feaaa1" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.1" + version: "4.0.2" build_resolvers: dependency: transitive description: @@ -109,18 +122,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.13" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: c9e32d21dd6626b5c163d48b037ce906bbe428bc23ab77bcd77bb21e593b6185 + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 url: "https://pub.dev" source: hosted - version: "7.2.11" + version: "7.3.2" built_collection: dependency: transitive description: @@ -133,42 +146,42 @@ packages: dependency: transitive description: name: built_value - sha256: c9aabae0718ec394e5bc3c7272e6bb0dc0b32201a08fe185ec1d8401d3e39309 + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.8.1" + version: "8.9.2" cached_network_image: dependency: "direct main" description: name: cached_network_image - sha256: "28ea9690a8207179c319965c13cd8df184d5ee721ae2ce60f398ced1219cea1f" + sha256: "4a5d8d2c728b0f3d0245f69f921d7be90cae4c2fd5288f773088672c0893f819" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.0" cached_network_image_platform_interface: dependency: transitive description: name: cached_network_image_platform_interface - sha256: "9e90e78ae72caa874a323d78fa6301b3fb8fa7ea76a8f96dc5b5bf79f283bf2f" + sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.1.1" cached_network_image_web: dependency: transitive description: name: cached_network_image_web - sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" + sha256: "6322dde7a5ad92202e64df659241104a43db20ed594c41ca18de1014598d7996" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.3.0" change: dependency: transitive description: name: change - sha256: "75b6e28073433946a987e6082d00f08676a8260a6aa68cac8594c10611e7e9b9" + sha256: "65db7f966dc7e786687f49900a94c5f08b0eb9ca8c4a3e7eed3a55e980b455e2" url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.4" characters: dependency: transitive description: @@ -185,14 +198,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.3" + checks: + dependency: transitive + description: + name: checks + sha256: aad431b45a8ae2fa26db8c22e385b9cdec73f72986a1d9d9f2017f4c39ecf5c9 + url: "https://pub.dev" + source: hosted + version: "0.3.0" + ci: + dependency: transitive + description: + name: ci + sha256: "145d095ce05cddac4d797a158bc4cf3b6016d1fe63d8c3d2fbd7212590adca13" + url: "https://pub.dev" + source: hosted + version: "0.1.0" cider: dependency: "direct dev" description: name: cider - sha256: "2d449e99f0c2db791bfcbf013a3c2f7c0ef48c0085d8340686ff25ce8b94fd9b" + sha256: dfff70e9324f99e315857c596c31f54cb7380cfa20dfdfdca11a3631e05b7d3e url: "https://pub.dev" source: hosted - version: "0.2.5" + version: "0.2.8" cli_util: dependency: transitive description: @@ -245,26 +274,26 @@ packages: dependency: transitive description: name: convert - sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" cross_file: dependency: transitive description: name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.3+8" + version: "0.3.4+2" crypto: dependency: transitive description: name: crypto - sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.6" csslib: dependency: transitive description: @@ -273,14 +302,38 @@ packages: url: "https://pub.dev" source: hosted version: "0.17.3" + custom_lint: + dependency: transitive + description: + name: custom_lint + sha256: "22bd87a362f433ba6aae127a7bac2838645270737f3721b180916d7c5946cb5d" + url: "https://pub.dev" + source: hosted + version: "0.5.11" + custom_lint_builder: + dependency: transitive + description: + name: custom_lint_builder + sha256: "0d48e002438950f9582e574ef806b2bea5719d8d14c0f9f754fbad729bcf3b19" + url: "https://pub.dev" + source: hosted + version: "0.5.14" + custom_lint_core: + dependency: transitive + description: + name: custom_lint_core + sha256: "2952837953022de610dacb464f045594854ced6506ac7f76af28d4a6490e189b" + url: "https://pub.dev" + source: hosted + version: "0.5.14" dart_style: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.7" dbus: dependency: transitive description: @@ -293,26 +346,34 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" + sha256: a7fd703482b391a87d60b6061d04dfdeab07826b96f9abd8f5ed98068acc0074 url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "10.1.2" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" dio: dependency: "direct main" description: name: dio - sha256: "797e1e341c3dd2f69f2dad42564a6feff3bfb87187d05abb93b9609e6f1645c3" + sha256: "5598aa796bbf4699afd5c67c0f5f6e2ed542afc956884b9cd58c306966efc260" url: "https://pub.dev" source: hosted - version: "5.4.0" + version: "5.7.0" + dio_web_adapter: + dependency: transitive + description: + name: dio_web_adapter + sha256: "33259a9276d6cea88774a0000cfae0d861003497755969c92faa223108620dc8" + url: "https://pub.dev" + source: hosted + version: "2.0.0" dotted_border: dependency: "direct main" description: @@ -341,63 +402,63 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.3" file: dependency: "direct main" description: name: file - sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "6.1.4" + version: "7.0.1" file_picker: dependency: "direct main" description: name: file_picker - sha256: be325344c1f3070354a1d84a231a1ba75ea85d413774ec4bdf444c023342e030 + sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3" url: "https://pub.dev" source: hosted - version: "5.5.0" + version: "8.0.7" fixnum: dependency: transitive description: name: fixnum - sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" fl_chart: dependency: "direct main" description: name: fl_chart - sha256: "48a1b69be9544e2b03d9a8e843affd89e43f3194c9248776222efcb4206bb1ec" + sha256: d0f0d49112f2f4b192481c16d05b6418bd7820e021e265a3c22db98acf7ed7fb url: "https://pub.dev" source: hosted - version: "0.62.0" + version: "0.68.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" flutter_cache_manager: - dependency: transitive + dependency: "direct main" description: name: flutter_cache_manager - sha256: "8207f27539deb83732fdda03e259349046a39a4c767269285f449ade355d54ba" + sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "3.4.1" flutter_colorpicker: dependency: "direct main" description: name: flutter_colorpicker - sha256: "458a6ed8ea480eb16ff892aedb4b7092b2804affd7e046591fb03127e8d8ef8b" + sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.1.0" flutter_highlighting: dependency: "direct main" description: @@ -406,70 +467,86 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.0+11.8.0" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 + url: "https://pub.dev" + source: hosted + version: "0.20.5" flutter_html: dependency: "direct main" description: name: flutter_html - sha256: "342c7908f0a67bcec62b6e0f7cf23e23bafe7f64693665dd35be98d5e783bdfd" + sha256: "02ad69e813ecfc0728a455e4bf892b9379983e050722b1dce00192ee2e41d1ee" url: "https://pub.dev" source: hosted - version: "3.0.0-alpha.6" + version: "3.0.0-beta.2" flutter_image_compress: dependency: "direct main" description: name: flutter_image_compress - sha256: f159d2e8c4ed04b8e36994124fd4a5017a0f01e831ae3358c74095c340e9ae5e + sha256: "45a3071868092a61b11044c70422b04d39d4d9f2ef536f3c5b11fb65a1e7dd90" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.3.0" flutter_image_compress_common: dependency: transitive description: name: flutter_image_compress_common - sha256: "7cad12802628706655920089cfe9ee1d1098300e7f39a079eb160458bbc47652" + sha256: "7f79bc6c8a363063620b4e372fa86bc691e1cb28e58048cd38e030692fbd99ee" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.5" flutter_image_compress_macos: dependency: transitive description: name: flutter_image_compress_macos - sha256: fea1e3d71150d03373916b832c49b5c2f56c3e7e13da82a929274a2c6f88251e + sha256: "26df6385512e92b3789dc76b613b54b55c457a7f1532e59078b04bf189782d47" url: "https://pub.dev" source: hosted - version: "1.0.1" + version: "1.0.2" + flutter_image_compress_ohos: + dependency: transitive + description: + name: flutter_image_compress_ohos + sha256: e76b92bbc830ee08f5b05962fc78a532011fcd2041f620b5400a593e96da3f51 + url: "https://pub.dev" + source: hosted + version: "0.0.3" flutter_image_compress_platform_interface: dependency: transitive description: name: flutter_image_compress_platform_interface - sha256: eb4f055138b29b04498ebcb6d569aaaee34b64d75fb74ea0d40f9790bf47ee9d + sha256: "579cb3947fd4309103afe6442a01ca01e1e6f93dc53bb4cbd090e8ce34a41889" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.5" flutter_image_compress_web: dependency: transitive description: name: flutter_image_compress_web - sha256: da41cc3859f19d11c7d10be615f6a9dcf0907e7daffde7442bf4cc2486663660 + sha256: f02fe352b17f82b72f481de45add240db062a2585850bea1667e82cc4cd6c311 url: "https://pub.dev" source: hosted - version: "0.1.3+2" + version: "0.1.4+1" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + sha256: "619817c4b65b322b5104b6bb6dfe6cda62d9729bd7ad4303ecc8b4e690a67a77" url: "https://pub.dev" source: hosted - version: "0.13.1" + version: "0.14.1" flutter_lints: dependency: "direct dev" description: name: flutter_lints - sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c" url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "4.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -479,74 +556,74 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: b068ffc46f82a55844acfa4fdbb61fad72fa2aef0905548419d97f0f95c456da + sha256: "9b78450b89f059e96c9ebb355fa6b3df1d6b330436e0b885fb49594c41721398" url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.0.23" flutter_riverpod: - dependency: "direct main" + dependency: transitive description: name: flutter_riverpod - sha256: da9591d1f8d5881628ccd5c25c40e74fc3eef50ba45e40c3905a06e1712412d5 + sha256: "2fd9f58a39b7269cb3495b09245000fcd267243518157a7c2f832189fb64f013" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "3.0.0-dev.3" flutter_secure_storage: dependency: "direct main" description: name: flutter_secure_storage - sha256: "22dbf16f23a4bcf9d35e51be1c84ad5bb6f627750565edd70dab70f3ff5fff8f" + sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0" url: "https://pub.dev" source: hosted - version: "8.1.0" + version: "9.2.2" flutter_secure_storage_linux: dependency: transitive description: name: flutter_secure_storage_linux - sha256: "3d5032e314774ee0e1a7d0a9f5e2793486f0dff2dd9ef5a23f4e3fb2a0ae6a9e" + sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.1" flutter_secure_storage_macos: dependency: transitive description: name: flutter_secure_storage_macos - sha256: bd33935b4b628abd0b86c8ca20655c5b36275c3a3f5194769a7b3f37c905369c + sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.2" flutter_secure_storage_platform_interface: dependency: transitive description: name: flutter_secure_storage_platform_interface - sha256: "0d4d3a5dd4db28c96ae414d7ba3b8422fd735a8255642774803b2532c9a61d7e" + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.2" flutter_secure_storage_web: dependency: transitive description: name: flutter_secure_storage_web - sha256: "30f84f102df9dcdaa2241866a958c2ec976902ebdaa8883fbfe525f1f2f3cf20" + sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.2.1" flutter_secure_storage_windows: dependency: transitive description: name: flutter_secure_storage_windows - sha256: "38f9501c7cb6f38961ef0e1eacacee2b2d4715c63cc83fe56449c4d3d0b47255" + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "3.1.2" flutter_svg: dependency: "direct main" description: name: flutter_svg - sha256: d39e7f95621fc84376bc0f7d504f05c3a41488c562f4a8ad410569127507402c + sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.0.10+1" flutter_test: dependency: "direct dev" description: flutter @@ -561,26 +638,26 @@ packages: dependency: "direct dev" description: name: freezed - sha256: "6c5031daae12c7072b3a87eff98983076434b4889ef2a44384d0cae3f82372ba" + sha256: "44c19278dd9d89292cf46e97dc0c1e52ce03275f40a97c5a348e802a924bf40e" url: "https://pub.dev" source: hosted - version: "2.4.6" + version: "2.5.7" freezed_annotation: dependency: "direct main" description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" glob: dependency: transitive description: @@ -593,18 +670,18 @@ packages: dependency: "direct main" description: name: google_fonts - sha256: f0b8d115a13ecf827013ec9fc883390ccc0e87a96ed5347a3114cac177ef18e8 + sha256: b1ac0fe2832c9cc95e5e88b57d627c5e68c223b9657f4b96e1487aa9098c7b82 url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "6.2.1" graphs: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" highlighting: dependency: "direct main" description: @@ -613,6 +690,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.9.0+11.8.0" + hooks_riverpod: + dependency: "direct main" + description: + name: hooks_riverpod + sha256: "169d7bf1d61c749c065e09388841906b2ff963794b932867372ed4f423a8d877" + url: "https://pub.dev" + source: hosted + version: "3.0.0-dev.3" + hotreloader: + dependency: transitive + description: + name: hotreloader + sha256: ed56fdc1f3a8ac924e717257621d09e9ec20e308ab6352a73a50a1d7a4d9158e + url: "https://pub.dev" + source: hosted + version: "4.2.0" html: dependency: transitive description: @@ -625,10 +718,10 @@ packages: dependency: transitive description: name: http - sha256: a2bbf9d017fcced29139daa8ed2bba4ece450ab222871df93ca9eec6f80c34ba + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.2" http_multi_server: dependency: transitive description: @@ -646,13 +739,13 @@ packages: source: hosted version: "4.0.2" image: - dependency: transitive + dependency: "direct main" description: name: image - sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" + sha256: f31d52537dc417fdcde36088fdf11d191026fd5e4fae742491ebd40e5a8bea7d url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "4.3.0" image_editor: dependency: "direct main" description: @@ -683,19 +776,20 @@ packages: image_gallery_saver: dependency: "direct main" description: - name: image_gallery_saver - sha256: "0aba74216a4d9b0561510cb968015d56b701ba1bd94aace26aacdd8ae5761816" - url: "https://pub.dev" - source: hosted + path: "." + ref: knottx-latest + resolved-ref: "24fd8207a4491c42ed907060bb5bf40c2430131f" + url: "https://github.com/knottx/image_gallery_saver.git" + source: git version: "2.0.3" intl: dependency: "direct main" description: name: intl - sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf url: "https://pub.dev" source: hosted - version: "0.18.1" + version: "0.19.0" io: dependency: transitive description: @@ -724,74 +818,90 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: aa1f5a8912615733e0fdc7a02af03308933c93235bdc8d50d0b0c8a8ccb0b969 + sha256: ea1432d167339ea9b5bb153f0571d0039607a873d6e04e0117af043f14a1fd4b url: "https://pub.dev" source: hosted - version: "6.7.1" + version: "6.8.0" kana_kit: dependency: "direct main" description: name: kana_kit - sha256: "4a8f019d15aa5d369720b0c33b50e50009e4f65633ac94157075b3e46a98de0c" + sha256: "4e99cfddae947971c327ef3d8d82d35cf036c046c7f460583785d48c0f777fa3" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.1" leak_tracker: dependency: transitive description: name: leak_tracker - sha256: "78eb209deea09858f5269f5a5b02be4049535f568c07b275096836f01ea323fa" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.0" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: b46c5e37c19120a8a01918cfaf293547f47269f7cb4b0058f21531c2465d6ef0 + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: name: leak_tracker_testing - sha256: a597f72a664dbd293f3bfc51f9ba69816f84dcd403cdac7066cb3f6003f3ab47 + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" lints: dependency: transitive description: name: lints - sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "4.0.0" + list_counter: + dependency: transitive + description: + name: list_counter + sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 + url: "https://pub.dev" + source: hosted + version: "1.0.2" logging: dependency: transitive description: name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" markdown: dependency: transitive description: name: markdown - sha256: "1b134d9f8ff2da15cb298efe6cd8b7d2a78958c1b00384ebcbdf13fe340a6c90" + sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 url: "https://pub.dev" source: hosted - version: "7.2.1" + version: "7.2.2" marker: dependency: transitive description: @@ -812,10 +922,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" matrix2d: dependency: "direct main" description: @@ -827,11 +937,12 @@ packages: media_kit: dependency: "direct main" description: - name: media_kit - sha256: "3289062540e3b8b9746e5c50d95bd78a9289826b7227e253dff806d002b9e67a" - url: "https://pub.dev" - source: hosted - version: "1.1.10+1" + path: media_kit + ref: "28292926cfd1e75611f7dcf39248124197d131cf" + resolved-ref: "28292926cfd1e75611f7dcf39248124197d131cf" + url: "https://github.com/4ster1sk/media-kit.git" + source: git + version: "1.1.11" media_kit_libs_android_video: dependency: transitive description: @@ -868,42 +979,43 @@ packages: dependency: "direct main" description: name: media_kit_libs_video - sha256: "3688e0c31482074578652bf038ce6301a5d21e1eda6b54fc3117ffeb4bdba067" + sha256: "20bb4aefa8fece282b59580e1cd8528117297083a6640c98c2e98cfc96b93288" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" media_kit_libs_windows_video: dependency: transitive description: name: media_kit_libs_windows_video - sha256: "7bace5f35d9afcc7f9b5cdadb7541d2191a66bb3fc71bfa11c1395b3360f6122" + sha256: "32654572167825c42c55466f5d08eee23ea11061c84aa91b09d0e0f69bdd0887" url: "https://pub.dev" source: hosted - version: "1.0.9" + version: "1.0.10" media_kit_native_event_loop: dependency: transitive description: name: media_kit_native_event_loop - sha256: a605cf185499d14d58935b8784955a92a4bf0ff4e19a23de3d17a9106303930e + sha256: "7d82e3b3e9ded5c35c3146c5ba1da3118d1dd8ac3435bac7f29f458181471b40" url: "https://pub.dev" source: hosted - version: "1.0.8" + version: "1.0.9" media_kit_video: dependency: "direct main" description: - name: media_kit_video - sha256: c048d11a19e379aebbe810647636e3fc6d18374637e2ae12def4ff8a4b99a882 - url: "https://pub.dev" - source: hosted - version: "1.2.4" + path: media_kit_video + ref: "28292926cfd1e75611f7dcf39248124197d131cf" + resolved-ref: "28292926cfd1e75611f7dcf39248124197d131cf" + url: "https://github.com/4ster1sk/media-kit.git" + source: git + version: "1.2.5" meta: dependency: transitive description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mfm: dependency: "direct main" description: @@ -924,16 +1036,16 @@ packages: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.6" misskey_dart: dependency: "direct main" description: path: "." ref: HEAD - resolved-ref: "91d4c3f631c872678a87d85e463b84866ac9b7be" + resolved-ref: "00a2300cd3cab6db1f0c3d2a7c2b5a64dbb50819" url: "https://github.com/shiosyakeyakini-info/misskey_dart.git" source: git version: "1.0.0" @@ -945,22 +1057,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.4.4" - numerus: - dependency: transitive - description: - name: numerus - sha256: "49cd96fe774dd1f574fc9117ed67e8a2b06a612f723e87ef3119456a7729d837" - url: "https://pub.dev" - source: hosted - version: "2.2.0" octo_image: dependency: transitive description: name: octo_image - sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" package_config: dependency: transitive description: @@ -973,18 +1077,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "7e76fad405b3e4016cd39d08f455a4eb5199723cf594cd1b8916d47140d93017" + sha256: df3eb3e0aed5c1107bb0fdb80a8e82e778114958b1c5ac5644fb1ac9cae8a998 url: "https://pub.dev" source: hosted - version: "4.2.0" + version: "8.1.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" path: dependency: "direct main" description: @@ -1013,26 +1117,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "477184d672607c0a3bf68fbbf601805f92ef79c82b64b4d6eb318cbca4c48668" + sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.2.12" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "5a7999be66e000916500be4f15a3633ebceb8302719b47b9cc49ce924125350f" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" path_provider_linux: dependency: transitive description: @@ -1053,10 +1157,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "8bc9f22eee8690981c22aa7fc602f5c85b497a6fb2ceb35ee5a5e5ed85ad8170" + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.0" percent_indicator: dependency: "direct main" description: @@ -1069,42 +1173,50 @@ packages: dependency: "direct main" description: name: permission_handler - sha256: bc56bfe9d3f44c3c612d8d393bd9b174eb796d706759f9b495ac254e4294baa5 + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" url: "https://pub.dev" source: hosted - version: "10.4.5" + version: "11.3.1" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "59c6322171c29df93a22d150ad95f3aa19ed86542eaec409ab2691b8f35f9a47" + sha256: "71bbecfee799e65aff7c744761a57e817e73b738fedf62ab7afd5593da21f9f1" url: "https://pub.dev" source: hosted - version: "10.3.6" + version: "12.0.13" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: "99e220bce3f8877c78e4ace901082fb29fa1b4ebde529ad0932d8d664b34f3f5" + sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 url: "https://pub.dev" source: hosted - version: "9.1.4" + version: "9.4.5" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: af26edbbb1f2674af65a8f4b56e1a6f526156bc273d0e65dd8075fab51c78851 + url: "https://pub.dev" + source: hosted + version: "0.1.3+2" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: "6760eb5ef34589224771010805bea6054ad28453906936f843a8cc4d3a55c4a4" + sha256: e9c8eadee926c4532d0305dff94b85bf961f16759c3af791486613152af4b4f9 url: "https://pub.dev" source: hosted - version: "3.12.0" + version: "4.2.3" permission_handler_windows: dependency: transitive description: name: permission_handler_windows - sha256: cc074aace208760f1eee6aa4fae766b45d947df85bc831cde77009cdb4720098 + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.2.1" petitparser: dependency: transitive description: @@ -1117,10 +1229,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -1129,14 +1241,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "43ac87de6e10afabc85c445745a7b799e04de84cebaa4fd7bf55a5e1e9604d29" - url: "https://pub.dev" - source: hosted - version: "3.7.4" pool: dependency: transitive description: @@ -1157,10 +1261,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" punycode: dependency: "direct main" description: @@ -1173,10 +1277,10 @@ packages: dependency: "direct main" description: name: receive_sharing_intent - sha256: "912bebb551bce75a14098891fd750305b30d53eba0d61cc70cd9973be9866e8d" + sha256: "252e5b5018aebfa93a068bdf08dc58152b3ac5958a22b1027e9ccbbe71912115" url: "https://pub.dev" source: hosted - version: "1.4.5" + version: "1.5.4" reorderables: dependency: "direct main" description: @@ -1197,74 +1301,58 @@ packages: dependency: transitive description: name: riverpod - sha256: "942999ee48b899f8a46a860f1e13cee36f2f77609eb54c5b7a669bb20d550b11" - url: "https://pub.dev" - source: hosted - version: "2.4.9" - rxdart: - dependency: transitive - description: - name: rxdart - sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" - url: "https://pub.dev" - source: hosted - version: "0.27.7" - safe_local_storage: - dependency: transitive - description: - name: safe_local_storage - sha256: ede4eb6cb7d88a116b3d3bf1df70790b9e2038bc37cb19112e381217c74d9440 + sha256: "0f41a697a17609a7ac18e5fe0d5bdbe4c1ff7e7da6523baf46a203df0c44eaf2" url: "https://pub.dev" source: hosted - version: "1.0.2" - screen_brightness: + version: "3.0.0-dev.3" + riverpod_analyzer_utils: dependency: transitive description: - name: screen_brightness - sha256: ed8da4a4511e79422fc1aa88138e920e4008cd312b72cdaa15ccb426c0faaedd + name: riverpod_analyzer_utils + sha256: b6e782db97522de3ad797210bd3babbdb0a67da899aaa6ffbb6572108bdbf48d url: "https://pub.dev" source: hosted - version: "0.2.2+1" - screen_brightness_android: - dependency: transitive + version: "1.0.0-dev.1" + riverpod_annotation: + dependency: "direct main" description: - name: screen_brightness_android - sha256: "3df10961e3a9e968a5e076fe27e7f4741fa8a1d3950bdeb48cf121ed529d0caf" + name: riverpod_annotation + sha256: "79452c7ba2e8f48c7309c73be5aaa101eec5fe7948dfd26659b883fb276858b4" url: "https://pub.dev" source: hosted - version: "0.1.0+2" - screen_brightness_ios: - dependency: transitive + version: "3.0.0-dev.3" + riverpod_generator: + dependency: "direct dev" description: - name: screen_brightness_ios - sha256: "99adc3ca5490b8294284aad5fcc87f061ad685050e03cf45d3d018fe398fd9a2" + name: riverpod_generator + sha256: "9f3cb7b43e9151fef1cc80031b3ad9fb5d0fe64577cc18e1627061d743823213" url: "https://pub.dev" source: hosted - version: "0.1.0" - screen_brightness_macos: - dependency: transitive + version: "3.0.0-dev.11" + riverpod_lint: + dependency: "direct dev" description: - name: screen_brightness_macos - sha256: "64b34e7e3f4900d7687c8e8fb514246845a73ecec05ab53483ed025bd4a899fd" + name: riverpod_lint + sha256: "8ddb6be92f0de4704d6109405aebc7436b15b847abf0d9f647039afe48dc0050" url: "https://pub.dev" source: hosted - version: "0.1.0+1" - screen_brightness_platform_interface: + version: "3.0.0-dev.4" + rxdart: dependency: transitive description: - name: screen_brightness_platform_interface - sha256: b211d07f0c96637a15fb06f6168617e18030d5d74ad03795dd8547a52717c171 + name: rxdart + sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb" url: "https://pub.dev" source: hosted - version: "0.1.0" - screen_brightness_windows: + version: "0.27.7" + safe_local_storage: dependency: transitive description: - name: screen_brightness_windows - sha256: "9261bf33d0fc2707d8cf16339ce25768100a65e70af0fcabaf032fc12408ba86" + name: safe_local_storage + sha256: e9a21b6fec7a8aa62cc2585ff4c1b127df42f3185adbd2aca66b47abe2e80236 url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "2.0.1" screen_retriever: dependency: transitive description: @@ -1285,82 +1373,82 @@ packages: dependency: "direct main" description: name: share_plus - sha256: f74fc3f1cbd99f39760182e176802f693fa0ec9625c045561cfad54681ea93dd + sha256: ef3489a969683c4f3d0239010cc8b7a2a46543a8d139e111c06c558875083544 url: "https://pub.dev" source: hosted - version: "7.2.1" + version: "9.0.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 + sha256: "0f9e4418835d1b2c3ae78fdb918251959106cefdbc4dd43526e182f80e82f6d4" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "4.0.0" shared_preference_app_group: dependency: "direct main" description: name: shared_preference_app_group - sha256: e08d8a6b69648a1ca84c821993ad8dd8d358c17c25055ef33852014147583265 + sha256: "478ad76d03397a930a964d3b0e64f9d4fca816c83c1c059542032eafeb29b607" url: "https://pub.dev" source: hosted - version: "1.0.0+1" + version: "1.1.1" shared_preferences: dependency: "direct main" description: name: shared_preferences - sha256: "81429e4481e1ccfb51ede496e916348668fd0921627779233bd24cc3ff6abd02" + sha256: "746e5369a43170c25816cc472ee016d3a66bc13fcf430c0bc41ad7b4b2922051" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.3.2" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: "8568a389334b6e83415b6aae55378e158fbc2314e074983362d20c562780fb06" + sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.3.3" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "7708d83064f38060c7b39db12aefe449cb8cdc031d6062280087bc4cdb988f5c" + sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d" url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.5.3" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "22e2ecac9419b4246d7c22bfbbda589e3acf5c0351137d87dd2939d984d37c3b" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "7b15ffb9387ea3e237bb7a66b8a23d2147663d391cafc5c8f37b2e7b4bde5d21" + sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.4.2" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.1" shelf: dependency: transitive description: @@ -1373,10 +1461,18 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" + simple_logger: + dependency: "direct main" + description: + name: simple_logger + sha256: "1de79f22bf31e5c33b91e9e302394dac02d8269d474848d33153c3a15c08e970" + url: "https://pub.dev" + source: hosted + version: "1.10.0" sky_engine: dependency: transitive description: flutter @@ -1406,24 +1502,56 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" sqflite: dependency: transitive description: name: sqflite - sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + sha256: "79a297dc3cc137e758c6a4baf83342b039e5a6d2436fcdf3f96a00adaaf2ad62" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" + sqflite_android: + dependency: transitive + description: + name: sqflite_android + sha256: "78f489aab276260cdd26676d2169446c7ecd3484bbd5fead4ca14f3ed4dd9ee3" + url: "https://pub.dev" + source: hosted + version: "2.4.0" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: bb4738f15b23352822f4c42a531677e5c6f522e079461fd240ead29d8d8a54a6 + sha256: "4468b24876d673418a7b7147e5a08a715b4998a7ae69227acafaab762e0e5490" url: "https://pub.dev" source: hosted - version: "2.5.0+2" - stack_trace: + version: "2.5.4+5" + sqflite_darwin: + dependency: transitive + description: + name: sqflite_darwin + sha256: "769733dddf94622d5541c73e4ddc6aa7b252d865285914b6fcd54a63c4b4f027" + url: "https://pub.dev" + source: hosted + version: "2.4.1-1" + sqflite_platform_interface: dependency: transitive + description: + name: sqflite_platform_interface + sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" + url: "https://pub.dev" + source: hosted + version: "2.4.0" + stack_trace: + dependency: "direct main" description: name: stack_trace sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" @@ -1466,10 +1594,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" + sha256: "69fe30f3a8b04a0be0c15ae6490fc859a78ef4c43ae2dd5e8a623d45bfcf9225" url: "https://pub.dev" source: hosted - version: "3.1.0+1" + version: "3.3.0+3" term_glyph: dependency: transitive description: @@ -1482,10 +1610,10 @@ packages: dependency: transitive description: name: test_api - sha256: "5c2f730018264d276c20e4f1503fd1308dfbbae39ec8ee63c5236311ac06954b" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.6.1" + version: "0.7.2" timing: dependency: transitive description: @@ -1506,130 +1634,130 @@ packages: dependency: "direct main" description: name: twemoji_v2 - sha256: e573f62ef26b1261c6a5e027096187a9b49da77d14bdeb54b16a55bd1127d482 + sha256: "1894d08626e6f07cb3dcd6286855407b322c157445008b54f683058fb868e1fb" url: "https://pub.dev" source: hosted - version: "0.5.3" + version: "0.6.0" typed_data: dependency: transitive description: name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.4.0" universal_platform: dependency: transitive description: name: universal_platform - sha256: d315be0f6641898b280ffa34e2ddb14f3d12b1a37882557869646e0cc363d0cc + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" url: "https://pub.dev" source: hosted - version: "1.0.0+1" + version: "1.1.0" uri_parser: dependency: transitive description: name: uri_parser - sha256: "6543c9fd86d2862fac55d800a43e67c0dcd1a41677cb69c2f8edfe73bbcf1835" + sha256: ff4d2c720aca3f4f7d5445e23b11b2d15ef8af5ddce5164643f38ff962dcb270 url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "3.0.0" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: d25bb0ca00432a5e1ee40e69c36c85863addf7cc45e433769d61bed3fe81fd96 + sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" url: "https://pub.dev" source: hosted - version: "6.2.3" + version: "6.3.1" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + sha256: "8fc3bae0b68c02c47c5c86fa8bfa74471d42687b0eded01b78de87872db745e2" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.12" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.3.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + sha256: e2b9622b4007f97f504cd64c0128309dfb978ae66adbe944125ed9e1750f06af url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.2.0" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "769549c999acdb42b8bcfa7c43d72bf79a382ca7441ab18a808e101149daf672" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.1" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: a932c3a8082e118f80a475ce692fde89dc20fddb24c57360b96bc56f7035de1f + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "44cf3aabcedde30f2dba119a9dea3b0f2672fbe6fa96e85536251d678216b3c4" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.3" uuid: dependency: "direct main" description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.5.1" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "18f6690295af52d081f6808f2f7c69f0eed6d7e23a71539d75f4aeb8f0062172" + sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_graphics_codec: dependency: transitive description: name: vector_graphics_codec - sha256: "531d20465c10dfac7f5cd90b60bbe4dd9921f1ec4ca54c83ebb176dbacb7bb2d" + sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_graphics_compiler: dependency: transitive description: name: vector_graphics_compiler - sha256: "03012b0a33775c5530576b70240308080e1d5050f0faf000118c20e6463bc0ad" + sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" url: "https://pub.dev" source: hosted - version: "1.1.9+2" + version: "1.1.11+1" vector_math: dependency: transitive description: @@ -1658,34 +1786,34 @@ packages: dependency: transitive description: name: vm_service - sha256: b3d56ff4341b8f182b96aceb2fa20e3dcb336b9f867bc0eafc0de10f1048e957 + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "13.0.0" + version: "14.2.5" volume_controller: dependency: "direct main" description: name: volume_controller - sha256: "189bdc7a554f476b412e4c8b2f474562b09d74bc458c23667356bce3ca1d48c9" + sha256: c71d4c62631305df63b72da79089e078af2659649301807fa746088f365cb48e url: "https://pub.dev" source: hosted - version: "2.0.7" + version: "2.0.8" wakelock_plus: dependency: transitive description: name: wakelock_plus - sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d + sha256: bf4ee6f17a2fa373ed3753ad0e602b7603f8c75af006d5b9bdade263928c0484 url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.8" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385" + sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" watcher: dependency: transitive description: @@ -1698,82 +1826,82 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.5" webview_flutter: dependency: "direct main" description: name: webview_flutter - sha256: "71e1bfaef41016c8d5954291df5e9f8c6172f1f6ff3af01b5656456ddb11f94c" + sha256: ec81f57aa1611f8ebecf1d2259da4ef052281cb5ad624131c93546c79ccc7736 url: "https://pub.dev" source: hosted - version: "4.4.4" + version: "4.9.0" webview_flutter_android: dependency: "direct main" description: name: webview_flutter_android - sha256: "161af93c2abaf94ef2192bffb53a3658b2d721a3bf99b69aa1e47814ee18cc96" + sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558" url: "https://pub.dev" source: hosted - version: "3.13.2" + version: "3.16.9" webview_flutter_platform_interface: dependency: transitive description: name: webview_flutter_platform_interface - sha256: "80b40ae4fb959957eef9fa8970b6c9accda9f49fc45c2b75154696a8e8996cfe" + sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d url: "https://pub.dev" source: hosted - version: "2.9.1" + version: "2.10.0" webview_flutter_wkwebview: dependency: "direct main" description: name: webview_flutter_wkwebview - sha256: "4d062ad505390ecef1c4bfb6001cd857a51e00912cc9dfb66edb1886a9ebd80c" + sha256: d4034901d96357beb1b6717ebf7d583c88e40cfc6eb85fe76dd1bf0979a9f251 url: "https://pub.dev" source: hosted - version: "3.10.2" + version: "3.16.0" win32: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: e5c39a90447e7c81cfec14b041cdbd0d0916bd9ebbc7fe02ab69568be703b9bd url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.6.0" win32_registry: dependency: transitive description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "21ec76dfc731550fd3e2ce7a33a9ea90b828fdf19a5c3bcf556fa992cfa99852" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.5" window_manager: dependency: "direct main" description: name: window_manager - sha256: b3c895bdf936c77b83c5254bec2e6b3f066710c1f89c38b20b8acc382b525494 + sha256: "8699323b30da4cdbe2aa2e7c9de567a6abd8a97d9a5c850a3c86dcd0b34bbfbf" url: "https://pub.dev" source: hosted - version: "0.3.8" + version: "0.3.9" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.1.0" xml: dependency: transitive description: @@ -1791,5 +1919,5 @@ packages: source: hosted version: "3.1.2" sdks: - dart: ">=3.2.0 <4.0.0" - flutter: ">=3.16.0" + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/pubspec.yaml b/pubspec.yaml index 4fbe226b8..f245e0733 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: miria description: Miria is Misskey Client for Mobile App. publish_to: 'none' # Remove this line if you wish to publish to pub.dev -version: 1.0.18+100 +version: 2.0.0+112 environment: sdk: '>=3.0.0 <4.0.0' @@ -13,8 +13,7 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - flutter_html: 3.0.0-alpha.6 - flutter_riverpod: ^2.3.2 + flutter_html: ^3.0.0-beta.2 misskey_dart: git: https://github.com/shiosyakeyakini-info/misskey_dart.git mfm: ^1.0.5 @@ -22,21 +21,21 @@ dependencies: path_provider: ^2.0.14 dio: ^5.1.1 path: ^1.8.2 - auto_route: ^6.0.5 + auto_route: ^8.1.3 freezed_annotation: ^2.2.0 url_launcher: ^6.1.10 - intl: ^0.18.0 + intl: ^0.19.0 scrollable_positioned_list: ^0.3.6 kana_kit: ^2.0.0 cached_network_image: ^3.2.3 json_annotation: ^4.8.0 shared_preferences: ^2.1.0 badges: ^3.1.1 - flutter_secure_storage: ^8.0.0 + flutter_secure_storage: ^9.2.2 google_fonts: ^6.1.0 - uuid: ^3.0.7 - file_picker: ^5.3.0 - package_info_plus: ^4.0.0 + uuid: ^4.4.0 + file_picker: ^8.0.3 + package_info_plus: ^8.0.0 reorderables: ^0.6.0 confetti: ^0.7.0 flutter_highlighting: ^0.9.0+11.8.0 @@ -44,32 +43,40 @@ dependencies: dotted_border: ^2.0.0+3 visibility_detector: ^0.4.0+2 percent_indicator: ^4.2.3 - fl_chart: ^0.62.0 - receive_sharing_intent: ^1.4.5 - share_plus: ^7.0.2 + fl_chart: ^0.68.0 + receive_sharing_intent: 1.5.4 + share_plus: ^9.0.0 mfm_parser: ^1.0.3 flutter_svg: ^2.0.6 image_editor: ^1.3.0 json5: ^0.8.0 - file: ^6.1.4 + file: ^7.0.0 image_gallery_saver: ^2.0.3 - permission_handler: ^10.4.2 - device_info_plus: ^9.0.2 + permission_handler: ^11.3.1 + device_info_plus: ^10.1.0 colorfilter_generator: ^0.0.8 matrix2d: ^1.0.4 - twemoji_v2: ^0.5.3 + # TODO: DISCONTINUED + twemoji_v2: ^0.6.0 flutter_image_compress: ^2.0.4 webview_flutter: ^4.3.0 webview_flutter_android: ^3.12.0 webview_flutter_wkwebview: ^3.9.0 - media_kit: ^1.1.8+2 - media_kit_video: ^1.2.1 - media_kit_libs_video: ^1.0.3 - flutter_colorpicker: ^1.0.3 + media_kit: ^1.1.11 + media_kit_video: ^1.2.5 + media_kit_libs_video: ^1.0.5 + flutter_colorpicker: ^1.1.0 volume_controller: ^2.0.7 window_manager: ^0.3.8 shared_preference_app_group: ^1.0.0+1 + stack_trace: ^1.11.1 + flutter_cache_manager: ^3.3.2 + riverpod_annotation: ^3.0.0-dev.3 + simple_logger: ^1.9.0+3 + hooks_riverpod: ^3.0.0-dev.3 + flutter_hooks: ^0.20.5 punycode: ^1.0.0 + image: ^4.3.0 dependency_overrides: image_editor: @@ -84,20 +91,35 @@ dependency_overrides: git: url: https://github.com/shiosyakeyakini-info/flutter_image_editor_fix_ios_color_option.git path: ./image_editor_platform_interface/ - + image_gallery_saver: + git: + url: https://github.com/knottx/image_gallery_saver.git + ref: knottx-latest + media_kit: + git: + url: https://github.com/4ster1sk/media-kit.git + ref: 28292926cfd1e75611f7dcf39248124197d131cf + path: ./media_kit/ + media_kit_video: + git: + url: https://github.com/4ster1sk/media-kit.git + ref: 28292926cfd1e75611f7dcf39248124197d131cf + path: ./media_kit_video/ dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^2.0.0 - auto_route_generator: ^6.0.0 + flutter_lints: ^4.0.0 + auto_route_generator: ^8.0.0 build_runner: ^2.3.3 freezed: ^2.3.2 json_serializable: ^6.6.1 - flutter_launcher_icons: ^0.13.1 + flutter_launcher_icons: ^0.14.1 mockito: ^5.4.2 cider: ^0.2.3 + riverpod_generator: ^3.0.0-dev.3 + riverpod_lint: ^3.0.0-dev.4 flutter: uses-material-design: true @@ -107,11 +129,12 @@ flutter: - assets/images/ flutter_launcher_icons: - android: "launcher_icon" + android: true ios: true image_path: "assets/images/icon.png" - adaptive_icon_background: "#ffffff" - adaptive_icon_foreground: "assets/images/icon_adaptive.png" + adaptive_icon_background: "#ace601" + adaptive_icon_foreground: "assets/images/icon_adaptive_foreground.png" + adaptive_icon_monochrome: "assets/images/icon_adaptive_monochrome.png" # Not working? min_sdk_android: 21 windows: generate: true diff --git a/snap/gui/miria.desktop b/snap/gui/miria.desktop new file mode 100644 index 000000000..58a7039b8 --- /dev/null +++ b/snap/gui/miria.desktop @@ -0,0 +1,15 @@ +[Desktop Entry] +Version=1.0 +Name=Miria +GenericName=Misskey Client App +GenericName[ja]=Misskeyクライアントアプリ +Type=Application +Exec=miria +Icon=${SNAP}/meta/gui/icon.png +Comment=Misskey client app for mobile (Linux build) +Comment[ja]=モバイル向けのMisskeyクライアントアプリ(Linux向けビルド) +Keywords=Misskey;Miria;みりあ +Terminal=false +StartupNotify=false +StartupWMClass=miria +Categories=InstantMessaging;Network;GTK diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml new file mode 100644 index 000000000..d36acbed0 --- /dev/null +++ b/snap/snapcraft.yaml @@ -0,0 +1,114 @@ +name: miria # 'snapcraft register miria'で名前を予約する必要あり +title: Miria +type: app +summary: Misskey Client App # 79文字まで +description: | + Miria is Misskey Client App for iOS, Android and many targets which made by Flutter. + Miria includes these features. + - Login, Logout, Management multiple servers and accounts + - Show home, local, hybrid(social), global timelines, list, antenna and channel + - Show note with MFM (Completely Supported). + - Note, Renote, Quoted renote, renote to any channel + - Reaction + - Show Notifications + - Edit antenna and list + - Explore + - Announcements + - Favorite + - Search notes and users + - Page (show only) + - Show Server Information (online users, job queue, ads, custom emojis) +license: AGPL-3.0 +website: https://shiosyakeyakini.info/miria_web +source-code: https://github.com/shiosyakeyakini-info/miria +issues: https://github.com/shiosyakeyakini-info/miria/issues +donation: https://shiosyakeyakini.fanbox.cc +contact: sorairo@shiosyakeyakini.info +icon: assets/images/icon.png +adopt-info: miria +base: core22 +grade: stable +confinement: strict +compression: lzo # 起動速度の向上(xz比) + +apps: + miria: + command: miria + extensions: [gnome] + plugs: + - home + - unity7 + - network + - audio-playback + # 以下はユーザーが接続を許可するまで使用不可 + - removable-media # カメラのSDカードから直接取り込むユーザー向け + - password-manager-service # ログイン情報の保存&読み込みのため必須 + environment: + LD_LIBRARY_PATH: $LD_LIBRARY_PATH:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/blas:$SNAP/usr/lib/$CRAFT_ARCH_TRIPLET_BUILD_FOR/lapack + +parts: + miria: + source: . + #plugin: flutter + plugin: nil + build-environment: + - PATH: "$CRAFT_PART_BUILD/FlutterSDK/bin:$PATH" + build-packages: + - libmpv-dev + - libsecret-1-dev + # flutterプラグインで使用されるパッケージ + - clang + - curl + - git + - cmake + - ninja-build + - unzip + - jq + stage-packages: + - libmpv1 + - libsecret-1-0 + override-pull: | + craftctl default + craftctl set version=$(cat pubspec.yaml | grep "version[:]" | cut -d " " -f 2) + override-build: | + # flutterプラグインの処理を代替 + set +e + rm -rf $CRAFT_PART_BUILD/FlutterSDK + git clone --depth 1 -b $(jq -r .flutter .fvmrc) https://github.com/flutter/flutter.git $CRAFT_PART_BUILD/FlutterSDK + flutter precache --linux + flutter pub get + set -e + #flutter pub run build_runner build --delete-conflicting-outputs + flutter build linux --release --verbose --target lib/main.dart + cp -r build/linux/*/release/bundle/* $CRAFT_PART_INSTALL/ + + zenity: + # Integrate custom dialogs in your snap - doc - snapcraft.io + # https://forum.snapcraft.io/t/integrate-custom-dialogs-in-your-snap/10825 + plugin: nil + stage-packages: + - zenity + prime: + - usr/bin/zenity + - usr/share/zenity/* + - usr/share/doc/*/copyright* + + cleanup: + after: [miria, zenity] # Make this part run last; list all your other parts here + plugin: nil + build-snaps: [gnome-42-2204, gtk-common-themes, core22] # List all content-snaps you're using here + override-prime: | + set -eux + for snap in "gnome-42-2204" "gtk-common-themes" "core22"; do # List all content-snaps you're using here + cd "/snap/$snap/current" && find . -type f,l -exec rm -f "$SNAPCRAFT_PRIME/{}" "$SNAPCRAFT_PRIME/usr/{}" \; + done + +layout: + # Fix resource relocation problem of zenity part + /usr/share/zenity: + symlink: $SNAP/usr/share/zenity + +lint: + ignore: + - library: + - lib/libmedia_kit_native_event_loop.so diff --git a/test/repository/account_repository/auth_test_data.dart b/test/repository/account_repository/auth_test_data.dart index d29152224..938b1230e 100644 --- a/test/repository/account_repository/auth_test_data.dart +++ b/test/repository/account_repository/auth_test_data.dart @@ -3,13 +3,13 @@ class AuthTestData { "links": [ { "rel": "http://nodeinfo.diaspora.software/ns/schema/2.1", - "href": "https://calckey.jp/nodeinfo/2.1" + "href": "https://calckey.jp/nodeinfo/2.1", }, { "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0", - "href": "https://calckey.jp/nodeinfo/2.0" + "href": "https://calckey.jp/nodeinfo/2.0", } - ] + ], }; static Map calckeyNodeInfo2 = { @@ -17,18 +17,18 @@ class AuthTestData { "software": { "name": "calckey", "version": "14.0.0-rc2c-jp3", - "homepage": "https://calckey.org/" + "homepage": "https://calckey.org/", }, "protocols": ["activitypub"], "services": { "inbound": [], - "outbound": ["atom1.0", "rss2.0"] + "outbound": ["atom1.0", "rss2.0"], }, "openRegistrations": false, "usage": { "users": {"total": 447, "activeHalfyear": 445, "activeMonth": 237}, "localPosts": 53977, - "localComments": 0 + "localComments": 0, }, "metadata": { "nodeName": "Calckey.jp", @@ -54,17 +54,17 @@ class AuthTestData { "enableEmail": true, "enableServiceWorker": true, "proxyAccountName": "proxy", - "themeColor": "#31748f" - } + "themeColor": "#31748f", + }, }; static Map oldVerMisskeyNodeInfo = { "links": [ { "rel": "http://nodeinfo.diaspora.software/ns/schema/2.0", - "href": "https://misskey.dev/nodeinfo/2.0" + "href": "https://misskey.dev/nodeinfo/2.0", } - ] + ], }; static Map oldVerMisskeyNodeInfo2 = { @@ -73,7 +73,7 @@ class AuthTestData { "protocols": ["activitypub"], "services": { "inbound": [], - "outbound": ["atom1.0", "rss2.0"] + "outbound": ["atom1.0", "rss2.0"], }, "openRegistrations": true, "usage": {"users": {}}, @@ -91,25 +91,25 @@ class AuthTestData { "text": "✨ 最新のお知らせは ?[#misskeydevinfo](https://misskey.dev/tags/misskeydevinfo)をご確認ください :smiling_ai:\n**【必ずご確認ください】[サーバーの移行に伴う重要なお知らせ](https://misskey.dev/notes/9e6e8didf7)**\n\n詳しい情報は ?[info](https://misskey.dev/@cv_k/pages/info) ページをご確認ください\n運営費の寄付・支援は https://fantia.jp/takimura で受付しています。\n\nアイコンの変更方法は https://misskey.dev/notes/8w71u4lo9w をご覧ください。", "image": null, - "title": "Misskeyへようこそ" + "title": "Misskeyへようこそ", }, { "text": "⭐Approved as a pinned account\n⭐More request to add custom icons(free user:1icon per month)\n⭐User name posted on ?[info](https://misskey.dev/@cv_k/pages/info)page\n
[*** Donate ***](https://liberapay.com/misskey.dev)
\n⭐ピン留めアカウントとして公認\n⭐より多くのカスタム絵文字の追加リクエスト(非会員は一ヶ月につき1個まで)\n⭐?[info](https://misskey.dev/@cv_k/pages/info)ページにてユーザー名の掲載\n\n支払いにはクレジットカード及びコンビニ決済をご利用いただけます。\n**New** Premiumの特典がないお得な100円プラン始めました。✌✌✌\n\n
[*** 詳細・登録はこちら ***](https://fantia.jp/takimura)
\nFantia招待コード:9A848327\n\n以下からBraveを30日間ご利用いただくことでも支援していただけます!\nhttps://brave.com/mis911", "image": null, - "title": "misskey.dev Premium" + "title": "misskey.dev Premium", }, { "text": "How to Use \nhttps://misskey.dev/notes/5c79e2a0fe0a36003970239f\nTerms of service\nhttps://misskey.dev/@cv_k/pages/tos\nInfo\nhttps://misskey.dev/@cv_k/pages/info\n
-----
使い方\nhttps://misskey.dev/notes/5c79e505c9c298003288f8c8\n利用規約\nhttps://misskey.dev/@cv_k/pages/tos\nInfoページ\nhttps://misskey.dev/@cv_k/pages/info", "image": null, - "title": "Misskey Information" + "title": "Misskey Information", }, { "text": "カスタム絵文字の依頼について:128x128px以上のpngもしくはsvg画像を添付し、 @cv_k までReplyまたはDMしてください。追加の検討を致します。\n\nRegarding local accounts and remote accounts : Accounts that repeat posts that violate the laws of Japan are freezed.\n
-----
アカウント/リモートアカウントに関して : 日本国の法律に抵触する投稿を繰り返し行うアカウントは凍結されます。", "image": null, - "title": "Misskey Information 2" + "title": "Misskey Information 2", } ], "disableRegistration": false, @@ -121,8 +121,8 @@ class AuthTestData { "enableGithubIntegration": true, "enableDiscordIntegration": true, "enableEmail": false, - "enableServiceWorker": true - } + "enableServiceWorker": true, + }, }; static String oldVerMisskeyMeta = r''' diff --git a/test/repository/account_repository/open_mi_auth_test.dart b/test/repository/account_repository/open_mi_auth_test.dart index e8f9cd2a1..d22953502 100644 --- a/test/repository/account_repository/open_mi_auth_test.dart +++ b/test/repository/account_repository/open_mi_auth_test.dart @@ -1,16 +1,16 @@ -import 'dart:convert'; +import "dart:convert"; -import 'package:dio/dio.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/repository/account_repository.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; +import "package:dio/dio.dart"; +import "package:flutter_test/flutter_test.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/account_repository.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; -import '../../test_util/mock.mocks.dart'; -import '../../test_util/test_datas.dart'; -import 'auth_test_data.dart'; +import "../../test_util/mock.mocks.dart"; +import "../../test_util/test_datas.dart"; +import "auth_test_data.dart"; void main() { test("誤ったホスト名を入力するとエラーを返すこと", () async { @@ -19,23 +19,38 @@ void main() { ); final accountRepository = provider.read(accountRepositoryProvider.notifier); - expect(() => accountRepository.openMiAuth("https://misskey.io/"), - throwsA(isA())); + expect( + () => accountRepository.openMiAuth("https://misskey.io/"), + throwsA(isA()), + ); }); test("Activity Pub非対応サーバーの場合、エラーを返すこと", () { final dio = MockDio(); + // ignore: discarded_futures when(dio.getUri(any)).thenAnswer((_) async => throw TestData.response404); final provider = ProviderContainer(overrides: [dioProvider.overrideWithValue(dio)]); final accountRepository = provider.read(accountRepositoryProvider.notifier); - expect(() async => await accountRepository.openMiAuth("sawakai.space"), - throwsA(isA())); - verify(dio.getUri(argThat(equals(Uri( - scheme: "https", - host: "sawakai.space", - pathSegments: [".well-known", "nodeinfo"]))))); + expect( + () async => await accountRepository.openMiAuth("sawakai.space"), + throwsA(isA()), + ); + verify( + // ignore: discarded_futures + dio.getUri( + argThat( + equals( + Uri( + scheme: "https", + host: "sawakai.space", + pathSegments: [".well-known", "nodeinfo"], + ), + ), + ), + ), + ); }); // test("非対応のソフトウェアの場合、エラーを返すこと", () async { @@ -48,7 +63,7 @@ void main() { // final provider = ProviderContainer( // overrides: [ // dioProvider.overrideWithValue(dio), - // misskeyProvider.overrideWith((ref, arg) => mockMisskey), + // misskeyProvider.overrideWith((ref) => mockMisskey), // ], // ); // final accountRepository = provider.read(accountRepositoryProvider.notifier); @@ -68,27 +83,36 @@ void main() { test("Misskeyの場合でも、バージョンが古い場合、エラーを返すこと", () async { final dio = MockDio(); - when(dio.getUri(any)).thenAnswer((_) async => Response( + when(dio.getUri(any)).thenAnswer( + (_) async => Response( requestOptions: RequestOptions(), - data: AuthTestData.oldVerMisskeyNodeInfo)); - when(dio.get(any)).thenAnswer((realInvocation) async => Response( + data: AuthTestData.oldVerMisskeyNodeInfo, + ), + ); + when(dio.get(any)).thenAnswer( + (realInvocation) async => Response( requestOptions: RequestOptions(), - data: AuthTestData.oldVerMisskeyNodeInfo2)); + data: AuthTestData.oldVerMisskeyNodeInfo2, + ), + ); final mockMisskey = MockMisskey(); when(mockMisskey.endpoints()).thenAnswer((_) async => []); - when(mockMisskey.meta()).thenAnswer((_) async => - MetaResponse.fromJson(jsonDecode(AuthTestData.oldVerMisskeyMeta))); + when(mockMisskey.meta()).thenAnswer( + (_) async => + MetaResponse.fromJson(jsonDecode(AuthTestData.oldVerMisskeyMeta)), + ); final provider = ProviderContainer( overrides: [ dioProvider.overrideWithValue(dio), - misskeyProvider.overrideWith((ref, arg) => mockMisskey), - misskeyWithoutAccountProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), + misskeyWithoutAccountProvider.overrideWith((ref) => mockMisskey), ], ); final accountRepository = provider.read(accountRepositoryProvider.notifier); await expectLater( - () async => await accountRepository.openMiAuth("misskey.dev"), - throwsA(isA())); + () async => await accountRepository.openMiAuth("misskey.dev"), + throwsA(isA()), + ); }); } diff --git a/test/test_util/default_root_widget.dart b/test/test_util/default_root_widget.dart index 8ee8aba07..e6400808b 100644 --- a/test/test_util/default_root_widget.dart +++ b/test/test_util/default_root_widget.dart @@ -1,12 +1,13 @@ -import 'package:auto_route/auto_route.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_gen/gen_l10n/app_localizations.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; -import 'package:miria/main.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/error_dialog_listener.dart'; -import 'package:miria/view/common/sharing_intent_listener.dart'; -import 'package:miria/view/themes/app_theme_scope.dart'; +import "package:auto_route/auto_route.dart"; +import "package:flutter/material.dart"; +import "package:flutter_gen/gen_l10n/app_localizations.dart"; +import "package:flutter_localizations/flutter_localizations.dart"; +import "package:miria/main.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/dialog/dialog_scope.dart"; +import "package:miria/view/common/error_dialog_listener.dart"; +import "package:miria/view/common/sharing_intent_listener.dart"; +import "package:miria/view/themes/app_theme_scope.dart"; class DefaultRootWidget extends StatefulWidget { final AppRouter? router; @@ -46,15 +47,18 @@ class DefaultRootWidgetState extends State { child: SharingIntentListener( router: router, child: ErrorDialogListener( - child: widget ?? Container(), + child: DialogScope( + child: widget ?? Container(), + ), ), ), ); }, routerConfig: router.config( - deepLinkBuilder: widget.initialRoute != null - ? (_) => DeepLink([widget.initialRoute!]) - : null), + deepLinkBuilder: widget.initialRoute != null + ? (_) => DeepLink([widget.initialRoute!]) + : null, + ), ); } } @@ -62,7 +66,7 @@ class DefaultRootWidgetState extends State { class DefaultRootNoRouterWidget extends StatelessWidget { final Widget child; - const DefaultRootNoRouterWidget({super.key, required this.child}); + const DefaultRootNoRouterWidget({required this.child, super.key}); @override Widget build(BuildContext context) { diff --git a/test/test_util/mock.dart b/test/test_util/mock.dart index e6a003c62..cbaa2be3b 100644 --- a/test/test_util/mock.dart +++ b/test/test_util/mock.dart @@ -1,19 +1,19 @@ -import 'dart:io'; +import "dart:io"; -import 'package:dio/dio.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:miria/repository/account_repository.dart'; -import 'package:miria/repository/account_settings_repository.dart'; -import 'package:miria/repository/emoji_repository.dart'; -import 'package:miria/repository/general_settings_repository.dart'; -import 'package:miria/repository/note_repository.dart'; -import 'package:miria/repository/tab_settings_repository.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; -import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; +import "package:dio/dio.dart"; +import "package:file_picker/file_picker.dart"; +import "package:flutter_cache_manager/flutter_cache_manager.dart"; +import "package:miria/repository/account_repository.dart"; +import "package:miria/repository/account_settings_repository.dart"; +import "package:miria/repository/emoji_repository.dart"; +import "package:miria/repository/general_settings_repository.dart"; +import "package:miria/repository/note_repository.dart"; +import "package:miria/repository/tab_settings_repository.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/annotations.dart"; +import "package:mockito/mockito.dart"; +import "package:plugin_platform_interface/plugin_platform_interface.dart"; +import "package:url_launcher_platform_interface/url_launcher_platform_interface.dart"; @GenerateNiceMocks([ // レポジトリ @@ -49,14 +49,14 @@ import 'package:url_launcher_platform_interface/url_launcher_platform_interface. // プラグインとか MockSpec(), MockSpec(), - MockSpec(), - MockSpec(), + MockSpec(), + MockSpec(), MockSpec(as: #MockFilePickerPlatform), MockSpec<$MockBaseCacheManager>(as: #MockBaseCacheManager), MockSpec<$MockUrlLauncherPlatform>(as: #MockUrlLauncherPlatform), ]) // ignore: unused_import -import 'mock.mocks.dart'; +import "mock.mocks.dart"; class $MockBaseCacheManager extends Mock implements BaseCacheManager {} diff --git a/test/test_util/mock.mocks.dart b/test/test_util/mock.mocks.dart index eebccb561..6b0aba293 100644 --- a/test/test_util/mock.mocks.dart +++ b/test/test_util/mock.mocks.dart @@ -3,46 +3,38 @@ // Do not manually edit this file. // ignore_for_file: no_leading_underscores_for_library_prefixes -import 'dart:async' as _i18; -import 'dart:collection' as _i31; -import 'dart:io' as _i12; -import 'dart:typed_data' as _i28; -import 'dart:ui' as _i19; - -import 'package:dio/dio.dart' as _i11; -import 'package:file/file.dart' as _i14; -import 'package:file_picker/file_picker.dart' as _i36; -import 'package:flutter_cache_manager/flutter_cache_manager.dart' as _i15; -import 'package:flutter_riverpod/flutter_riverpod.dart' as _i4; -import 'package:miria/model/account.dart' as _i6; +import 'dart:async' as _i15; +import 'dart:io' as _i10; +import 'dart:typed_data' as _i25; +import 'dart:ui' as _i16; + +import 'package:dio/dio.dart' as _i9; +import 'package:file/file.dart' as _i11; +import 'package:file_picker/file_picker.dart' as _i28; +import 'package:flutter_cache_manager/flutter_cache_manager.dart' as _i12; +import 'package:miria/model/account.dart' as _i7; import 'package:miria/model/account_settings.dart' as _i2; -import 'package:miria/model/acct.dart' as _i21; +import 'package:miria/model/acct.dart' as _i18; import 'package:miria/model/general_settings.dart' as _i3; -import 'package:miria/model/misskey_emoji_data.dart' as _i23; -import 'package:miria/model/tab_setting.dart' as _i17; -import 'package:miria/repository/account_repository.dart' as _i25; -import 'package:miria/repository/account_settings_repository.dart' as _i20; -import 'package:miria/repository/emoji_repository.dart' as _i22; -import 'package:miria/repository/general_settings_repository.dart' as _i24; -import 'package:miria/repository/note_repository.dart' as _i26; -import 'package:miria/repository/tab_settings_repository.dart' as _i16; -import 'package:misskey_dart/misskey_dart.dart' as _i5; -import 'package:misskey_dart/src/data/ping_response.dart' as _i10; -import 'package:misskey_dart/src/data/stats_response.dart' as _i9; -import 'package:misskey_dart/src/data/streaming/streaming_request.dart' as _i30; -import 'package:misskey_dart/src/enums/broadcast_event_type.dart' as _i34; -import 'package:misskey_dart/src/enums/channel.dart' as _i29; -import 'package:misskey_dart/src/enums/channel_event_type.dart' as _i32; -import 'package:misskey_dart/src/enums/note_updated_event_type.dart' as _i33; -import 'package:misskey_dart/src/misskey_flash.dart' as _i8; -import 'package:misskey_dart/src/services/api_service.dart' as _i7; +import 'package:miria/model/misskey_emoji_data.dart' as _i20; +import 'package:miria/model/tab_setting.dart' as _i14; +import 'package:miria/repository/account_repository.dart' as _i22; +import 'package:miria/repository/account_settings_repository.dart' as _i17; +import 'package:miria/repository/emoji_repository.dart' as _i19; +import 'package:miria/repository/general_settings_repository.dart' as _i21; +import 'package:miria/repository/note_repository.dart' as _i23; +import 'package:miria/repository/shared_preference_controller.dart' as _i4; +import 'package:miria/repository/tab_settings_repository.dart' as _i13; +import 'package:misskey_dart/misskey_dart.dart' as _i6; +import 'package:misskey_dart/src/data/streaming/streaming_request.dart' as _i26; +import 'package:misskey_dart/src/services/api_service.dart' as _i8; import 'package:mockito/mockito.dart' as _i1; -import 'package:mockito/src/dummies.dart' as _i27; +import 'package:mockito/src/dummies.dart' as _i24; +import 'package:riverpod_annotation/riverpod_annotation.dart' as _i5; import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart' - as _i37; -import 'package:web_socket_channel/web_socket_channel.dart' as _i13; + as _i29; -import 'mock.dart' as _i35; +import 'mock.dart' as _i27; // ignore_for_file: type=lint // ignore_for_file: avoid_redundant_argument_values @@ -79,9 +71,9 @@ class _FakeGeneralSettings_1 extends _i1.SmartFake ); } -class _FakeNotifierProviderRef_2 extends _i1.SmartFake - implements _i4.NotifierProviderRef { - _FakeNotifierProviderRef_2( +class _FakeSharedPreferenceController_2 extends _i1.SmartFake + implements _i4.SharedPreferenceController { + _FakeSharedPreferenceController_2( Object parent, Invocation parentInvocation, ) : super( @@ -90,8 +82,9 @@ class _FakeNotifierProviderRef_2 extends _i1.SmartFake ); } -class _FakeMisskey_3 extends _i1.SmartFake implements _i5.Misskey { - _FakeMisskey_3( +class _FakeAutoDisposeNotifierProviderRef_3 extends _i1.SmartFake + implements _i5.AutoDisposeNotifierProviderRef { + _FakeAutoDisposeNotifierProviderRef_3( Object parent, Invocation parentInvocation, ) : super( @@ -100,8 +93,8 @@ class _FakeMisskey_3 extends _i1.SmartFake implements _i5.Misskey { ); } -class _FakeAccount_4 extends _i1.SmartFake implements _i6.Account { - _FakeAccount_4( +class _FakeMisskey_4 extends _i1.SmartFake implements _i6.Misskey { + _FakeMisskey_4( Object parent, Invocation parentInvocation, ) : super( @@ -110,8 +103,8 @@ class _FakeAccount_4 extends _i1.SmartFake implements _i6.Account { ); } -class _FakeApiService_5 extends _i1.SmartFake implements _i7.ApiService { - _FakeApiService_5( +class _FakeAccount_5 extends _i1.SmartFake implements _i7.Account { + _FakeAccount_5( Object parent, Invocation parentInvocation, ) : super( @@ -120,9 +113,8 @@ class _FakeApiService_5 extends _i1.SmartFake implements _i7.ApiService { ); } -class _FakeStreamingService_6 extends _i1.SmartFake - implements _i5.StreamingService { - _FakeStreamingService_6( +class _FakeApiService_6 extends _i1.SmartFake implements _i8.ApiService { + _FakeApiService_6( Object parent, Invocation parentInvocation, ) : super( @@ -131,8 +123,9 @@ class _FakeStreamingService_6 extends _i1.SmartFake ); } -class _FakeMisskeyNotes_7 extends _i1.SmartFake implements _i5.MisskeyNotes { - _FakeMisskeyNotes_7( +class _FakeWebSocketController_7 extends _i1.SmartFake + implements _i6.WebSocketController { + _FakeWebSocketController_7( Object parent, Invocation parentInvocation, ) : super( @@ -141,9 +134,8 @@ class _FakeMisskeyNotes_7 extends _i1.SmartFake implements _i5.MisskeyNotes { ); } -class _FakeMisskeyChannels_8 extends _i1.SmartFake - implements _i5.MisskeyChannels { - _FakeMisskeyChannels_8( +class _FakeMisskeyNotes_8 extends _i1.SmartFake implements _i6.MisskeyNotes { + _FakeMisskeyNotes_8( Object parent, Invocation parentInvocation, ) : super( @@ -152,8 +144,9 @@ class _FakeMisskeyChannels_8 extends _i1.SmartFake ); } -class _FakeMisskeyUsers_9 extends _i1.SmartFake implements _i5.MisskeyUsers { - _FakeMisskeyUsers_9( +class _FakeMisskeyChannels_9 extends _i1.SmartFake + implements _i6.MisskeyChannels { + _FakeMisskeyChannels_9( Object parent, Invocation parentInvocation, ) : super( @@ -162,8 +155,8 @@ class _FakeMisskeyUsers_9 extends _i1.SmartFake implements _i5.MisskeyUsers { ); } -class _FakeMisskeyI_10 extends _i1.SmartFake implements _i5.MisskeyI { - _FakeMisskeyI_10( +class _FakeMisskeyUsers_10 extends _i1.SmartFake implements _i6.MisskeyUsers { + _FakeMisskeyUsers_10( Object parent, Invocation parentInvocation, ) : super( @@ -172,8 +165,8 @@ class _FakeMisskeyI_10 extends _i1.SmartFake implements _i5.MisskeyI { ); } -class _FakeMisskeyClips_11 extends _i1.SmartFake implements _i5.MisskeyClips { - _FakeMisskeyClips_11( +class _FakeMisskeyI_11 extends _i1.SmartFake implements _i6.MisskeyI { + _FakeMisskeyI_11( Object parent, Invocation parentInvocation, ) : super( @@ -182,9 +175,8 @@ class _FakeMisskeyClips_11 extends _i1.SmartFake implements _i5.MisskeyClips { ); } -class _FakeMisskeyAntenna_12 extends _i1.SmartFake - implements _i5.MisskeyAntenna { - _FakeMisskeyAntenna_12( +class _FakeMisskeyClips_12 extends _i1.SmartFake implements _i6.MisskeyClips { + _FakeMisskeyClips_12( Object parent, Invocation parentInvocation, ) : super( @@ -193,8 +185,9 @@ class _FakeMisskeyAntenna_12 extends _i1.SmartFake ); } -class _FakeMisskeyDrive_13 extends _i1.SmartFake implements _i5.MisskeyDrive { - _FakeMisskeyDrive_13( +class _FakeMisskeyAntenna_13 extends _i1.SmartFake + implements _i6.MisskeyAntenna { + _FakeMisskeyAntenna_13( Object parent, Invocation parentInvocation, ) : super( @@ -203,9 +196,8 @@ class _FakeMisskeyDrive_13 extends _i1.SmartFake implements _i5.MisskeyDrive { ); } -class _FakeMisskeyFollowing_14 extends _i1.SmartFake - implements _i5.MisskeyFollowing { - _FakeMisskeyFollowing_14( +class _FakeMisskeyDrive_14 extends _i1.SmartFake implements _i6.MisskeyDrive { + _FakeMisskeyDrive_14( Object parent, Invocation parentInvocation, ) : super( @@ -214,9 +206,9 @@ class _FakeMisskeyFollowing_14 extends _i1.SmartFake ); } -class _FakeMisskeyBlocking_15 extends _i1.SmartFake - implements _i5.MisskeyBlocking { - _FakeMisskeyBlocking_15( +class _FakeMisskeyFollowing_15 extends _i1.SmartFake + implements _i6.MisskeyFollowing { + _FakeMisskeyFollowing_15( Object parent, Invocation parentInvocation, ) : super( @@ -225,8 +217,9 @@ class _FakeMisskeyBlocking_15 extends _i1.SmartFake ); } -class _FakeMisskeyMute_16 extends _i1.SmartFake implements _i5.MisskeyMute { - _FakeMisskeyMute_16( +class _FakeMisskeyBlocking_16 extends _i1.SmartFake + implements _i6.MisskeyBlocking { + _FakeMisskeyBlocking_16( Object parent, Invocation parentInvocation, ) : super( @@ -235,9 +228,8 @@ class _FakeMisskeyMute_16 extends _i1.SmartFake implements _i5.MisskeyMute { ); } -class _FakeMisskeyRenoteMute_17 extends _i1.SmartFake - implements _i5.MisskeyRenoteMute { - _FakeMisskeyRenoteMute_17( +class _FakeMisskeyMute_17 extends _i1.SmartFake implements _i6.MisskeyMute { + _FakeMisskeyMute_17( Object parent, Invocation parentInvocation, ) : super( @@ -246,9 +238,9 @@ class _FakeMisskeyRenoteMute_17 extends _i1.SmartFake ); } -class _FakeMisskeyFederation_18 extends _i1.SmartFake - implements _i5.MisskeyFederation { - _FakeMisskeyFederation_18( +class _FakeMisskeyRenoteMute_18 extends _i1.SmartFake + implements _i6.MisskeyRenoteMute { + _FakeMisskeyRenoteMute_18( Object parent, Invocation parentInvocation, ) : super( @@ -257,8 +249,9 @@ class _FakeMisskeyFederation_18 extends _i1.SmartFake ); } -class _FakeMisskeyRoles_19 extends _i1.SmartFake implements _i5.MisskeyRoles { - _FakeMisskeyRoles_19( +class _FakeMisskeyFederation_19 extends _i1.SmartFake + implements _i6.MisskeyFederation { + _FakeMisskeyFederation_19( Object parent, Invocation parentInvocation, ) : super( @@ -267,9 +260,8 @@ class _FakeMisskeyRoles_19 extends _i1.SmartFake implements _i5.MisskeyRoles { ); } -class _FakeMisskeyHashtags_20 extends _i1.SmartFake - implements _i5.MisskeyHashtags { - _FakeMisskeyHashtags_20( +class _FakeMisskeyRoles_20 extends _i1.SmartFake implements _i6.MisskeyRoles { + _FakeMisskeyRoles_20( Object parent, Invocation parentInvocation, ) : super( @@ -278,8 +270,9 @@ class _FakeMisskeyHashtags_20 extends _i1.SmartFake ); } -class _FakeMisskeyAp_21 extends _i1.SmartFake implements _i5.MisskeyAp { - _FakeMisskeyAp_21( +class _FakeMisskeyHashtags_21 extends _i1.SmartFake + implements _i6.MisskeyHashtags { + _FakeMisskeyHashtags_21( Object parent, Invocation parentInvocation, ) : super( @@ -288,8 +281,8 @@ class _FakeMisskeyAp_21 extends _i1.SmartFake implements _i5.MisskeyAp { ); } -class _FakeMisskeyPages_22 extends _i1.SmartFake implements _i5.MisskeyPages { - _FakeMisskeyPages_22( +class _FakeMisskeyAp_22 extends _i1.SmartFake implements _i6.MisskeyAp { + _FakeMisskeyAp_22( Object parent, Invocation parentInvocation, ) : super( @@ -298,8 +291,8 @@ class _FakeMisskeyPages_22 extends _i1.SmartFake implements _i5.MisskeyPages { ); } -class _FakeMisskeyFlash_23 extends _i1.SmartFake implements _i8.MisskeyFlash { - _FakeMisskeyFlash_23( +class _FakeMisskeyPages_23 extends _i1.SmartFake implements _i6.MisskeyPages { + _FakeMisskeyPages_23( Object parent, Invocation parentInvocation, ) : super( @@ -308,9 +301,8 @@ class _FakeMisskeyFlash_23 extends _i1.SmartFake implements _i8.MisskeyFlash { ); } -class _FakeMisskeyReversi_24 extends _i1.SmartFake - implements _i5.MisskeyReversi { - _FakeMisskeyReversi_24( +class _FakeMisskeyFlash_24 extends _i1.SmartFake implements _i6.MisskeyFlash { + _FakeMisskeyFlash_24( Object parent, Invocation parentInvocation, ) : super( @@ -319,9 +311,9 @@ class _FakeMisskeyReversi_24 extends _i1.SmartFake ); } -class _FakeMisskeyBubbleGame_25 extends _i1.SmartFake - implements _i5.MisskeyBubbleGame { - _FakeMisskeyBubbleGame_25( +class _FakeMisskeyReversi_25 extends _i1.SmartFake + implements _i6.MisskeyReversi { + _FakeMisskeyReversi_25( Object parent, Invocation parentInvocation, ) : super( @@ -330,9 +322,9 @@ class _FakeMisskeyBubbleGame_25 extends _i1.SmartFake ); } -class _FakeEmojisResponse_26 extends _i1.SmartFake - implements _i5.EmojisResponse { - _FakeEmojisResponse_26( +class _FakeMisskeyBubbleGame_26 extends _i1.SmartFake + implements _i6.MisskeyBubbleGame { + _FakeMisskeyBubbleGame_26( Object parent, Invocation parentInvocation, ) : super( @@ -341,8 +333,9 @@ class _FakeEmojisResponse_26 extends _i1.SmartFake ); } -class _FakeEmojiResponse_27 extends _i1.SmartFake implements _i5.EmojiResponse { - _FakeEmojiResponse_27( +class _FakeEmojisResponse_27 extends _i1.SmartFake + implements _i6.EmojisResponse { + _FakeEmojisResponse_27( Object parent, Invocation parentInvocation, ) : super( @@ -351,8 +344,8 @@ class _FakeEmojiResponse_27 extends _i1.SmartFake implements _i5.EmojiResponse { ); } -class _FakeMetaResponse_28 extends _i1.SmartFake implements _i5.MetaResponse { - _FakeMetaResponse_28( +class _FakeEmojiResponse_28 extends _i1.SmartFake implements _i6.EmojiResponse { + _FakeEmojiResponse_28( Object parent, Invocation parentInvocation, ) : super( @@ -361,8 +354,8 @@ class _FakeMetaResponse_28 extends _i1.SmartFake implements _i5.MetaResponse { ); } -class _FakeStatsResponse_29 extends _i1.SmartFake implements _i9.StatsResponse { - _FakeStatsResponse_29( +class _FakeMetaResponse_29 extends _i1.SmartFake implements _i6.MetaResponse { + _FakeMetaResponse_29( Object parent, Invocation parentInvocation, ) : super( @@ -371,8 +364,8 @@ class _FakeStatsResponse_29 extends _i1.SmartFake implements _i9.StatsResponse { ); } -class _FakePingResponse_30 extends _i1.SmartFake implements _i10.PingResponse { - _FakePingResponse_30( +class _FakeStatsResponse_30 extends _i1.SmartFake implements _i6.StatsResponse { + _FakeStatsResponse_30( Object parent, Invocation parentInvocation, ) : super( @@ -381,9 +374,8 @@ class _FakePingResponse_30 extends _i1.SmartFake implements _i10.PingResponse { ); } -class _FakeServerInfoResponse_31 extends _i1.SmartFake - implements _i5.ServerInfoResponse { - _FakeServerInfoResponse_31( +class _FakePingResponse_31 extends _i1.SmartFake implements _i6.PingResponse { + _FakePingResponse_31( Object parent, Invocation parentInvocation, ) : super( @@ -392,9 +384,9 @@ class _FakeServerInfoResponse_31 extends _i1.SmartFake ); } -class _FakeGetOnlineUsersCountResponse_32 extends _i1.SmartFake - implements _i5.GetOnlineUsersCountResponse { - _FakeGetOnlineUsersCountResponse_32( +class _FakeServerInfoResponse_32 extends _i1.SmartFake + implements _i6.ServerInfoResponse { + _FakeServerInfoResponse_32( Object parent, Invocation parentInvocation, ) : super( @@ -403,9 +395,9 @@ class _FakeGetOnlineUsersCountResponse_32 extends _i1.SmartFake ); } -class _FakeSocketController_33 extends _i1.SmartFake - implements _i5.SocketController { - _FakeSocketController_33( +class _FakeGetOnlineUsersCountResponse_33 extends _i1.SmartFake + implements _i6.GetOnlineUsersCountResponse { + _FakeGetOnlineUsersCountResponse_33( Object parent, Invocation parentInvocation, ) : super( @@ -414,7 +406,7 @@ class _FakeSocketController_33 extends _i1.SmartFake ); } -class _FakeAntenna_34 extends _i1.SmartFake implements _i5.Antenna { +class _FakeAntenna_34 extends _i1.SmartFake implements _i6.Antenna { _FakeAntenna_34( Object parent, Invocation parentInvocation, @@ -425,7 +417,7 @@ class _FakeAntenna_34 extends _i1.SmartFake implements _i5.Antenna { } class _FakeApShowResponse_35 extends _i1.SmartFake - implements _i5.ApShowResponse { + implements _i6.ApShowResponse { _FakeApShowResponse_35( Object parent, Invocation parentInvocation, @@ -436,7 +428,7 @@ class _FakeApShowResponse_35 extends _i1.SmartFake } class _FakeCommunityChannel_36 extends _i1.SmartFake - implements _i5.CommunityChannel { + implements _i6.CommunityChannel { _FakeCommunityChannel_36( Object parent, Invocation parentInvocation, @@ -446,7 +438,7 @@ class _FakeCommunityChannel_36 extends _i1.SmartFake ); } -class _FakeClip_37 extends _i1.SmartFake implements _i5.Clip { +class _FakeClip_37 extends _i1.SmartFake implements _i6.Clip { _FakeClip_37( Object parent, Invocation parentInvocation, @@ -457,7 +449,7 @@ class _FakeClip_37 extends _i1.SmartFake implements _i5.Clip { } class _FakeMisskeyDriveFiles_38 extends _i1.SmartFake - implements _i5.MisskeyDriveFiles { + implements _i6.MisskeyDriveFiles { _FakeMisskeyDriveFiles_38( Object parent, Invocation parentInvocation, @@ -468,7 +460,7 @@ class _FakeMisskeyDriveFiles_38 extends _i1.SmartFake } class _FakeMisskeyDriveFolders_39 extends _i1.SmartFake - implements _i5.MisskeyDriveFolders { + implements _i6.MisskeyDriveFolders { _FakeMisskeyDriveFolders_39( Object parent, Invocation parentInvocation, @@ -478,8 +470,8 @@ class _FakeMisskeyDriveFolders_39 extends _i1.SmartFake ); } -class _FakeDriveFolder_40 extends _i1.SmartFake implements _i5.DriveFolder { - _FakeDriveFolder_40( +class _FakeDriveResponse_40 extends _i1.SmartFake implements _i6.DriveResponse { + _FakeDriveResponse_40( Object parent, Invocation parentInvocation, ) : super( @@ -488,8 +480,8 @@ class _FakeDriveFolder_40 extends _i1.SmartFake implements _i5.DriveFolder { ); } -class _FakeDriveFile_41 extends _i1.SmartFake implements _i5.DriveFile { - _FakeDriveFile_41( +class _FakeDriveFolder_41 extends _i1.SmartFake implements _i6.DriveFolder { + _FakeDriveFolder_41( Object parent, Invocation parentInvocation, ) : super( @@ -498,9 +490,8 @@ class _FakeDriveFile_41 extends _i1.SmartFake implements _i5.DriveFile { ); } -class _FakeFederationShowInstanceResponse_42 extends _i1.SmartFake - implements _i5.FederationShowInstanceResponse { - _FakeFederationShowInstanceResponse_42( +class _FakeDriveFile_42 extends _i1.SmartFake implements _i6.DriveFile { + _FakeDriveFile_42( Object parent, Invocation parentInvocation, ) : super( @@ -509,9 +500,9 @@ class _FakeFederationShowInstanceResponse_42 extends _i1.SmartFake ); } -class _FakeMisskeyFollowingRequests_43 extends _i1.SmartFake - implements _i5.MisskeyFollowingRequests { - _FakeMisskeyFollowingRequests_43( +class _FakeFederationShowInstanceResponse_43 extends _i1.SmartFake + implements _i6.FederationShowInstanceResponse { + _FakeFederationShowInstanceResponse_43( Object parent, Invocation parentInvocation, ) : super( @@ -520,8 +511,9 @@ class _FakeMisskeyFollowingRequests_43 extends _i1.SmartFake ); } -class _FakeUserLite_44 extends _i1.SmartFake implements _i5.UserLite { - _FakeUserLite_44( +class _FakeMisskeyFollowingRequests_44 extends _i1.SmartFake + implements _i6.MisskeyFollowingRequests { + _FakeMisskeyFollowingRequests_44( Object parent, Invocation parentInvocation, ) : super( @@ -530,8 +522,8 @@ class _FakeUserLite_44 extends _i1.SmartFake implements _i5.UserLite { ); } -class _FakeHashtag_45 extends _i1.SmartFake implements _i5.Hashtag { - _FakeHashtag_45( +class _FakeUserLite_45 extends _i1.SmartFake implements _i6.UserLite { + _FakeUserLite_45( Object parent, Invocation parentInvocation, ) : super( @@ -540,9 +532,8 @@ class _FakeHashtag_45 extends _i1.SmartFake implements _i5.Hashtag { ); } -class _FakeMisskeyIRegistry_46 extends _i1.SmartFake - implements _i5.MisskeyIRegistry { - _FakeMisskeyIRegistry_46( +class _FakeHashtag_46 extends _i1.SmartFake implements _i6.Hashtag { + _FakeHashtag_46( Object parent, Invocation parentInvocation, ) : super( @@ -551,8 +542,9 @@ class _FakeMisskeyIRegistry_46 extends _i1.SmartFake ); } -class _FakeMeDetailed_47 extends _i1.SmartFake implements _i5.MeDetailed { - _FakeMeDetailed_47( +class _FakeMisskeyIRegistry_47 extends _i1.SmartFake + implements _i6.MisskeyIRegistry { + _FakeMisskeyIRegistry_47( Object parent, Invocation parentInvocation, ) : super( @@ -561,9 +553,8 @@ class _FakeMeDetailed_47 extends _i1.SmartFake implements _i5.MeDetailed { ); } -class _FakeMisskeyNotesReactions_48 extends _i1.SmartFake - implements _i5.MisskeyNotesReactions { - _FakeMisskeyNotesReactions_48( +class _FakeMeDetailed_48 extends _i1.SmartFake implements _i6.MeDetailed { + _FakeMeDetailed_48( Object parent, Invocation parentInvocation, ) : super( @@ -572,9 +563,9 @@ class _FakeMisskeyNotesReactions_48 extends _i1.SmartFake ); } -class _FakeMisskeyNotesFavorites_49 extends _i1.SmartFake - implements _i5.MisskeyNotesFavorites { - _FakeMisskeyNotesFavorites_49( +class _FakeMisskeyNotesReactions_49 extends _i1.SmartFake + implements _i6.MisskeyNotesReactions { + _FakeMisskeyNotesReactions_49( Object parent, Invocation parentInvocation, ) : super( @@ -583,9 +574,9 @@ class _FakeMisskeyNotesFavorites_49 extends _i1.SmartFake ); } -class _FakeMisskeyNotesPolls_50 extends _i1.SmartFake - implements _i5.MisskeyNotesPolls { - _FakeMisskeyNotesPolls_50( +class _FakeMisskeyNotesFavorites_50 extends _i1.SmartFake + implements _i6.MisskeyNotesFavorites { + _FakeMisskeyNotesFavorites_50( Object parent, Invocation parentInvocation, ) : super( @@ -594,9 +585,9 @@ class _FakeMisskeyNotesPolls_50 extends _i1.SmartFake ); } -class _FakeMisskeyNotesThreadMuting_51 extends _i1.SmartFake - implements _i5.MisskeyNotesThreadMuting { - _FakeMisskeyNotesThreadMuting_51( +class _FakeMisskeyNotesPolls_51 extends _i1.SmartFake + implements _i6.MisskeyNotesPolls { + _FakeMisskeyNotesPolls_51( Object parent, Invocation parentInvocation, ) : super( @@ -605,8 +596,9 @@ class _FakeMisskeyNotesThreadMuting_51 extends _i1.SmartFake ); } -class _FakeNote_52 extends _i1.SmartFake implements _i5.Note { - _FakeNote_52( +class _FakeMisskeyNotesThreadMuting_52 extends _i1.SmartFake + implements _i6.MisskeyNotesThreadMuting { + _FakeMisskeyNotesThreadMuting_52( Object parent, Invocation parentInvocation, ) : super( @@ -615,9 +607,8 @@ class _FakeNote_52 extends _i1.SmartFake implements _i5.Note { ); } -class _FakeNotesStateResponse_53 extends _i1.SmartFake - implements _i5.NotesStateResponse { - _FakeNotesStateResponse_53( +class _FakeNote_53 extends _i1.SmartFake implements _i6.Note { + _FakeNote_53( Object parent, Invocation parentInvocation, ) : super( @@ -626,9 +617,9 @@ class _FakeNotesStateResponse_53 extends _i1.SmartFake ); } -class _FakeNotesTranslateResponse_54 extends _i1.SmartFake - implements _i5.NotesTranslateResponse { - _FakeNotesTranslateResponse_54( +class _FakeNotesStateResponse_54 extends _i1.SmartFake + implements _i6.NotesStateResponse { + _FakeNotesStateResponse_54( Object parent, Invocation parentInvocation, ) : super( @@ -637,9 +628,9 @@ class _FakeNotesTranslateResponse_54 extends _i1.SmartFake ); } -class _FakeRolesListResponse_55 extends _i1.SmartFake - implements _i5.RolesListResponse { - _FakeRolesListResponse_55( +class _FakeNotesTranslateResponse_55 extends _i1.SmartFake + implements _i6.NotesTranslateResponse { + _FakeNotesTranslateResponse_55( Object parent, Invocation parentInvocation, ) : super( @@ -648,9 +639,9 @@ class _FakeRolesListResponse_55 extends _i1.SmartFake ); } -class _FakeMisskeyUsersLists_56 extends _i1.SmartFake - implements _i5.MisskeyUsersLists { - _FakeMisskeyUsersLists_56( +class _FakeRolesListResponse_56 extends _i1.SmartFake + implements _i6.RolesListResponse { + _FakeRolesListResponse_56( Object parent, Invocation parentInvocation, ) : super( @@ -659,8 +650,9 @@ class _FakeMisskeyUsersLists_56 extends _i1.SmartFake ); } -class _FakeUserDetailed_57 extends _i1.SmartFake implements _i5.UserDetailed { - _FakeUserDetailed_57( +class _FakeMisskeyUsersLists_57 extends _i1.SmartFake + implements _i6.MisskeyUsersLists { + _FakeMisskeyUsersLists_57( Object parent, Invocation parentInvocation, ) : super( @@ -669,8 +661,8 @@ class _FakeUserDetailed_57 extends _i1.SmartFake implements _i5.UserDetailed { ); } -class _FakeBaseOptions_58 extends _i1.SmartFake implements _i11.BaseOptions { - _FakeBaseOptions_58( +class _FakeUserDetailed_58 extends _i1.SmartFake implements _i6.UserDetailed { + _FakeUserDetailed_58( Object parent, Invocation parentInvocation, ) : super( @@ -679,9 +671,8 @@ class _FakeBaseOptions_58 extends _i1.SmartFake implements _i11.BaseOptions { ); } -class _FakeHttpClientAdapter_59 extends _i1.SmartFake - implements _i11.HttpClientAdapter { - _FakeHttpClientAdapter_59( +class _FakeBaseOptions_59 extends _i1.SmartFake implements _i9.BaseOptions { + _FakeBaseOptions_59( Object parent, Invocation parentInvocation, ) : super( @@ -690,8 +681,9 @@ class _FakeHttpClientAdapter_59 extends _i1.SmartFake ); } -class _FakeTransformer_60 extends _i1.SmartFake implements _i11.Transformer { - _FakeTransformer_60( +class _FakeHttpClientAdapter_60 extends _i1.SmartFake + implements _i9.HttpClientAdapter { + _FakeHttpClientAdapter_60( Object parent, Invocation parentInvocation, ) : super( @@ -700,8 +692,8 @@ class _FakeTransformer_60 extends _i1.SmartFake implements _i11.Transformer { ); } -class _FakeInterceptors_61 extends _i1.SmartFake implements _i11.Interceptors { - _FakeInterceptors_61( +class _FakeTransformer_61 extends _i1.SmartFake implements _i9.Transformer { + _FakeTransformer_61( Object parent, Invocation parentInvocation, ) : super( @@ -710,8 +702,8 @@ class _FakeInterceptors_61 extends _i1.SmartFake implements _i11.Interceptors { ); } -class _FakeResponse_62 extends _i1.SmartFake implements _i11.Response { - _FakeResponse_62( +class _FakeInterceptors_62 extends _i1.SmartFake implements _i9.Interceptors { + _FakeInterceptors_62( Object parent, Invocation parentInvocation, ) : super( @@ -720,8 +712,8 @@ class _FakeResponse_62 extends _i1.SmartFake implements _i11.Response { ); } -class _FakeDuration_63 extends _i1.SmartFake implements Duration { - _FakeDuration_63( +class _FakeResponse_63 extends _i1.SmartFake implements _i9.Response { + _FakeResponse_63( Object parent, Invocation parentInvocation, ) : super( @@ -730,9 +722,8 @@ class _FakeDuration_63 extends _i1.SmartFake implements Duration { ); } -class _FakeHttpClientRequest_64 extends _i1.SmartFake - implements _i12.HttpClientRequest { - _FakeHttpClientRequest_64( +class _FakeDuration_64 extends _i1.SmartFake implements Duration { + _FakeDuration_64( Object parent, Invocation parentInvocation, ) : super( @@ -741,9 +732,9 @@ class _FakeHttpClientRequest_64 extends _i1.SmartFake ); } -class _FakeWebSocketChannel_65 extends _i1.SmartFake - implements _i13.WebSocketChannel { - _FakeWebSocketChannel_65( +class _FakeHttpClientRequest_65 extends _i1.SmartFake + implements _i10.HttpClientRequest { + _FakeHttpClientRequest_65( Object parent, Invocation parentInvocation, ) : super( @@ -752,8 +743,9 @@ class _FakeWebSocketChannel_65 extends _i1.SmartFake ); } -class _FakeFile_66 extends _i1.SmartFake implements _i14.File { - _FakeFile_66( +class _FakeStreamingController_66 extends _i1.SmartFake + implements _i6.StreamingController { + _FakeStreamingController_66( Object parent, Invocation parentInvocation, ) : super( @@ -762,8 +754,18 @@ class _FakeFile_66 extends _i1.SmartFake implements _i14.File { ); } -class _FakeFileInfo_67 extends _i1.SmartFake implements _i15.FileInfo { - _FakeFileInfo_67( +class _FakeFile_67 extends _i1.SmartFake implements _i11.File { + _FakeFile_67( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeFileInfo_68 extends _i1.SmartFake implements _i12.FileInfo { + _FakeFileInfo_68( Object parent, Invocation parentInvocation, ) : super( @@ -776,13 +778,13 @@ class _FakeFileInfo_67 extends _i1.SmartFake implements _i15.FileInfo { /// /// See the documentation for Mockito's code generation for more information. class MockTabSettingsRepository extends _i1.Mock - implements _i16.TabSettingsRepository { + implements _i13.TabSettingsRepository { @override - Iterable<_i17.TabSetting> get tabSettings => (super.noSuchMethod( + Iterable<_i14.TabSetting> get tabSettings => (super.noSuchMethod( Invocation.getter(#tabSettings), - returnValue: <_i17.TabSetting>[], - returnValueForMissingStub: <_i17.TabSetting>[], - ) as Iterable<_i17.TabSetting>); + returnValue: <_i14.TabSetting>[], + returnValueForMissingStub: <_i14.TabSetting>[], + ) as Iterable<_i14.TabSetting>); @override bool get hasListeners => (super.noSuchMethod( @@ -792,49 +794,49 @@ class MockTabSettingsRepository extends _i1.Mock ) as bool); @override - _i18.Future load() => (super.noSuchMethod( + _i15.Future load() => (super.noSuchMethod( Invocation.method( #load, [], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future save(List<_i17.TabSetting>? tabSettings) => + _i15.Future save(List<_i14.TabSetting>? tabSettings) => (super.noSuchMethod( Invocation.method( #save, [tabSettings], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future removeAccount(_i6.Account? account) => (super.noSuchMethod( + _i15.Future removeAccount(_i7.Account? account) => (super.noSuchMethod( Invocation.method( #removeAccount, [account], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future initializeTabSettings(_i6.Account? account) => + _i15.Future initializeTabSettings(_i7.Account? account) => (super.noSuchMethod( Invocation.method( #initializeTabSettings, [account], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - void addListener(_i19.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -843,7 +845,7 @@ class MockTabSettingsRepository extends _i1.Mock ); @override - void removeListener(_i19.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -874,7 +876,7 @@ class MockTabSettingsRepository extends _i1.Mock /// /// See the documentation for Mockito's code generation for more information. class MockAccountSettingsRepository extends _i1.Mock - implements _i20.AccountSettingsRepository { + implements _i17.AccountSettingsRepository { @override Iterable<_i2.AccountSettings> get accountSettings => (super.noSuchMethod( Invocation.getter(#accountSettings), @@ -890,37 +892,37 @@ class MockAccountSettingsRepository extends _i1.Mock ) as bool); @override - _i18.Future load() => (super.noSuchMethod( + _i15.Future load() => (super.noSuchMethod( Invocation.method( #load, [], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future save(_i2.AccountSettings? settings) => (super.noSuchMethod( + _i15.Future save(_i2.AccountSettings? settings) => (super.noSuchMethod( Invocation.method( #save, [settings], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future removeAccount(_i6.Account? account) => (super.noSuchMethod( + _i15.Future removeAccount(_i7.Account? account) => (super.noSuchMethod( Invocation.method( #removeAccount, [account], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i2.AccountSettings fromAcct(_i21.Acct? acct) => (super.noSuchMethod( + _i2.AccountSettings fromAcct(_i18.Acct? acct) => (super.noSuchMethod( Invocation.method( #fromAcct, [acct], @@ -942,7 +944,7 @@ class MockAccountSettingsRepository extends _i1.Mock ) as _i2.AccountSettings); @override - _i2.AccountSettings fromAccount(_i6.Account? account) => (super.noSuchMethod( + _i2.AccountSettings fromAccount(_i7.Account? account) => (super.noSuchMethod( Invocation.method( #fromAccount, [account], @@ -964,7 +966,7 @@ class MockAccountSettingsRepository extends _i1.Mock ) as _i2.AccountSettings); @override - void addListener(_i19.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -973,7 +975,7 @@ class MockAccountSettingsRepository extends _i1.Mock ); @override - void removeListener(_i19.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -1003,9 +1005,9 @@ class MockAccountSettingsRepository extends _i1.Mock /// A class which mocks [EmojiRepository]. /// /// See the documentation for Mockito's code generation for more information. -class MockEmojiRepository extends _i1.Mock implements _i22.EmojiRepository { +class MockEmojiRepository extends _i1.Mock implements _i19.EmojiRepository { @override - set emoji(List<_i22.EmojiRepositoryData>? _emoji) => super.noSuchMethod( + set emoji(List<_i19.EmojiRepositoryData>? _emoji) => super.noSuchMethod( Invocation.setter( #emoji, _emoji, @@ -1014,37 +1016,47 @@ class MockEmojiRepository extends _i1.Mock implements _i22.EmojiRepository { ); @override - _i18.Future loadFromSourceIfNeed() => (super.noSuchMethod( + set emojiMap(Map? _emojiMap) => + super.noSuchMethod( + Invocation.setter( + #emojiMap, + _emojiMap, + ), + returnValueForMissingStub: null, + ); + + @override + _i15.Future loadFromSourceIfNeed() => (super.noSuchMethod( Invocation.method( #loadFromSourceIfNeed, [], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future loadFromSource() => (super.noSuchMethod( + _i15.Future loadFromSource() => (super.noSuchMethod( Invocation.method( #loadFromSource, [], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future loadFromLocalCache() => (super.noSuchMethod( + _i15.Future loadFromLocalCache() => (super.noSuchMethod( Invocation.method( #loadFromLocalCache, [], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future> searchEmojis( + _i15.Future> searchEmojis( String? name, { int? limit = 30, }) => @@ -1054,31 +1066,31 @@ class MockEmojiRepository extends _i1.Mock implements _i22.EmojiRepository { [name], {#limit: limit}, ), - returnValue: _i18.Future>.value( - <_i23.MisskeyEmojiData>[]), + returnValue: _i15.Future>.value( + <_i20.MisskeyEmojiData>[]), returnValueForMissingStub: - _i18.Future>.value( - <_i23.MisskeyEmojiData>[]), - ) as _i18.Future>); + _i15.Future>.value( + <_i20.MisskeyEmojiData>[]), + ) as _i15.Future>); @override - List<_i23.MisskeyEmojiData> defaultEmojis({int? limit}) => + List<_i20.MisskeyEmojiData> defaultEmojis({int? limit}) => (super.noSuchMethod( Invocation.method( #defaultEmojis, [], {#limit: limit}, ), - returnValue: <_i23.MisskeyEmojiData>[], - returnValueForMissingStub: <_i23.MisskeyEmojiData>[], - ) as List<_i23.MisskeyEmojiData>); + returnValue: <_i20.MisskeyEmojiData>[], + returnValueForMissingStub: <_i20.MisskeyEmojiData>[], + ) as List<_i20.MisskeyEmojiData>); } /// A class which mocks [GeneralSettingsRepository]. /// /// See the documentation for Mockito's code generation for more information. class MockGeneralSettingsRepository extends _i1.Mock - implements _i24.GeneralSettingsRepository { + implements _i21.GeneralSettingsRepository { @override _i3.GeneralSettings get settings => (super.noSuchMethod( Invocation.getter(#settings), @@ -1100,28 +1112,28 @@ class MockGeneralSettingsRepository extends _i1.Mock ) as bool); @override - _i18.Future load() => (super.noSuchMethod( + _i15.Future load() => (super.noSuchMethod( Invocation.method( #load, [], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future update(_i3.GeneralSettings? settings) => + _i15.Future update(_i3.GeneralSettings? settings) => (super.noSuchMethod( Invocation.method( #update, [settings], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - void addListener(_i19.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -1130,7 +1142,7 @@ class MockGeneralSettingsRepository extends _i1.Mock ); @override - void removeListener(_i19.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -1160,30 +1172,45 @@ class MockGeneralSettingsRepository extends _i1.Mock /// A class which mocks [AccountRepository]. /// /// See the documentation for Mockito's code generation for more information. -class MockAccountRepository extends _i1.Mock implements _i25.AccountRepository { +class MockAccountRepository extends _i1.Mock implements _i22.AccountRepository { + @override + _i4.SharedPreferenceController get sharedPreferenceController => + (super.noSuchMethod( + Invocation.getter(#sharedPreferenceController), + returnValue: _FakeSharedPreferenceController_2( + this, + Invocation.getter(#sharedPreferenceController), + ), + returnValueForMissingStub: _FakeSharedPreferenceController_2( + this, + Invocation.getter(#sharedPreferenceController), + ), + ) as _i4.SharedPreferenceController); + @override - _i4.NotifierProviderRef> get ref => (super.noSuchMethod( + _i5.AutoDisposeNotifierProviderRef> get ref => + (super.noSuchMethod( Invocation.getter(#ref), - returnValue: _FakeNotifierProviderRef_2>( + returnValue: _FakeAutoDisposeNotifierProviderRef_3>( this, Invocation.getter(#ref), ), returnValueForMissingStub: - _FakeNotifierProviderRef_2>( + _FakeAutoDisposeNotifierProviderRef_3>( this, Invocation.getter(#ref), ), - ) as _i4.NotifierProviderRef>); + ) as _i5.AutoDisposeNotifierProviderRef>); @override - List<_i6.Account> get state => (super.noSuchMethod( + List<_i7.Account> get state => (super.noSuchMethod( Invocation.getter(#state), - returnValue: <_i6.Account>[], - returnValueForMissingStub: <_i6.Account>[], - ) as List<_i6.Account>); + returnValue: <_i7.Account>[], + returnValueForMissingStub: <_i7.Account>[], + ) as List<_i7.Account>); @override - set state(List<_i6.Account>? value) => super.noSuchMethod( + set state(List<_i7.Account>? value) => super.noSuchMethod( Invocation.setter( #state, value, @@ -1192,60 +1219,60 @@ class MockAccountRepository extends _i1.Mock implements _i25.AccountRepository { ); @override - List<_i6.Account> build() => (super.noSuchMethod( + List<_i7.Account> build() => (super.noSuchMethod( Invocation.method( #build, [], ), - returnValue: <_i6.Account>[], - returnValueForMissingStub: <_i6.Account>[], - ) as List<_i6.Account>); + returnValue: <_i7.Account>[], + returnValueForMissingStub: <_i7.Account>[], + ) as List<_i7.Account>); @override - _i18.Future load() => (super.noSuchMethod( + _i15.Future load() => (super.noSuchMethod( Invocation.method( #load, [], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future updateI(_i6.Account? account) => (super.noSuchMethod( + _i15.Future updateI(_i7.Account? account) => (super.noSuchMethod( Invocation.method( #updateI, [account], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future updateMeta(_i6.Account? account) => (super.noSuchMethod( + _i15.Future updateMeta(_i7.Account? account) => (super.noSuchMethod( Invocation.method( #updateMeta, [account], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future loadFromSourceIfNeed(_i21.Acct? acct) => + _i15.Future loadFromSourceIfNeed(_i18.Acct? acct) => (super.noSuchMethod( Invocation.method( #loadFromSourceIfNeed, [acct], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future createUnreadAnnouncement( - _i6.Account? account, - _i5.AnnouncementsResponse? announcement, + _i15.Future createUnreadAnnouncement( + _i7.Account? account, + _i6.AnnouncementsResponse? announcement, ) => (super.noSuchMethod( Invocation.method( @@ -1255,55 +1282,55 @@ class MockAccountRepository extends _i1.Mock implements _i25.AccountRepository { announcement, ], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future removeUnreadAnnouncement(_i6.Account? account) => + _i15.Future removeUnreadAnnouncement(_i7.Account? account) => (super.noSuchMethod( Invocation.method( #removeUnreadAnnouncement, [account], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future addUnreadNotification(_i6.Account? account) => + _i15.Future addUnreadNotification(_i7.Account? account) => (super.noSuchMethod( Invocation.method( #addUnreadNotification, [account], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future readAllNotification(_i6.Account? account) => + _i15.Future readAllNotification(_i7.Account? account) => (super.noSuchMethod( Invocation.method( #readAllNotification, [account], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future remove(_i6.Account? account) => (super.noSuchMethod( + _i15.Future remove(_i7.Account? account) => (super.noSuchMethod( Invocation.method( #remove, [account], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future loginAsPassword( + _i15.Future loginAsPassword( String? server, String? userId, String? password, @@ -1317,12 +1344,12 @@ class MockAccountRepository extends _i1.Mock implements _i25.AccountRepository { password, ], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future loginAsToken( + _i15.Future loginAsToken( String? server, String? token, ) => @@ -1334,32 +1361,32 @@ class MockAccountRepository extends _i1.Mock implements _i25.AccountRepository { token, ], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future openMiAuth(String? server) => (super.noSuchMethod( + _i15.Future openMiAuth(String? server) => (super.noSuchMethod( Invocation.method( #openMiAuth, [server], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future validateMiAuth(String? server) => (super.noSuchMethod( + _i15.Future validateMiAuth(String? server) => (super.noSuchMethod( Invocation.method( #validateMiAuth, [server], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future reorder( + _i15.Future reorder( int? oldIndex, int? newIndex, ) => @@ -1371,14 +1398,14 @@ class MockAccountRepository extends _i1.Mock implements _i25.AccountRepository { newIndex, ], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override bool updateShouldNotify( - List<_i6.Account>? previous, - List<_i6.Account>? next, + List<_i7.Account>? previous, + List<_i7.Account>? next, ) => (super.noSuchMethod( Invocation.method( @@ -1396,32 +1423,32 @@ class MockAccountRepository extends _i1.Mock implements _i25.AccountRepository { /// A class which mocks [NoteRepository]. /// /// See the documentation for Mockito's code generation for more information. -class MockNoteRepository extends _i1.Mock implements _i26.NoteRepository { +class MockNoteRepository extends _i1.Mock implements _i23.NoteRepository { @override - _i5.Misskey get misskey => (super.noSuchMethod( + _i6.Misskey get misskey => (super.noSuchMethod( Invocation.getter(#misskey), - returnValue: _FakeMisskey_3( + returnValue: _FakeMisskey_4( this, Invocation.getter(#misskey), ), - returnValueForMissingStub: _FakeMisskey_3( + returnValueForMissingStub: _FakeMisskey_4( this, Invocation.getter(#misskey), ), - ) as _i5.Misskey); + ) as _i6.Misskey); @override - _i6.Account get account => (super.noSuchMethod( + _i7.Account get account => (super.noSuchMethod( Invocation.getter(#account), - returnValue: _FakeAccount_4( + returnValue: _FakeAccount_5( this, Invocation.getter(#account), ), - returnValueForMissingStub: _FakeAccount_4( + returnValueForMissingStub: _FakeAccount_5( this, Invocation.getter(#account), ), - ) as _i6.Account); + ) as _i7.Account); @override List> get softMuteWordContents => (super.noSuchMethod( @@ -1452,18 +1479,18 @@ class MockNoteRepository extends _i1.Mock implements _i26.NoteRepository { ) as List); @override - Map get notes => (super.noSuchMethod( + Map get notes => (super.noSuchMethod( Invocation.getter(#notes), - returnValue: {}, - returnValueForMissingStub: {}, - ) as Map); + returnValue: {}, + returnValueForMissingStub: {}, + ) as Map); @override - Map get noteStatuses => (super.noSuchMethod( + Map get noteStatuses => (super.noSuchMethod( Invocation.getter(#noteStatuses), - returnValue: {}, - returnValueForMissingStub: {}, - ) as Map); + returnValue: {}, + returnValueForMissingStub: {}, + ) as Map); @override bool get hasListeners => (super.noSuchMethod( @@ -1474,8 +1501,8 @@ class MockNoteRepository extends _i1.Mock implements _i26.NoteRepository { @override void updateMute( - List<_i5.MuteWord>? softMuteWords, - List<_i5.MuteWord>? hardMuteWords, + List<_i6.MuteWord>? softMuteWords, + List<_i6.MuteWord>? hardMuteWords, ) => super.noSuchMethod( Invocation.method( @@ -1491,7 +1518,7 @@ class MockNoteRepository extends _i1.Mock implements _i26.NoteRepository { @override void updateNoteStatus( String? id, - _i26.NoteStatus Function(_i26.NoteStatus)? statusPredicate, { + _i23.NoteStatus Function(_i23.NoteStatus)? statusPredicate, { bool? isNotify = true, }) => super.noSuchMethod( @@ -1507,7 +1534,7 @@ class MockNoteRepository extends _i1.Mock implements _i26.NoteRepository { ); @override - void registerNote(_i5.Note? note) => super.noSuchMethod( + void registerNote(_i6.Note? note) => super.noSuchMethod( Invocation.method( #registerNote, [note], @@ -1516,7 +1543,7 @@ class MockNoteRepository extends _i1.Mock implements _i26.NoteRepository { ); @override - void registerAll(Iterable<_i5.Note>? notes) => super.noSuchMethod( + void registerAll(Iterable<_i6.Note>? notes) => super.noSuchMethod( Invocation.method( #registerAll, [notes], @@ -1525,14 +1552,14 @@ class MockNoteRepository extends _i1.Mock implements _i26.NoteRepository { ); @override - _i18.Future refresh(String? noteId) => (super.noSuchMethod( + _i15.Future refresh(String? noteId) => (super.noSuchMethod( Invocation.method( #refresh, [noteId], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override void delete(String? noteId) => super.noSuchMethod( @@ -1544,7 +1571,7 @@ class MockNoteRepository extends _i1.Mock implements _i26.NoteRepository { ); @override - void addListener(_i19.VoidCallback? listener) => super.noSuchMethod( + void addListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #addListener, [listener], @@ -1553,7 +1580,7 @@ class MockNoteRepository extends _i1.Mock implements _i26.NoteRepository { ); @override - void removeListener(_i19.VoidCallback? listener) => super.noSuchMethod( + void removeListener(_i16.VoidCallback? listener) => super.noSuchMethod( Invocation.method( #removeListener, [listener], @@ -1583,35 +1610,35 @@ class MockNoteRepository extends _i1.Mock implements _i26.NoteRepository { /// A class which mocks [Misskey]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskey extends _i1.Mock implements _i5.Misskey { +class MockMisskey extends _i1.Mock implements _i6.Misskey { @override String get host => (super.noSuchMethod( Invocation.getter(#host), - returnValue: _i27.dummyValue( + returnValue: _i24.dummyValue( this, Invocation.getter(#host), ), - returnValueForMissingStub: _i27.dummyValue( + returnValueForMissingStub: _i24.dummyValue( this, Invocation.getter(#host), ), ) as String); @override - _i7.ApiService get apiService => (super.noSuchMethod( + _i8.ApiService get apiService => (super.noSuchMethod( Invocation.getter(#apiService), - returnValue: _FakeApiService_5( + returnValue: _FakeApiService_6( this, Invocation.getter(#apiService), ), - returnValueForMissingStub: _FakeApiService_5( + returnValueForMissingStub: _FakeApiService_6( this, Invocation.getter(#apiService), ), - ) as _i7.ApiService); + ) as _i8.ApiService); @override - set apiService(_i7.ApiService? _apiService) => super.noSuchMethod( + set apiService(_i8.ApiService? _apiService) => super.noSuchMethod( Invocation.setter( #apiService, _apiService, @@ -1620,20 +1647,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.StreamingService get streamingService => (super.noSuchMethod( + _i6.WebSocketController get streamingService => (super.noSuchMethod( Invocation.getter(#streamingService), - returnValue: _FakeStreamingService_6( + returnValue: _FakeWebSocketController_7( this, Invocation.getter(#streamingService), ), - returnValueForMissingStub: _FakeStreamingService_6( + returnValueForMissingStub: _FakeWebSocketController_7( this, Invocation.getter(#streamingService), ), - ) as _i5.StreamingService); + ) as _i6.WebSocketController); @override - set streamingService(_i5.StreamingService? _streamingService) => + set streamingService(_i6.WebSocketController? _streamingService) => super.noSuchMethod( Invocation.setter( #streamingService, @@ -1643,20 +1670,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyNotes get notes => (super.noSuchMethod( + _i6.MisskeyNotes get notes => (super.noSuchMethod( Invocation.getter(#notes), - returnValue: _FakeMisskeyNotes_7( + returnValue: _FakeMisskeyNotes_8( this, Invocation.getter(#notes), ), - returnValueForMissingStub: _FakeMisskeyNotes_7( + returnValueForMissingStub: _FakeMisskeyNotes_8( this, Invocation.getter(#notes), ), - ) as _i5.MisskeyNotes); + ) as _i6.MisskeyNotes); @override - set notes(_i5.MisskeyNotes? _notes) => super.noSuchMethod( + set notes(_i6.MisskeyNotes? _notes) => super.noSuchMethod( Invocation.setter( #notes, _notes, @@ -1665,20 +1692,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyChannels get channels => (super.noSuchMethod( + _i6.MisskeyChannels get channels => (super.noSuchMethod( Invocation.getter(#channels), - returnValue: _FakeMisskeyChannels_8( + returnValue: _FakeMisskeyChannels_9( this, Invocation.getter(#channels), ), - returnValueForMissingStub: _FakeMisskeyChannels_8( + returnValueForMissingStub: _FakeMisskeyChannels_9( this, Invocation.getter(#channels), ), - ) as _i5.MisskeyChannels); + ) as _i6.MisskeyChannels); @override - set channels(_i5.MisskeyChannels? _channels) => super.noSuchMethod( + set channels(_i6.MisskeyChannels? _channels) => super.noSuchMethod( Invocation.setter( #channels, _channels, @@ -1687,20 +1714,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyUsers get users => (super.noSuchMethod( + _i6.MisskeyUsers get users => (super.noSuchMethod( Invocation.getter(#users), - returnValue: _FakeMisskeyUsers_9( + returnValue: _FakeMisskeyUsers_10( this, Invocation.getter(#users), ), - returnValueForMissingStub: _FakeMisskeyUsers_9( + returnValueForMissingStub: _FakeMisskeyUsers_10( this, Invocation.getter(#users), ), - ) as _i5.MisskeyUsers); + ) as _i6.MisskeyUsers); @override - set users(_i5.MisskeyUsers? _users) => super.noSuchMethod( + set users(_i6.MisskeyUsers? _users) => super.noSuchMethod( Invocation.setter( #users, _users, @@ -1709,20 +1736,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyI get i => (super.noSuchMethod( + _i6.MisskeyI get i => (super.noSuchMethod( Invocation.getter(#i), - returnValue: _FakeMisskeyI_10( + returnValue: _FakeMisskeyI_11( this, Invocation.getter(#i), ), - returnValueForMissingStub: _FakeMisskeyI_10( + returnValueForMissingStub: _FakeMisskeyI_11( this, Invocation.getter(#i), ), - ) as _i5.MisskeyI); + ) as _i6.MisskeyI); @override - set i(_i5.MisskeyI? _i) => super.noSuchMethod( + set i(_i6.MisskeyI? _i) => super.noSuchMethod( Invocation.setter( #i, _i, @@ -1731,20 +1758,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyClips get clips => (super.noSuchMethod( + _i6.MisskeyClips get clips => (super.noSuchMethod( Invocation.getter(#clips), - returnValue: _FakeMisskeyClips_11( + returnValue: _FakeMisskeyClips_12( this, Invocation.getter(#clips), ), - returnValueForMissingStub: _FakeMisskeyClips_11( + returnValueForMissingStub: _FakeMisskeyClips_12( this, Invocation.getter(#clips), ), - ) as _i5.MisskeyClips); + ) as _i6.MisskeyClips); @override - set clips(_i5.MisskeyClips? _clips) => super.noSuchMethod( + set clips(_i6.MisskeyClips? _clips) => super.noSuchMethod( Invocation.setter( #clips, _clips, @@ -1753,20 +1780,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyAntenna get antennas => (super.noSuchMethod( + _i6.MisskeyAntenna get antennas => (super.noSuchMethod( Invocation.getter(#antennas), - returnValue: _FakeMisskeyAntenna_12( + returnValue: _FakeMisskeyAntenna_13( this, Invocation.getter(#antennas), ), - returnValueForMissingStub: _FakeMisskeyAntenna_12( + returnValueForMissingStub: _FakeMisskeyAntenna_13( this, Invocation.getter(#antennas), ), - ) as _i5.MisskeyAntenna); + ) as _i6.MisskeyAntenna); @override - set antennas(_i5.MisskeyAntenna? _antennas) => super.noSuchMethod( + set antennas(_i6.MisskeyAntenna? _antennas) => super.noSuchMethod( Invocation.setter( #antennas, _antennas, @@ -1775,20 +1802,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyDrive get drive => (super.noSuchMethod( + _i6.MisskeyDrive get drive => (super.noSuchMethod( Invocation.getter(#drive), - returnValue: _FakeMisskeyDrive_13( + returnValue: _FakeMisskeyDrive_14( this, Invocation.getter(#drive), ), - returnValueForMissingStub: _FakeMisskeyDrive_13( + returnValueForMissingStub: _FakeMisskeyDrive_14( this, Invocation.getter(#drive), ), - ) as _i5.MisskeyDrive); + ) as _i6.MisskeyDrive); @override - set drive(_i5.MisskeyDrive? _drive) => super.noSuchMethod( + set drive(_i6.MisskeyDrive? _drive) => super.noSuchMethod( Invocation.setter( #drive, _drive, @@ -1797,20 +1824,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyFollowing get following => (super.noSuchMethod( + _i6.MisskeyFollowing get following => (super.noSuchMethod( Invocation.getter(#following), - returnValue: _FakeMisskeyFollowing_14( + returnValue: _FakeMisskeyFollowing_15( this, Invocation.getter(#following), ), - returnValueForMissingStub: _FakeMisskeyFollowing_14( + returnValueForMissingStub: _FakeMisskeyFollowing_15( this, Invocation.getter(#following), ), - ) as _i5.MisskeyFollowing); + ) as _i6.MisskeyFollowing); @override - set following(_i5.MisskeyFollowing? _following) => super.noSuchMethod( + set following(_i6.MisskeyFollowing? _following) => super.noSuchMethod( Invocation.setter( #following, _following, @@ -1819,20 +1846,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyBlocking get blocking => (super.noSuchMethod( + _i6.MisskeyBlocking get blocking => (super.noSuchMethod( Invocation.getter(#blocking), - returnValue: _FakeMisskeyBlocking_15( + returnValue: _FakeMisskeyBlocking_16( this, Invocation.getter(#blocking), ), - returnValueForMissingStub: _FakeMisskeyBlocking_15( + returnValueForMissingStub: _FakeMisskeyBlocking_16( this, Invocation.getter(#blocking), ), - ) as _i5.MisskeyBlocking); + ) as _i6.MisskeyBlocking); @override - set blocking(_i5.MisskeyBlocking? _blocking) => super.noSuchMethod( + set blocking(_i6.MisskeyBlocking? _blocking) => super.noSuchMethod( Invocation.setter( #blocking, _blocking, @@ -1841,20 +1868,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyMute get mute => (super.noSuchMethod( + _i6.MisskeyMute get mute => (super.noSuchMethod( Invocation.getter(#mute), - returnValue: _FakeMisskeyMute_16( + returnValue: _FakeMisskeyMute_17( this, Invocation.getter(#mute), ), - returnValueForMissingStub: _FakeMisskeyMute_16( + returnValueForMissingStub: _FakeMisskeyMute_17( this, Invocation.getter(#mute), ), - ) as _i5.MisskeyMute); + ) as _i6.MisskeyMute); @override - set mute(_i5.MisskeyMute? _mute) => super.noSuchMethod( + set mute(_i6.MisskeyMute? _mute) => super.noSuchMethod( Invocation.setter( #mute, _mute, @@ -1863,20 +1890,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyRenoteMute get renoteMute => (super.noSuchMethod( + _i6.MisskeyRenoteMute get renoteMute => (super.noSuchMethod( Invocation.getter(#renoteMute), - returnValue: _FakeMisskeyRenoteMute_17( + returnValue: _FakeMisskeyRenoteMute_18( this, Invocation.getter(#renoteMute), ), - returnValueForMissingStub: _FakeMisskeyRenoteMute_17( + returnValueForMissingStub: _FakeMisskeyRenoteMute_18( this, Invocation.getter(#renoteMute), ), - ) as _i5.MisskeyRenoteMute); + ) as _i6.MisskeyRenoteMute); @override - set renoteMute(_i5.MisskeyRenoteMute? _renoteMute) => super.noSuchMethod( + set renoteMute(_i6.MisskeyRenoteMute? _renoteMute) => super.noSuchMethod( Invocation.setter( #renoteMute, _renoteMute, @@ -1885,20 +1912,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyFederation get federation => (super.noSuchMethod( + _i6.MisskeyFederation get federation => (super.noSuchMethod( Invocation.getter(#federation), - returnValue: _FakeMisskeyFederation_18( + returnValue: _FakeMisskeyFederation_19( this, Invocation.getter(#federation), ), - returnValueForMissingStub: _FakeMisskeyFederation_18( + returnValueForMissingStub: _FakeMisskeyFederation_19( this, Invocation.getter(#federation), ), - ) as _i5.MisskeyFederation); + ) as _i6.MisskeyFederation); @override - set federation(_i5.MisskeyFederation? _federation) => super.noSuchMethod( + set federation(_i6.MisskeyFederation? _federation) => super.noSuchMethod( Invocation.setter( #federation, _federation, @@ -1907,20 +1934,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyRoles get roles => (super.noSuchMethod( + _i6.MisskeyRoles get roles => (super.noSuchMethod( Invocation.getter(#roles), - returnValue: _FakeMisskeyRoles_19( + returnValue: _FakeMisskeyRoles_20( this, Invocation.getter(#roles), ), - returnValueForMissingStub: _FakeMisskeyRoles_19( + returnValueForMissingStub: _FakeMisskeyRoles_20( this, Invocation.getter(#roles), ), - ) as _i5.MisskeyRoles); + ) as _i6.MisskeyRoles); @override - set roles(_i5.MisskeyRoles? _roles) => super.noSuchMethod( + set roles(_i6.MisskeyRoles? _roles) => super.noSuchMethod( Invocation.setter( #roles, _roles, @@ -1929,20 +1956,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyHashtags get hashtags => (super.noSuchMethod( + _i6.MisskeyHashtags get hashtags => (super.noSuchMethod( Invocation.getter(#hashtags), - returnValue: _FakeMisskeyHashtags_20( + returnValue: _FakeMisskeyHashtags_21( this, Invocation.getter(#hashtags), ), - returnValueForMissingStub: _FakeMisskeyHashtags_20( + returnValueForMissingStub: _FakeMisskeyHashtags_21( this, Invocation.getter(#hashtags), ), - ) as _i5.MisskeyHashtags); + ) as _i6.MisskeyHashtags); @override - set hashtags(_i5.MisskeyHashtags? _hashtags) => super.noSuchMethod( + set hashtags(_i6.MisskeyHashtags? _hashtags) => super.noSuchMethod( Invocation.setter( #hashtags, _hashtags, @@ -1951,20 +1978,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyAp get ap => (super.noSuchMethod( + _i6.MisskeyAp get ap => (super.noSuchMethod( Invocation.getter(#ap), - returnValue: _FakeMisskeyAp_21( + returnValue: _FakeMisskeyAp_22( this, Invocation.getter(#ap), ), - returnValueForMissingStub: _FakeMisskeyAp_21( + returnValueForMissingStub: _FakeMisskeyAp_22( this, Invocation.getter(#ap), ), - ) as _i5.MisskeyAp); + ) as _i6.MisskeyAp); @override - set ap(_i5.MisskeyAp? _ap) => super.noSuchMethod( + set ap(_i6.MisskeyAp? _ap) => super.noSuchMethod( Invocation.setter( #ap, _ap, @@ -1973,20 +2000,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyPages get pages => (super.noSuchMethod( + _i6.MisskeyPages get pages => (super.noSuchMethod( Invocation.getter(#pages), - returnValue: _FakeMisskeyPages_22( + returnValue: _FakeMisskeyPages_23( this, Invocation.getter(#pages), ), - returnValueForMissingStub: _FakeMisskeyPages_22( + returnValueForMissingStub: _FakeMisskeyPages_23( this, Invocation.getter(#pages), ), - ) as _i5.MisskeyPages); + ) as _i6.MisskeyPages); @override - set pages(_i5.MisskeyPages? _pages) => super.noSuchMethod( + set pages(_i6.MisskeyPages? _pages) => super.noSuchMethod( Invocation.setter( #pages, _pages, @@ -1995,20 +2022,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i8.MisskeyFlash get flash => (super.noSuchMethod( + _i6.MisskeyFlash get flash => (super.noSuchMethod( Invocation.getter(#flash), - returnValue: _FakeMisskeyFlash_23( + returnValue: _FakeMisskeyFlash_24( this, Invocation.getter(#flash), ), - returnValueForMissingStub: _FakeMisskeyFlash_23( + returnValueForMissingStub: _FakeMisskeyFlash_24( this, Invocation.getter(#flash), ), - ) as _i8.MisskeyFlash); + ) as _i6.MisskeyFlash); @override - set flash(_i8.MisskeyFlash? _flash) => super.noSuchMethod( + set flash(_i6.MisskeyFlash? _flash) => super.noSuchMethod( Invocation.setter( #flash, _flash, @@ -2017,20 +2044,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyReversi get reversi => (super.noSuchMethod( + _i6.MisskeyReversi get reversi => (super.noSuchMethod( Invocation.getter(#reversi), - returnValue: _FakeMisskeyReversi_24( + returnValue: _FakeMisskeyReversi_25( this, Invocation.getter(#reversi), ), - returnValueForMissingStub: _FakeMisskeyReversi_24( + returnValueForMissingStub: _FakeMisskeyReversi_25( this, Invocation.getter(#reversi), ), - ) as _i5.MisskeyReversi); + ) as _i6.MisskeyReversi); @override - set reversi(_i5.MisskeyReversi? _reversi) => super.noSuchMethod( + set reversi(_i6.MisskeyReversi? _reversi) => super.noSuchMethod( Invocation.setter( #reversi, _reversi, @@ -2039,20 +2066,20 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i5.MisskeyBubbleGame get bubbleGame => (super.noSuchMethod( + _i6.MisskeyBubbleGame get bubbleGame => (super.noSuchMethod( Invocation.getter(#bubbleGame), - returnValue: _FakeMisskeyBubbleGame_25( + returnValue: _FakeMisskeyBubbleGame_26( this, Invocation.getter(#bubbleGame), ), - returnValueForMissingStub: _FakeMisskeyBubbleGame_25( + returnValueForMissingStub: _FakeMisskeyBubbleGame_26( this, Invocation.getter(#bubbleGame), ), - ) as _i5.MisskeyBubbleGame); + ) as _i6.MisskeyBubbleGame); @override - set bubbleGame(_i5.MisskeyBubbleGame? _bubbleGame) => super.noSuchMethod( + set bubbleGame(_i6.MisskeyBubbleGame? _bubbleGame) => super.noSuchMethod( Invocation.setter( #bubbleGame, _bubbleGame, @@ -2061,38 +2088,38 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ); @override - _i18.Future> announcements( - _i5.AnnouncementsRequest? request) => + _i15.Future> announcements( + _i6.AnnouncementsRequest? request) => (super.noSuchMethod( Invocation.method( #announcements, [request], ), - returnValue: _i18.Future>.value( - <_i5.AnnouncementsResponse>[]), + returnValue: _i15.Future>.value( + <_i6.AnnouncementsResponse>[]), returnValueForMissingStub: - _i18.Future>.value( - <_i5.AnnouncementsResponse>[]), - ) as _i18.Future>); + _i15.Future>.value( + <_i6.AnnouncementsResponse>[]), + ) as _i15.Future>); @override - _i18.Future> endpoints() => (super.noSuchMethod( + _i15.Future> endpoints() => (super.noSuchMethod( Invocation.method( #endpoints, [], ), - returnValue: _i18.Future>.value([]), - returnValueForMissingStub: _i18.Future>.value([]), - ) as _i18.Future>); + returnValue: _i15.Future>.value([]), + returnValueForMissingStub: _i15.Future>.value([]), + ) as _i15.Future>); @override - _i18.Future<_i5.EmojisResponse> emojis() => (super.noSuchMethod( + _i15.Future<_i6.EmojisResponse> emojis() => (super.noSuchMethod( Invocation.method( #emojis, [], ), returnValue: - _i18.Future<_i5.EmojisResponse>.value(_FakeEmojisResponse_26( + _i15.Future<_i6.EmojisResponse>.value(_FakeEmojisResponse_27( this, Invocation.method( #emojis, @@ -2100,23 +2127,23 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ), )), returnValueForMissingStub: - _i18.Future<_i5.EmojisResponse>.value(_FakeEmojisResponse_26( + _i15.Future<_i6.EmojisResponse>.value(_FakeEmojisResponse_27( this, Invocation.method( #emojis, [], ), )), - ) as _i18.Future<_i5.EmojisResponse>); + ) as _i15.Future<_i6.EmojisResponse>); @override - _i18.Future<_i5.EmojiResponse> emoji(_i5.EmojiRequest? request) => + _i15.Future<_i6.EmojiResponse> emoji(_i6.EmojiRequest? request) => (super.noSuchMethod( Invocation.method( #emoji, [request], ), - returnValue: _i18.Future<_i5.EmojiResponse>.value(_FakeEmojiResponse_27( + returnValue: _i15.Future<_i6.EmojiResponse>.value(_FakeEmojiResponse_28( this, Invocation.method( #emoji, @@ -2124,22 +2151,22 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ), )), returnValueForMissingStub: - _i18.Future<_i5.EmojiResponse>.value(_FakeEmojiResponse_27( + _i15.Future<_i6.EmojiResponse>.value(_FakeEmojiResponse_28( this, Invocation.method( #emoji, [request], ), )), - ) as _i18.Future<_i5.EmojiResponse>); + ) as _i15.Future<_i6.EmojiResponse>); @override - _i18.Future<_i5.MetaResponse> meta() => (super.noSuchMethod( + _i15.Future<_i6.MetaResponse> meta() => (super.noSuchMethod( Invocation.method( #meta, [], ), - returnValue: _i18.Future<_i5.MetaResponse>.value(_FakeMetaResponse_28( + returnValue: _i15.Future<_i6.MetaResponse>.value(_FakeMetaResponse_29( this, Invocation.method( #meta, @@ -2147,22 +2174,22 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ), )), returnValueForMissingStub: - _i18.Future<_i5.MetaResponse>.value(_FakeMetaResponse_28( + _i15.Future<_i6.MetaResponse>.value(_FakeMetaResponse_29( this, Invocation.method( #meta, [], ), )), - ) as _i18.Future<_i5.MetaResponse>); + ) as _i15.Future<_i6.MetaResponse>); @override - _i18.Future<_i9.StatsResponse> stats() => (super.noSuchMethod( + _i15.Future<_i6.StatsResponse> stats() => (super.noSuchMethod( Invocation.method( #stats, [], ), - returnValue: _i18.Future<_i9.StatsResponse>.value(_FakeStatsResponse_29( + returnValue: _i15.Future<_i6.StatsResponse>.value(_FakeStatsResponse_30( this, Invocation.method( #stats, @@ -2170,22 +2197,22 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ), )), returnValueForMissingStub: - _i18.Future<_i9.StatsResponse>.value(_FakeStatsResponse_29( + _i15.Future<_i6.StatsResponse>.value(_FakeStatsResponse_30( this, Invocation.method( #stats, [], ), )), - ) as _i18.Future<_i9.StatsResponse>); + ) as _i15.Future<_i6.StatsResponse>); @override - _i18.Future<_i10.PingResponse> ping() => (super.noSuchMethod( + _i15.Future<_i6.PingResponse> ping() => (super.noSuchMethod( Invocation.method( #ping, [], ), - returnValue: _i18.Future<_i10.PingResponse>.value(_FakePingResponse_30( + returnValue: _i15.Future<_i6.PingResponse>.value(_FakePingResponse_31( this, Invocation.method( #ping, @@ -2193,48 +2220,48 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ), )), returnValueForMissingStub: - _i18.Future<_i10.PingResponse>.value(_FakePingResponse_30( + _i15.Future<_i6.PingResponse>.value(_FakePingResponse_31( this, Invocation.method( #ping, [], ), )), - ) as _i18.Future<_i10.PingResponse>); + ) as _i15.Future<_i6.PingResponse>); @override - _i18.Future<_i5.ServerInfoResponse> serverInfo() => (super.noSuchMethod( + _i15.Future<_i6.ServerInfoResponse> serverInfo() => (super.noSuchMethod( Invocation.method( #serverInfo, [], ), - returnValue: _i18.Future<_i5.ServerInfoResponse>.value( - _FakeServerInfoResponse_31( + returnValue: _i15.Future<_i6.ServerInfoResponse>.value( + _FakeServerInfoResponse_32( this, Invocation.method( #serverInfo, [], ), )), - returnValueForMissingStub: _i18.Future<_i5.ServerInfoResponse>.value( - _FakeServerInfoResponse_31( + returnValueForMissingStub: _i15.Future<_i6.ServerInfoResponse>.value( + _FakeServerInfoResponse_32( this, Invocation.method( #serverInfo, [], ), )), - ) as _i18.Future<_i5.ServerInfoResponse>); + ) as _i15.Future<_i6.ServerInfoResponse>); @override - _i18.Future<_i5.GetOnlineUsersCountResponse> getOnlineUsersCount() => + _i15.Future<_i6.GetOnlineUsersCountResponse> getOnlineUsersCount() => (super.noSuchMethod( Invocation.method( #getOnlineUsersCount, [], ), - returnValue: _i18.Future<_i5.GetOnlineUsersCountResponse>.value( - _FakeGetOnlineUsersCountResponse_32( + returnValue: _i15.Future<_i6.GetOnlineUsersCountResponse>.value( + _FakeGetOnlineUsersCountResponse_33( this, Invocation.method( #getOnlineUsersCount, @@ -2242,838 +2269,56 @@ class MockMisskey extends _i1.Mock implements _i5.Misskey { ), )), returnValueForMissingStub: - _i18.Future<_i5.GetOnlineUsersCountResponse>.value( - _FakeGetOnlineUsersCountResponse_32( + _i15.Future<_i6.GetOnlineUsersCountResponse>.value( + _FakeGetOnlineUsersCountResponse_33( this, Invocation.method( #getOnlineUsersCount, [], ), )), - ) as _i18.Future<_i5.GetOnlineUsersCountResponse>); + ) as _i15.Future<_i6.GetOnlineUsersCountResponse>); @override - _i18.Future> + _i15.Future> getAvatarDecorations() => (super.noSuchMethod( Invocation.method( #getAvatarDecorations, [], ), returnValue: - _i18.Future>.value( - <_i5.GetAvatarDecorationsResponse>[]), + _i15.Future>.value( + <_i6.GetAvatarDecorationsResponse>[]), returnValueForMissingStub: - _i18.Future>.value( - <_i5.GetAvatarDecorationsResponse>[]), - ) as _i18.Future>); + _i15.Future>.value( + <_i6.GetAvatarDecorationsResponse>[]), + ) as _i15.Future>); @override - _i18.Future> pinnedUsers() => (super.noSuchMethod( + _i15.Future> pinnedUsers() => (super.noSuchMethod( Invocation.method( #pinnedUsers, [], ), returnValue: - _i18.Future>.value(<_i5.UserDetailed>[]), - returnValueForMissingStub: - _i18.Future>.value(<_i5.UserDetailed>[]), - ) as _i18.Future>); - - @override - _i5.SocketController homeTimelineStream({ - required _i5.HomeTimelineParameter? parameter, - _i18.FutureOr Function(_i5.Note)? onNoteReceived, - _i18.FutureOr Function( - String, - _i5.TimelineReacted, - )? onReacted, - _i18.FutureOr Function( - String, - _i5.TimelineReacted, - )? onUnreacted, - _i18.FutureOr Function( - String, - DateTime, - )? onDeleted, - _i18.FutureOr Function( - String, - _i5.TimelineVoted, - )? onVoted, - _i18.FutureOr Function( - String, - _i5.NoteEdited, - )? onUpdated, - }) => - (super.noSuchMethod( - Invocation.method( - #homeTimelineStream, - [], - { - #parameter: parameter, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - returnValue: _FakeSocketController_33( - this, - Invocation.method( - #homeTimelineStream, - [], - { - #parameter: parameter, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - ), - returnValueForMissingStub: _FakeSocketController_33( - this, - Invocation.method( - #homeTimelineStream, - [], - { - #parameter: parameter, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - ), - ) as _i5.SocketController); - - @override - _i5.SocketController localTimelineStream({ - required _i5.LocalTimelineParameter? parameter, - _i18.FutureOr Function(_i5.Note)? onNoteReceived, - _i18.FutureOr Function( - String, - _i5.TimelineReacted, - )? onReacted, - _i18.FutureOr Function( - String, - _i5.TimelineReacted, - )? onUnreacted, - _i18.FutureOr Function( - String, - DateTime, - )? onDeleted, - _i18.FutureOr Function( - String, - _i5.TimelineVoted, - )? onVoted, - _i18.FutureOr Function( - String, - _i5.NoteEdited, - )? onUpdated, - }) => - (super.noSuchMethod( - Invocation.method( - #localTimelineStream, - [], - { - #parameter: parameter, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - returnValue: _FakeSocketController_33( - this, - Invocation.method( - #localTimelineStream, - [], - { - #parameter: parameter, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - ), - returnValueForMissingStub: _FakeSocketController_33( - this, - Invocation.method( - #localTimelineStream, - [], - { - #parameter: parameter, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - ), - ) as _i5.SocketController); - - @override - _i5.SocketController globalTimelineStream({ - required _i5.GlobalTimelineParameter? parameter, - _i18.FutureOr Function(_i5.Note)? onNoteReceived, - _i18.FutureOr Function( - String, - _i5.TimelineReacted, - )? onReacted, - _i18.FutureOr Function( - String, - _i5.TimelineReacted, - )? onUnreacted, - _i18.FutureOr Function( - String, - DateTime, - )? onDeleted, - _i18.FutureOr Function( - String, - _i5.TimelineVoted, - )? onVoted, - _i18.FutureOr Function( - String, - _i5.NoteEdited, - )? onUpdated, - }) => - (super.noSuchMethod( - Invocation.method( - #globalTimelineStream, - [], - { - #parameter: parameter, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - returnValue: _FakeSocketController_33( - this, - Invocation.method( - #globalTimelineStream, - [], - { - #parameter: parameter, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - ), - returnValueForMissingStub: _FakeSocketController_33( - this, - Invocation.method( - #globalTimelineStream, - [], - { - #parameter: parameter, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - ), - ) as _i5.SocketController); - - @override - _i5.SocketController hybridTimelineStream({ - required _i5.HybridTimelineParameter? parameter, - _i18.FutureOr Function(_i5.Note)? onNoteReceived, - _i18.FutureOr Function( - String, - _i5.TimelineReacted, - )? onReacted, - _i18.FutureOr Function( - String, - _i5.TimelineReacted, - )? onUnreacted, - _i18.FutureOr Function( - String, - DateTime, - )? onDeleted, - _i18.FutureOr Function( - String, - _i5.TimelineVoted, - )? onVoted, - _i18.FutureOr Function( - String, - _i5.NoteEdited, - )? onUpdated, - }) => - (super.noSuchMethod( - Invocation.method( - #hybridTimelineStream, - [], - { - #parameter: parameter, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - returnValue: _FakeSocketController_33( - this, - Invocation.method( - #hybridTimelineStream, - [], - { - #parameter: parameter, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - ), - returnValueForMissingStub: _FakeSocketController_33( - this, - Invocation.method( - #hybridTimelineStream, - [], - { - #parameter: parameter, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - ), - ) as _i5.SocketController); - - @override - _i5.SocketController roleTimelineStream({ - required String? roleId, - _i18.FutureOr Function(_i5.Note)? onNoteReceived, - _i18.FutureOr Function( - String, - _i5.TimelineReacted, - )? onReacted, - _i18.FutureOr Function( - String, - _i5.TimelineReacted, - )? onUnreacted, - _i18.FutureOr Function( - String, - DateTime, - )? onDeleted, - _i18.FutureOr Function( - String, - _i5.TimelineVoted, - )? onVoted, - _i18.FutureOr Function( - String, - _i5.NoteEdited, - )? onUpdated, - }) => - (super.noSuchMethod( - Invocation.method( - #roleTimelineStream, - [], - { - #roleId: roleId, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - returnValue: _FakeSocketController_33( - this, - Invocation.method( - #roleTimelineStream, - [], - { - #roleId: roleId, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - ), - returnValueForMissingStub: _FakeSocketController_33( - this, - Invocation.method( - #roleTimelineStream, - [], - { - #roleId: roleId, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - ), - ) as _i5.SocketController); - - @override - _i5.SocketController channelStream({ - required String? channelId, - _i18.FutureOr Function(_i5.Note)? onNoteReceived, - _i18.FutureOr Function( - String, - _i5.TimelineReacted, - )? onReacted, - _i18.FutureOr Function( - String, - _i5.TimelineReacted, - )? onUnreacted, - _i18.FutureOr Function( - String, - DateTime, - )? onDeleted, - _i18.FutureOr Function( - String, - _i5.TimelineVoted, - )? onVoted, - _i18.FutureOr Function( - String, - _i5.NoteEdited, - )? onUpdated, - }) => - (super.noSuchMethod( - Invocation.method( - #channelStream, - [], - { - #channelId: channelId, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - returnValue: _FakeSocketController_33( - this, - Invocation.method( - #channelStream, - [], - { - #channelId: channelId, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - ), - returnValueForMissingStub: _FakeSocketController_33( - this, - Invocation.method( - #channelStream, - [], - { - #channelId: channelId, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - ), - ) as _i5.SocketController); - - @override - _i5.SocketController userListStream({ - required String? listId, - _i18.FutureOr Function(_i5.Note)? onNoteReceived, - _i18.FutureOr Function(_i5.UserLite)? onUserAdded, - _i18.FutureOr Function(_i5.UserLite)? onUserRemoved, - _i18.FutureOr Function( - String, - _i5.TimelineReacted, - )? onReacted, - _i18.FutureOr Function( - String, - _i5.TimelineReacted, - )? onUnreacted, - _i18.FutureOr Function( - String, - _i5.NoteEdited, - )? onUpdated, - _i18.FutureOr Function(DateTime)? onDeleted, - _i18.FutureOr Function( - String, - _i5.TimelineVoted, - )? onVoted, - }) => - (super.noSuchMethod( - Invocation.method( - #userListStream, - [], - { - #listId: listId, - #onNoteReceived: onNoteReceived, - #onUserAdded: onUserAdded, - #onUserRemoved: onUserRemoved, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onUpdated: onUpdated, - #onDeleted: onDeleted, - #onVoted: onVoted, - }, - ), - returnValue: _FakeSocketController_33( - this, - Invocation.method( - #userListStream, - [], - { - #listId: listId, - #onNoteReceived: onNoteReceived, - #onUserAdded: onUserAdded, - #onUserRemoved: onUserRemoved, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onUpdated: onUpdated, - #onDeleted: onDeleted, - #onVoted: onVoted, - }, - ), - ), - returnValueForMissingStub: _FakeSocketController_33( - this, - Invocation.method( - #userListStream, - [], - { - #listId: listId, - #onNoteReceived: onNoteReceived, - #onUserAdded: onUserAdded, - #onUserRemoved: onUserRemoved, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onUpdated: onUpdated, - #onDeleted: onDeleted, - #onVoted: onVoted, - }, - ), - ), - ) as _i5.SocketController); - - @override - _i5.SocketController antennaStream({ - required String? antennaId, - _i18.FutureOr Function(_i5.Note)? onNoteReceived, - _i18.FutureOr Function( - String, - _i5.TimelineReacted, - )? onReacted, - _i18.FutureOr Function( - String, - _i5.TimelineReacted, - )? onUnreacted, - _i18.FutureOr Function( - String, - DateTime, - )? onDeleted, - _i18.FutureOr Function( - String, - _i5.TimelineVoted, - )? onVoted, - _i18.FutureOr Function( - String, - _i5.NoteEdited, - )? onUpdated, - }) => - (super.noSuchMethod( - Invocation.method( - #antennaStream, - [], - { - #antennaId: antennaId, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - returnValue: _FakeSocketController_33( - this, - Invocation.method( - #antennaStream, - [], - { - #antennaId: antennaId, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - ), - returnValueForMissingStub: _FakeSocketController_33( - this, - Invocation.method( - #antennaStream, - [], - { - #antennaId: antennaId, - #onNoteReceived: onNoteReceived, - #onReacted: onReacted, - #onUnreacted: onUnreacted, - #onDeleted: onDeleted, - #onVoted: onVoted, - #onUpdated: onUpdated, - }, - ), - ), - ) as _i5.SocketController); - - @override - _i5.SocketController serverStatsLogStream( - _i18.FutureOr Function(List<_i5.StatsLogResponse>)? onLogReceived, - _i18.FutureOr Function(_i5.StatsLogResponse)? onEventReceived, - ) => - (super.noSuchMethod( - Invocation.method( - #serverStatsLogStream, - [ - onLogReceived, - onEventReceived, - ], - ), - returnValue: _FakeSocketController_33( - this, - Invocation.method( - #serverStatsLogStream, - [ - onLogReceived, - onEventReceived, - ], - ), - ), - returnValueForMissingStub: _FakeSocketController_33( - this, - Invocation.method( - #serverStatsLogStream, - [ - onLogReceived, - onEventReceived, - ], - ), - ), - ) as _i5.SocketController); - - @override - _i5.SocketController queueStatsLogStream( - _i18.FutureOr Function(List<_i5.QueueStatsLogResponse>)? - onLogReceived, - _i18.FutureOr Function(_i5.QueueStatsLogResponse)? onEventReceived, - ) => - (super.noSuchMethod( - Invocation.method( - #queueStatsLogStream, - [ - onLogReceived, - onEventReceived, - ], - ), - returnValue: _FakeSocketController_33( - this, - Invocation.method( - #queueStatsLogStream, - [ - onLogReceived, - onEventReceived, - ], - ), - ), - returnValueForMissingStub: _FakeSocketController_33( - this, - Invocation.method( - #queueStatsLogStream, - [ - onLogReceived, - onEventReceived, - ], - ), - ), - ) as _i5.SocketController); - - @override - _i5.SocketController mainStream({ - _i18.FutureOr Function(_i5.Emoji)? onEmojiAdded, - _i18.FutureOr Function(Iterable<_i5.Emoji>)? onEmojiUpdated, - _i18.FutureOr Function(Iterable<_i5.Emoji>)? onEmojiDeleted, - _i18.FutureOr Function(_i5.AnnouncementsResponse)? - onAnnouncementCreated, - _i18.FutureOr Function(_i5.INotificationsResponse)? onNotification, - _i18.FutureOr Function(_i5.Note)? onMention, - _i18.FutureOr Function(_i5.Note)? onReply, - _i18.FutureOr Function(_i5.Note)? onRenote, - _i18.FutureOr Function(_i5.UserDetailedNotMe)? onFollow, - _i18.FutureOr Function(_i5.UserLite)? onFollowed, - _i18.FutureOr Function(_i5.UserDetailedNotMe)? onUnfollow, - _i18.FutureOr Function(_i5.MeDetailed)? onMeUpdated, - _i18.FutureOr Function()? onReadAllNotifications, - _i18.FutureOr Function(_i5.INotificationsResponse)? - onUnreadNotification, - _i18.FutureOr Function(String)? onUnreadMention, - _i18.FutureOr Function()? onReadAllUnreadMentions, - _i18.FutureOr Function(String)? onUnreadSpecifiedNote, - _i18.FutureOr Function()? onReadAllUnreadSpecifiedNotes, - _i18.FutureOr Function(_i5.UserLite)? onReceiveFollowRequest, - _i18.FutureOr Function()? onReadAllAnnouncements, - }) => - (super.noSuchMethod( - Invocation.method( - #mainStream, - [], - { - #onEmojiAdded: onEmojiAdded, - #onEmojiUpdated: onEmojiUpdated, - #onEmojiDeleted: onEmojiDeleted, - #onAnnouncementCreated: onAnnouncementCreated, - #onNotification: onNotification, - #onMention: onMention, - #onReply: onReply, - #onRenote: onRenote, - #onFollow: onFollow, - #onFollowed: onFollowed, - #onUnfollow: onUnfollow, - #onMeUpdated: onMeUpdated, - #onReadAllNotifications: onReadAllNotifications, - #onUnreadNotification: onUnreadNotification, - #onUnreadMention: onUnreadMention, - #onReadAllUnreadMentions: onReadAllUnreadMentions, - #onUnreadSpecifiedNote: onUnreadSpecifiedNote, - #onReadAllUnreadSpecifiedNotes: onReadAllUnreadSpecifiedNotes, - #onReceiveFollowRequest: onReceiveFollowRequest, - #onReadAllAnnouncements: onReadAllAnnouncements, - }, - ), - returnValue: _FakeSocketController_33( - this, - Invocation.method( - #mainStream, - [], - { - #onEmojiAdded: onEmojiAdded, - #onEmojiUpdated: onEmojiUpdated, - #onEmojiDeleted: onEmojiDeleted, - #onAnnouncementCreated: onAnnouncementCreated, - #onNotification: onNotification, - #onMention: onMention, - #onReply: onReply, - #onRenote: onRenote, - #onFollow: onFollow, - #onFollowed: onFollowed, - #onUnfollow: onUnfollow, - #onMeUpdated: onMeUpdated, - #onReadAllNotifications: onReadAllNotifications, - #onUnreadNotification: onUnreadNotification, - #onUnreadMention: onUnreadMention, - #onReadAllUnreadMentions: onReadAllUnreadMentions, - #onUnreadSpecifiedNote: onUnreadSpecifiedNote, - #onReadAllUnreadSpecifiedNotes: onReadAllUnreadSpecifiedNotes, - #onReceiveFollowRequest: onReceiveFollowRequest, - #onReadAllAnnouncements: onReadAllAnnouncements, - }, - ), - ), - returnValueForMissingStub: _FakeSocketController_33( - this, - Invocation.method( - #mainStream, - [], - { - #onEmojiAdded: onEmojiAdded, - #onEmojiUpdated: onEmojiUpdated, - #onEmojiDeleted: onEmojiDeleted, - #onAnnouncementCreated: onAnnouncementCreated, - #onNotification: onNotification, - #onMention: onMention, - #onReply: onReply, - #onRenote: onRenote, - #onFollow: onFollow, - #onFollowed: onFollowed, - #onUnfollow: onUnfollow, - #onMeUpdated: onMeUpdated, - #onReadAllNotifications: onReadAllNotifications, - #onUnreadNotification: onUnreadNotification, - #onUnreadMention: onUnreadMention, - #onReadAllUnreadMentions: onReadAllUnreadMentions, - #onUnreadSpecifiedNote: onUnreadSpecifiedNote, - #onReadAllUnreadSpecifiedNotes: onReadAllUnreadSpecifiedNotes, - #onReceiveFollowRequest: onReceiveFollowRequest, - #onReadAllAnnouncements: onReadAllAnnouncements, - }, - ), - ), - ) as _i5.SocketController); - - @override - _i18.Future startStreaming() => (super.noSuchMethod( - Invocation.method( - #startStreaming, - [], - ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + _i15.Future>.value(<_i6.UserDetailed>[]), + returnValueForMissingStub: + _i15.Future>.value(<_i6.UserDetailed>[]), + ) as _i15.Future>); } /// A class which mocks [MisskeyAntenna]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskeyAntenna extends _i1.Mock implements _i5.MisskeyAntenna { +class MockMisskeyAntenna extends _i1.Mock implements _i6.MisskeyAntenna { @override - _i18.Future<_i5.Antenna> create(_i5.AntennasCreateRequest? request) => + _i15.Future<_i6.Antenna> create(_i6.AntennasCreateRequest? request) => (super.noSuchMethod( Invocation.method( #create, [request], ), - returnValue: _i18.Future<_i5.Antenna>.value(_FakeAntenna_34( + returnValue: _i15.Future<_i6.Antenna>.value(_FakeAntenna_34( this, Invocation.method( #create, @@ -3081,57 +2326,57 @@ class MockMisskeyAntenna extends _i1.Mock implements _i5.MisskeyAntenna { ), )), returnValueForMissingStub: - _i18.Future<_i5.Antenna>.value(_FakeAntenna_34( + _i15.Future<_i6.Antenna>.value(_FakeAntenna_34( this, Invocation.method( #create, [request], ), )), - ) as _i18.Future<_i5.Antenna>); + ) as _i15.Future<_i6.Antenna>); @override - _i18.Future delete(_i5.AntennasDeleteRequest? request) => + _i15.Future delete(_i6.AntennasDeleteRequest? request) => (super.noSuchMethod( Invocation.method( #delete, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future> list() => (super.noSuchMethod( + _i15.Future> list() => (super.noSuchMethod( Invocation.method( #list, [], ), - returnValue: _i18.Future>.value(<_i5.Antenna>[]), + returnValue: _i15.Future>.value(<_i6.Antenna>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Antenna>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Antenna>[]), + ) as _i15.Future>); @override - _i18.Future> notes(_i5.AntennasNotesRequest? request) => + _i15.Future> notes(_i6.AntennasNotesRequest? request) => (super.noSuchMethod( Invocation.method( #notes, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future<_i5.Antenna> show(_i5.AntennasShowRequest? request) => + _i15.Future<_i6.Antenna> show(_i6.AntennasShowRequest? request) => (super.noSuchMethod( Invocation.method( #show, [request], ), - returnValue: _i18.Future<_i5.Antenna>.value(_FakeAntenna_34( + returnValue: _i15.Future<_i6.Antenna>.value(_FakeAntenna_34( this, Invocation.method( #show, @@ -3139,40 +2384,40 @@ class MockMisskeyAntenna extends _i1.Mock implements _i5.MisskeyAntenna { ), )), returnValueForMissingStub: - _i18.Future<_i5.Antenna>.value(_FakeAntenna_34( + _i15.Future<_i6.Antenna>.value(_FakeAntenna_34( this, Invocation.method( #show, [request], ), )), - ) as _i18.Future<_i5.Antenna>); + ) as _i15.Future<_i6.Antenna>); @override - _i18.Future update(_i5.AntennasUpdateRequest? request) => + _i15.Future update(_i6.AntennasUpdateRequest? request) => (super.noSuchMethod( Invocation.method( #update, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); } /// A class which mocks [MisskeyAp]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskeyAp extends _i1.Mock implements _i5.MisskeyAp { +class MockMisskeyAp extends _i1.Mock implements _i6.MisskeyAp { @override - _i18.Future<_i5.ApShowResponse> show(_i5.ApShowRequest? request) => + _i15.Future<_i6.ApShowResponse> show(_i6.ApShowRequest? request) => (super.noSuchMethod( Invocation.method( #show, [request], ), returnValue: - _i18.Future<_i5.ApShowResponse>.value(_FakeApShowResponse_35( + _i15.Future<_i6.ApShowResponse>.value(_FakeApShowResponse_35( this, Invocation.method( #show, @@ -3180,69 +2425,82 @@ class MockMisskeyAp extends _i1.Mock implements _i5.MisskeyAp { ), )), returnValueForMissingStub: - _i18.Future<_i5.ApShowResponse>.value(_FakeApShowResponse_35( + _i15.Future<_i6.ApShowResponse>.value(_FakeApShowResponse_35( this, Invocation.method( #show, [request], ), )), - ) as _i18.Future<_i5.ApShowResponse>); + ) as _i15.Future<_i6.ApShowResponse>); } /// A class which mocks [MisskeyBlocking]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskeyBlocking extends _i1.Mock implements _i5.MisskeyBlocking { +class MockMisskeyBlocking extends _i1.Mock implements _i6.MisskeyBlocking { @override - _i18.Future create(_i5.BlockCreateRequest? request) => + _i15.Future create(_i6.BlockCreateRequest? request) => (super.noSuchMethod( Invocation.method( #create, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future delete(_i5.BlockDeleteRequest? request) => + _i15.Future delete(_i6.BlockDeleteRequest? request) => (super.noSuchMethod( Invocation.method( #delete, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); + + @override + _i15.Future> list(_i6.BlockingListRequest? request) => + (super.noSuchMethod( + Invocation.method( + #list, + [request], + ), + returnValue: + _i15.Future>.value(<_i6.Blocking>[]), + returnValueForMissingStub: + _i15.Future>.value(<_i6.Blocking>[]), + ) as _i15.Future>); } /// A class which mocks [MisskeyChannels]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskeyChannels extends _i1.Mock implements _i5.MisskeyChannels { +class MockMisskeyChannels extends _i1.Mock implements _i6.MisskeyChannels { @override - _i18.Future> timeline( - _i5.ChannelsTimelineRequest? request) => + _i15.Future> timeline( + _i6.ChannelsTimelineRequest? request) => (super.noSuchMethod( Invocation.method( #timeline, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future<_i5.CommunityChannel> show(_i5.ChannelsShowRequest? request) => + _i15.Future<_i6.CommunityChannel> show(_i6.ChannelsShowRequest? request) => (super.noSuchMethod( Invocation.method( #show, [request], ), returnValue: - _i18.Future<_i5.CommunityChannel>.value(_FakeCommunityChannel_36( + _i15.Future<_i6.CommunityChannel>.value(_FakeCommunityChannel_36( this, Invocation.method( #show, @@ -3250,98 +2508,98 @@ class MockMisskeyChannels extends _i1.Mock implements _i5.MisskeyChannels { ), )), returnValueForMissingStub: - _i18.Future<_i5.CommunityChannel>.value(_FakeCommunityChannel_36( + _i15.Future<_i6.CommunityChannel>.value(_FakeCommunityChannel_36( this, Invocation.method( #show, [request], ), )), - ) as _i18.Future<_i5.CommunityChannel>); + ) as _i15.Future<_i6.CommunityChannel>); @override - _i18.Future> followed( - _i5.ChannelsFollowedRequest? request) => + _i15.Future> followed( + _i6.ChannelsFollowedRequest? request) => (super.noSuchMethod( Invocation.method( #followed, [request], ), - returnValue: _i18.Future>.value( - <_i5.CommunityChannel>[]), + returnValue: _i15.Future>.value( + <_i6.CommunityChannel>[]), returnValueForMissingStub: - _i18.Future>.value( - <_i5.CommunityChannel>[]), - ) as _i18.Future>); + _i15.Future>.value( + <_i6.CommunityChannel>[]), + ) as _i15.Future>); @override - _i18.Future> myFavorite( - _i5.ChannelsMyFavoriteRequest? request) => + _i15.Future> myFavorite( + _i6.ChannelsMyFavoriteRequest? request) => (super.noSuchMethod( Invocation.method( #myFavorite, [request], ), - returnValue: _i18.Future>.value( - <_i5.CommunityChannel>[]), + returnValue: _i15.Future>.value( + <_i6.CommunityChannel>[]), returnValueForMissingStub: - _i18.Future>.value( - <_i5.CommunityChannel>[]), - ) as _i18.Future>); + _i15.Future>.value( + <_i6.CommunityChannel>[]), + ) as _i15.Future>); @override - _i18.Future> featured() => (super.noSuchMethod( + _i15.Future> featured() => (super.noSuchMethod( Invocation.method( #featured, [], ), - returnValue: _i18.Future>.value( - <_i5.CommunityChannel>[]), + returnValue: _i15.Future>.value( + <_i6.CommunityChannel>[]), returnValueForMissingStub: - _i18.Future>.value( - <_i5.CommunityChannel>[]), - ) as _i18.Future>); + _i15.Future>.value( + <_i6.CommunityChannel>[]), + ) as _i15.Future>); @override - _i18.Future> owned( - _i5.ChannelsOwnedRequest? request) => + _i15.Future> owned( + _i6.ChannelsOwnedRequest? request) => (super.noSuchMethod( Invocation.method( #owned, [request], ), - returnValue: _i18.Future>.value( - <_i5.CommunityChannel>[]), + returnValue: _i15.Future>.value( + <_i6.CommunityChannel>[]), returnValueForMissingStub: - _i18.Future>.value( - <_i5.CommunityChannel>[]), - ) as _i18.Future>); + _i15.Future>.value( + <_i6.CommunityChannel>[]), + ) as _i15.Future>); @override - _i18.Future> search( - _i5.ChannelsSearchRequest? request) => + _i15.Future> search( + _i6.ChannelsSearchRequest? request) => (super.noSuchMethod( Invocation.method( #search, [request], ), - returnValue: _i18.Future>.value( - <_i5.CommunityChannel>[]), + returnValue: _i15.Future>.value( + <_i6.CommunityChannel>[]), returnValueForMissingStub: - _i18.Future>.value( - <_i5.CommunityChannel>[]), - ) as _i18.Future>); + _i15.Future>.value( + <_i6.CommunityChannel>[]), + ) as _i15.Future>); @override - _i18.Future<_i5.CommunityChannel> create( - _i5.ChannelsCreateRequest? request) => + _i15.Future<_i6.CommunityChannel> create( + _i6.ChannelsCreateRequest? request) => (super.noSuchMethod( Invocation.method( #create, [request], ), returnValue: - _i18.Future<_i5.CommunityChannel>.value(_FakeCommunityChannel_36( + _i15.Future<_i6.CommunityChannel>.value(_FakeCommunityChannel_36( this, Invocation.method( #create, @@ -3349,240 +2607,240 @@ class MockMisskeyChannels extends _i1.Mock implements _i5.MisskeyChannels { ), )), returnValueForMissingStub: - _i18.Future<_i5.CommunityChannel>.value(_FakeCommunityChannel_36( + _i15.Future<_i6.CommunityChannel>.value(_FakeCommunityChannel_36( this, Invocation.method( #create, [request], ), )), - ) as _i18.Future<_i5.CommunityChannel>); + ) as _i15.Future<_i6.CommunityChannel>); @override - _i18.Future update(_i5.ChannelsUpdateRequest? request) => + _i15.Future update(_i6.ChannelsUpdateRequest? request) => (super.noSuchMethod( Invocation.method( #update, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future favorite(_i5.ChannelsFavoriteRequest? request) => + _i15.Future favorite(_i6.ChannelsFavoriteRequest? request) => (super.noSuchMethod( Invocation.method( #favorite, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future unfavorite(_i5.ChannelsUnfavoriteRequest? request) => + _i15.Future unfavorite(_i6.ChannelsUnfavoriteRequest? request) => (super.noSuchMethod( Invocation.method( #unfavorite, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future follow(_i5.ChannelsFollowRequest? request) => + _i15.Future follow(_i6.ChannelsFollowRequest? request) => (super.noSuchMethod( Invocation.method( #follow, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future unfollow(_i5.ChannelsUnfollowRequest? request) => + _i15.Future unfollow(_i6.ChannelsUnfollowRequest? request) => (super.noSuchMethod( Invocation.method( #unfollow, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); } /// A class which mocks [MisskeyClips]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskeyClips extends _i1.Mock implements _i5.MisskeyClips { +class MockMisskeyClips extends _i1.Mock implements _i6.MisskeyClips { @override - _i18.Future> list() => (super.noSuchMethod( + _i15.Future> list() => (super.noSuchMethod( Invocation.method( #list, [], ), - returnValue: _i18.Future>.value(<_i5.Clip>[]), + returnValue: _i15.Future>.value(<_i6.Clip>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Clip>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Clip>[]), + ) as _i15.Future>); @override - _i18.Future> myFavorites() => (super.noSuchMethod( + _i15.Future> myFavorites() => (super.noSuchMethod( Invocation.method( #myFavorites, [], ), - returnValue: _i18.Future>.value(<_i5.Clip>[]), + returnValue: _i15.Future>.value(<_i6.Clip>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Clip>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Clip>[]), + ) as _i15.Future>); @override - _i18.Future> notes(_i5.ClipsNotesRequest? request) => + _i15.Future> notes(_i6.ClipsNotesRequest? request) => (super.noSuchMethod( Invocation.method( #notes, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future addNote(_i5.ClipsAddNoteRequest? request) => + _i15.Future addNote(_i6.ClipsAddNoteRequest? request) => (super.noSuchMethod( Invocation.method( #addNote, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future removeNote(_i5.ClipsRemoveNoteRequest? request) => + _i15.Future removeNote(_i6.ClipsRemoveNoteRequest? request) => (super.noSuchMethod( Invocation.method( #removeNote, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future<_i5.Clip> create(_i5.ClipsCreateRequest? request) => + _i15.Future<_i6.Clip> create(_i6.ClipsCreateRequest? request) => (super.noSuchMethod( Invocation.method( #create, [request], ), - returnValue: _i18.Future<_i5.Clip>.value(_FakeClip_37( + returnValue: _i15.Future<_i6.Clip>.value(_FakeClip_37( this, Invocation.method( #create, [request], ), )), - returnValueForMissingStub: _i18.Future<_i5.Clip>.value(_FakeClip_37( + returnValueForMissingStub: _i15.Future<_i6.Clip>.value(_FakeClip_37( this, Invocation.method( #create, [request], ), )), - ) as _i18.Future<_i5.Clip>); + ) as _i15.Future<_i6.Clip>); @override - _i18.Future delete(_i5.ClipsDeleteRequest? request) => + _i15.Future delete(_i6.ClipsDeleteRequest? request) => (super.noSuchMethod( Invocation.method( #delete, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future<_i5.Clip> update(_i5.ClipsUpdateRequest? request) => + _i15.Future<_i6.Clip> update(_i6.ClipsUpdateRequest? request) => (super.noSuchMethod( Invocation.method( #update, [request], ), - returnValue: _i18.Future<_i5.Clip>.value(_FakeClip_37( + returnValue: _i15.Future<_i6.Clip>.value(_FakeClip_37( this, Invocation.method( #update, [request], ), )), - returnValueForMissingStub: _i18.Future<_i5.Clip>.value(_FakeClip_37( + returnValueForMissingStub: _i15.Future<_i6.Clip>.value(_FakeClip_37( this, Invocation.method( #update, [request], ), )), - ) as _i18.Future<_i5.Clip>); + ) as _i15.Future<_i6.Clip>); @override - _i18.Future<_i5.Clip> show(_i5.ClipsShowRequest? request) => + _i15.Future<_i6.Clip> show(_i6.ClipsShowRequest? request) => (super.noSuchMethod( Invocation.method( #show, [request], ), - returnValue: _i18.Future<_i5.Clip>.value(_FakeClip_37( + returnValue: _i15.Future<_i6.Clip>.value(_FakeClip_37( this, Invocation.method( #show, [request], ), )), - returnValueForMissingStub: _i18.Future<_i5.Clip>.value(_FakeClip_37( + returnValueForMissingStub: _i15.Future<_i6.Clip>.value(_FakeClip_37( this, Invocation.method( #show, [request], ), )), - ) as _i18.Future<_i5.Clip>); + ) as _i15.Future<_i6.Clip>); @override - _i18.Future favorite(_i5.ClipsFavoriteRequest? request) => + _i15.Future favorite(_i6.ClipsFavoriteRequest? request) => (super.noSuchMethod( Invocation.method( #favorite, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future unfavorite(_i5.ClipsUnfavoriteRequest? request) => + _i15.Future unfavorite(_i6.ClipsUnfavoriteRequest? request) => (super.noSuchMethod( Invocation.method( #unfavorite, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); } /// A class which mocks [MisskeyDrive]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskeyDrive extends _i1.Mock implements _i5.MisskeyDrive { +class MockMisskeyDrive extends _i1.Mock implements _i6.MisskeyDrive { @override - _i5.MisskeyDriveFiles get files => (super.noSuchMethod( + _i6.MisskeyDriveFiles get files => (super.noSuchMethod( Invocation.getter(#files), returnValue: _FakeMisskeyDriveFiles_38( this, @@ -3592,10 +2850,10 @@ class MockMisskeyDrive extends _i1.Mock implements _i5.MisskeyDrive { this, Invocation.getter(#files), ), - ) as _i5.MisskeyDriveFiles); + ) as _i6.MisskeyDriveFiles); @override - _i5.MisskeyDriveFolders get folders => (super.noSuchMethod( + _i6.MisskeyDriveFolders get folders => (super.noSuchMethod( Invocation.getter(#folders), returnValue: _FakeMisskeyDriveFolders_39( this, @@ -3605,50 +2863,73 @@ class MockMisskeyDrive extends _i1.Mock implements _i5.MisskeyDrive { this, Invocation.getter(#folders), ), - ) as _i5.MisskeyDriveFolders); + ) as _i6.MisskeyDriveFolders); @override - _i18.Future> stream( - _i5.DriveStreamRequest? request) => + _i15.Future<_i6.DriveResponse> drive() => (super.noSuchMethod( + Invocation.method( + #drive, + [], + ), + returnValue: _i15.Future<_i6.DriveResponse>.value(_FakeDriveResponse_40( + this, + Invocation.method( + #drive, + [], + ), + )), + returnValueForMissingStub: + _i15.Future<_i6.DriveResponse>.value(_FakeDriveResponse_40( + this, + Invocation.method( + #drive, + [], + ), + )), + ) as _i15.Future<_i6.DriveResponse>); + + @override + _i15.Future> stream( + _i6.DriveStreamRequest? request) => (super.noSuchMethod( Invocation.method( #stream, [request], ), returnValue: - _i18.Future>.value(<_i5.DriveFile>[]), + _i15.Future>.value(<_i6.DriveFile>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.DriveFile>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.DriveFile>[]), + ) as _i15.Future>); } /// A class which mocks [MisskeyDriveFolders]. /// /// See the documentation for Mockito's code generation for more information. class MockMisskeyDriveFolders extends _i1.Mock - implements _i5.MisskeyDriveFolders { + implements _i6.MisskeyDriveFolders { @override - _i18.Future> folders( - _i5.DriveFoldersRequest? request) => + _i15.Future> folders( + _i6.DriveFoldersRequest? request) => (super.noSuchMethod( Invocation.method( #folders, [request], ), returnValue: - _i18.Future>.value(<_i5.DriveFolder>[]), + _i15.Future>.value(<_i6.DriveFolder>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.DriveFolder>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.DriveFolder>[]), + ) as _i15.Future>); @override - _i18.Future<_i5.DriveFolder> create(_i5.DriveFoldersCreateRequest? request) => + _i15.Future<_i6.DriveFolder> create(_i6.DriveFoldersCreateRequest? request) => (super.noSuchMethod( Invocation.method( #create, [request], ), - returnValue: _i18.Future<_i5.DriveFolder>.value(_FakeDriveFolder_40( + returnValue: _i15.Future<_i6.DriveFolder>.value(_FakeDriveFolder_41( this, Invocation.method( #create, @@ -3656,48 +2937,48 @@ class MockMisskeyDriveFolders extends _i1.Mock ), )), returnValueForMissingStub: - _i18.Future<_i5.DriveFolder>.value(_FakeDriveFolder_40( + _i15.Future<_i6.DriveFolder>.value(_FakeDriveFolder_41( this, Invocation.method( #create, [request], ), )), - ) as _i18.Future<_i5.DriveFolder>); + ) as _i15.Future<_i6.DriveFolder>); @override - _i18.Future delete(_i5.DriveFoldersDeleteRequest? request) => + _i15.Future delete(_i6.DriveFoldersDeleteRequest? request) => (super.noSuchMethod( Invocation.method( #delete, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future> find( - _i5.DriveFoldersFindRequest? request) => + _i15.Future> find( + _i6.DriveFoldersFindRequest? request) => (super.noSuchMethod( Invocation.method( #find, [request], ), returnValue: - _i18.Future>.value(<_i5.DriveFolder>[]), + _i15.Future>.value(<_i6.DriveFolder>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.DriveFolder>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.DriveFolder>[]), + ) as _i15.Future>); @override - _i18.Future<_i5.DriveFolder> show(_i5.DriveFoldersShowRequest? request) => + _i15.Future<_i6.DriveFolder> show(_i6.DriveFoldersShowRequest? request) => (super.noSuchMethod( Invocation.method( #show, [request], ), - returnValue: _i18.Future<_i5.DriveFolder>.value(_FakeDriveFolder_40( + returnValue: _i15.Future<_i6.DriveFolder>.value(_FakeDriveFolder_41( this, Invocation.method( #show, @@ -3705,23 +2986,23 @@ class MockMisskeyDriveFolders extends _i1.Mock ), )), returnValueForMissingStub: - _i18.Future<_i5.DriveFolder>.value(_FakeDriveFolder_40( + _i15.Future<_i6.DriveFolder>.value(_FakeDriveFolder_41( this, Invocation.method( #show, [request], ), )), - ) as _i18.Future<_i5.DriveFolder>); + ) as _i15.Future<_i6.DriveFolder>); @override - _i18.Future<_i5.DriveFolder> update(_i5.DriveFoldersUpdateRequest? request) => + _i15.Future<_i6.DriveFolder> update(_i6.DriveFoldersUpdateRequest? request) => (super.noSuchMethod( Invocation.method( #update, [request], ), - returnValue: _i18.Future<_i5.DriveFolder>.value(_FakeDriveFolder_40( + returnValue: _i15.Future<_i6.DriveFolder>.value(_FakeDriveFolder_41( this, Invocation.method( #update, @@ -3729,24 +3010,24 @@ class MockMisskeyDriveFolders extends _i1.Mock ), )), returnValueForMissingStub: - _i18.Future<_i5.DriveFolder>.value(_FakeDriveFolder_40( + _i15.Future<_i6.DriveFolder>.value(_FakeDriveFolder_41( this, Invocation.method( #update, [request], ), )), - ) as _i18.Future<_i5.DriveFolder>); + ) as _i15.Future<_i6.DriveFolder>); } /// A class which mocks [MisskeyDriveFiles]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskeyDriveFiles extends _i1.Mock implements _i5.MisskeyDriveFiles { +class MockMisskeyDriveFiles extends _i1.Mock implements _i6.MisskeyDriveFiles { @override - _i18.Future<_i5.DriveFile> create( - _i5.DriveFilesCreateRequest? request, - _i12.File? fileContent, + _i15.Future<_i6.DriveFile> create( + _i6.DriveFilesCreateRequest? request, + _i10.File? fileContent, ) => (super.noSuchMethod( Invocation.method( @@ -3756,7 +3037,7 @@ class MockMisskeyDriveFiles extends _i1.Mock implements _i5.MisskeyDriveFiles { fileContent, ], ), - returnValue: _i18.Future<_i5.DriveFile>.value(_FakeDriveFile_41( + returnValue: _i15.Future<_i6.DriveFile>.value(_FakeDriveFile_42( this, Invocation.method( #create, @@ -3767,7 +3048,7 @@ class MockMisskeyDriveFiles extends _i1.Mock implements _i5.MisskeyDriveFiles { ), )), returnValueForMissingStub: - _i18.Future<_i5.DriveFile>.value(_FakeDriveFile_41( + _i15.Future<_i6.DriveFile>.value(_FakeDriveFile_42( this, Invocation.method( #create, @@ -3777,12 +3058,12 @@ class MockMisskeyDriveFiles extends _i1.Mock implements _i5.MisskeyDriveFiles { ], ), )), - ) as _i18.Future<_i5.DriveFile>); + ) as _i15.Future<_i6.DriveFile>); @override - _i18.Future<_i5.DriveFile> createAsBinary( - _i5.DriveFilesCreateRequest? request, - _i28.Uint8List? fileContent, + _i15.Future<_i6.DriveFile> createAsBinary( + _i6.DriveFilesCreateRequest? request, + _i25.Uint8List? fileContent, ) => (super.noSuchMethod( Invocation.method( @@ -3792,7 +3073,7 @@ class MockMisskeyDriveFiles extends _i1.Mock implements _i5.MisskeyDriveFiles { fileContent, ], ), - returnValue: _i18.Future<_i5.DriveFile>.value(_FakeDriveFile_41( + returnValue: _i15.Future<_i6.DriveFile>.value(_FakeDriveFile_42( this, Invocation.method( #createAsBinary, @@ -3803,7 +3084,7 @@ class MockMisskeyDriveFiles extends _i1.Mock implements _i5.MisskeyDriveFiles { ), )), returnValueForMissingStub: - _i18.Future<_i5.DriveFile>.value(_FakeDriveFile_41( + _i15.Future<_i6.DriveFile>.value(_FakeDriveFile_42( this, Invocation.method( #createAsBinary, @@ -3813,16 +3094,16 @@ class MockMisskeyDriveFiles extends _i1.Mock implements _i5.MisskeyDriveFiles { ], ), )), - ) as _i18.Future<_i5.DriveFile>); + ) as _i15.Future<_i6.DriveFile>); @override - _i18.Future<_i5.DriveFile> update(_i5.DriveFilesUpdateRequest? request) => + _i15.Future<_i6.DriveFile> update(_i6.DriveFilesUpdateRequest? request) => (super.noSuchMethod( Invocation.method( #update, [request], ), - returnValue: _i18.Future<_i5.DriveFile>.value(_FakeDriveFile_41( + returnValue: _i15.Future<_i6.DriveFile>.value(_FakeDriveFile_42( this, Invocation.method( #update, @@ -3830,100 +3111,100 @@ class MockMisskeyDriveFiles extends _i1.Mock implements _i5.MisskeyDriveFiles { ), )), returnValueForMissingStub: - _i18.Future<_i5.DriveFile>.value(_FakeDriveFile_41( + _i15.Future<_i6.DriveFile>.value(_FakeDriveFile_42( this, Invocation.method( #update, [request], ), )), - ) as _i18.Future<_i5.DriveFile>); + ) as _i15.Future<_i6.DriveFile>); @override - _i18.Future delete(_i5.DriveFilesDeleteRequest? request) => + _i15.Future delete(_i6.DriveFilesDeleteRequest? request) => (super.noSuchMethod( Invocation.method( #delete, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future> files(_i5.DriveFilesRequest? request) => + _i15.Future> files(_i6.DriveFilesRequest? request) => (super.noSuchMethod( Invocation.method( #files, [request], ), returnValue: - _i18.Future>.value(<_i5.DriveFile>[]), + _i15.Future>.value(<_i6.DriveFile>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.DriveFile>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.DriveFile>[]), + ) as _i15.Future>); @override - _i18.Future> find( - _i5.DriveFilesFindRequest? request) => + _i15.Future> find( + _i6.DriveFilesFindRequest? request) => (super.noSuchMethod( Invocation.method( #find, [request], ), returnValue: - _i18.Future>.value(<_i5.DriveFile>[]), + _i15.Future>.value(<_i6.DriveFile>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.DriveFile>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.DriveFile>[]), + ) as _i15.Future>); @override - _i18.Future> attachedNotes( - _i5.DriveFilesAttachedNotesRequest? request) => + _i15.Future> attachedNotes( + _i6.DriveFilesAttachedNotesRequest? request) => (super.noSuchMethod( Invocation.method( #attachedNotes, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future checkExistence( - _i5.DriveFilesCheckExistenceRequest? request) => + _i15.Future checkExistence( + _i6.DriveFilesCheckExistenceRequest? request) => (super.noSuchMethod( Invocation.method( #checkExistence, [request], ), - returnValue: _i18.Future.value(false), - returnValueForMissingStub: _i18.Future.value(false), - ) as _i18.Future); + returnValue: _i15.Future.value(false), + returnValueForMissingStub: _i15.Future.value(false), + ) as _i15.Future); @override - _i18.Future> findByHash( - _i5.DriveFilesFindByHashRequest? request) => + _i15.Future> findByHash( + _i6.DriveFilesFindByHashRequest? request) => (super.noSuchMethod( Invocation.method( #findByHash, [request], ), returnValue: - _i18.Future>.value(<_i5.DriveFile>[]), + _i15.Future>.value(<_i6.DriveFile>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.DriveFile>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.DriveFile>[]), + ) as _i15.Future>); @override - _i18.Future<_i5.DriveFile> show(_i5.DriveFilesShowRequest? request) => + _i15.Future<_i6.DriveFile> show(_i6.DriveFilesShowRequest? request) => (super.noSuchMethod( Invocation.method( #show, [request], ), - returnValue: _i18.Future<_i5.DriveFile>.value(_FakeDriveFile_41( + returnValue: _i15.Future<_i6.DriveFile>.value(_FakeDriveFile_42( this, Invocation.method( #show, @@ -3931,42 +3212,42 @@ class MockMisskeyDriveFiles extends _i1.Mock implements _i5.MisskeyDriveFiles { ), )), returnValueForMissingStub: - _i18.Future<_i5.DriveFile>.value(_FakeDriveFile_41( + _i15.Future<_i6.DriveFile>.value(_FakeDriveFile_42( this, Invocation.method( #show, [request], ), )), - ) as _i18.Future<_i5.DriveFile>); + ) as _i15.Future<_i6.DriveFile>); @override - _i18.Future uploadFromUrl( - _i5.DriveFilesUploadFromUrlRequest? request) => + _i15.Future uploadFromUrl( + _i6.DriveFilesUploadFromUrlRequest? request) => (super.noSuchMethod( Invocation.method( #uploadFromUrl, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); } /// A class which mocks [MisskeyFederation]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskeyFederation extends _i1.Mock implements _i5.MisskeyFederation { +class MockMisskeyFederation extends _i1.Mock implements _i6.MisskeyFederation { @override - _i18.Future<_i5.FederationShowInstanceResponse> showInstance( - _i5.FederationShowInstanceRequest? request) => + _i15.Future<_i6.FederationShowInstanceResponse> showInstance( + _i6.FederationShowInstanceRequest? request) => (super.noSuchMethod( Invocation.method( #showInstance, [request], ), - returnValue: _i18.Future<_i5.FederationShowInstanceResponse>.value( - _FakeFederationShowInstanceResponse_42( + returnValue: _i15.Future<_i6.FederationShowInstanceResponse>.value( + _FakeFederationShowInstanceResponse_43( this, Invocation.method( #showInstance, @@ -3974,56 +3255,56 @@ class MockMisskeyFederation extends _i1.Mock implements _i5.MisskeyFederation { ), )), returnValueForMissingStub: - _i18.Future<_i5.FederationShowInstanceResponse>.value( - _FakeFederationShowInstanceResponse_42( + _i15.Future<_i6.FederationShowInstanceResponse>.value( + _FakeFederationShowInstanceResponse_43( this, Invocation.method( #showInstance, [request], ), )), - ) as _i18.Future<_i5.FederationShowInstanceResponse>); + ) as _i15.Future<_i6.FederationShowInstanceResponse>); @override - _i18.Future> users( - _i5.FederationUsersRequest? request) => + _i15.Future> users( + _i6.FederationUsersRequest? request) => (super.noSuchMethod( Invocation.method( #users, [request], ), returnValue: - _i18.Future>.value(<_i5.UserDetailed>[]), + _i15.Future>.value(<_i6.UserDetailed>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.UserDetailed>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.UserDetailed>[]), + ) as _i15.Future>); } /// A class which mocks [MisskeyFollowing]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskeyFollowing extends _i1.Mock implements _i5.MisskeyFollowing { +class MockMisskeyFollowing extends _i1.Mock implements _i6.MisskeyFollowing { @override - _i5.MisskeyFollowingRequests get requests => (super.noSuchMethod( + _i6.MisskeyFollowingRequests get requests => (super.noSuchMethod( Invocation.getter(#requests), - returnValue: _FakeMisskeyFollowingRequests_43( + returnValue: _FakeMisskeyFollowingRequests_44( this, Invocation.getter(#requests), ), - returnValueForMissingStub: _FakeMisskeyFollowingRequests_43( + returnValueForMissingStub: _FakeMisskeyFollowingRequests_44( this, Invocation.getter(#requests), ), - ) as _i5.MisskeyFollowingRequests); + ) as _i6.MisskeyFollowingRequests); @override - _i18.Future<_i5.UserLite> create(_i5.FollowingCreateRequest? request) => + _i15.Future<_i6.UserLite> create(_i6.FollowingCreateRequest? request) => (super.noSuchMethod( Invocation.method( #create, [request], ), - returnValue: _i18.Future<_i5.UserLite>.value(_FakeUserLite_44( + returnValue: _i15.Future<_i6.UserLite>.value(_FakeUserLite_45( this, Invocation.method( #create, @@ -4031,23 +3312,23 @@ class MockMisskeyFollowing extends _i1.Mock implements _i5.MisskeyFollowing { ), )), returnValueForMissingStub: - _i18.Future<_i5.UserLite>.value(_FakeUserLite_44( + _i15.Future<_i6.UserLite>.value(_FakeUserLite_45( this, Invocation.method( #create, [request], ), )), - ) as _i18.Future<_i5.UserLite>); + ) as _i15.Future<_i6.UserLite>); @override - _i18.Future<_i5.UserLite> delete(_i5.FollowingDeleteRequest? request) => + _i15.Future<_i6.UserLite> delete(_i6.FollowingDeleteRequest? request) => (super.noSuchMethod( Invocation.method( #delete, [request], ), - returnValue: _i18.Future<_i5.UserLite>.value(_FakeUserLite_44( + returnValue: _i15.Future<_i6.UserLite>.value(_FakeUserLite_45( this, Invocation.method( #delete, @@ -4055,24 +3336,24 @@ class MockMisskeyFollowing extends _i1.Mock implements _i5.MisskeyFollowing { ), )), returnValueForMissingStub: - _i18.Future<_i5.UserLite>.value(_FakeUserLite_44( + _i15.Future<_i6.UserLite>.value(_FakeUserLite_45( this, Invocation.method( #delete, [request], ), )), - ) as _i18.Future<_i5.UserLite>); + ) as _i15.Future<_i6.UserLite>); @override - _i18.Future<_i5.UserLite> invalidate( - _i5.FollowingInvalidateRequest? request) => + _i15.Future<_i6.UserLite> invalidate( + _i6.FollowingInvalidateRequest? request) => (super.noSuchMethod( Invocation.method( #invalidate, [request], ), - returnValue: _i18.Future<_i5.UserLite>.value(_FakeUserLite_44( + returnValue: _i15.Future<_i6.UserLite>.value(_FakeUserLite_45( this, Invocation.method( #invalidate, @@ -4080,63 +3361,87 @@ class MockMisskeyFollowing extends _i1.Mock implements _i5.MisskeyFollowing { ), )), returnValueForMissingStub: - _i18.Future<_i5.UserLite>.value(_FakeUserLite_44( + _i15.Future<_i6.UserLite>.value(_FakeUserLite_45( this, Invocation.method( #invalidate, [request], ), )), - ) as _i18.Future<_i5.UserLite>); + ) as _i15.Future<_i6.UserLite>); + + @override + _i15.Future<_i6.UserLite> update(_i6.FollowingUpdateRequest? request) => + (super.noSuchMethod( + Invocation.method( + #update, + [request], + ), + returnValue: _i15.Future<_i6.UserLite>.value(_FakeUserLite_45( + this, + Invocation.method( + #update, + [request], + ), + )), + returnValueForMissingStub: + _i15.Future<_i6.UserLite>.value(_FakeUserLite_45( + this, + Invocation.method( + #update, + [request], + ), + )), + ) as _i15.Future<_i6.UserLite>); @override - _i18.Future updateAll(_i5.FollowingUpdateAllRequest? request) => + _i15.Future updateAll(_i6.FollowingUpdateAllRequest? request) => (super.noSuchMethod( Invocation.method( #updateAll, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); } /// A class which mocks [MisskeyHashtags]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskeyHashtags extends _i1.Mock implements _i5.MisskeyHashtags { +class MockMisskeyHashtags extends _i1.Mock implements _i6.MisskeyHashtags { @override - _i18.Future> list(_i5.HashtagsListRequest? request) => + _i15.Future> list(_i6.HashtagsListRequest? request) => (super.noSuchMethod( Invocation.method( #list, [request], ), - returnValue: _i18.Future>.value(<_i5.Hashtag>[]), + returnValue: _i15.Future>.value(<_i6.Hashtag>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Hashtag>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Hashtag>[]), + ) as _i15.Future>); @override - _i18.Future> search(_i5.HashtagsSearchRequest? request) => + _i15.Future> search(_i6.HashtagsSearchRequest? request) => (super.noSuchMethod( Invocation.method( #search, [request], ), - returnValue: _i18.Future>.value([]), + returnValue: _i15.Future>.value([]), returnValueForMissingStub: - _i18.Future>.value([]), - ) as _i18.Future>); + _i15.Future>.value([]), + ) as _i15.Future>); @override - _i18.Future<_i5.Hashtag> show(_i5.HashtagsShowRequest? request) => + _i15.Future<_i6.Hashtag> show(_i6.HashtagsShowRequest? request) => (super.noSuchMethod( Invocation.method( #show, [request], ), - returnValue: _i18.Future<_i5.Hashtag>.value(_FakeHashtag_45( + returnValue: _i15.Future<_i6.Hashtag>.value(_FakeHashtag_46( this, Invocation.method( #show, @@ -4144,495 +3449,570 @@ class MockMisskeyHashtags extends _i1.Mock implements _i5.MisskeyHashtags { ), )), returnValueForMissingStub: - _i18.Future<_i5.Hashtag>.value(_FakeHashtag_45( + _i15.Future<_i6.Hashtag>.value(_FakeHashtag_46( this, Invocation.method( #show, [request], ), )), - ) as _i18.Future<_i5.Hashtag>); + ) as _i15.Future<_i6.Hashtag>); @override - _i18.Future> trend() => + _i15.Future> trend() => (super.noSuchMethod( Invocation.method( #trend, [], ), - returnValue: _i18.Future>.value( - <_i5.HashtagsTrendResponse>[]), + returnValue: _i15.Future>.value( + <_i6.HashtagsTrendResponse>[]), returnValueForMissingStub: - _i18.Future>.value( - <_i5.HashtagsTrendResponse>[]), - ) as _i18.Future>); + _i15.Future>.value( + <_i6.HashtagsTrendResponse>[]), + ) as _i15.Future>); @override - _i18.Future> users( - _i5.HashtagsUsersRequest? request) => + _i15.Future> users( + _i6.HashtagsUsersRequest? request) => (super.noSuchMethod( Invocation.method( #users, [request], ), returnValue: - _i18.Future>.value(<_i5.UserDetailed>[]), + _i15.Future>.value(<_i6.UserDetailed>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.UserDetailed>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.UserDetailed>[]), + ) as _i15.Future>); } /// A class which mocks [MisskeyI]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskeyI extends _i1.Mock implements _i5.MisskeyI { +class MockMisskeyI extends _i1.Mock implements _i6.MisskeyI { @override - _i5.MisskeyIRegistry get registry => (super.noSuchMethod( + _i6.MisskeyIRegistry get registry => (super.noSuchMethod( Invocation.getter(#registry), - returnValue: _FakeMisskeyIRegistry_46( + returnValue: _FakeMisskeyIRegistry_47( this, Invocation.getter(#registry), ), - returnValueForMissingStub: _FakeMisskeyIRegistry_46( + returnValueForMissingStub: _FakeMisskeyIRegistry_47( this, Invocation.getter(#registry), ), - ) as _i5.MisskeyIRegistry); + ) as _i6.MisskeyIRegistry); + + @override + _i15.Future<_i6.MeDetailed> i() => (super.noSuchMethod( + Invocation.method( + #i, + [], + ), + returnValue: _i15.Future<_i6.MeDetailed>.value(_FakeMeDetailed_48( + this, + Invocation.method( + #i, + [], + ), + )), + returnValueForMissingStub: + _i15.Future<_i6.MeDetailed>.value(_FakeMeDetailed_48( + this, + Invocation.method( + #i, + [], + ), + )), + ) as _i15.Future<_i6.MeDetailed>); + + @override + _i15.Future> notifications( + _i6.INotificationsRequest? request) => + (super.noSuchMethod( + Invocation.method( + #notifications, + [request], + ), + returnValue: _i15.Future>.value( + <_i6.INotificationsResponse>[]), + returnValueForMissingStub: + _i15.Future>.value( + <_i6.INotificationsResponse>[]), + ) as _i15.Future>); + + @override + _i15.Future readAnnouncement(_i6.IReadAnnouncementRequest? request) => + (super.noSuchMethod( + Invocation.method( + #readAnnouncement, + [request], + ), + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); + + @override + _i15.Future> favorites( + _i6.IFavoritesRequest? request) => + (super.noSuchMethod( + Invocation.method( + #favorites, + [request], + ), + returnValue: _i15.Future>.value( + <_i6.IFavoritesResponse>[]), + returnValueForMissingStub: + _i15.Future>.value( + <_i6.IFavoritesResponse>[]), + ) as _i15.Future>); @override - _i18.Future<_i5.MeDetailed> i() => (super.noSuchMethod( + _i15.Future<_i6.MeDetailed> update(_i6.IUpdateRequest? request) => + (super.noSuchMethod( Invocation.method( - #i, - [], + #update, + [request], ), - returnValue: _i18.Future<_i5.MeDetailed>.value(_FakeMeDetailed_47( + returnValue: _i15.Future<_i6.MeDetailed>.value(_FakeMeDetailed_48( this, Invocation.method( - #i, - [], + #update, + [request], ), )), returnValueForMissingStub: - _i18.Future<_i5.MeDetailed>.value(_FakeMeDetailed_47( + _i15.Future<_i6.MeDetailed>.value(_FakeMeDetailed_48( this, Invocation.method( - #i, - [], + #update, + [request], ), )), - ) as _i18.Future<_i5.MeDetailed>); + ) as _i15.Future<_i6.MeDetailed>); @override - _i18.Future> notifications( - _i5.INotificationsRequest? request) => + _i15.Future> pageLikes( + _i6.IPageLikesRequest? request) => (super.noSuchMethod( Invocation.method( - #notifications, + #pageLikes, [request], ), - returnValue: _i18.Future>.value( - <_i5.INotificationsResponse>[]), + returnValue: _i15.Future>.value( + <_i6.IPageLikesResponse>[]), returnValueForMissingStub: - _i18.Future>.value( - <_i5.INotificationsResponse>[]), - ) as _i18.Future>); + _i15.Future>.value( + <_i6.IPageLikesResponse>[]), + ) as _i15.Future>); @override - _i18.Future readAnnouncement(_i5.IReadAnnouncementRequest? request) => + _i15.Future> pages(_i6.IPagesRequest? request) => (super.noSuchMethod( Invocation.method( - #readAnnouncement, + #pages, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future>.value(<_i6.Page>[]), + returnValueForMissingStub: + _i15.Future>.value(<_i6.Page>[]), + ) as _i15.Future>); @override - _i18.Future> favorites( - _i5.IFavoritesRequest? request) => + _i15.Future<_i6.MeDetailed> pin(_i6.IPinRequest? request) => (super.noSuchMethod( Invocation.method( - #favorites, + #pin, [request], ), - returnValue: _i18.Future>.value( - <_i5.IFavoritesResponse>[]), + returnValue: _i15.Future<_i6.MeDetailed>.value(_FakeMeDetailed_48( + this, + Invocation.method( + #pin, + [request], + ), + )), returnValueForMissingStub: - _i18.Future>.value( - <_i5.IFavoritesResponse>[]), - ) as _i18.Future>); + _i15.Future<_i6.MeDetailed>.value(_FakeMeDetailed_48( + this, + Invocation.method( + #pin, + [request], + ), + )), + ) as _i15.Future<_i6.MeDetailed>); @override - _i18.Future<_i5.MeDetailed> update(_i5.IUpdateRequest? request) => + _i15.Future<_i6.MeDetailed> unpin(_i6.IUnpinRequest? request) => (super.noSuchMethod( Invocation.method( - #update, + #unpin, [request], ), - returnValue: _i18.Future<_i5.MeDetailed>.value(_FakeMeDetailed_47( + returnValue: _i15.Future<_i6.MeDetailed>.value(_FakeMeDetailed_48( this, Invocation.method( - #update, + #unpin, [request], ), )), returnValueForMissingStub: - _i18.Future<_i5.MeDetailed>.value(_FakeMeDetailed_47( + _i15.Future<_i6.MeDetailed>.value(_FakeMeDetailed_48( this, Invocation.method( - #update, + #unpin, [request], ), )), - ) as _i18.Future<_i5.MeDetailed>); + ) as _i15.Future<_i6.MeDetailed>); } /// A class which mocks [MisskeyNotes]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskeyNotes extends _i1.Mock implements _i5.MisskeyNotes { +class MockMisskeyNotes extends _i1.Mock implements _i6.MisskeyNotes { @override - _i5.MisskeyNotesReactions get reactions => (super.noSuchMethod( + _i6.MisskeyNotesReactions get reactions => (super.noSuchMethod( Invocation.getter(#reactions), - returnValue: _FakeMisskeyNotesReactions_48( + returnValue: _FakeMisskeyNotesReactions_49( this, Invocation.getter(#reactions), ), - returnValueForMissingStub: _FakeMisskeyNotesReactions_48( + returnValueForMissingStub: _FakeMisskeyNotesReactions_49( this, Invocation.getter(#reactions), ), - ) as _i5.MisskeyNotesReactions); + ) as _i6.MisskeyNotesReactions); @override - _i5.MisskeyNotesFavorites get favorites => (super.noSuchMethod( + _i6.MisskeyNotesFavorites get favorites => (super.noSuchMethod( Invocation.getter(#favorites), - returnValue: _FakeMisskeyNotesFavorites_49( + returnValue: _FakeMisskeyNotesFavorites_50( this, Invocation.getter(#favorites), ), - returnValueForMissingStub: _FakeMisskeyNotesFavorites_49( + returnValueForMissingStub: _FakeMisskeyNotesFavorites_50( this, Invocation.getter(#favorites), ), - ) as _i5.MisskeyNotesFavorites); + ) as _i6.MisskeyNotesFavorites); @override - _i5.MisskeyNotesPolls get polls => (super.noSuchMethod( + _i6.MisskeyNotesPolls get polls => (super.noSuchMethod( Invocation.getter(#polls), - returnValue: _FakeMisskeyNotesPolls_50( + returnValue: _FakeMisskeyNotesPolls_51( this, Invocation.getter(#polls), ), - returnValueForMissingStub: _FakeMisskeyNotesPolls_50( + returnValueForMissingStub: _FakeMisskeyNotesPolls_51( this, Invocation.getter(#polls), ), - ) as _i5.MisskeyNotesPolls); + ) as _i6.MisskeyNotesPolls); @override - _i5.MisskeyNotesThreadMuting get threadMuting => (super.noSuchMethod( + _i6.MisskeyNotesThreadMuting get threadMuting => (super.noSuchMethod( Invocation.getter(#threadMuting), - returnValue: _FakeMisskeyNotesThreadMuting_51( + returnValue: _FakeMisskeyNotesThreadMuting_52( this, Invocation.getter(#threadMuting), ), - returnValueForMissingStub: _FakeMisskeyNotesThreadMuting_51( + returnValueForMissingStub: _FakeMisskeyNotesThreadMuting_52( this, Invocation.getter(#threadMuting), ), - ) as _i5.MisskeyNotesThreadMuting); + ) as _i6.MisskeyNotesThreadMuting); @override - _i18.Future create(_i5.NotesCreateRequest? request) => + _i15.Future create(_i6.NotesCreateRequest? request) => (super.noSuchMethod( Invocation.method( #create, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future update(_i5.NotesUpdateRequest? request) => + _i15.Future update(_i6.NotesUpdateRequest? request) => (super.noSuchMethod( Invocation.method( #update, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future delete(_i5.NotesDeleteRequest? request) => + _i15.Future delete(_i6.NotesDeleteRequest? request) => (super.noSuchMethod( Invocation.method( #delete, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future> notes(_i5.NotesRequest? request) => + _i15.Future> notes(_i6.NotesRequest? request) => (super.noSuchMethod( Invocation.method( #notes, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future<_i5.Note> show(_i5.NotesShowRequest? request) => + _i15.Future<_i6.Note> show(_i6.NotesShowRequest? request) => (super.noSuchMethod( Invocation.method( #show, [request], ), - returnValue: _i18.Future<_i5.Note>.value(_FakeNote_52( + returnValue: _i15.Future<_i6.Note>.value(_FakeNote_53( this, Invocation.method( #show, [request], ), )), - returnValueForMissingStub: _i18.Future<_i5.Note>.value(_FakeNote_52( + returnValueForMissingStub: _i15.Future<_i6.Note>.value(_FakeNote_53( this, Invocation.method( #show, [request], ), )), - ) as _i18.Future<_i5.Note>); + ) as _i15.Future<_i6.Note>); @override - _i18.Future> homeTimeline( - _i5.NotesTimelineRequest? request) => + _i15.Future> homeTimeline( + _i6.NotesTimelineRequest? request) => (super.noSuchMethod( Invocation.method( #homeTimeline, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future> localTimeline( - _i5.NotesLocalTimelineRequest? request) => + _i15.Future> localTimeline( + _i6.NotesLocalTimelineRequest? request) => (super.noSuchMethod( Invocation.method( #localTimeline, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future> hybridTimeline( - _i5.NotesHybridTimelineRequest? request) => + _i15.Future> hybridTimeline( + _i6.NotesHybridTimelineRequest? request) => (super.noSuchMethod( Invocation.method( #hybridTimeline, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future> globalTimeline( - _i5.NotesGlobalTimelineRequest? request) => + _i15.Future> globalTimeline( + _i6.NotesGlobalTimelineRequest? request) => (super.noSuchMethod( Invocation.method( #globalTimeline, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future> userListTimeline( - _i5.UserListTimelineRequest? request) => + _i15.Future> userListTimeline( + _i6.UserListTimelineRequest? request) => (super.noSuchMethod( Invocation.method( #userListTimeline, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future<_i5.NotesStateResponse> state(_i5.NotesStateRequest? request) => + _i15.Future<_i6.NotesStateResponse> state(_i6.NotesStateRequest? request) => (super.noSuchMethod( Invocation.method( #state, [request], ), - returnValue: _i18.Future<_i5.NotesStateResponse>.value( - _FakeNotesStateResponse_53( + returnValue: _i15.Future<_i6.NotesStateResponse>.value( + _FakeNotesStateResponse_54( this, Invocation.method( #state, [request], ), )), - returnValueForMissingStub: _i18.Future<_i5.NotesStateResponse>.value( - _FakeNotesStateResponse_53( + returnValueForMissingStub: _i15.Future<_i6.NotesStateResponse>.value( + _FakeNotesStateResponse_54( this, Invocation.method( #state, [request], ), )), - ) as _i18.Future<_i5.NotesStateResponse>); + ) as _i15.Future<_i6.NotesStateResponse>); @override - _i18.Future> search(_i5.NotesSearchRequest? request) => + _i15.Future> search(_i6.NotesSearchRequest? request) => (super.noSuchMethod( Invocation.method( #search, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future> searchByTag( - _i5.NotesSearchByTagRequest? request) => + _i15.Future> searchByTag( + _i6.NotesSearchByTagRequest? request) => (super.noSuchMethod( Invocation.method( #searchByTag, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future> renotes(_i5.NotesRenoteRequest? request) => + _i15.Future> renotes(_i6.NotesRenoteRequest? request) => (super.noSuchMethod( Invocation.method( #renotes, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future> replies(_i5.NotesRepliesRequest? request) => + _i15.Future> replies(_i6.NotesRepliesRequest? request) => (super.noSuchMethod( Invocation.method( #replies, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future> children(_i5.NotesChildrenRequest? request) => + _i15.Future> children(_i6.NotesChildrenRequest? request) => (super.noSuchMethod( Invocation.method( #children, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future> conversation( - _i5.NotesConversationRequest? request) => + _i15.Future> conversation( + _i6.NotesConversationRequest? request) => (super.noSuchMethod( Invocation.method( #conversation, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future> featured(_i5.NotesFeaturedRequest? request) => + _i15.Future> featured(_i6.NotesFeaturedRequest? request) => (super.noSuchMethod( Invocation.method( #featured, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future> mentions(_i5.NotesMentionsRequest? request) => + _i15.Future> mentions(_i6.NotesMentionsRequest? request) => (super.noSuchMethod( Invocation.method( #mentions, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future> clips(_i5.NotesClipsRequest? request) => + _i15.Future> clips(_i6.NotesClipsRequest? request) => (super.noSuchMethod( Invocation.method( #clips, [request], ), - returnValue: _i18.Future>.value(<_i5.Clip>[]), + returnValue: _i15.Future>.value(<_i6.Clip>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Clip>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Clip>[]), + ) as _i15.Future>); @override - _i18.Future unrenote(_i5.NotesUnrenoteRequest? request) => + _i15.Future unrenote(_i6.NotesUnrenoteRequest? request) => (super.noSuchMethod( Invocation.method( #unrenote, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future<_i5.NotesTranslateResponse> translate( - _i5.NotesTranslateRequest? request) => + _i15.Future<_i6.NotesTranslateResponse> translate( + _i6.NotesTranslateRequest? request) => (super.noSuchMethod( Invocation.method( #translate, [request], ), - returnValue: _i18.Future<_i5.NotesTranslateResponse>.value( - _FakeNotesTranslateResponse_54( + returnValue: _i15.Future<_i6.NotesTranslateResponse>.value( + _FakeNotesTranslateResponse_55( this, Invocation.method( #translate, @@ -4640,185 +4020,199 @@ class MockMisskeyNotes extends _i1.Mock implements _i5.MisskeyNotes { ), )), returnValueForMissingStub: - _i18.Future<_i5.NotesTranslateResponse>.value( - _FakeNotesTranslateResponse_54( + _i15.Future<_i6.NotesTranslateResponse>.value( + _FakeNotesTranslateResponse_55( this, Invocation.method( #translate, [request], ), )), - ) as _i18.Future<_i5.NotesTranslateResponse>); + ) as _i15.Future<_i6.NotesTranslateResponse>); } /// A class which mocks [MisskeyNotesFavorites]. /// /// See the documentation for Mockito's code generation for more information. class MockMisskeyNotesFavorites extends _i1.Mock - implements _i5.MisskeyNotesFavorites { + implements _i6.MisskeyNotesFavorites { @override - _i18.Future create(_i5.NotesFavoritesCreateRequest? request) => + _i15.Future create(_i6.NotesFavoritesCreateRequest? request) => (super.noSuchMethod( Invocation.method( #create, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future delete(_i5.NotesFavoritesDeleteRequest? request) => + _i15.Future delete(_i6.NotesFavoritesDeleteRequest? request) => (super.noSuchMethod( Invocation.method( #delete, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); } /// A class which mocks [MisskeyNotesReactions]. /// /// See the documentation for Mockito's code generation for more information. class MockMisskeyNotesReactions extends _i1.Mock - implements _i5.MisskeyNotesReactions { + implements _i6.MisskeyNotesReactions { @override - _i18.Future create(_i5.NotesReactionsCreateRequest? request) => + _i15.Future create(_i6.NotesReactionsCreateRequest? request) => (super.noSuchMethod( Invocation.method( #create, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future delete(_i5.NotesReactionsDeleteRequest? request) => + _i15.Future delete(_i6.NotesReactionsDeleteRequest? request) => (super.noSuchMethod( Invocation.method( #delete, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future> reactions( - _i5.NotesReactionsRequest? request) => + _i15.Future> reactions( + _i6.NotesReactionsRequest? request) => (super.noSuchMethod( Invocation.method( #reactions, [request], ), - returnValue: _i18.Future>.value( - <_i5.NotesReactionsResponse>[]), + returnValue: _i15.Future>.value( + <_i6.NotesReactionsResponse>[]), returnValueForMissingStub: - _i18.Future>.value( - <_i5.NotesReactionsResponse>[]), - ) as _i18.Future>); + _i15.Future>.value( + <_i6.NotesReactionsResponse>[]), + ) as _i15.Future>); } /// A class which mocks [MisskeyNotesPolls]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskeyNotesPolls extends _i1.Mock implements _i5.MisskeyNotesPolls { +class MockMisskeyNotesPolls extends _i1.Mock implements _i6.MisskeyNotesPolls { @override - _i18.Future vote(_i5.NotesPollsVoteRequest? request) => + _i15.Future vote(_i6.NotesPollsVoteRequest? request) => (super.noSuchMethod( Invocation.method( #vote, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future> recommendation( - _i5.NotesPollsRecommendationRequest? request) => + _i15.Future> recommendation( + _i6.NotesPollsRecommendationRequest? request) => (super.noSuchMethod( Invocation.method( #recommendation, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); } /// A class which mocks [MisskeyRenoteMute]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskeyRenoteMute extends _i1.Mock implements _i5.MisskeyRenoteMute { +class MockMisskeyRenoteMute extends _i1.Mock implements _i6.MisskeyRenoteMute { @override - _i18.Future create(_i5.RenoteMuteCreateRequest? request) => + _i15.Future create(_i6.RenoteMuteCreateRequest? request) => (super.noSuchMethod( Invocation.method( #create, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future delete(_i5.RenoteMuteDeleteRequest? request) => + _i15.Future delete(_i6.RenoteMuteDeleteRequest? request) => (super.noSuchMethod( Invocation.method( #delete, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); + + @override + _i15.Future> list( + _i6.RenoteMuteListRequest? request) => + (super.noSuchMethod( + Invocation.method( + #list, + [request], + ), + returnValue: + _i15.Future>.value(<_i6.RenoteMuting>[]), + returnValueForMissingStub: + _i15.Future>.value(<_i6.RenoteMuting>[]), + ) as _i15.Future>); } /// A class which mocks [MisskeyRoles]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskeyRoles extends _i1.Mock implements _i5.MisskeyRoles { +class MockMisskeyRoles extends _i1.Mock implements _i6.MisskeyRoles { @override - _i18.Future> list() => (super.noSuchMethod( + _i15.Future> list() => (super.noSuchMethod( Invocation.method( #list, [], ), - returnValue: _i18.Future>.value( - <_i5.RolesListResponse>[]), + returnValue: _i15.Future>.value( + <_i6.RolesListResponse>[]), returnValueForMissingStub: - _i18.Future>.value( - <_i5.RolesListResponse>[]), - ) as _i18.Future>); + _i15.Future>.value( + <_i6.RolesListResponse>[]), + ) as _i15.Future>); @override - _i18.Future> users( - _i5.RolesUsersRequest? request) => + _i15.Future> users( + _i6.RolesUsersRequest? request) => (super.noSuchMethod( Invocation.method( #users, [request], ), - returnValue: _i18.Future>.value( - <_i5.RolesUsersResponse>[]), + returnValue: _i15.Future>.value( + <_i6.RolesUsersResponse>[]), returnValueForMissingStub: - _i18.Future>.value( - <_i5.RolesUsersResponse>[]), - ) as _i18.Future>); + _i15.Future>.value( + <_i6.RolesUsersResponse>[]), + ) as _i15.Future>); @override - _i18.Future<_i5.RolesListResponse> show(_i5.RolesShowRequest? request) => + _i15.Future<_i6.RolesListResponse> show(_i6.RolesShowRequest? request) => (super.noSuchMethod( Invocation.method( #show, [request], ), returnValue: - _i18.Future<_i5.RolesListResponse>.value(_FakeRolesListResponse_55( + _i15.Future<_i6.RolesListResponse>.value(_FakeRolesListResponse_56( this, Invocation.method( #show, @@ -4826,53 +4220,53 @@ class MockMisskeyRoles extends _i1.Mock implements _i5.MisskeyRoles { ), )), returnValueForMissingStub: - _i18.Future<_i5.RolesListResponse>.value(_FakeRolesListResponse_55( + _i15.Future<_i6.RolesListResponse>.value(_FakeRolesListResponse_56( this, Invocation.method( #show, [request], ), )), - ) as _i18.Future<_i5.RolesListResponse>); + ) as _i15.Future<_i6.RolesListResponse>); @override - _i18.Future> notes(_i5.RolesNotesRequest? request) => + _i15.Future> notes(_i6.RolesNotesRequest? request) => (super.noSuchMethod( Invocation.method( #notes, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); } /// A class which mocks [MisskeyUsers]. /// /// See the documentation for Mockito's code generation for more information. -class MockMisskeyUsers extends _i1.Mock implements _i5.MisskeyUsers { +class MockMisskeyUsers extends _i1.Mock implements _i6.MisskeyUsers { @override - _i5.MisskeyUsersLists get list => (super.noSuchMethod( + _i6.MisskeyUsersLists get list => (super.noSuchMethod( Invocation.getter(#list), - returnValue: _FakeMisskeyUsersLists_56( + returnValue: _FakeMisskeyUsersLists_57( this, Invocation.getter(#list), ), - returnValueForMissingStub: _FakeMisskeyUsersLists_56( + returnValueForMissingStub: _FakeMisskeyUsersLists_57( this, Invocation.getter(#list), ), - ) as _i5.MisskeyUsersLists); + ) as _i6.MisskeyUsersLists); @override - _i18.Future<_i5.UserDetailed> show(_i5.UsersShowRequest? request) => + _i15.Future<_i6.UserDetailed> show(_i6.UsersShowRequest? request) => (super.noSuchMethod( Invocation.method( #show, [request], ), - returnValue: _i18.Future<_i5.UserDetailed>.value(_FakeUserDetailed_57( + returnValue: _i15.Future<_i6.UserDetailed>.value(_FakeUserDetailed_58( this, Invocation.method( #show, @@ -4880,38 +4274,38 @@ class MockMisskeyUsers extends _i1.Mock implements _i5.MisskeyUsers { ), )), returnValueForMissingStub: - _i18.Future<_i5.UserDetailed>.value(_FakeUserDetailed_57( + _i15.Future<_i6.UserDetailed>.value(_FakeUserDetailed_58( this, Invocation.method( #show, [request], ), )), - ) as _i18.Future<_i5.UserDetailed>); + ) as _i15.Future<_i6.UserDetailed>); @override - _i18.Future> showByIds( - _i5.UsersShowByIdsRequest? request) => + _i15.Future> showByIds( + _i6.UsersShowByIdsRequest? request) => (super.noSuchMethod( Invocation.method( #showByIds, [request], ), returnValue: - _i18.Future>.value(<_i5.UserDetailed>[]), + _i15.Future>.value(<_i6.UserDetailed>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.UserDetailed>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.UserDetailed>[]), + ) as _i15.Future>); @override - _i18.Future<_i5.UserDetailed> showByName( - _i5.UsersShowByUserNameRequest? request) => + _i15.Future<_i6.UserDetailed> showByName( + _i6.UsersShowByUserNameRequest? request) => (super.noSuchMethod( Invocation.method( #showByName, [request], ), - returnValue: _i18.Future<_i5.UserDetailed>.value(_FakeUserDetailed_57( + returnValue: _i15.Future<_i6.UserDetailed>.value(_FakeUserDetailed_58( this, Invocation.method( #showByName, @@ -4919,218 +4313,231 @@ class MockMisskeyUsers extends _i1.Mock implements _i5.MisskeyUsers { ), )), returnValueForMissingStub: - _i18.Future<_i5.UserDetailed>.value(_FakeUserDetailed_57( + _i15.Future<_i6.UserDetailed>.value(_FakeUserDetailed_58( this, Invocation.method( #showByName, [request], ), )), - ) as _i18.Future<_i5.UserDetailed>); + ) as _i15.Future<_i6.UserDetailed>); @override - _i18.Future> notes(_i5.UsersNotesRequest? request) => + _i15.Future> notes(_i6.UsersNotesRequest? request) => (super.noSuchMethod( Invocation.method( #notes, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future> clips(_i5.UsersClipsRequest? request) => + _i15.Future> clips(_i6.UsersClipsRequest? request) => (super.noSuchMethod( Invocation.method( #clips, [request], ), - returnValue: _i18.Future>.value(<_i5.Clip>[]), + returnValue: _i15.Future>.value(<_i6.Clip>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Clip>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Clip>[]), + ) as _i15.Future>); @override - _i18.Future> followers( - _i5.UsersFollowersRequest? request) => + _i15.Future> followers( + _i6.UsersFollowersRequest? request) => (super.noSuchMethod( Invocation.method( #followers, [request], ), returnValue: - _i18.Future>.value(<_i5.Following>[]), + _i15.Future>.value(<_i6.Following>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Following>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Following>[]), + ) as _i15.Future>); @override - _i18.Future> following( - _i5.UsersFollowingRequest? request) => + _i15.Future> following( + _i6.UsersFollowingRequest? request) => (super.noSuchMethod( Invocation.method( #following, [request], ), returnValue: - _i18.Future>.value(<_i5.Following>[]), + _i15.Future>.value(<_i6.Following>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Following>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Following>[]), + ) as _i15.Future>); @override - _i18.Future reportAbuse(_i5.UsersReportAbuseRequest? request) => + _i15.Future reportAbuse(_i6.UsersReportAbuseRequest? request) => (super.noSuchMethod( Invocation.method( #reportAbuse, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future> reactions( - _i5.UsersReactionsRequest? request) => + _i15.Future> reactions( + _i6.UsersReactionsRequest? request) => (super.noSuchMethod( Invocation.method( #reactions, [request], ), - returnValue: _i18.Future>.value( - <_i5.UsersReactionsResponse>[]), + returnValue: _i15.Future>.value( + <_i6.UsersReactionsResponse>[]), returnValueForMissingStub: - _i18.Future>.value( - <_i5.UsersReactionsResponse>[]), - ) as _i18.Future>); + _i15.Future>.value( + <_i6.UsersReactionsResponse>[]), + ) as _i15.Future>); @override - _i18.Future> search(_i5.UsersSearchRequest? request) => + _i15.Future> search(_i6.UsersSearchRequest? request) => (super.noSuchMethod( Invocation.method( #search, [request], ), - returnValue: _i18.Future>.value(<_i5.User>[]), + returnValue: _i15.Future>.value(<_i6.User>[]), + returnValueForMissingStub: + _i15.Future>.value(<_i6.User>[]), + ) as _i15.Future>); + + @override + _i15.Future> searchByUsernameAndHost( + _i6.UsersSearchByUsernameAndHostRequest? request) => + (super.noSuchMethod( + Invocation.method( + #searchByUsernameAndHost, + [request], + ), + returnValue: _i15.Future>.value(<_i6.User>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.User>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.User>[]), + ) as _i15.Future>); @override - _i18.Future> + _i15.Future> getFrequentlyRepliedUsers( - _i5.UsersGetFrequentlyRepliedUsersRequest? request) => + _i6.UsersGetFrequentlyRepliedUsersRequest? request) => (super.noSuchMethod( Invocation.method( #getFrequentlyRepliedUsers, [request], ), - returnValue: _i18.Future< - Iterable<_i5.UsersGetFrequentlyRepliedUsersResponse>>.value( - <_i5.UsersGetFrequentlyRepliedUsersResponse>[]), - returnValueForMissingStub: _i18.Future< - Iterable<_i5.UsersGetFrequentlyRepliedUsersResponse>>.value( - <_i5.UsersGetFrequentlyRepliedUsersResponse>[]), - ) as _i18 - .Future>); + returnValue: _i15.Future< + Iterable<_i6.UsersGetFrequentlyRepliedUsersResponse>>.value( + <_i6.UsersGetFrequentlyRepliedUsersResponse>[]), + returnValueForMissingStub: _i15.Future< + Iterable<_i6.UsersGetFrequentlyRepliedUsersResponse>>.value( + <_i6.UsersGetFrequentlyRepliedUsersResponse>[]), + ) as _i15 + .Future>); @override - _i18.Future> recommendation( - _i5.UsersRecommendationRequest? request) => + _i15.Future> recommendation( + _i6.UsersRecommendationRequest? request) => (super.noSuchMethod( Invocation.method( #recommendation, [request], ), - returnValue: _i18.Future>.value(<_i5.User>[]), + returnValue: _i15.Future>.value(<_i6.User>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.User>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.User>[]), + ) as _i15.Future>); @override - _i18.Future> users( - _i5.UsersUsersRequest? request) => + _i15.Future> users( + _i6.UsersUsersRequest? request) => (super.noSuchMethod( Invocation.method( #users, [request], ), returnValue: - _i18.Future>.value(<_i5.UserDetailed>[]), + _i15.Future>.value(<_i6.UserDetailed>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.UserDetailed>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.UserDetailed>[]), + ) as _i15.Future>); @override - _i18.Future updateMemo(_i5.UsersUpdateMemoRequest? request) => + _i15.Future updateMemo(_i6.UsersUpdateMemoRequest? request) => (super.noSuchMethod( Invocation.method( #updateMemo, [request], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future> flashs(_i5.UsersFlashsRequest? request) => + _i15.Future> flashs(_i6.UsersFlashsRequest? request) => (super.noSuchMethod( Invocation.method( #flashs, [request], ), - returnValue: _i18.Future>.value(<_i5.Flash>[]), + returnValue: _i15.Future>.value(<_i6.Flash>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Flash>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Flash>[]), + ) as _i15.Future>); @override - _i18.Future> featuredNotes( - _i5.UsersFeaturedNotesRequest? request) => + _i15.Future> featuredNotes( + _i6.UsersFeaturedNotesRequest? request) => (super.noSuchMethod( Invocation.method( #featuredNotes, [request], ), - returnValue: _i18.Future>.value(<_i5.Note>[]), + returnValue: _i15.Future>.value(<_i6.Note>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Note>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Note>[]), + ) as _i15.Future>); @override - _i18.Future> pages(_i5.UsersPagesRequest? request) => + _i15.Future> pages(_i6.UsersPagesRequest? request) => (super.noSuchMethod( Invocation.method( #pages, [request], ), - returnValue: _i18.Future>.value(<_i5.Page>[]), + returnValue: _i15.Future>.value(<_i6.Page>[]), returnValueForMissingStub: - _i18.Future>.value(<_i5.Page>[]), - ) as _i18.Future>); + _i15.Future>.value(<_i6.Page>[]), + ) as _i15.Future>); } /// A class which mocks [Dio]. /// /// See the documentation for Mockito's code generation for more information. -class MockDio extends _i1.Mock implements _i11.Dio { +class MockDio extends _i1.Mock implements _i9.Dio { @override - _i11.BaseOptions get options => (super.noSuchMethod( + _i9.BaseOptions get options => (super.noSuchMethod( Invocation.getter(#options), - returnValue: _FakeBaseOptions_58( + returnValue: _FakeBaseOptions_59( this, Invocation.getter(#options), ), - returnValueForMissingStub: _FakeBaseOptions_58( + returnValueForMissingStub: _FakeBaseOptions_59( this, Invocation.getter(#options), ), - ) as _i11.BaseOptions); + ) as _i9.BaseOptions); @override - set options(_i11.BaseOptions? _options) => super.noSuchMethod( + set options(_i9.BaseOptions? _options) => super.noSuchMethod( Invocation.setter( #options, _options, @@ -5139,20 +4546,20 @@ class MockDio extends _i1.Mock implements _i11.Dio { ); @override - _i11.HttpClientAdapter get httpClientAdapter => (super.noSuchMethod( + _i9.HttpClientAdapter get httpClientAdapter => (super.noSuchMethod( Invocation.getter(#httpClientAdapter), - returnValue: _FakeHttpClientAdapter_59( + returnValue: _FakeHttpClientAdapter_60( this, Invocation.getter(#httpClientAdapter), ), - returnValueForMissingStub: _FakeHttpClientAdapter_59( + returnValueForMissingStub: _FakeHttpClientAdapter_60( this, Invocation.getter(#httpClientAdapter), ), - ) as _i11.HttpClientAdapter); + ) as _i9.HttpClientAdapter); @override - set httpClientAdapter(_i11.HttpClientAdapter? _httpClientAdapter) => + set httpClientAdapter(_i9.HttpClientAdapter? _httpClientAdapter) => super.noSuchMethod( Invocation.setter( #httpClientAdapter, @@ -5162,20 +4569,20 @@ class MockDio extends _i1.Mock implements _i11.Dio { ); @override - _i11.Transformer get transformer => (super.noSuchMethod( + _i9.Transformer get transformer => (super.noSuchMethod( Invocation.getter(#transformer), - returnValue: _FakeTransformer_60( + returnValue: _FakeTransformer_61( this, Invocation.getter(#transformer), ), - returnValueForMissingStub: _FakeTransformer_60( + returnValueForMissingStub: _FakeTransformer_61( this, Invocation.getter(#transformer), ), - ) as _i11.Transformer); + ) as _i9.Transformer); @override - set transformer(_i11.Transformer? _transformer) => super.noSuchMethod( + set transformer(_i9.Transformer? _transformer) => super.noSuchMethod( Invocation.setter( #transformer, _transformer, @@ -5184,17 +4591,17 @@ class MockDio extends _i1.Mock implements _i11.Dio { ); @override - _i11.Interceptors get interceptors => (super.noSuchMethod( + _i9.Interceptors get interceptors => (super.noSuchMethod( Invocation.getter(#interceptors), - returnValue: _FakeInterceptors_61( + returnValue: _FakeInterceptors_62( this, Invocation.getter(#interceptors), ), - returnValueForMissingStub: _FakeInterceptors_61( + returnValueForMissingStub: _FakeInterceptors_62( this, Invocation.getter(#interceptors), ), - ) as _i11.Interceptors); + ) as _i9.Interceptors); @override void close({bool? force = false}) => super.noSuchMethod( @@ -5207,12 +4614,12 @@ class MockDio extends _i1.Mock implements _i11.Dio { ); @override - _i18.Future<_i11.Response> head( + _i15.Future<_i9.Response> head( String? path, { Object? data, Map? queryParameters, - _i11.Options? options, - _i11.CancelToken? cancelToken, + _i9.Options? options, + _i9.CancelToken? cancelToken, }) => (super.noSuchMethod( Invocation.method( @@ -5225,7 +4632,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { #cancelToken: cancelToken, }, ), - returnValue: _i18.Future<_i11.Response>.value(_FakeResponse_62( + returnValue: _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #head, @@ -5239,7 +4646,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #head, @@ -5252,14 +4659,14 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); @override - _i18.Future<_i11.Response> headUri( + _i15.Future<_i9.Response> headUri( Uri? uri, { Object? data, - _i11.Options? options, - _i11.CancelToken? cancelToken, + _i9.Options? options, + _i9.CancelToken? cancelToken, }) => (super.noSuchMethod( Invocation.method( @@ -5271,7 +4678,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { #cancelToken: cancelToken, }, ), - returnValue: _i18.Future<_i11.Response>.value(_FakeResponse_62( + returnValue: _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #headUri, @@ -5284,7 +4691,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #headUri, @@ -5296,16 +4703,16 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); @override - _i18.Future<_i11.Response> get( + _i15.Future<_i9.Response> get( String? path, { Object? data, Map? queryParameters, - _i11.Options? options, - _i11.CancelToken? cancelToken, - _i11.ProgressCallback? onReceiveProgress, + _i9.Options? options, + _i9.CancelToken? cancelToken, + _i9.ProgressCallback? onReceiveProgress, }) => (super.noSuchMethod( Invocation.method( @@ -5319,7 +4726,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { #onReceiveProgress: onReceiveProgress, }, ), - returnValue: _i18.Future<_i11.Response>.value(_FakeResponse_62( + returnValue: _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #get, @@ -5334,7 +4741,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #get, @@ -5348,15 +4755,15 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); @override - _i18.Future<_i11.Response> getUri( + _i15.Future<_i9.Response> getUri( Uri? uri, { Object? data, - _i11.Options? options, - _i11.CancelToken? cancelToken, - _i11.ProgressCallback? onReceiveProgress, + _i9.Options? options, + _i9.CancelToken? cancelToken, + _i9.ProgressCallback? onReceiveProgress, }) => (super.noSuchMethod( Invocation.method( @@ -5369,7 +4776,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { #onReceiveProgress: onReceiveProgress, }, ), - returnValue: _i18.Future<_i11.Response>.value(_FakeResponse_62( + returnValue: _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #getUri, @@ -5383,7 +4790,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #getUri, @@ -5396,17 +4803,17 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); @override - _i18.Future<_i11.Response> post( + _i15.Future<_i9.Response> post( String? path, { Object? data, Map? queryParameters, - _i11.Options? options, - _i11.CancelToken? cancelToken, - _i11.ProgressCallback? onSendProgress, - _i11.ProgressCallback? onReceiveProgress, + _i9.Options? options, + _i9.CancelToken? cancelToken, + _i9.ProgressCallback? onSendProgress, + _i9.ProgressCallback? onReceiveProgress, }) => (super.noSuchMethod( Invocation.method( @@ -5421,7 +4828,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { #onReceiveProgress: onReceiveProgress, }, ), - returnValue: _i18.Future<_i11.Response>.value(_FakeResponse_62( + returnValue: _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #post, @@ -5437,7 +4844,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #post, @@ -5452,16 +4859,16 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); @override - _i18.Future<_i11.Response> postUri( + _i15.Future<_i9.Response> postUri( Uri? uri, { Object? data, - _i11.Options? options, - _i11.CancelToken? cancelToken, - _i11.ProgressCallback? onSendProgress, - _i11.ProgressCallback? onReceiveProgress, + _i9.Options? options, + _i9.CancelToken? cancelToken, + _i9.ProgressCallback? onSendProgress, + _i9.ProgressCallback? onReceiveProgress, }) => (super.noSuchMethod( Invocation.method( @@ -5475,7 +4882,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { #onReceiveProgress: onReceiveProgress, }, ), - returnValue: _i18.Future<_i11.Response>.value(_FakeResponse_62( + returnValue: _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #postUri, @@ -5490,7 +4897,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #postUri, @@ -5504,17 +4911,17 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); @override - _i18.Future<_i11.Response> put( + _i15.Future<_i9.Response> put( String? path, { Object? data, Map? queryParameters, - _i11.Options? options, - _i11.CancelToken? cancelToken, - _i11.ProgressCallback? onSendProgress, - _i11.ProgressCallback? onReceiveProgress, + _i9.Options? options, + _i9.CancelToken? cancelToken, + _i9.ProgressCallback? onSendProgress, + _i9.ProgressCallback? onReceiveProgress, }) => (super.noSuchMethod( Invocation.method( @@ -5529,7 +4936,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { #onReceiveProgress: onReceiveProgress, }, ), - returnValue: _i18.Future<_i11.Response>.value(_FakeResponse_62( + returnValue: _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #put, @@ -5545,7 +4952,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #put, @@ -5560,16 +4967,16 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); @override - _i18.Future<_i11.Response> putUri( + _i15.Future<_i9.Response> putUri( Uri? uri, { Object? data, - _i11.Options? options, - _i11.CancelToken? cancelToken, - _i11.ProgressCallback? onSendProgress, - _i11.ProgressCallback? onReceiveProgress, + _i9.Options? options, + _i9.CancelToken? cancelToken, + _i9.ProgressCallback? onSendProgress, + _i9.ProgressCallback? onReceiveProgress, }) => (super.noSuchMethod( Invocation.method( @@ -5583,7 +4990,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { #onReceiveProgress: onReceiveProgress, }, ), - returnValue: _i18.Future<_i11.Response>.value(_FakeResponse_62( + returnValue: _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #putUri, @@ -5598,7 +5005,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #putUri, @@ -5612,17 +5019,17 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); @override - _i18.Future<_i11.Response> patch( + _i15.Future<_i9.Response> patch( String? path, { Object? data, Map? queryParameters, - _i11.Options? options, - _i11.CancelToken? cancelToken, - _i11.ProgressCallback? onSendProgress, - _i11.ProgressCallback? onReceiveProgress, + _i9.Options? options, + _i9.CancelToken? cancelToken, + _i9.ProgressCallback? onSendProgress, + _i9.ProgressCallback? onReceiveProgress, }) => (super.noSuchMethod( Invocation.method( @@ -5637,7 +5044,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { #onReceiveProgress: onReceiveProgress, }, ), - returnValue: _i18.Future<_i11.Response>.value(_FakeResponse_62( + returnValue: _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #patch, @@ -5653,7 +5060,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #patch, @@ -5668,16 +5075,16 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); @override - _i18.Future<_i11.Response> patchUri( + _i15.Future<_i9.Response> patchUri( Uri? uri, { Object? data, - _i11.Options? options, - _i11.CancelToken? cancelToken, - _i11.ProgressCallback? onSendProgress, - _i11.ProgressCallback? onReceiveProgress, + _i9.Options? options, + _i9.CancelToken? cancelToken, + _i9.ProgressCallback? onSendProgress, + _i9.ProgressCallback? onReceiveProgress, }) => (super.noSuchMethod( Invocation.method( @@ -5691,7 +5098,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { #onReceiveProgress: onReceiveProgress, }, ), - returnValue: _i18.Future<_i11.Response>.value(_FakeResponse_62( + returnValue: _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #patchUri, @@ -5706,7 +5113,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #patchUri, @@ -5720,15 +5127,15 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); @override - _i18.Future<_i11.Response> delete( + _i15.Future<_i9.Response> delete( String? path, { Object? data, Map? queryParameters, - _i11.Options? options, - _i11.CancelToken? cancelToken, + _i9.Options? options, + _i9.CancelToken? cancelToken, }) => (super.noSuchMethod( Invocation.method( @@ -5741,7 +5148,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { #cancelToken: cancelToken, }, ), - returnValue: _i18.Future<_i11.Response>.value(_FakeResponse_62( + returnValue: _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #delete, @@ -5755,7 +5162,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #delete, @@ -5768,14 +5175,14 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); @override - _i18.Future<_i11.Response> deleteUri( + _i15.Future<_i9.Response> deleteUri( Uri? uri, { Object? data, - _i11.Options? options, - _i11.CancelToken? cancelToken, + _i9.Options? options, + _i9.CancelToken? cancelToken, }) => (super.noSuchMethod( Invocation.method( @@ -5787,7 +5194,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { #cancelToken: cancelToken, }, ), - returnValue: _i18.Future<_i11.Response>.value(_FakeResponse_62( + returnValue: _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #deleteUri, @@ -5800,7 +5207,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #deleteUri, @@ -5812,19 +5219,19 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); @override - _i18.Future<_i11.Response> download( + _i15.Future<_i9.Response> download( String? urlPath, dynamic savePath, { - _i11.ProgressCallback? onReceiveProgress, + _i9.ProgressCallback? onReceiveProgress, Map? queryParameters, - _i11.CancelToken? cancelToken, + _i9.CancelToken? cancelToken, bool? deleteOnError = true, String? lengthHeader = r'content-length', Object? data, - _i11.Options? options, + _i9.Options? options, }) => (super.noSuchMethod( Invocation.method( @@ -5844,7 +5251,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), returnValue: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #download, @@ -5864,7 +5271,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #download, @@ -5883,18 +5290,18 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); @override - _i18.Future<_i11.Response> downloadUri( + _i15.Future<_i9.Response> downloadUri( Uri? uri, dynamic savePath, { - _i11.ProgressCallback? onReceiveProgress, - _i11.CancelToken? cancelToken, + _i9.ProgressCallback? onReceiveProgress, + _i9.CancelToken? cancelToken, bool? deleteOnError = true, String? lengthHeader = r'content-length', Object? data, - _i11.Options? options, + _i9.Options? options, }) => (super.noSuchMethod( Invocation.method( @@ -5913,7 +5320,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), returnValue: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #downloadUri, @@ -5932,7 +5339,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #downloadUri, @@ -5950,17 +5357,17 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); @override - _i18.Future<_i11.Response> request( + _i15.Future<_i9.Response> request( String? url, { Object? data, Map? queryParameters, - _i11.CancelToken? cancelToken, - _i11.Options? options, - _i11.ProgressCallback? onSendProgress, - _i11.ProgressCallback? onReceiveProgress, + _i9.CancelToken? cancelToken, + _i9.Options? options, + _i9.ProgressCallback? onSendProgress, + _i9.ProgressCallback? onReceiveProgress, }) => (super.noSuchMethod( Invocation.method( @@ -5975,7 +5382,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { #onReceiveProgress: onReceiveProgress, }, ), - returnValue: _i18.Future<_i11.Response>.value(_FakeResponse_62( + returnValue: _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #request, @@ -5991,7 +5398,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #request, @@ -6006,16 +5413,16 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); @override - _i18.Future<_i11.Response> requestUri( + _i15.Future<_i9.Response> requestUri( Uri? uri, { Object? data, - _i11.CancelToken? cancelToken, - _i11.Options? options, - _i11.ProgressCallback? onSendProgress, - _i11.ProgressCallback? onReceiveProgress, + _i9.CancelToken? cancelToken, + _i9.Options? options, + _i9.ProgressCallback? onSendProgress, + _i9.ProgressCallback? onReceiveProgress, }) => (super.noSuchMethod( Invocation.method( @@ -6029,7 +5436,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { #onReceiveProgress: onReceiveProgress, }, ), - returnValue: _i18.Future<_i11.Response>.value(_FakeResponse_62( + returnValue: _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #requestUri, @@ -6044,7 +5451,7 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #requestUri, @@ -6058,16 +5465,16 @@ class MockDio extends _i1.Mock implements _i11.Dio { }, ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); @override - _i18.Future<_i11.Response> fetch(_i11.RequestOptions? requestOptions) => + _i15.Future<_i9.Response> fetch(_i9.RequestOptions? requestOptions) => (super.noSuchMethod( Invocation.method( #fetch, [requestOptions], ), - returnValue: _i18.Future<_i11.Response>.value(_FakeResponse_62( + returnValue: _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #fetch, @@ -6075,28 +5482,28 @@ class MockDio extends _i1.Mock implements _i11.Dio { ), )), returnValueForMissingStub: - _i18.Future<_i11.Response>.value(_FakeResponse_62( + _i15.Future<_i9.Response>.value(_FakeResponse_63( this, Invocation.method( #fetch, [requestOptions], ), )), - ) as _i18.Future<_i11.Response>); + ) as _i15.Future<_i9.Response>); } /// A class which mocks [HttpClient]. /// /// See the documentation for Mockito's code generation for more information. -class MockHttpClient extends _i1.Mock implements _i12.HttpClient { +class MockHttpClient extends _i1.Mock implements _i10.HttpClient { @override Duration get idleTimeout => (super.noSuchMethod( Invocation.getter(#idleTimeout), - returnValue: _FakeDuration_63( + returnValue: _FakeDuration_64( this, Invocation.getter(#idleTimeout), ), - returnValueForMissingStub: _FakeDuration_63( + returnValueForMissingStub: _FakeDuration_64( this, Invocation.getter(#idleTimeout), ), @@ -6156,7 +5563,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { @override set authenticate( - _i18.Future Function( + _i15.Future Function( Uri, String, String?, @@ -6171,7 +5578,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { @override set connectionFactory( - _i18.Future<_i12.ConnectionTask<_i12.Socket>> Function( + _i15.Future<_i10.ConnectionTask<_i10.Socket>> Function( Uri, String?, int?, @@ -6195,7 +5602,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { @override set authenticateProxy( - _i18.Future Function( + _i15.Future Function( String, int, String, @@ -6212,7 +5619,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { @override set badCertificateCallback( bool Function( - _i12.X509Certificate, + _i10.X509Certificate, String, int, )? callback) => @@ -6234,7 +5641,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ); @override - _i18.Future<_i12.HttpClientRequest> open( + _i15.Future<_i10.HttpClientRequest> open( String? method, String? host, int? port, @@ -6251,7 +5658,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ], ), returnValue: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #open, @@ -6264,7 +5671,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ), )), returnValueForMissingStub: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #open, @@ -6276,10 +5683,10 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ], ), )), - ) as _i18.Future<_i12.HttpClientRequest>); + ) as _i15.Future<_i10.HttpClientRequest>); @override - _i18.Future<_i12.HttpClientRequest> openUrl( + _i15.Future<_i10.HttpClientRequest> openUrl( String? method, Uri? url, ) => @@ -6292,7 +5699,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ], ), returnValue: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #openUrl, @@ -6303,7 +5710,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ), )), returnValueForMissingStub: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #openUrl, @@ -6313,10 +5720,10 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ], ), )), - ) as _i18.Future<_i12.HttpClientRequest>); + ) as _i15.Future<_i10.HttpClientRequest>); @override - _i18.Future<_i12.HttpClientRequest> get( + _i15.Future<_i10.HttpClientRequest> get( String? host, int? port, String? path, @@ -6331,7 +5738,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ], ), returnValue: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #get, @@ -6343,7 +5750,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ), )), returnValueForMissingStub: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #get, @@ -6354,16 +5761,16 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ], ), )), - ) as _i18.Future<_i12.HttpClientRequest>); + ) as _i15.Future<_i10.HttpClientRequest>); @override - _i18.Future<_i12.HttpClientRequest> getUrl(Uri? url) => (super.noSuchMethod( + _i15.Future<_i10.HttpClientRequest> getUrl(Uri? url) => (super.noSuchMethod( Invocation.method( #getUrl, [url], ), returnValue: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #getUrl, @@ -6371,17 +5778,17 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ), )), returnValueForMissingStub: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #getUrl, [url], ), )), - ) as _i18.Future<_i12.HttpClientRequest>); + ) as _i15.Future<_i10.HttpClientRequest>); @override - _i18.Future<_i12.HttpClientRequest> post( + _i15.Future<_i10.HttpClientRequest> post( String? host, int? port, String? path, @@ -6396,7 +5803,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ], ), returnValue: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #post, @@ -6408,7 +5815,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ), )), returnValueForMissingStub: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #post, @@ -6419,16 +5826,16 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ], ), )), - ) as _i18.Future<_i12.HttpClientRequest>); + ) as _i15.Future<_i10.HttpClientRequest>); @override - _i18.Future<_i12.HttpClientRequest> postUrl(Uri? url) => (super.noSuchMethod( + _i15.Future<_i10.HttpClientRequest> postUrl(Uri? url) => (super.noSuchMethod( Invocation.method( #postUrl, [url], ), returnValue: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #postUrl, @@ -6436,17 +5843,17 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ), )), returnValueForMissingStub: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #postUrl, [url], ), )), - ) as _i18.Future<_i12.HttpClientRequest>); + ) as _i15.Future<_i10.HttpClientRequest>); @override - _i18.Future<_i12.HttpClientRequest> put( + _i15.Future<_i10.HttpClientRequest> put( String? host, int? port, String? path, @@ -6461,7 +5868,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ], ), returnValue: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #put, @@ -6473,7 +5880,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ), )), returnValueForMissingStub: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #put, @@ -6484,16 +5891,16 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ], ), )), - ) as _i18.Future<_i12.HttpClientRequest>); + ) as _i15.Future<_i10.HttpClientRequest>); @override - _i18.Future<_i12.HttpClientRequest> putUrl(Uri? url) => (super.noSuchMethod( + _i15.Future<_i10.HttpClientRequest> putUrl(Uri? url) => (super.noSuchMethod( Invocation.method( #putUrl, [url], ), returnValue: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #putUrl, @@ -6501,17 +5908,17 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ), )), returnValueForMissingStub: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #putUrl, [url], ), )), - ) as _i18.Future<_i12.HttpClientRequest>); + ) as _i15.Future<_i10.HttpClientRequest>); @override - _i18.Future<_i12.HttpClientRequest> delete( + _i15.Future<_i10.HttpClientRequest> delete( String? host, int? port, String? path, @@ -6526,7 +5933,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ], ), returnValue: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #delete, @@ -6538,7 +5945,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ), )), returnValueForMissingStub: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #delete, @@ -6549,17 +5956,17 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ], ), )), - ) as _i18.Future<_i12.HttpClientRequest>); + ) as _i15.Future<_i10.HttpClientRequest>); @override - _i18.Future<_i12.HttpClientRequest> deleteUrl(Uri? url) => + _i15.Future<_i10.HttpClientRequest> deleteUrl(Uri? url) => (super.noSuchMethod( Invocation.method( #deleteUrl, [url], ), returnValue: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #deleteUrl, @@ -6567,17 +5974,17 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ), )), returnValueForMissingStub: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #deleteUrl, [url], ), )), - ) as _i18.Future<_i12.HttpClientRequest>); + ) as _i15.Future<_i10.HttpClientRequest>); @override - _i18.Future<_i12.HttpClientRequest> patch( + _i15.Future<_i10.HttpClientRequest> patch( String? host, int? port, String? path, @@ -6592,7 +5999,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ], ), returnValue: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #patch, @@ -6604,7 +6011,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ), )), returnValueForMissingStub: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #patch, @@ -6615,16 +6022,16 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ], ), )), - ) as _i18.Future<_i12.HttpClientRequest>); + ) as _i15.Future<_i10.HttpClientRequest>); @override - _i18.Future<_i12.HttpClientRequest> patchUrl(Uri? url) => (super.noSuchMethod( + _i15.Future<_i10.HttpClientRequest> patchUrl(Uri? url) => (super.noSuchMethod( Invocation.method( #patchUrl, [url], ), returnValue: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #patchUrl, @@ -6632,17 +6039,17 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ), )), returnValueForMissingStub: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #patchUrl, [url], ), )), - ) as _i18.Future<_i12.HttpClientRequest>); + ) as _i15.Future<_i10.HttpClientRequest>); @override - _i18.Future<_i12.HttpClientRequest> head( + _i15.Future<_i10.HttpClientRequest> head( String? host, int? port, String? path, @@ -6657,7 +6064,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ], ), returnValue: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #head, @@ -6669,7 +6076,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ), )), returnValueForMissingStub: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #head, @@ -6680,16 +6087,16 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ], ), )), - ) as _i18.Future<_i12.HttpClientRequest>); + ) as _i15.Future<_i10.HttpClientRequest>); @override - _i18.Future<_i12.HttpClientRequest> headUrl(Uri? url) => (super.noSuchMethod( + _i15.Future<_i10.HttpClientRequest> headUrl(Uri? url) => (super.noSuchMethod( Invocation.method( #headUrl, [url], ), returnValue: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #headUrl, @@ -6697,20 +6104,20 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ), )), returnValueForMissingStub: - _i18.Future<_i12.HttpClientRequest>.value(_FakeHttpClientRequest_64( + _i15.Future<_i10.HttpClientRequest>.value(_FakeHttpClientRequest_65( this, Invocation.method( #headUrl, [url], ), )), - ) as _i18.Future<_i12.HttpClientRequest>); + ) as _i15.Future<_i10.HttpClientRequest>); @override void addCredentials( Uri? url, String? realm, - _i12.HttpClientCredentials? credentials, + _i10.HttpClientCredentials? credentials, ) => super.noSuchMethod( Invocation.method( @@ -6729,7 +6136,7 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { String? host, int? port, String? realm, - _i12.HttpClientCredentials? credentials, + _i10.HttpClientCredentials? credentials, ) => super.noSuchMethod( Invocation.method( @@ -6755,378 +6162,339 @@ class MockHttpClient extends _i1.Mock implements _i12.HttpClient { ); } -/// A class which mocks [SocketController]. +/// A class which mocks [StreamingController]. /// /// See the documentation for Mockito's code generation for more information. -class MockSocketController extends _i1.Mock implements _i5.SocketController { +class MockStreamingController extends _i1.Mock + implements _i6.StreamingController { @override - _i5.StreamingService get service => (super.noSuchMethod( - Invocation.getter(#service), - returnValue: _FakeStreamingService_6( - this, - Invocation.getter(#service), - ), - returnValueForMissingStub: _FakeStreamingService_6( - this, - Invocation.getter(#service), + void sendRequest( + _i6.StreamingRequestType? type, + _i26.StreamingRequestBody? body, + ) => + super.noSuchMethod( + Invocation.method( + #sendRequest, + [ + type, + body, + ], ), - ) as _i5.StreamingService); + returnValueForMissingStub: null, + ); @override - String get id => (super.noSuchMethod( - Invocation.getter(#id), - returnValue: _i27.dummyValue( - this, - Invocation.getter(#id), - ), - returnValueForMissingStub: _i27.dummyValue( - this, - Invocation.getter(#id), + _i15.Stream<_i6.StreamingResponse> addChannel( + _i6.Channel? channel, + Map? parameters, + String? id, + ) => + (super.noSuchMethod( + Invocation.method( + #addChannel, + [ + channel, + parameters, + id, + ], ), - ) as String); + returnValue: _i15.Stream<_i6.StreamingResponse>.empty(), + returnValueForMissingStub: _i15.Stream<_i6.StreamingResponse>.empty(), + ) as _i15.Stream<_i6.StreamingResponse>); @override - _i29.Channel get channel => (super.noSuchMethod( - Invocation.getter(#channel), - returnValue: _i29.Channel.homeTimeline, - returnValueForMissingStub: _i29.Channel.homeTimeline, - ) as _i29.Channel); - - @override - bool get isDisconnected => (super.noSuchMethod( - Invocation.getter(#isDisconnected), - returnValue: false, - returnValueForMissingStub: false, - ) as bool); + _i15.Future removeChannel(String? id) => (super.noSuchMethod( + Invocation.method( + #removeChannel, + [id], + ), + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - set isDisconnected(bool? _isDisconnected) => super.noSuchMethod( - Invocation.setter( - #isDisconnected, - _isDisconnected, + void subNote(String? noteId) => super.noSuchMethod( + Invocation.method( + #subNote, + [noteId], ), returnValueForMissingStub: null, ); @override - _i13.WebSocketChannel get webSocketChannel => (super.noSuchMethod( - Invocation.getter(#webSocketChannel), - returnValue: _FakeWebSocketChannel_65( - this, - Invocation.getter(#webSocketChannel), - ), - returnValueForMissingStub: _FakeWebSocketChannel_65( - this, - Invocation.getter(#webSocketChannel), - ), - ) as _i13.WebSocketChannel); - - @override - void connect() => super.noSuchMethod( + void unsubNote(String? noteId) => super.noSuchMethod( Invocation.method( - #connect, - [], + #unsubNote, + [noteId], ), returnValueForMissingStub: null, ); @override - void disconnect() => super.noSuchMethod( + void requestLog( + String? id, + int? length, + ) => + super.noSuchMethod( Invocation.method( - #disconnect, - [], + #requestLog, + [ + id, + length, + ], ), returnValueForMissingStub: null, ); @override - void reconnect() => super.noSuchMethod( + _i15.Stream<_i6.StreamingResponse> localTimelineStream({ + required _i6.LocalTimelineParameter? parameter, + required String? id, + }) => + (super.noSuchMethod( Invocation.method( - #reconnect, + #localTimelineStream, [], + { + #parameter: parameter, + #id: id, + }, ), - returnValueForMissingStub: null, - ); + returnValue: _i15.Stream<_i6.StreamingResponse>.empty(), + returnValueForMissingStub: _i15.Stream<_i6.StreamingResponse>.empty(), + ) as _i15.Stream<_i6.StreamingResponse>); @override - _i18.Future subNote(String? noteId) => (super.noSuchMethod( + _i15.Stream<_i6.StreamingResponse> globalTimelineStream({ + required _i6.GlobalTimelineParameter? parameter, + required String? id, + }) => + (super.noSuchMethod( Invocation.method( - #subNote, - [noteId], + #globalTimelineStream, + [], + { + #parameter: parameter, + #id: id, + }, ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Stream<_i6.StreamingResponse>.empty(), + returnValueForMissingStub: _i15.Stream<_i6.StreamingResponse>.empty(), + ) as _i15.Stream<_i6.StreamingResponse>); @override - _i18.Future unsubNote(String? noteId) => (super.noSuchMethod( + _i15.Stream<_i6.StreamingResponse> hybridTimelineStream({ + required _i6.HybridTimelineParameter? parameter, + required String? id, + }) => + (super.noSuchMethod( Invocation.method( - #unsubNote, - [noteId], + #hybridTimelineStream, + [], + { + #parameter: parameter, + #id: id, + }, ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Stream<_i6.StreamingResponse>.empty(), + returnValueForMissingStub: _i15.Stream<_i6.StreamingResponse>.empty(), + ) as _i15.Stream<_i6.StreamingResponse>); @override - _i18.Future requestLog({ + _i15.Stream<_i6.StreamingResponse> roleTimelineStream({ + required String? roleId, String? id, - int? length, }) => (super.noSuchMethod( Invocation.method( - #requestLog, + #roleTimelineStream, [], { + #roleId: roleId, #id: id, - #length: length, }, ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Stream<_i6.StreamingResponse>.empty(), + returnValueForMissingStub: _i15.Stream<_i6.StreamingResponse>.empty(), + ) as _i15.Stream<_i6.StreamingResponse>); @override - _i18.Future send( - _i5.StreamingRequestType? requestType, - _i30.StreamingRequestBody? body, - ) => + _i15.Stream<_i6.StreamingResponse> channelStream({ + required String? channelId, + String? id, + }) => (super.noSuchMethod( Invocation.method( - #send, - [ - requestType, - body, - ], - ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); -} - -/// A class which mocks [StreamingService]. -/// -/// See the documentation for Mockito's code generation for more information. -class MockStreamingService extends _i1.Mock implements _i5.StreamingService { - @override - String get host => (super.noSuchMethod( - Invocation.getter(#host), - returnValue: _i27.dummyValue( - this, - Invocation.getter(#host), - ), - returnValueForMissingStub: _i27.dummyValue( - this, - Invocation.getter(#host), + #channelStream, + [], + { + #channelId: channelId, + #id: id, + }, ), - ) as String); + returnValue: _i15.Stream<_i6.StreamingResponse>.empty(), + returnValueForMissingStub: _i15.Stream<_i6.StreamingResponse>.empty(), + ) as _i15.Stream<_i6.StreamingResponse>); @override - _i31.HashMap get streamingChannelControllers => + _i15.Stream<_i6.StreamingResponse> userListStream({ + required String? listId, + String? id, + }) => (super.noSuchMethod( - Invocation.getter(#streamingChannelControllers), - returnValue: - _i27.dummyValue<_i31.HashMap>( - this, - Invocation.getter(#streamingChannelControllers), - ), - returnValueForMissingStub: - _i27.dummyValue<_i31.HashMap>( - this, - Invocation.getter(#streamingChannelControllers), - ), - ) as _i31.HashMap); - - @override - set subscription(_i18.StreamSubscription? _subscription) => - super.noSuchMethod( - Invocation.setter( - #subscription, - _subscription, - ), - returnValueForMissingStub: null, - ); - - @override - _i13.WebSocketChannel get webSocketChannel => (super.noSuchMethod( - Invocation.getter(#webSocketChannel), - returnValue: _FakeWebSocketChannel_65( - this, - Invocation.getter(#webSocketChannel), - ), - returnValueForMissingStub: _FakeWebSocketChannel_65( - this, - Invocation.getter(#webSocketChannel), + Invocation.method( + #userListStream, + [], + { + #listId: listId, + #id: id, + }, ), - ) as _i13.WebSocketChannel); + returnValue: _i15.Stream<_i6.StreamingResponse>.empty(), + returnValueForMissingStub: _i15.Stream<_i6.StreamingResponse>.empty(), + ) as _i15.Stream<_i6.StreamingResponse>); @override - _i18.Future onChannelEventReceived( + _i15.Stream<_i6.StreamingResponse> antennaStream({ + required String? antennaId, String? id, - _i32.ChannelEventType? type, - dynamic body, - ) => + }) => (super.noSuchMethod( Invocation.method( - #onChannelEventReceived, - [ - id, - type, - body, - ], + #antennaStream, + [], + { + #antennaId: antennaId, + #id: id, + }, ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Stream<_i6.StreamingResponse>.empty(), + returnValueForMissingStub: _i15.Stream<_i6.StreamingResponse>.empty(), + ) as _i15.Stream<_i6.StreamingResponse>); @override - _i18.Future onNoteUpdatedEventReceived( - String? id, - _i33.NoteUpdatedEventType? type, - Map? body, - ) => + _i15.Stream<_i6.StreamingResponse> serverStatsLogStream( + {required String? id}) => (super.noSuchMethod( Invocation.method( - #onNoteUpdatedEventReceived, - [ - id, - type, - body, - ], + #serverStatsLogStream, + [], + {#id: id}, ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Stream<_i6.StreamingResponse>.empty(), + returnValueForMissingStub: _i15.Stream<_i6.StreamingResponse>.empty(), + ) as _i15.Stream<_i6.StreamingResponse>); @override - _i18.Future onBroadcastEventReceived( - _i34.BroadcastEventType? type, - Map? body, - ) => + _i15.Stream<_i6.StreamingResponse> queueStatsLogStream( + {required String? id}) => (super.noSuchMethod( Invocation.method( - #onBroadcastEventReceived, - [ - type, - body, - ], + #queueStatsLogStream, + [], + {#id: id}, ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Stream<_i6.StreamingResponse>.empty(), + returnValueForMissingStub: _i15.Stream<_i6.StreamingResponse>.empty(), + ) as _i15.Stream<_i6.StreamingResponse>); @override - _i18.Future startStreaming() => (super.noSuchMethod( + _i15.Stream<_i6.StreamingResponse> mainStream({required String? id}) => + (super.noSuchMethod( Invocation.method( - #startStreaming, + #mainStream, [], + {#id: id}, ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Stream<_i6.StreamingResponse>.empty(), + returnValueForMissingStub: _i15.Stream<_i6.StreamingResponse>.empty(), + ) as _i15.Stream<_i6.StreamingResponse>); @override - _i5.SocketController connect({ - String? id, - required _i29.Channel? channel, - _i18.Future Function( - _i32.ChannelEventType, - dynamic, - )? onChannelEventReceived, - _i18.Future Function( - String, - _i33.NoteUpdatedEventType, - Map, - )? onNoteUpdatedEventReceived, - _i18.Future Function( - _i34.BroadcastEventType, - Map, - )? onBroadcastEventReceived, - Map? parameters, + _i15.Stream<_i6.StreamingResponse> homeTimelineStream({ + required _i6.HomeTimelineParameter? parameter, + required String? id, }) => (super.noSuchMethod( Invocation.method( - #connect, + #homeTimelineStream, [], { + #parameter: parameter, #id: id, - #channel: channel, - #onChannelEventReceived: onChannelEventReceived, - #onNoteUpdatedEventReceived: onNoteUpdatedEventReceived, - #onBroadcastEventReceived: onBroadcastEventReceived, - #parameters: parameters, }, ), - returnValue: _FakeSocketController_33( + returnValue: _i15.Stream<_i6.StreamingResponse>.empty(), + returnValueForMissingStub: _i15.Stream<_i6.StreamingResponse>.empty(), + ) as _i15.Stream<_i6.StreamingResponse>); +} + +/// A class which mocks [WebSocketController]. +/// +/// See the documentation for Mockito's code generation for more information. +class MockWebSocketController extends _i1.Mock + implements _i6.WebSocketController { + @override + bool get isClosed => (super.noSuchMethod( + Invocation.getter(#isClosed), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); + + @override + _i15.Future<_i6.StreamingController> stream() => (super.noSuchMethod( + Invocation.method( + #stream, + [], + ), + returnValue: _i15.Future<_i6.StreamingController>.value( + _FakeStreamingController_66( this, Invocation.method( - #connect, + #stream, [], - { - #id: id, - #channel: channel, - #onChannelEventReceived: onChannelEventReceived, - #onNoteUpdatedEventReceived: onNoteUpdatedEventReceived, - #onBroadcastEventReceived: onBroadcastEventReceived, - #parameters: parameters, - }, ), - ), - returnValueForMissingStub: _FakeSocketController_33( + )), + returnValueForMissingStub: _i15.Future<_i6.StreamingController>.value( + _FakeStreamingController_66( this, Invocation.method( - #connect, + #stream, [], - { - #id: id, - #channel: channel, - #onChannelEventReceived: onChannelEventReceived, - #onNoteUpdatedEventReceived: onNoteUpdatedEventReceived, - #onBroadcastEventReceived: onBroadcastEventReceived, - #parameters: parameters, - }, ), - ), - ) as _i5.SocketController); - - @override - _i18.Future close() => (super.noSuchMethod( - Invocation.method( - #close, - [], - ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + )), + ) as _i15.Future<_i6.StreamingController>); @override - _i18.Future restart() => (super.noSuchMethod( + _i15.Future reconnect() => (super.noSuchMethod( Invocation.method( - #restart, + #reconnect, [], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); } /// A class which mocks [FakeFilePickerPlatform]. /// /// See the documentation for Mockito's code generation for more information. class MockFilePickerPlatform extends _i1.Mock - implements _i35.FakeFilePickerPlatform { + implements _i27.FakeFilePickerPlatform { @override - _i18.Future<_i36.FilePickerResult?> pickFiles({ + _i15.Future<_i28.FilePickerResult?> pickFiles({ String? dialogTitle, String? initialDirectory, - _i36.FileType? type = _i36.FileType.any, + _i28.FileType? type = _i28.FileType.any, List? allowedExtensions, - dynamic Function(_i36.FilePickerStatus)? onFileLoading, + dynamic Function(_i28.FilePickerStatus)? onFileLoading, bool? allowCompression = true, + int? compressionQuality = 30, bool? allowMultiple = false, bool? withData = false, bool? withReadStream = false, bool? lockParentWindow = false, + bool? readSequential = false, }) => (super.noSuchMethod( Invocation.method( @@ -7139,28 +6507,30 @@ class MockFilePickerPlatform extends _i1.Mock #allowedExtensions: allowedExtensions, #onFileLoading: onFileLoading, #allowCompression: allowCompression, + #compressionQuality: compressionQuality, #allowMultiple: allowMultiple, #withData: withData, #withReadStream: withReadStream, #lockParentWindow: lockParentWindow, + #readSequential: readSequential, }, ), - returnValue: _i18.Future<_i36.FilePickerResult?>.value(), - returnValueForMissingStub: _i18.Future<_i36.FilePickerResult?>.value(), - ) as _i18.Future<_i36.FilePickerResult?>); + returnValue: _i15.Future<_i28.FilePickerResult?>.value(), + returnValueForMissingStub: _i15.Future<_i28.FilePickerResult?>.value(), + ) as _i15.Future<_i28.FilePickerResult?>); @override - _i18.Future clearTemporaryFiles() => (super.noSuchMethod( + _i15.Future clearTemporaryFiles() => (super.noSuchMethod( Invocation.method( #clearTemporaryFiles, [], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future getDirectoryPath({ + _i15.Future getDirectoryPath({ String? dialogTitle, bool? lockParentWindow = false, String? initialDirectory, @@ -7175,17 +6545,18 @@ class MockFilePickerPlatform extends _i1.Mock #initialDirectory: initialDirectory, }, ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future saveFile({ + _i15.Future saveFile({ String? dialogTitle, String? fileName, String? initialDirectory, - _i36.FileType? type = _i36.FileType.any, + _i28.FileType? type = _i28.FileType.any, List? allowedExtensions, + _i25.Uint8List? bytes, bool? lockParentWindow = false, }) => (super.noSuchMethod( @@ -7198,21 +6569,22 @@ class MockFilePickerPlatform extends _i1.Mock #initialDirectory: initialDirectory, #type: type, #allowedExtensions: allowedExtensions, + #bytes: bytes, #lockParentWindow: lockParentWindow, }, ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); } /// A class which mocks [$MockBaseCacheManager]. /// /// See the documentation for Mockito's code generation for more information. class MockBaseCacheManager extends _i1.Mock - implements _i35.$MockBaseCacheManager { + implements _i27.$MockBaseCacheManager { @override - _i18.Future<_i14.File> getSingleFile( + _i15.Future<_i11.File> getSingleFile( String? url, { String? key, Map? headers, @@ -7226,7 +6598,7 @@ class MockBaseCacheManager extends _i1.Mock #headers: headers, }, ), - returnValue: _i18.Future<_i14.File>.value(_FakeFile_66( + returnValue: _i15.Future<_i11.File>.value(_FakeFile_67( this, Invocation.method( #getSingleFile, @@ -7237,7 +6609,7 @@ class MockBaseCacheManager extends _i1.Mock }, ), )), - returnValueForMissingStub: _i18.Future<_i14.File>.value(_FakeFile_66( + returnValueForMissingStub: _i15.Future<_i11.File>.value(_FakeFile_67( this, Invocation.method( #getSingleFile, @@ -7248,10 +6620,10 @@ class MockBaseCacheManager extends _i1.Mock }, ), )), - ) as _i18.Future<_i14.File>); + ) as _i15.Future<_i11.File>); @override - _i18.Stream<_i15.FileInfo> getFile( + _i15.Stream<_i12.FileInfo> getFile( String? url, { String? key, Map? headers, @@ -7265,12 +6637,12 @@ class MockBaseCacheManager extends _i1.Mock #headers: headers, }, ), - returnValue: _i18.Stream<_i15.FileInfo>.empty(), - returnValueForMissingStub: _i18.Stream<_i15.FileInfo>.empty(), - ) as _i18.Stream<_i15.FileInfo>); + returnValue: _i15.Stream<_i12.FileInfo>.empty(), + returnValueForMissingStub: _i15.Stream<_i12.FileInfo>.empty(), + ) as _i15.Stream<_i12.FileInfo>); @override - _i18.Stream<_i15.FileResponse> getFileStream( + _i15.Stream<_i12.FileResponse> getFileStream( String? url, { String? key, Map? headers, @@ -7286,12 +6658,12 @@ class MockBaseCacheManager extends _i1.Mock #withProgress: withProgress, }, ), - returnValue: _i18.Stream<_i15.FileResponse>.empty(), - returnValueForMissingStub: _i18.Stream<_i15.FileResponse>.empty(), - ) as _i18.Stream<_i15.FileResponse>); + returnValue: _i15.Stream<_i12.FileResponse>.empty(), + returnValueForMissingStub: _i15.Stream<_i12.FileResponse>.empty(), + ) as _i15.Stream<_i12.FileResponse>); @override - _i18.Future<_i15.FileInfo> downloadFile( + _i15.Future<_i12.FileInfo> downloadFile( String? url, { String? key, Map? authHeaders, @@ -7307,7 +6679,7 @@ class MockBaseCacheManager extends _i1.Mock #force: force, }, ), - returnValue: _i18.Future<_i15.FileInfo>.value(_FakeFileInfo_67( + returnValue: _i15.Future<_i12.FileInfo>.value(_FakeFileInfo_68( this, Invocation.method( #downloadFile, @@ -7320,7 +6692,7 @@ class MockBaseCacheManager extends _i1.Mock ), )), returnValueForMissingStub: - _i18.Future<_i15.FileInfo>.value(_FakeFileInfo_67( + _i15.Future<_i12.FileInfo>.value(_FakeFileInfo_68( this, Invocation.method( #downloadFile, @@ -7332,10 +6704,10 @@ class MockBaseCacheManager extends _i1.Mock }, ), )), - ) as _i18.Future<_i15.FileInfo>); + ) as _i15.Future<_i12.FileInfo>); @override - _i18.Future<_i15.FileInfo?> getFileFromCache( + _i15.Future<_i12.FileInfo?> getFileFromCache( String? key, { bool? ignoreMemCache = false, }) => @@ -7345,25 +6717,25 @@ class MockBaseCacheManager extends _i1.Mock [key], {#ignoreMemCache: ignoreMemCache}, ), - returnValue: _i18.Future<_i15.FileInfo?>.value(), - returnValueForMissingStub: _i18.Future<_i15.FileInfo?>.value(), - ) as _i18.Future<_i15.FileInfo?>); + returnValue: _i15.Future<_i12.FileInfo?>.value(), + returnValueForMissingStub: _i15.Future<_i12.FileInfo?>.value(), + ) as _i15.Future<_i12.FileInfo?>); @override - _i18.Future<_i15.FileInfo?> getFileFromMemory(String? key) => + _i15.Future<_i12.FileInfo?> getFileFromMemory(String? key) => (super.noSuchMethod( Invocation.method( #getFileFromMemory, [key], ), - returnValue: _i18.Future<_i15.FileInfo?>.value(), - returnValueForMissingStub: _i18.Future<_i15.FileInfo?>.value(), - ) as _i18.Future<_i15.FileInfo?>); + returnValue: _i15.Future<_i12.FileInfo?>.value(), + returnValueForMissingStub: _i15.Future<_i12.FileInfo?>.value(), + ) as _i15.Future<_i12.FileInfo?>); @override - _i18.Future<_i14.File> putFile( + _i15.Future<_i11.File> putFile( String? url, - _i28.Uint8List? fileBytes, { + _i25.Uint8List? fileBytes, { String? key, String? eTag, Duration? maxAge = const Duration(days: 30), @@ -7383,7 +6755,7 @@ class MockBaseCacheManager extends _i1.Mock #fileExtension: fileExtension, }, ), - returnValue: _i18.Future<_i14.File>.value(_FakeFile_66( + returnValue: _i15.Future<_i11.File>.value(_FakeFile_67( this, Invocation.method( #putFile, @@ -7399,7 +6771,7 @@ class MockBaseCacheManager extends _i1.Mock }, ), )), - returnValueForMissingStub: _i18.Future<_i14.File>.value(_FakeFile_66( + returnValueForMissingStub: _i15.Future<_i11.File>.value(_FakeFile_67( this, Invocation.method( #putFile, @@ -7415,12 +6787,12 @@ class MockBaseCacheManager extends _i1.Mock }, ), )), - ) as _i18.Future<_i14.File>); + ) as _i15.Future<_i11.File>); @override - _i18.Future<_i14.File> putFileStream( + _i15.Future<_i11.File> putFileStream( String? url, - _i18.Stream>? source, { + _i15.Stream>? source, { String? key, String? eTag, Duration? maxAge = const Duration(days: 30), @@ -7440,7 +6812,7 @@ class MockBaseCacheManager extends _i1.Mock #fileExtension: fileExtension, }, ), - returnValue: _i18.Future<_i14.File>.value(_FakeFile_66( + returnValue: _i15.Future<_i11.File>.value(_FakeFile_67( this, Invocation.method( #putFileStream, @@ -7456,7 +6828,7 @@ class MockBaseCacheManager extends _i1.Mock }, ), )), - returnValueForMissingStub: _i18.Future<_i14.File>.value(_FakeFile_66( + returnValueForMissingStub: _i15.Future<_i11.File>.value(_FakeFile_67( this, Invocation.method( #putFileStream, @@ -7472,56 +6844,56 @@ class MockBaseCacheManager extends _i1.Mock }, ), )), - ) as _i18.Future<_i14.File>); + ) as _i15.Future<_i11.File>); @override - _i18.Future removeFile(String? key) => (super.noSuchMethod( + _i15.Future removeFile(String? key) => (super.noSuchMethod( Invocation.method( #removeFile, [key], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future emptyCache() => (super.noSuchMethod( + _i15.Future emptyCache() => (super.noSuchMethod( Invocation.method( #emptyCache, [], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future dispose() => (super.noSuchMethod( + _i15.Future dispose() => (super.noSuchMethod( Invocation.method( #dispose, [], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); } /// A class which mocks [$MockUrlLauncherPlatform]. /// /// See the documentation for Mockito's code generation for more information. class MockUrlLauncherPlatform extends _i1.Mock - implements _i35.$MockUrlLauncherPlatform { + implements _i27.$MockUrlLauncherPlatform { @override - _i18.Future canLaunch(String? url) => (super.noSuchMethod( + _i15.Future canLaunch(String? url) => (super.noSuchMethod( Invocation.method( #canLaunch, [url], ), - returnValue: _i18.Future.value(false), - returnValueForMissingStub: _i18.Future.value(false), - ) as _i18.Future); + returnValue: _i15.Future.value(false), + returnValueForMissingStub: _i15.Future.value(false), + ) as _i15.Future); @override - _i18.Future launch( + _i15.Future launch( String? url, { required bool? useSafariVC, required bool? useWebView, @@ -7545,14 +6917,14 @@ class MockUrlLauncherPlatform extends _i1.Mock #webOnlyWindowName: webOnlyWindowName, }, ), - returnValue: _i18.Future.value(false), - returnValueForMissingStub: _i18.Future.value(false), - ) as _i18.Future); + returnValue: _i15.Future.value(false), + returnValueForMissingStub: _i15.Future.value(false), + ) as _i15.Future); @override - _i18.Future launchUrl( + _i15.Future launchUrl( String? url, - _i37.LaunchOptions? options, + _i29.LaunchOptions? options, ) => (super.noSuchMethod( Invocation.method( @@ -7562,39 +6934,39 @@ class MockUrlLauncherPlatform extends _i1.Mock options, ], ), - returnValue: _i18.Future.value(false), - returnValueForMissingStub: _i18.Future.value(false), - ) as _i18.Future); + returnValue: _i15.Future.value(false), + returnValueForMissingStub: _i15.Future.value(false), + ) as _i15.Future); @override - _i18.Future closeWebView() => (super.noSuchMethod( + _i15.Future closeWebView() => (super.noSuchMethod( Invocation.method( #closeWebView, [], ), - returnValue: _i18.Future.value(), - returnValueForMissingStub: _i18.Future.value(), - ) as _i18.Future); + returnValue: _i15.Future.value(), + returnValueForMissingStub: _i15.Future.value(), + ) as _i15.Future); @override - _i18.Future supportsMode(_i37.PreferredLaunchMode? mode) => + _i15.Future supportsMode(_i29.PreferredLaunchMode? mode) => (super.noSuchMethod( Invocation.method( #supportsMode, [mode], ), - returnValue: _i18.Future.value(false), - returnValueForMissingStub: _i18.Future.value(false), - ) as _i18.Future); + returnValue: _i15.Future.value(false), + returnValueForMissingStub: _i15.Future.value(false), + ) as _i15.Future); @override - _i18.Future supportsCloseForMode(_i37.PreferredLaunchMode? mode) => + _i15.Future supportsCloseForMode(_i29.PreferredLaunchMode? mode) => (super.noSuchMethod( Invocation.method( #supportsCloseForMode, [mode], ), - returnValue: _i18.Future.value(false), - returnValueForMissingStub: _i18.Future.value(false), - ) as _i18.Future); + returnValue: _i15.Future.value(false), + returnValueForMissingStub: _i15.Future.value(false), + ) as _i15.Future); } diff --git a/test/test_util/test_datas.dart b/test/test_util/test_datas.dart index 4642be7c0..1607850e7 100644 --- a/test/test_util/test_datas.dart +++ b/test/test_util/test_datas.dart @@ -1,20 +1,25 @@ -import 'package:dio/dio.dart'; -import 'package:flutter/services.dart'; -import 'package:miria/model/account.dart'; -import 'package:miria/model/misskey_emoji_data.dart'; -import 'package:miria/repository/emoji_repository.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:json5/json5.dart'; +import "package:dio/dio.dart"; +import "package:flutter/services.dart"; +import "package:json5/json5.dart"; +import "package:miria/model/account.dart"; +import "package:miria/model/misskey_emoji_data.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/emoji_repository.dart"; +import "package:misskey_dart/misskey_dart.dart"; +// ignore: avoid_classes_with_only_static_members class TestData { static Account account = Account( - host: "example.miria.shiosyakeyakini.info", - userId: "ai", - i: i1, - meta: meta); - + host: "example.miria.shiosyakeyakini.info", + userId: "ai", + i: i1, + meta: meta, + ); + static AccountContext accountContext = + AccountContext(getAccount: account, postAccount: account); // i - static MeDetailed i1 = MeDetailed.fromJson(JSON5.parse(r""" + static MeDetailed i1 = MeDetailed.fromJson( + JSON5.parse(r""" { id: '7rkr3b1c1c', name: '藍', @@ -335,12 +340,14 @@ class TestData { securityKeysList: [], } -""")); +"""), + ); // note /// 自身のノート(藍ちゃん)1 - static Note note1 = Note.fromJson(JSON5.parse(r''' + static Note note1 = Note.fromJson( + JSON5.parse(""" { id: '9g3rcngj3e', createdAt: '2023-06-17T16:08:52.675Z', @@ -381,9 +388,11 @@ class TestData { renoteId: null, } - ''')); + """), + ); - static Note note2 = Note.fromJson(JSON5.parse(r''' + static Note note2 = Note.fromJson( + JSON5.parse(r""" { id: '9g4rtxu236', createdAt: '2023-06-18T09:10:05.450Z', @@ -419,10 +428,12 @@ class TestData { replyId: null, renoteId: null, } - ''')); + """), + ); /// 自身でないノート1 - static Note note3AsAnotherUser = Note.fromJson(JSON5.parse(r''' + static Note note3AsAnotherUser = Note.fromJson( + JSON5.parse(""" { id: '9g2ja0y8ix', createdAt: '2023-06-16T19:35:07.088Z', @@ -464,10 +475,12 @@ class TestData { replyId: null, renoteId: null, } - ''')); + """), + ); /// 自身のノート(投票込みのノート) - static Note note4AsVote = Note.fromJson(JSON5.parse(''' + static Note note4AsVote = Note.fromJson( + JSON5.parse(""" { id: '9h7cbiu7ab', createdAt: '2023-07-15T08:58:52.831Z', @@ -533,10 +546,12 @@ class TestData { }, } -''')); +"""), + ); /// 自身でないノート2 - static Note note5AsAnotherUser = Note.fromJson(JSON5.parse(r''' + static Note note5AsAnotherUser = Note.fromJson( + JSON5.parse(r""" { id: '9gdpe2xkeo', createdAt: '2023-06-24T15:11:41.912Z', @@ -645,10 +660,12 @@ class TestData { renoteId: null, myReaction: ':ultra_igyo@.:', } - ''')); + """), + ); /// Renote - static Note note6AsRenote = Note.fromJson(JSON5.parse(''' + static Note note6AsRenote = Note.fromJson( + JSON5.parse(""" { id: '9lmbcrob34', createdAt: '2023-11-03T15:07:13.307Z', @@ -738,27 +755,33 @@ class TestData { myReaction: ':miria@.:', }, } -''')); +"""), + ); // ドライブ(フォルダ) - static DriveFolder folder1 = DriveFolder.fromJson(JSON5.parse(r''' + static DriveFolder folder1 = DriveFolder.fromJson( + JSON5.parse(""" { id: '9ettn0mv95', createdAt: '2023-05-16T12:35:31.447Z', name: '秘蔵の藍ちゃんフォルダ', parentId: null, - }''')); + }"""), + ); - static DriveFolder folder1Child = DriveFolder.fromJson(JSON5.parse(r''' + static DriveFolder folder1Child = DriveFolder.fromJson( + JSON5.parse(""" { id: '9ettn0mv95', createdAt: '2023-05-16T12:35:31.447Z', name: 'えっちなやつ', parentId: '9ettn0mv95', - }''')); + }"""), + ); // ドライブ(ファイル) - static DriveFile drive1 = DriveFile.fromJson(JSON5.parse(r''' + static DriveFile drive1 = DriveFile.fromJson( + JSON5.parse(r""" { id: '9g6yuyisp3', createdAt: '2023-06-19T22:02:22.660Z', @@ -780,9 +803,11 @@ class TestData { userId: null, user: null, } - ''')); + """), + ); - static DriveFile drive2AsVideo = DriveFile.fromJson(JSON5.parse(r''' + static DriveFile drive2AsVideo = DriveFile.fromJson( + JSON5.parse(""" { id: '9g0kvlw8d3', createdAt: '2023-06-15T10:44:21.272Z', @@ -801,18 +826,22 @@ class TestData { userId: null, user: null, } - ''')); + """), + ); static Future get binaryImage async => Uint8List.fromList( - (await rootBundle.load("assets/images/icon.png")).buffer.asUint8List()); + (await rootBundle.load("assets/images/icon.png")).buffer.asUint8List(), + ); static Future> get binaryImageResponse async => Response( - requestOptions: RequestOptions(), - statusCode: 200, - data: await binaryImage); + requestOptions: RequestOptions(), + statusCode: 200, + data: await binaryImage, + ); // ユーザー情報 - static UserLite user1 = UserLite.fromJson(JSON5.parse(r''' + static UserLite user1 = UserLite.fromJson( + JSON5.parse(""" { id: '7rkr3b1c1c', name: '藍', @@ -825,11 +854,13 @@ class TestData { emojis: {}, onlineStatus: 'online', badgeRoles: [], -}''')); +}"""), + ); static String user1ExpectId = "7rkr3b1c1c"; static UserDetailedNotMeWithRelations detailedUser1 = - UserDetailedNotMeWithRelations.fromJson(JSON5.parse(r''' + UserDetailedNotMeWithRelations.fromJson( + JSON5.parse(r""" { id: '7z9zua5kyv', name: 'おいしいBot', @@ -996,10 +1027,12 @@ class TestData { isBlocked: false, isMuted: false, isRenoteMuted: false, -} ''')); +} """), + ); static UserDetailedNotMeWithRelations detailedUser2 = - UserDetailedNotMeWithRelations.fromJson(JSON5.parse(r''' + UserDetailedNotMeWithRelations.fromJson( + JSON5.parse(r""" { id: '9gbzuv2cze', name: '藍ちゃんにおじさん構文でメンションを送るbot', @@ -1106,13 +1139,15 @@ class TestData { isBlocked: false, isMuted: false, isRenoteMuted: false, -}''')); +}"""), + ); static String detailedUser2ExpectedId = "9gbzuv2cze"; // ユーザー情報 static UserDetailedNotMeWithRelations usersShowResponse1 = - UserDetailedNotMeWithRelations.fromJson(JSON5.parse(r''' + UserDetailedNotMeWithRelations.fromJson( + JSON5.parse(r""" { id: '7rkr3b1c1c', name: '藍', @@ -1221,10 +1256,12 @@ class TestData { isRenoteMuted: false, } - ''')); + """), + ); static UserDetailedNotMeWithRelations usersShowResponse2 = - UserDetailedNotMeWithRelations.fromJson(JSON5.parse(r''' + UserDetailedNotMeWithRelations.fromJson( + JSON5.parse(r""" { id: '7z9zua5kyv', name: 'おいしいBot', @@ -1395,10 +1432,12 @@ class TestData { isRenoteMuted: false, } - ''')); + """), + ); static UserDetailedNotMeWithRelations usersShowResponse3AsRemoteUser = - UserDetailedNotMeWithRelations.fromJson(JSON5.parse(r''' + UserDetailedNotMeWithRelations.fromJson( + JSON5.parse(r''' { id: '9i08deo0vj', name: 'あけおめらんか~', @@ -1476,10 +1515,12 @@ class TestData { isMuted: false, isRenoteMuted: false, } -''')); +'''), + ); static UserDetailedNotMeWithRelations usersShowResponse3AsLocalUser = - UserDetailedNotMeWithRelations.fromJson(JSON5.parse(''' + UserDetailedNotMeWithRelations.fromJson( + JSON5.parse(''' { id: '9i07ia9bf0', name: 'あけおめらんか~', @@ -1563,81 +1604,86 @@ class TestData { notify: 'none', } -''')); +'''), + ); // カスタム絵文字 static UnicodeEmojiData unicodeEmoji1 = const UnicodeEmojiData(char: "♥"); static CustomEmojiData customEmoji1 = CustomEmojiData( - baseName: "ai_yay", - hostedName: "misskey.io", - url: Uri.parse("https://s3.arkjp.net/emoji/ai_yay.apng"), - isCurrentServer: true, - isSensitive: false); + baseName: "ai_yay", + hostedName: "misskey.io", + url: Uri.parse("https://s3.arkjp.net/emoji/ai_yay.apng"), + isCurrentServer: true, + isSensitive: false, + ); static EmojiRepositoryData unicodeEmojiRepositoryData1 = EmojiRepositoryData( - emoji: unicodeEmoji1, - category: "symbols", - kanaName: "へあt", - aliases: ["heart", "ハート"], - kanaAliases: ["へあt", "ハート"]); + emoji: unicodeEmoji1, + category: "symbols", + kanaName: "へあt", + aliases: ["heart", "ハート"], + kanaAliases: ["へあt", "ハート"], + ); static EmojiRepositoryData customEmojiRepositoryData1 = EmojiRepositoryData( - emoji: customEmoji1, - category: "02 Ai", - kanaName: "あいやy", - aliases: [ - 'yay_ai', - '藍', - 'あい', - 'ばんざい', - 'バンザイ', - 'ばんざーい', - 'やった', - 'やったぁ', - 'わぁい', - 'わーい', - 'やったー', - 'やったぁ', - 'うれしい', - 'ハッピー', - 'たのしい', - 'わーいわーい', - 'よろこび', - 'よろこぶ', - '', - 'happy', - 'yay', - 'ai', - 'praise', - ], - kanaAliases: [ - 'やyあい', - '藍', - 'あい', - 'ばんざい', - 'バンザイ', - 'ばんざーい', - 'やった', - 'やったぁ', - 'わぁい', - 'わーい', - 'やったー', - 'やったぁ', - 'うれしい', - 'ハッピー', - 'たのしい', - 'わーいわーい', - 'よろこび', - 'よろこぶ', - '', - 'はppy', - 'やy', - 'あい', - 'pらいせ', - ]); + emoji: customEmoji1, + category: "02 Ai", + kanaName: "あいやy", + aliases: [ + "yay_ai", + "藍", + "あい", + "ばんざい", + "バンザイ", + "ばんざーい", + "やった", + "やったぁ", + "わぁい", + "わーい", + "やったー", + "やったぁ", + "うれしい", + "ハッピー", + "たのしい", + "わーいわーい", + "よろこび", + "よろこぶ", + "", + "happy", + "yay", + "ai", + "praise", + ], + kanaAliases: [ + "やyあい", + "藍", + "あい", + "ばんざい", + "バンザイ", + "ばんざーい", + "やった", + "やったぁ", + "わぁい", + "わーい", + "やったー", + "やったぁ", + "うれしい", + "ハッピー", + "たのしい", + "わーいわーい", + "よろこび", + "よろこぶ", + "", + "はppy", + "やy", + "あい", + "pらいせ", + ], + ); // チャンネル - static CommunityChannel channel1 = CommunityChannel.fromJson(JSON5.parse(r''' + static CommunityChannel channel1 = CommunityChannel.fromJson( + JSON5.parse(r""" { id: '9axtmmcxuy', createdAt: '2023-02-07T13:07:28.305Z', @@ -1655,11 +1701,13 @@ class TestData { isFavorited: true, hasUnreadNote: false, } - ''')); + """), + ); static String expectChannel1DescriptionContaining = "ありがとうブルーアーカイブ"; - static CommunityChannel channel2 = CommunityChannel.fromJson(JSON5.parse(r''' + static CommunityChannel channel2 = CommunityChannel.fromJson( + JSON5.parse(r""" { id: '9b3chwrm7f', createdAt: '2023-02-11T09:54:32.098Z', @@ -1679,10 +1727,12 @@ class TestData { isFavorited: true, hasUnreadNote: false, } - ''')); + """), + ); // アンテナ - static Antenna antenna = Antenna.fromJson(JSON5.parse(r''' + static Antenna antenna = Antenna.fromJson( + JSON5.parse(""" { id: '9f7kcbzcoe', createdAt: '2023-05-26T03:24:02.856Z', @@ -1715,9 +1765,11 @@ class TestData { isActive: true, hasUnreadNote: false, } -''')); +"""), + ); - static Clip clip = Clip.fromJson(JSON5.parse(r''' + static Clip clip = Clip.fromJson( + JSON5.parse(""" { id: '9crm7l2n4k', createdAt: '2023-03-25T14:12:37.103Z', @@ -1743,9 +1795,11 @@ class TestData { isFavorited: false, } -''')); +"""), + ); - static RolesListResponse role = RolesListResponse.fromJson(JSON5.parse(''' + static RolesListResponse role = RolesListResponse.fromJson( + JSON5.parse(""" { id: '9diazxez3m', createdAt: '2023-04-13T06:28:30.827Z', @@ -1901,10 +1955,11 @@ class TestData { }, usersCount: 0, } -''')); +"""), + ); - static HashtagsTrendResponse hashtagTrends = - HashtagsTrendResponse.fromJson(JSON5.parse(r''' + static HashtagsTrendResponse hashtagTrends = HashtagsTrendResponse.fromJson( + JSON5.parse(""" { tag: 'ろぐぼチャレンジ', chart: [ @@ -1931,9 +1986,11 @@ class TestData { ], usersCount: 15, } -''')); +"""), + ); - static Hashtag hashtag = Hashtag.fromJson(JSON5.parse(r''' + static Hashtag hashtag = Hashtag.fromJson( + JSON5.parse(""" { tag: 'アークナイツ', mentionedUsersCount: 531, @@ -1943,9 +2000,11 @@ class TestData { attachedLocalUsersCount: 2, attachedRemoteUsersCount: 65, } -''')); +"""), + ); - static MetaResponse meta = MetaResponse.fromJson(JSON5.parse(r''' + static MetaResponse meta = MetaResponse.fromJson( + JSON5.parse(""" { maintainerName: 'そらいろ', maintainerEmail: 'sorairo@shiosyakeyakini.info', @@ -2037,11 +2096,13 @@ class TestData { miauth: true, }, } -''')); +"""), + ); // Dio - static DioError response404 = DioError( - requestOptions: RequestOptions(), - response: Response(requestOptions: RequestOptions(), statusCode: 404), - type: DioErrorType.unknown); + static DioException response404 = DioException( + requestOptions: RequestOptions(), + response: Response(requestOptions: RequestOptions(), statusCode: 404), + type: DioExceptionType.unknown, + ); } diff --git a/test/test_util/widget_tester_extension.dart b/test/test_util/widget_tester_extension.dart index 8f24452d4..f3d15a833 100644 --- a/test/test_util/widget_tester_extension.dart +++ b/test/test_util/widget_tester_extension.dart @@ -1,5 +1,5 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_test/flutter_test.dart'; +import "package:flutter/material.dart"; +import "package:flutter_test/flutter_test.dart"; extension WidgetTestExtension on WidgetTester { TextEditingController textEditingController(Finder finder) { @@ -12,11 +12,15 @@ extension WidgetTestExtension on WidgetTester { } Future pageNation() async { - await tap(find.descendant( + await tap( + find.descendant( of: find.descendant( - of: find.byType(Center), - matching: find.byType(IconButton).hitTestable()), - matching: find.byIcon(Icons.keyboard_arrow_down))); + of: find.byType(Center), + matching: find.byType(IconButton).hitTestable(), + ), + matching: find.byIcon(Icons.keyboard_arrow_down), + ), + ); await pumpAndSettle(); } } diff --git a/test/view/antenna_list_page/antenna_list_page_test.dart b/test/view/antenna_list_page/antenna_list_page_test.dart index 88740ab9f..9c9da211e 100644 --- a/test/view/antenna_list_page/antenna_list_page_test.dart +++ b/test/view/antenna_list_page/antenna_list_page_test.dart @@ -1,12 +1,12 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:mockito/mockito.dart'; +import "package:flutter_test/flutter_test.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:mockito/mockito.dart"; -import '../../test_util/default_root_widget.dart'; -import '../../test_util/mock.mocks.dart'; -import '../../test_util/test_datas.dart'; +import "../../test_util/default_root_widget.dart"; +import "../../test_util/mock.mocks.dart"; +import "../../test_util/test_datas.dart"; void main() { group("アンテナ一覧", () { @@ -16,11 +16,14 @@ void main() { when(misskey.antennas).thenReturn(antennas); when(antennas.list()).thenAnswer((_) async => [TestData.antenna]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( - initialRoute: AntennaRoute(account: TestData.account), - ))); + initialRoute: AntennaRoute(accountContext: TestData.accountContext), + ), + ), + ); await tester.pumpAndSettle(); expect(find.text(TestData.antenna.name), findsOneWidget); diff --git a/test/view/antenna_notes_page/antenna_notes_page_test.dart b/test/view/antenna_notes_page/antenna_notes_page_test.dart index a9ad80725..8db963d87 100644 --- a/test/view/antenna_notes_page/antenna_notes_page_test.dart +++ b/test/view/antenna_notes_page/antenna_notes_page_test.dart @@ -1,14 +1,14 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; +import "package:flutter_test/flutter_test.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; -import '../../test_util/default_root_widget.dart'; -import '../../test_util/mock.mocks.dart'; -import '../../test_util/test_datas.dart'; -import '../../test_util/widget_tester_extension.dart'; +import "../../test_util/default_root_widget.dart"; +import "../../test_util/mock.mocks.dart"; +import "../../test_util/test_datas.dart"; +import "../../test_util/widget_tester_extension.dart"; void main() { group("アンテナノート一覧", () { @@ -18,21 +18,41 @@ void main() { when(misskey.antennas).thenReturn(antennas); when(antennas.notes(any)).thenAnswer((_) async => [TestData.note1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( initialRoute: AntennaNotesRoute( - account: TestData.account, antenna: TestData.antenna), - ))); + accountContext: TestData.accountContext, + antenna: TestData.antenna, + ), + ), + ), + ); await tester.pumpAndSettle(); expect(find.text(TestData.note1.text!), findsOneWidget); - verify(antennas.notes(argThat( - equals(AntennasNotesRequest(antennaId: TestData.antenna.id))))); + verify( + antennas.notes( + argThat( + equals(AntennasNotesRequest(antennaId: TestData.antenna.id)), + ), + ), + ); await tester.pageNation(); - verify(antennas.notes(argThat(equals(AntennasNotesRequest( - antennaId: TestData.antenna.id, untilId: TestData.note1.id))))); + verify( + antennas.notes( + argThat( + equals( + AntennasNotesRequest( + antennaId: TestData.antenna.id, + untilId: TestData.note1.id, + ), + ), + ), + ), + ); }); }); } diff --git a/test/view/channel_detail_page/channel_detail_page_test.dart b/test/view/channel_detail_page/channel_detail_page_test.dart index 64f7dcace..a4632fe45 100644 --- a/test/view/channel_detail_page/channel_detail_page_test.dart +++ b/test/view/channel_detail_page/channel_detail_page_test.dart @@ -1,14 +1,14 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; - -import '../../test_util/default_root_widget.dart'; -import '../../test_util/mock.mocks.dart'; -import '../../test_util/test_datas.dart'; -import '../../test_util/widget_tester_extension.dart'; +import "package:flutter_test/flutter_test.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; + +import "../../test_util/default_root_widget.dart"; +import "../../test_util/mock.mocks.dart"; +import "../../test_util/test_datas.dart"; +import "../../test_util/widget_tester_extension.dart"; void main() { group("チャンネル詳細", () { @@ -19,33 +19,48 @@ void main() { when(channel.show(any)) .thenAnswer((_) async => TestData.channel1.copyWith(bannerUrl: null)); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( initialRoute: ChannelDetailRoute( - account: TestData.account, channelId: TestData.channel1.id), - ))); + accountContext: TestData.accountContext, + channelId: TestData.channel1.id, + ), + ), + ), + ); await tester.pumpAndSettle(); expect( - find.textContaining(TestData.expectChannel1DescriptionContaining, - findRichText: true), - findsOneWidget); + find.textContaining( + TestData.expectChannel1DescriptionContaining, + findRichText: true, + ), + findsOneWidget, + ); }); testWidgets("チャンネルがセンシティブの場合、センシティブである旨が表示されること", (tester) async { final channel = MockMisskeyChannels(); final misskey = MockMisskey(); when(misskey.channels).thenReturn(channel); - when(channel.show(any)).thenAnswer((_) async => - TestData.channel1.copyWith(bannerUrl: null, isSensitive: true)); + when(channel.show(any)).thenAnswer( + (_) async => + TestData.channel1.copyWith(bannerUrl: null, isSensitive: true), + ); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( initialRoute: ChannelDetailRoute( - account: TestData.account, channelId: TestData.channel1.id), - ))); + accountContext: TestData.accountContext, + channelId: TestData.channel1.id, + ), + ), + ), + ); await tester.pumpAndSettle(); expect(find.textContaining("センシティブ"), findsOneWidget); @@ -55,92 +70,140 @@ void main() { final channel = MockMisskeyChannels(); final misskey = MockMisskey(); when(misskey.channels).thenReturn(channel); - when(channel.show(any)).thenAnswer((_) async => - TestData.channel1.copyWith(bannerUrl: null, isFavorited: false)); + when(channel.show(any)).thenAnswer( + (_) async => + TestData.channel1.copyWith(bannerUrl: null, isFavorited: false), + ); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( initialRoute: ChannelDetailRoute( - account: TestData.account, channelId: TestData.channel1.id), - ))); + accountContext: TestData.accountContext, + channelId: TestData.channel1.id, + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text("お気に入りに入れるで")); await tester.pumpAndSettle(); - expect(find.text("お気に入り"), findsOneWidget); + expect(find.text("お気に入り中"), findsOneWidget); - verify(channel.favorite(argThat( - equals(ChannelsFavoriteRequest(channelId: TestData.channel1.id))))); + verify( + channel.favorite( + argThat( + equals(ChannelsFavoriteRequest(channelId: TestData.channel1.id)), + ), + ), + ); }); testWidgets("チャンネルをお気に入りに設定している場合、お気に入りを解除できること", (tester) async { final channel = MockMisskeyChannels(); final misskey = MockMisskey(); when(misskey.channels).thenReturn(channel); - when(channel.show(any)).thenAnswer((_) async => - TestData.channel1.copyWith(bannerUrl: null, isFavorited: true)); + when(channel.show(any)).thenAnswer( + (_) async => + TestData.channel1.copyWith(bannerUrl: null, isFavorited: true), + ); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( initialRoute: ChannelDetailRoute( - account: TestData.account, channelId: TestData.channel1.id), - ))); + accountContext: TestData.accountContext, + channelId: TestData.channel1.id, + ), + ), + ), + ); await tester.pumpAndSettle(); - await tester.tap(find.text("お気に入り")); + await tester.tap(find.text("お気に入り中")); await tester.pumpAndSettle(); expect(find.text("お気に入りに入れるで"), findsOneWidget); - verify(channel.unfavorite(argThat( - equals(ChannelsUnfavoriteRequest(channelId: TestData.channel1.id))))); + verify( + channel.unfavorite( + argThat( + equals(ChannelsUnfavoriteRequest(channelId: TestData.channel1.id)), + ), + ), + ); }); testWidgets("チャンネルをフォローしていない場合、フォローすることができること", (tester) async { final channel = MockMisskeyChannels(); final misskey = MockMisskey(); when(misskey.channels).thenReturn(channel); - when(channel.show(any)).thenAnswer((_) async => - TestData.channel1.copyWith(bannerUrl: null, isFollowing: false)); + when(channel.show(any)).thenAnswer( + (_) async => + TestData.channel1.copyWith(bannerUrl: null, isFollowing: false), + ); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( initialRoute: ChannelDetailRoute( - account: TestData.account, channelId: TestData.channel1.id), - ))); + accountContext: TestData.accountContext, + channelId: TestData.channel1.id, + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text("フォローするで")); await tester.pumpAndSettle(); expect(find.text("フォロー中"), findsOneWidget); - verify(channel.follow(argThat( - equals(ChannelsFollowRequest(channelId: TestData.channel1.id))))); + verify( + channel.follow( + argThat( + equals(ChannelsFollowRequest(channelId: TestData.channel1.id)), + ), + ), + ); }); testWidgets("チャンネルをフォローしている場合、フォロー解除をすることができること", (tester) async { final channel = MockMisskeyChannels(); final misskey = MockMisskey(); when(misskey.channels).thenReturn(channel); - when(channel.show(any)).thenAnswer((_) async => - TestData.channel1.copyWith(bannerUrl: null, isFollowing: true)); + when(channel.show(any)).thenAnswer( + (_) async => + TestData.channel1.copyWith(bannerUrl: null, isFollowing: true), + ); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( initialRoute: ChannelDetailRoute( - account: TestData.account, channelId: TestData.channel1.id), - ))); + accountContext: TestData.accountContext, + channelId: TestData.channel1.id, + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text("フォロー中")); await tester.pumpAndSettle(); expect(find.text("フォローするで"), findsOneWidget); - verify(channel.unfollow(argThat( - equals(ChannelsUnfollowRequest(channelId: TestData.channel1.id))))); + verify( + channel.unfollow( + argThat( + equals(ChannelsUnfollowRequest(channelId: TestData.channel1.id)), + ), + ), + ); }); }); @@ -149,30 +212,52 @@ void main() { final channel = MockMisskeyChannels(); final misskey = MockMisskey(); when(misskey.channels).thenReturn(channel); - when(channel.show(any)).thenAnswer((_) async => - TestData.channel1.copyWith(bannerUrl: null, isFollowing: false)); + when(channel.show(any)).thenAnswer( + (_) async => + TestData.channel1.copyWith(bannerUrl: null, isFollowing: false), + ); when(channel.timeline(any)) .thenAnswer((realInvocation) async => [TestData.note1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( initialRoute: ChannelDetailRoute( - account: TestData.account, channelId: TestData.channel1.id), - ))); + accountContext: TestData.accountContext, + channelId: TestData.channel1.id, + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text("タイムライン")); await tester.pumpAndSettle(); expect(find.text(TestData.note1.text!), findsOneWidget); - verify(channel.timeline(argThat(predicate( - (e) => e.channelId == TestData.channel1.id)))); + verify( + channel.timeline( + argThat( + predicate( + (e) => e.channelId == TestData.channel1.id, + ), + ), + ), + ); await tester.pageNation(); - verify(channel.timeline(argThat(predicate((e) => - e.channelId == TestData.channel1.id && - e.untilId == TestData.note1.id)))); + verify( + channel.timeline( + argThat( + predicate( + (e) => + e.channelId == TestData.channel1.id && + e.untilId == TestData.note1.id, + ), + ), + ), + ); }); }); } diff --git a/test/view/channel_page/channel_page_test.dart b/test/view/channel_page/channel_page_test.dart index 2bb42c5c3..5b2783ce1 100644 --- a/test/view/channel_page/channel_page_test.dart +++ b/test/view/channel_page/channel_page_test.dart @@ -1,15 +1,15 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; - -import '../../test_util/default_root_widget.dart'; -import '../../test_util/mock.mocks.dart'; -import '../../test_util/test_datas.dart'; -import '../../test_util/widget_tester_extension.dart'; +import "package:flutter/material.dart"; +import "package:flutter_test/flutter_test.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; + +import "../../test_util/default_root_widget.dart"; +import "../../test_util/mock.mocks.dart"; +import "../../test_util/test_datas.dart"; +import "../../test_util/widget_tester_extension.dart"; void main() { group("チャンネル", () { @@ -19,13 +19,19 @@ void main() { final misskey = MockMisskey(); when(misskey.channels).thenReturn(channel); when(channel.search(any)).thenAnswer( - (_) async => [TestData.channel1.copyWith(bannerUrl: null)]); + (_) async => [TestData.channel1.copyWith(bannerUrl: null)], + ); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( - initialRoute: ChannelsRoute(account: TestData.account), - ))); + initialRoute: ChannelsRoute( + accountContext: TestData.accountContext, + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text("検索")); @@ -36,12 +42,25 @@ void main() { await tester.pumpAndSettle(); expect(find.text(TestData.channel1.name), findsOneWidget); - verify(channel.search( - argThat(equals(const ChannelsSearchRequest(query: "ゲーム開発部"))))); + verify( + channel.search( + argThat(equals(const ChannelsSearchRequest(query: "ゲーム開発部"))), + ), + ); await tester.pageNation(); - verify(channel.search(argThat(equals(ChannelsSearchRequest( - query: "ゲーム開発部", untilId: TestData.channel1.id))))); + verify( + channel.search( + argThat( + equals( + ChannelsSearchRequest( + query: "ゲーム開発部", + untilId: TestData.channel1.id, + ), + ), + ), + ), + ); }); }); @@ -51,13 +70,19 @@ void main() { final misskey = MockMisskey(); when(misskey.channels).thenReturn(channel); when(channel.featured()).thenAnswer( - (_) async => [TestData.channel1.copyWith(bannerUrl: null)]); + (_) async => [TestData.channel1.copyWith(bannerUrl: null)], + ); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( - initialRoute: ChannelsRoute(account: TestData.account), - ))); + initialRoute: ChannelsRoute( + accountContext: TestData.accountContext, + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text("トレンド")); @@ -73,21 +98,29 @@ void main() { final misskey = MockMisskey(); when(misskey.channels).thenReturn(channel); when(channel.myFavorite(any)).thenAnswer( - (_) async => [TestData.channel1.copyWith(bannerUrl: null)]); + (_) async => [TestData.channel1.copyWith(bannerUrl: null)], + ); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( - initialRoute: ChannelsRoute(account: TestData.account), - ))); + initialRoute: ChannelsRoute( + accountContext: TestData.accountContext, + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text("お気に入り")); await tester.pumpAndSettle(); expect(find.text(TestData.channel1.name), findsOneWidget); - verify(channel - .myFavorite(argThat(equals(const ChannelsMyFavoriteRequest())))); + verify( + channel + .myFavorite(argThat(equals(const ChannelsMyFavoriteRequest()))), + ); }); }); @@ -97,13 +130,19 @@ void main() { final misskey = MockMisskey(); when(misskey.channels).thenReturn(channel); when(channel.followed(any)).thenAnswer( - (_) async => [TestData.channel1.copyWith(bannerUrl: null)]); + (_) async => [TestData.channel1.copyWith(bannerUrl: null)], + ); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( - initialRoute: ChannelsRoute(account: TestData.account), - ))); + initialRoute: ChannelsRoute( + accountContext: TestData.accountContext, + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text("フォロー中")); @@ -111,10 +150,16 @@ void main() { expect(find.text(TestData.channel1.name), findsOneWidget); verify( - channel.followed(argThat(equals(const ChannelsFollowedRequest())))); + channel.followed(argThat(equals(const ChannelsFollowedRequest()))), + ); await tester.pageNation(); - verify(channel.followed(argThat( - equals(ChannelsFollowedRequest(untilId: TestData.channel1.id))))); + verify( + channel.followed( + argThat( + equals(ChannelsFollowedRequest(untilId: TestData.channel1.id)), + ), + ), + ); }); }); }); diff --git a/test/view/clip_detail_page/clip_detail_page_test.dart b/test/view/clip_detail_page/clip_detail_page_test.dart index cad20b12c..79e74613f 100644 --- a/test/view/clip_detail_page/clip_detail_page_test.dart +++ b/test/view/clip_detail_page/clip_detail_page_test.dart @@ -1,14 +1,14 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; +import "package:flutter_test/flutter_test.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; -import '../../test_util/default_root_widget.dart'; -import '../../test_util/mock.mocks.dart'; -import '../../test_util/test_datas.dart'; -import '../../test_util/widget_tester_extension.dart'; +import "../../test_util/default_root_widget.dart"; +import "../../test_util/mock.mocks.dart"; +import "../../test_util/test_datas.dart"; +import "../../test_util/widget_tester_extension.dart"; void main() { group("クリップのノート一覧", () { @@ -18,19 +18,33 @@ void main() { when(misskey.clips).thenReturn(clip); when(clip.notes(any)).thenAnswer((_) async => [TestData.note1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( initialRoute: ClipDetailRoute( - id: TestData.clip.id, account: TestData.account), - ))); + id: TestData.clip.id, + accountContext: TestData.accountContext, + ), + ), + ), + ); await tester.pumpAndSettle(); expect(find.text(TestData.note1.text!), findsOneWidget); await tester.pageNation(); - verify(clip.notes(argThat(equals(ClipsNotesRequest( - clipId: TestData.clip.id, untilId: TestData.note1.id))))) - .called(1); + verify( + clip.notes( + argThat( + equals( + ClipsNotesRequest( + clipId: TestData.clip.id, + untilId: TestData.note1.id, + ), + ), + ), + ), + ).called(1); }); }); } diff --git a/test/view/clip_list_page/clip_list_page_test.dart b/test/view/clip_list_page/clip_list_page_test.dart index 32729c355..b5af3f84e 100644 --- a/test/view/clip_list_page/clip_list_page_test.dart +++ b/test/view/clip_list_page/clip_list_page_test.dart @@ -1,12 +1,12 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:mockito/mockito.dart'; +import "package:flutter_test/flutter_test.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:mockito/mockito.dart"; -import '../../test_util/default_root_widget.dart'; -import '../../test_util/mock.mocks.dart'; -import '../../test_util/test_datas.dart'; +import "../../test_util/default_root_widget.dart"; +import "../../test_util/mock.mocks.dart"; +import "../../test_util/test_datas.dart"; void main() { group("クリップ一覧", () { @@ -16,11 +16,15 @@ void main() { when(misskey.clips).thenReturn(clip); when(clip.list()).thenAnswer((_) async => [TestData.clip]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( - initialRoute: ClipListRoute(account: TestData.account), - ))); + initialRoute: + ClipListRoute(accountContext: TestData.accountContext), + ), + ), + ); await tester.pumpAndSettle(); expect(find.text(TestData.clip.name!), findsOneWidget); diff --git a/test/view/common/misskey_notes/misskey_notes_test.dart b/test/view/common/misskey_notes/misskey_notes_test.dart index 243433d58..55a0e136e 100644 --- a/test/view/common/misskey_notes/misskey_notes_test.dart +++ b/test/view/common/misskey_notes/misskey_notes_test.dart @@ -1,26 +1,27 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_highlighting/flutter_highlighting.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/model/general_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/repository/note_repository.dart'; -import 'package:miria/view/common/account_scope.dart'; -import 'package:miria/view/common/misskey_notes/misskey_note.dart'; -import 'package:miria/view/common/misskey_notes/network_image.dart'; -import 'package:miria/view/common/misskey_notes/reaction_button.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; +import "package:flutter/material.dart"; +import "package:flutter_highlighting/flutter_highlighting.dart"; +import "package:flutter_test/flutter_test.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/general_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/note_repository.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/misskey_notes/misskey_note.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; +import "package:miria/view/common/misskey_notes/reaction_button.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; +import "package:url_launcher_platform_interface/url_launcher_platform_interface.dart"; -import '../../../test_util/default_root_widget.dart'; -import '../../../test_util/mock.mocks.dart'; -import '../../../test_util/test_datas.dart'; -import '../../../test_util/widget_tester_extension.dart'; -import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart'; +import "../../../test_util/default_root_widget.dart"; +import "../../../test_util/mock.mocks.dart"; +import "../../../test_util/test_datas.dart"; +import "../../../test_util/widget_tester_extension.dart"; Widget buildTestWidget({ - List overrides = const [], required Note note, + List overrides = const [], }) { final notesRepository = NoteRepository(MockMisskey(), TestData.account); notesRepository.registerNote(note); @@ -30,11 +31,11 @@ Widget buildTestWidget({ overrides: [ ...overrides, cacheManagerProvider.overrideWith((ref) => mockCacheManager), - notesProvider.overrideWith((ref, arg) => notesRepository) + notesProvider.overrideWith((ref, account) => notesRepository), ], child: DefaultRootNoRouterWidget( child: Scaffold( - body: AccountScope( + body: AccountContextScope.as( account: TestData.account, child: SingleChildScrollView(child: MisskeyNote(note: note)), ), @@ -49,40 +50,59 @@ void main() { testWidgets("ノートのテキストが表示されること", (tester) async { await tester.pumpWidget(buildTestWidget(note: TestData.note1)); await tester.pumpAndSettle(); - expect(find.textContaining(TestData.note1.text!, findRichText: true), - findsOneWidget); + expect( + find.textContaining(TestData.note1.text!, findRichText: true), + findsOneWidget, + ); }); testWidgets("Renoteの場合、Renoteの表示が行われること", (tester) async { await tester.pumpWidget(buildTestWidget(note: TestData.note6AsRenote)); await tester.pumpAndSettle(); expect( - find.textContaining(TestData.note6AsRenote.renote!.text!, - findRichText: true), - findsOneWidget); + find.textContaining( + TestData.note6AsRenote.renote!.text!, + findRichText: true, + ), + findsOneWidget, + ); expect( - find.textContaining("がリノート", findRichText: true), findsOneWidget); + find.textContaining("がリノート", findRichText: true), + findsOneWidget, + ); }); testWidgets("引用Renoteの場合、引用Renoteの表示が行われること", (tester) async { - await tester.pumpWidget(buildTestWidget( - note: TestData.note6AsRenote.copyWith(text: "こころがふたつある〜"))); + await tester.pumpWidget( + buildTestWidget( + note: TestData.note6AsRenote.copyWith(text: "こころがふたつある〜"), + ), + ); await tester.pumpAndSettle(); expect( - find.textContaining(TestData.note6AsRenote.renote!.text!, - findRichText: true), - findsOneWidget); - expect(find.textContaining("こころがふたつある〜", findRichText: true), - findsOneWidget); + find.textContaining( + TestData.note6AsRenote.renote!.text!, + findRichText: true, + ), + findsOneWidget, + ); expect( - find.textContaining("がRenote", findRichText: true), findsNothing); + find.textContaining("こころがふたつある〜", findRichText: true), + findsOneWidget, + ); + expect( + find.textContaining("がRenote", findRichText: true), + findsNothing, + ); }); }); group("MFM", () { testWidgets("コードブロックがあった場合、コードブロックで表示されること", (tester) async { - await tester - .pumpWidget(buildTestWidget(note: TestData.note1.copyWith(text: r''' + await tester.pumpWidget( + buildTestWidget( + note: TestData.note1.copyWith( + text: ''' ```js window.ai = "@ai uneune"; ``` @@ -92,7 +112,10 @@ printf("@ai uneune"); ```java System.out.println("@ai uneune"); ``` -'''))); +''', + ), + ), + ); await tester.pumpAndSettle(); expect(find.byType(HighlightView), findsNWidgets(3)); }); @@ -100,49 +123,73 @@ System.out.println("@ai uneune"); testWidgets("検索構文の検索を謳歌すると、検索が行われること", (tester) async { final mockUrlLauncher = MockUrlLauncherPlatform(); UrlLauncherPlatform.instance = mockUrlLauncher; - await tester.pumpWidget(buildTestWidget( - note: TestData.note1.copyWith(text: "藍ちゃんやっほー 検索"))); + await tester.pumpWidget( + buildTestWidget( + note: TestData.note1.copyWith(text: "藍ちゃんやっほー 検索"), + ), + ); await tester.pumpAndSettle(); - expect(tester.textEditingController(find.byType(TextField)).text, - "藍ちゃんやっほー"); + expect( + tester.textEditingController(find.byType(TextField)).text, + "藍ちゃんやっほー", + ); await tester.tap(find.text("検索")); await tester.pumpAndSettle(); - verify(mockUrlLauncher.launchUrl( - argThat(equals( - "https://google.com/search?q=%E8%97%8D%E3%81%A1%E3%82%83%E3%82%93%E3%82%84%E3%81%A3%E3%81%BB%E3%83%BC")), - any)) - .called(1); + verify( + mockUrlLauncher.launchUrl( + argThat( + equals( + "https://google.com/search?q=%E8%97%8D%E3%81%A1%E3%82%83%E3%82%93%E3%82%84%E3%81%A3%E3%81%BB%E3%83%BC", + ), + ), + any, + ), + ).called(1); }); }); group("注釈", () { testWidgets("注釈が設定されている場合、注釈が表示されること", (tester) async { await tester.pumpWidget( - buildTestWidget(note: TestData.note1.copyWith(cw: "えっちなやつ"))); + buildTestWidget(note: TestData.note1.copyWith(cw: "えっちなやつ")), + ); await tester.pumpAndSettle(); expect( - find.textContaining("えっちなやつ", findRichText: true), findsOneWidget); - expect(find.textContaining(TestData.note1.text!, findRichText: true), - findsNothing); + find.textContaining("えっちなやつ", findRichText: true), + findsOneWidget, + ); + expect( + find.textContaining(TestData.note1.text!, findRichText: true), + findsNothing, + ); }); testWidgets("続きを見るをタップすると、本文が表示されること", (tester) async { await tester.pumpWidget( - buildTestWidget(note: TestData.note1.copyWith(cw: "えっちなやつ"))); + buildTestWidget(note: TestData.note1.copyWith(cw: "えっちなやつ")), + ); await tester.pumpAndSettle(); await tester.tap(find.text("隠してあるのんの続きを見して")); await tester.pumpAndSettle(); expect( - find.textContaining("えっちなやつ", findRichText: true), findsOneWidget); - expect(find.textContaining(TestData.note1.text!, findRichText: true), - findsOneWidget); + find.textContaining("えっちなやつ", findRichText: true), + findsOneWidget, + ); + expect( + find.textContaining(TestData.note1.text!, findRichText: true), + findsOneWidget, + ); await tester.tap(find.text("隠す")); await tester.pumpAndSettle(); expect( - find.textContaining("えっちなやつ", findRichText: true), findsOneWidget); - expect(find.textContaining(TestData.note1.text!, findRichText: true), - findsNothing); + find.textContaining("えっちなやつ", findRichText: true), + findsOneWidget, + ); + expect( + find.textContaining(TestData.note1.text!, findRichText: true), + findsNothing, + ); }); }); @@ -151,13 +198,17 @@ System.out.println("@ai uneune"); final generalSettingsRepository = MockGeneralSettingsRepository(); when(generalSettingsRepository.settings) .thenReturn(const GeneralSettings(enableLongTextElipsed: true)); - await tester.pumpWidget(buildTestWidget( + await tester.pumpWidget( + buildTestWidget( overrides: [ generalSettingsRepositoryProvider - .overrideWith((ref) => generalSettingsRepository) + .overrideWith((ref) => generalSettingsRepository), ], note: TestData.note1.copyWith( - text: Iterable.generate(500, (index) => "あ").join("")))); + text: Iterable.generate(500, (index) => "あ").join(""), + ), + ), + ); await tester.pumpAndSettle(); expect(find.text("続きを表示"), findsOneWidget); }); @@ -167,19 +218,23 @@ System.out.println("@ai uneune"); final generalSettingsRepository = MockGeneralSettingsRepository(); when(generalSettingsRepository.settings) .thenReturn(const GeneralSettings(enableLongTextElipsed: true)); - await tester.pumpWidget(buildTestWidget( - overrides: [ - generalSettingsRepositoryProvider - .overrideWith((ref) => generalSettingsRepository) - ], - note: TestData.note1.copyWith(text: longText), - )); + await tester.pumpWidget( + buildTestWidget( + overrides: [ + generalSettingsRepositoryProvider + .overrideWith((ref) => generalSettingsRepository), + ], + note: TestData.note1.copyWith(text: longText), + ), + ); await tester.pumpAndSettle(); expect(find.textContaining(longText, findRichText: true), findsNothing); await tester.tap(find.text("続きを表示")); await tester.pumpAndSettle(); expect( - find.textContaining(longText, findRichText: true), findsOneWidget); + find.textContaining(longText, findRichText: true), + findsOneWidget, + ); }); }); @@ -188,10 +243,14 @@ System.out.println("@ai uneune"); await tester.pumpWidget(buildTestWidget(note: TestData.note4AsVote)); await tester.pumpAndSettle(); for (final choice in TestData.note4AsVote.poll!.choices) { - expect(find.textContaining(choice.text, findRichText: true), - findsOneWidget); - expect(find.textContaining("${choice.votes}票", findRichText: true), - findsOneWidget); + expect( + find.textContaining(choice.text, findRichText: true), + findsOneWidget, + ); + expect( + find.textContaining("${choice.votes}票", findRichText: true), + findsOneWidget, + ); } }); }); @@ -199,38 +258,54 @@ System.out.println("@ai uneune"); group("メディア", () { testWidgets("閲覧注意に設定されていない場合、画像が表示されること", (tester) async { await tester.runAsync(() async { - await tester.pumpWidget(buildTestWidget( + await tester.pumpWidget( + buildTestWidget( note: TestData.note1.copyWith( - fileIds: [TestData.drive1.id], - files: [TestData.drive1.copyWith(isSensitive: false)]))); + fileIds: [TestData.drive1.id], + files: [TestData.drive1.copyWith(isSensitive: false)], + ), + ), + ); await tester.pumpAndSettle(); await Future.delayed(const Duration(seconds: 1)); await tester.pumpAndSettle(); expect( - find.byWidgetPredicate((e) => - e is NetworkImageView && e.type == ImageType.imageThumbnail), - findsOneWidget); + find.byWidgetPredicate( + (e) => + e is NetworkImageView && e.type == ImageType.imageThumbnail, + ), + findsOneWidget, + ); }); }); testWidgets("閲覧注意に設定している場合、画像が表示されないこと 閲覧注意をタップすると画像が表示されること", (tester) async { await tester.runAsync(() async { - await tester.pumpWidget(buildTestWidget( + await tester.pumpWidget( + buildTestWidget( note: TestData.note1.copyWith( - fileIds: [TestData.drive1.id], - files: [TestData.drive1.copyWith(isSensitive: true)]))); + fileIds: [TestData.drive1.id], + files: [TestData.drive1.copyWith(isSensitive: true)], + ), + ), + ); + await tester.pumpAndSettle(); + await Future.delayed(const Duration(milliseconds: 200)); await tester.pumpAndSettle(); expect(find.text("センシティブ"), findsOneWidget); expect( - find.byWidgetPredicate((e) => - e is NetworkImageView && e.type == ImageType.imageThumbnail), - findsNothing); + find.byWidgetPredicate( + (e) => + e is NetworkImageView && e.type == ImageType.imageThumbnail, + ), + findsNothing, + ); await tester.tap(find.text("センシティブ")); await tester.pumpAndSettle(); @@ -238,9 +313,12 @@ System.out.println("@ai uneune"); await tester.pumpAndSettle(); expect( - find.byWidgetPredicate((e) => - e is NetworkImageView && e.type == ImageType.imageThumbnail), - findsOneWidget); + find.byWidgetPredicate( + (e) => + e is NetworkImageView && e.type == ImageType.imageThumbnail, + ), + findsOneWidget, + ); }); }); }); @@ -252,26 +330,40 @@ System.out.println("@ai uneune"); final mockMisskeyNotes = MockMisskeyNotes(); final mockMisskeyNotesReactions = MockMisskeyNotesReactions(); when(mockMisskey.notes).thenReturn(mockMisskeyNotes); + when(mockMisskeyNotes.featured(any)) + .thenAnswer((_) async => [TestData.note1]); when(mockMisskeyNotes.reactions).thenReturn(mockMisskeyNotesReactions); - when(mockMisskeyNotesReactions.reactions(any)).thenAnswer((_) async => [ - NotesReactionsResponse( - id: "reaction1", - createdAt: DateTime.now(), - user: UserLite.fromJson(TestData.detailedUser2.toJson()), - type: ":ai_yay:") - ]); - await tester.pumpWidget(buildTestWidget( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - note: TestData.note1)); + when(mockMisskeyNotesReactions.reactions(any)).thenAnswer( + (_) async => [ + NotesReactionsResponse( + id: "reaction1", + createdAt: DateTime.now(), + user: UserLite.fromJson(TestData.detailedUser2.toJson()), + type: ":ai_yay:", + ), + ], + ); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => mockMisskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); await tester.pumpAndSettle(); await tester.longPress(find.byType(ReactionButton).at(1)); await tester.pumpAndSettle(); - expect(find.text(TestData.detailedUser2.name!, findRichText: true), - findsOneWidget); + expect( + find.text(TestData.detailedUser2.name!, findRichText: true), + findsOneWidget, + ); await tester.pageNation(); - expect(find.text(TestData.detailedUser2.name!, findRichText: true), - findsNWidgets(2)); + expect( + find.text(TestData.detailedUser2.name!, findRichText: true), + findsNWidgets(2), + ); }); }); @@ -282,22 +374,35 @@ System.out.println("@ai uneune"); when(mockMisskey.notes).thenReturn(mockMisskeyNotes); when(mockMisskeyNotes.renotes(any)) .thenAnswer((_) async => [TestData.note6AsRenote]); - await tester.pumpWidget(buildTestWidget( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - note: TestData.note1)); + when(mockMisskeyNotes.featured(any)) + .thenAnswer((_) async => [TestData.note1]); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => mockMisskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); await tester.pumpAndSettle(); await tester.longPress(find.byType(RenoteButton)); await tester.pumpAndSettle(); expect( - find.textContaining(TestData.note6AsRenote.user.username, - findRichText: true), - findsOneWidget); + find.textContaining( + TestData.note6AsRenote.user.username, + findRichText: true, + ), + findsOneWidget, + ); await tester.pageNation(); expect( - find.textContaining(TestData.note6AsRenote.user.username, - findRichText: true), - findsNWidgets(2)); + find.textContaining( + TestData.note6AsRenote.user.username, + findRichText: true, + ), + findsNWidgets(2), + ); }); }); } diff --git a/test/view/common/misskey_notes/note_modal_sheet_test.dart b/test/view/common/misskey_notes/note_modal_sheet_test.dart index d37dbbd2b..086d849e7 100644 --- a/test/view/common/misskey_notes/note_modal_sheet_test.dart +++ b/test/view/common/misskey_notes/note_modal_sheet_test.dart @@ -1,70 +1,87 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/view/common/misskey_notes/note_modal_sheet.dart'; -import 'package:miria/view/dialogs/simple_message_dialog.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; - -import '../../../test_util/default_root_widget.dart'; -import '../../../test_util/mock.mocks.dart'; -import '../../../test_util/test_datas.dart'; +import "package:flutter/material.dart"; +import "package:flutter_test/flutter_test.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; + +import "../../../test_util/default_root_widget.dart"; +import "../../../test_util/mock.mocks.dart"; +import "../../../test_util/test_datas.dart"; void main() { group("お気に入り", () { testWidgets("該当のノートがお気に入りにされていない場合、お気に入り登録ができること", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: false, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: false, isMutedThread: false), + ); + when(misskeyNotes.featured(any)) + .thenAnswer((e) async => [TestData.note1]); final misskeyFavorites = MockMisskeyNotesFavorites(); when(misskeyNotes.favorites).thenAnswer((e) => misskeyFavorites); when(misskey.notes).thenReturn(misskeyNotes); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: TestData.note1, - targetNote: TestData.note1, - account: TestData.account, - noteBoundaryKey: GlobalKey()), - ), - ))); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); await tester.pumpAndSettle(); - - await tester.tap(find.text("お気に入り")); - - verify(misskeyFavorites.create(argThat( - equals(NotesFavoritesCreateRequest(noteId: TestData.note1.id))))); + await tester.tap(find.byIcon(Icons.more_horiz)); + await tester.pumpAndSettle(); + await tester.ensureVisible(find.text("お気に入り", skipOffstage: false)); + await tester.pumpAndSettle(); + await tester.tap(find.text("お気に入り", skipOffstage: false)); + + verify( + misskeyFavorites.create( + argThat( + equals(NotesFavoritesCreateRequest(noteId: TestData.note1.id)), + ), + ), + ); }); testWidgets("該当のノートがお気に入り済みの場合、お気に入り解除ができること", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: true, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: true, isMutedThread: false), + ); final misskeyFavorites = MockMisskeyNotesFavorites(); when(misskeyNotes.favorites).thenAnswer((e) => misskeyFavorites); when(misskey.notes).thenReturn(misskeyNotes); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: TestData.note1, - targetNote: TestData.note1, - account: TestData.account, - noteBoundaryKey: GlobalKey()), - ), - ))); + when(misskeyNotes.featured(any)) + .thenAnswer((_) async => [TestData.note1]); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); await tester.pumpAndSettle(); - - await tester.tap(find.text("お気に入り解除")); - - verify(misskeyFavorites.delete(argThat( - equals(NotesFavoritesDeleteRequest(noteId: TestData.note1.id))))); + await tester.tap(find.byIcon(Icons.more_horiz)); + await tester.pumpAndSettle(); + await tester.ensureVisible(find.text("お気に入り解除", skipOffstage: false)); + await tester.pumpAndSettle(); + await tester.tap(find.text("お気に入り解除", skipOffstage: false)); + + verify( + misskeyFavorites.delete( + argThat( + equals(NotesFavoritesDeleteRequest(noteId: TestData.note1.id)), + ), + ), + ); }); }); @@ -72,20 +89,23 @@ void main() { testWidgets("自分が投稿したノートの削除ができること", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: false, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: false, isMutedThread: false), + ); + when(misskeyNotes.featured(any)) + .thenAnswer((e) async => [TestData.note1]); when(misskey.notes).thenReturn(misskeyNotes); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: TestData.note1, - targetNote: TestData.note1, - account: TestData.account, - noteBoundaryKey: GlobalKey()), - ), - ))); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.more_horiz)); await tester.pumpAndSettle(); await tester.ensureVisible(find.text("削除", skipOffstage: false)); @@ -93,31 +113,44 @@ void main() { await tester.tap(find.text("削除")); await tester.pumpAndSettle(); - await tester.tap(find.byType(ElevatedButton).hitTestable()); + await tester.tap(find.byType(TextButton).hitTestable().at(0)); await tester.pumpAndSettle(); - verify(misskeyNotes.delete( - argThat(equals(NotesDeleteRequest(noteId: TestData.note1.id))))); + verify( + misskeyNotes.delete( + argThat(equals(NotesDeleteRequest(noteId: TestData.note1.id))), + ), + ); }); testWidgets("Renoteのみのノートは削除できないこと", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: false, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: false, isMutedThread: false), + ); + when(misskeyNotes.featured(any)).thenAnswer( + (e) async => [ + TestData.note1.copyWith( + text: null, + cw: null, + poll: null, + renote: TestData.note1, + ), + ], + ); when(misskey.notes).thenReturn(misskeyNotes); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: TestData.note1 - .copyWith(text: null, renote: TestData.note2), - targetNote: TestData.note2, - account: TestData.account, - noteBoundaryKey: GlobalKey()), - ), - ))); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.more_horiz)); await tester.pumpAndSettle(); expect(find.text("削除", skipOffstage: false), findsNothing); @@ -127,21 +160,23 @@ void main() { testWidgets("他人のノートを削除できないこと", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: false, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: false, isMutedThread: false), + ); + when(misskeyNotes.featured(any)) + .thenAnswer((e) async => [TestData.note3AsAnotherUser]); when(misskey.notes).thenReturn(misskeyNotes); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: TestData.note3AsAnotherUser - .copyWith(text: null, renote: TestData.note1), - targetNote: TestData.note1, - account: TestData.account, - noteBoundaryKey: GlobalKey()), - ), - ))); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.more_horiz)); await tester.pumpAndSettle(); expect(find.text("削除", skipOffstage: false), findsNothing); @@ -151,25 +186,27 @@ void main() { testWidgets("メディアのみのノートを削除できること", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: false, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: false, isMutedThread: false), + ); when(misskey.notes).thenReturn(misskeyNotes); final testNote = TestData.note1.copyWith( text: null, fileIds: [TestData.drive1.id], files: [TestData.drive1], ); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: testNote, - targetNote: testNote, - account: TestData.account, - noteBoundaryKey: GlobalKey()), - ), - ))); + when(misskeyNotes.featured(any)).thenAnswer((e) async => [testNote]); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.more_horiz)); await tester.pumpAndSettle(); expect(find.text("削除", skipOffstage: false), findsOneWidget); @@ -178,26 +215,37 @@ void main() { testWidgets("投票のみのノートを削除できること", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: false, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: false, isMutedThread: false), + ); when(misskey.notes).thenReturn(misskeyNotes); final testNote = TestData.note1.copyWith( - text: null, - poll: NotePoll(choices: const [ + text: null, + poll: NotePoll( + choices: const [ NotePollChoice(text: ":ai_yay:", votes: 1, isVoted: false), - NotePollChoice(text: ":ai_yay_superfast:", votes: 2, isVoted: false) - ], multiple: false, expiresAt: DateTime.now())); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: testNote, - targetNote: testNote, - account: TestData.account, - noteBoundaryKey: GlobalKey()), + NotePollChoice( + text: ":ai_yay_superfast:", + votes: 2, + isVoted: false, ), - ))); + ], + multiple: false, + expiresAt: DateTime.now(), + ), + ); + when(misskeyNotes.featured(any)).thenAnswer((e) async => [testNote]); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.more_horiz)); await tester.pumpAndSettle(); expect(find.text("削除", skipOffstage: false), findsOneWidget); @@ -206,21 +254,28 @@ void main() { testWidgets("自分がした引用Renoteを削除できること", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: false, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: false, isMutedThread: false), + ); when(misskey.notes).thenReturn(misskeyNotes); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: TestData.note1 - .copyWith(text: "やっほー", renote: TestData.note2), - targetNote: TestData.note2, - account: TestData.account, - noteBoundaryKey: GlobalKey()), - ), - ))); + when(misskeyNotes.featured(any)).thenAnswer( + (e) async => + [TestData.note1.copyWith(text: "やっほー", renote: TestData.note2)], + ); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.more_horiz)); + await tester.pumpAndSettle(); + + await tester.ensureVisible(find.text("削除", skipOffstage: false)); await tester.pumpAndSettle(); expect(find.text("削除", skipOffstage: false), findsOneWidget); @@ -230,25 +285,31 @@ void main() { testWidgets("自分がしたメディアつきテキストなしの引用Renoteを削除できること", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: false, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: false, isMutedThread: false), + ); when(misskey.notes).thenReturn(misskeyNotes); final note = TestData.note1.copyWith( - text: null, - renote: TestData.note2, - fileIds: [TestData.drive1.id], - files: [TestData.drive1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: note, - targetNote: TestData.note2, - account: TestData.account, - noteBoundaryKey: GlobalKey()), - ), - ))); + text: null, + renote: TestData.note2, + fileIds: [TestData.drive1.id], + files: [TestData.drive1], + ); + when(misskeyNotes.featured(any)).thenAnswer((e) async => [note]); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.more_horiz)); + await tester.pumpAndSettle(); + + await tester.ensureVisible(find.text("削除", skipOffstage: false)); await tester.pumpAndSettle(); expect(find.text("削除", skipOffstage: false), findsOneWidget); @@ -258,27 +319,41 @@ void main() { testWidgets("自分がした投票つきテキストなしの引用Renoteを削除できること", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: false, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: false, isMutedThread: false), + ); when(misskey.notes).thenReturn(misskeyNotes); final note = TestData.note1.copyWith( - text: null, - renote: TestData.note2, - poll: NotePoll(choices: const [ + text: null, + renote: TestData.note2, + poll: NotePoll( + choices: const [ NotePollChoice(text: ":ai_yay:", votes: 1, isVoted: false), - NotePollChoice(text: ":ai_yay_superfast:", votes: 2, isVoted: false) - ], multiple: false, expiresAt: DateTime.now())); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: note, - targetNote: TestData.note2, - account: TestData.account, - noteBoundaryKey: GlobalKey()), + NotePollChoice( + text: ":ai_yay_superfast:", + votes: 2, + isVoted: false, ), - ))); + ], + multiple: false, + expiresAt: DateTime.now(), + ), + ); + when(misskeyNotes.featured(any)).thenAnswer((e) async => [note]); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.more_horiz)); + await tester.pumpAndSettle(); + + await tester.ensureVisible(find.text("削除", skipOffstage: false)); await tester.pumpAndSettle(); expect(find.text("削除", skipOffstage: false), findsOneWidget); @@ -290,21 +365,25 @@ void main() { testWidgets("自分がしたノートをリノート解除できること", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: false, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: false, isMutedThread: false), + ); when(misskey.notes).thenReturn(misskeyNotes); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: TestData.note1 - .copyWith(text: null, renote: TestData.note2), - targetNote: TestData.note2, - account: TestData.account, - noteBoundaryKey: GlobalKey()), - ), - ))); + when(misskeyNotes.featured(any)).thenAnswer( + (e) async => + [TestData.note1.copyWith(text: null, renote: TestData.note2)], + ); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.more_horiz)); await tester.pumpAndSettle(); await tester.ensureVisible(find.text("リノートを解除する", skipOffstage: false)); @@ -312,55 +391,67 @@ void main() { await tester.tap(find.text("リノートを解除する")); await tester.pumpAndSettle(); - verify(misskeyNotes.delete( - argThat(equals(NotesDeleteRequest(noteId: TestData.note1.id))))); + verify( + misskeyNotes.delete( + argThat(equals(NotesDeleteRequest(noteId: TestData.note1.id))), + ), + ); }); testWidgets("自分がした引用Renoteをリノート解除できないこと", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: false, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: false, isMutedThread: false), + ); when(misskey.notes).thenReturn(misskeyNotes); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: TestData.note1 - .copyWith(text: "やっほー", renote: TestData.note2), - targetNote: TestData.note2, - account: TestData.account, - noteBoundaryKey: GlobalKey()), - ), - ))); + when(misskeyNotes.featured(any)).thenAnswer( + (e) async => + [TestData.note1.copyWith(text: "やっほー", renote: TestData.note2)], + ); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.more_horiz)); await tester.pumpAndSettle(); - expect(find.text("リノートを解除する", skipOffstage: false), findsOneWidget); + expect(find.text("リノートを解除する", skipOffstage: false), findsNothing); }); testWidgets("自分がしたメディアつきテキストなしの引用Renoteをリノート解除できないこと", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: false, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: false, isMutedThread: false), + ); when(misskey.notes).thenReturn(misskeyNotes); final note = TestData.note1.copyWith( - text: null, - renote: TestData.note2, - fileIds: [TestData.drive1.id], - files: [TestData.drive1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: note, - targetNote: TestData.note2, - account: TestData.account, - noteBoundaryKey: GlobalKey()), - ), - ))); + text: null, + renote: TestData.note2, + fileIds: [TestData.drive1.id], + files: [TestData.drive1], + ); + when(misskeyNotes.featured(any)).thenAnswer( + (e) async => [note], + ); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.more_horiz)); await tester.pumpAndSettle(); expect(find.text("リノートを解除する", skipOffstage: false), findsNothing); @@ -369,27 +460,38 @@ void main() { testWidgets("自分がした投票つきテキストなしの引用Renoteをリノート解除できないこと", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: false, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: false, isMutedThread: false), + ); when(misskey.notes).thenReturn(misskeyNotes); final note = TestData.note1.copyWith( - text: null, - renote: TestData.note2, - poll: NotePoll(choices: const [ + text: null, + renote: TestData.note2, + poll: NotePoll( + choices: const [ NotePollChoice(text: ":ai_yay:", votes: 1, isVoted: false), - NotePollChoice(text: ":ai_yay_superfast:", votes: 2, isVoted: false) - ], multiple: false, expiresAt: DateTime.now())); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: note, - targetNote: TestData.note2, - account: TestData.account, - noteBoundaryKey: GlobalKey()), + NotePollChoice( + text: ":ai_yay_superfast:", + votes: 2, + isVoted: false, ), - ))); + ], + multiple: false, + expiresAt: DateTime.now(), + ), + ); + when(misskeyNotes.featured(any)).thenAnswer((e) async => [note]); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.more_horiz)); await tester.pumpAndSettle(); expect(find.text("リノートを解除する", skipOffstage: false), findsNothing); @@ -398,22 +500,26 @@ void main() { testWidgets("他人のリノートをリノート解除できないこと", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: false, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: false, isMutedThread: false), + ); when(misskey.notes).thenReturn(misskeyNotes); final note = TestData.note3AsAnotherUser .copyWith(text: null, renote: TestData.note1); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: note, - targetNote: TestData.note1, - account: TestData.account, - noteBoundaryKey: GlobalKey()), - ), - ))); + when(misskey.notes).thenReturn(misskeyNotes); + when(misskeyNotes.featured(any)).thenAnswer((e) async => [note]); + + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.more_horiz)); await tester.pumpAndSettle(); expect(find.text("リノートを解除する", skipOffstage: false), findsNothing); @@ -424,22 +530,25 @@ void main() { testWidgets("他人のノートを通報できること", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: false, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: false, isMutedThread: false), + ); + when(misskeyNotes.featured(any)) + .thenAnswer((e) async => [TestData.note3AsAnotherUser]); final misskeyUsers = MockMisskeyUsers(); when(misskey.notes).thenReturn(misskeyNotes); when(misskey.users).thenReturn(misskeyUsers); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: TestData.note3AsAnotherUser, - targetNote: TestData.note3AsAnotherUser, - account: TestData.account, - noteBoundaryKey: GlobalKey()), - ), - ))); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.more_horiz)); await tester.pumpAndSettle(); await tester.ensureVisible(find.text("通報する", skipOffstage: false)); @@ -448,34 +557,47 @@ void main() { await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), "このひとわるものです!"); - await tester.tap(find.byType(ElevatedButton)); - await tester.pumpAndSettle(); + await tester.tap(find.byType(ElevatedButton).hitTestable()); + await tester.pump(); - expect(find.byType(SimpleMessageDialog), findsOneWidget); - await tester.tap(find.byType(ElevatedButton)); + expect(find.byType(Dialog), findsNWidgets(2)); + await tester.tap(find.byType(TextButton).hitTestable()); + await tester.pumpAndSettle(); - verify(misskeyUsers.reportAbuse(argThat(equals(UsersReportAbuseRequest( - userId: TestData.note3AsAnotherUser.userId, - comment: "このひとわるものです!"))))); + verify( + misskeyUsers.reportAbuse( + argThat( + equals( + UsersReportAbuseRequest( + userId: TestData.note3AsAnotherUser.userId, + comment: "このひとわるものです!", + ), + ), + ), + ), + ); }); testWidgets("自分のノートを通報できないこと", (tester) async { final misskey = MockMisskey(); final misskeyNotes = MockMisskeyNotes(); - when(misskeyNotes.state(any)).thenAnswer((_) async => - const NotesStateResponse(isFavorited: false, isMutedThread: false)); + when(misskeyNotes.state(any)).thenAnswer( + (_) async => + const NotesStateResponse(isFavorited: false, isMutedThread: false), + ); + when(misskeyNotes.featured(any)) + .thenAnswer((e) async => [TestData.note1]); when(misskey.notes).thenReturn(misskeyNotes); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => misskey)], - child: DefaultRootNoRouterWidget( - child: Scaffold( - body: NoteModalSheet( - baseNote: TestData.note1, - targetNote: TestData.note1, - account: TestData.account, - noteBoundaryKey: GlobalKey()), - ), - ))); + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => misskey)], + child: DefaultRootWidget( + initialRoute: ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.more_horiz)); await tester.pumpAndSettle(); expect(find.text("通報する", skipOffstage: false), findsNothing); diff --git a/test/view/common/note_create/mfm_fn_keyboard_test.dart b/test/view/common/note_create/mfm_fn_keyboard_test.dart index 353442e3b..cd67acebc 100644 --- a/test/view/common/note_create/mfm_fn_keyboard_test.dart +++ b/test/view/common/note_create/mfm_fn_keyboard_test.dart @@ -1,8 +1,8 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/model/input_completion_type.dart'; -import 'package:miria/view/common/note_create/input_completation.dart'; -import 'package:miria/view/common/note_create/mfm_fn_keyboard.dart'; +import "package:flutter_test/flutter_test.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/input_completion_type.dart"; +import "package:miria/view/common/note_create/input_completation.dart"; +import "package:miria/view/common/note_create/mfm_fn_keyboard.dart"; void main() { test("入力が空文字列のとき全ての関数を返す", () { diff --git a/test/view/explore_page/explore_page_test.dart b/test/view/explore_page/explore_page_test.dart index 62a9a5c4d..f8fe59154 100644 --- a/test/view/explore_page/explore_page_test.dart +++ b/test/view/explore_page/explore_page_test.dart @@ -1,14 +1,14 @@ -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; - -import '../../test_util/default_root_widget.dart'; -import '../../test_util/mock.mocks.dart'; -import '../../test_util/test_datas.dart'; -import '../../test_util/widget_tester_extension.dart'; +import "package:flutter_test/flutter_test.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; + +import "../../test_util/default_root_widget.dart"; +import "../../test_util/mock.mocks.dart"; +import "../../test_util/test_datas.dart"; +import "../../test_util/widget_tester_extension.dart"; void main() { group("みつける", () { @@ -19,18 +19,30 @@ void main() { when(misskey.notes).thenReturn(notes); when(notes.featured(any)).thenAnswer((_) async => [TestData.note1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( - initialRoute: ExploreRoute(account: TestData.account), - ))); + initialRoute: ExploreRoute( + accountContext: TestData.accountContext, + ), + ), + ), + ); await tester.pumpAndSettle(); expect(find.text(TestData.note1.text!), findsOneWidget); verify(notes.featured(argThat(equals(const NotesFeaturedRequest())))); await tester.pageNation(); - verify(notes.featured(argThat(equals( - NotesFeaturedRequest(untilId: TestData.note1.id, offset: 1))))); + verify( + notes.featured( + argThat( + equals( + NotesFeaturedRequest(untilId: TestData.note1.id, offset: 1), + ), + ), + ), + ); }); testWidgets("アンケートのノートを表示できること", (tester) async { @@ -42,22 +54,32 @@ void main() { when(polls.recommendation(any)) .thenAnswer((_) async => [TestData.note1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( - initialRoute: ExploreRoute(account: TestData.account), - ))); + initialRoute: + ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text("アンケート")); await tester.pumpAndSettle(); expect(find.text(TestData.note1.text!), findsOneWidget); - verify(polls.recommendation( - argThat(equals(const NotesPollsRecommendationRequest())))); + verify( + polls.recommendation( + argThat(equals(const NotesPollsRecommendationRequest())), + ), + ); await tester.pageNation(); - verify(polls.recommendation( - argThat(equals(const NotesPollsRecommendationRequest(offset: 1))))); + verify( + polls.recommendation( + argThat(equals(const NotesPollsRecommendationRequest(offset: 1))), + ), + ); }); }); @@ -69,11 +91,15 @@ void main() { when(misskey.pinnedUsers()) .thenAnswer((_) async => [TestData.detailedUser1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( - initialRoute: ExploreRoute(account: TestData.account), - ))); + initialRoute: + ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text("ユーザー")); await tester.pumpAndSettle(); @@ -90,11 +116,15 @@ void main() { when(users.users(any)) .thenAnswer((_) async => [TestData.detailedUser1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( - initialRoute: ExploreRoute(account: TestData.account), - ))); + initialRoute: + ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text("ユーザー")); await tester.pumpAndSettle(); @@ -102,17 +132,34 @@ void main() { await tester.pumpAndSettle(); expect(find.text(TestData.detailedUser1.name!), findsOneWidget); - verify(users.users(argThat(equals(const UsersUsersRequest( - state: UsersState.alive, - origin: Origin.local, - sort: UsersSortType.followerDescendant))))); + verify( + users.users( + argThat( + equals( + const UsersUsersRequest( + state: UsersState.alive, + origin: Origin.local, + sort: UsersSortType.followerDescendant, + ), + ), + ), + ), + ); await tester.pageNation(); - verify(users.users(argThat(equals(const UsersUsersRequest( - state: UsersState.alive, - origin: Origin.local, - sort: UsersSortType.followerDescendant, - offset: 1, - ))))); + verify( + users.users( + argThat( + equals( + const UsersUsersRequest( + state: UsersState.alive, + origin: Origin.local, + sort: UsersSortType.followerDescendant, + offset: 1, + ), + ), + ), + ), + ); }); testWidgets("リモートのユーザーを表示できること", (tester) async { @@ -124,11 +171,15 @@ void main() { when(users.users(any)) .thenAnswer((_) async => [TestData.detailedUser1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( - initialRoute: ExploreRoute(account: TestData.account), - ))); + initialRoute: + ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text("ユーザー")); await tester.pumpAndSettle(); @@ -136,17 +187,34 @@ void main() { await tester.pumpAndSettle(); expect(find.text(TestData.detailedUser1.name!), findsOneWidget); - verify(users.users(argThat(equals(const UsersUsersRequest( - state: UsersState.alive, - origin: Origin.remote, - sort: UsersSortType.followerDescendant))))); + verify( + users.users( + argThat( + equals( + const UsersUsersRequest( + state: UsersState.alive, + origin: Origin.remote, + sort: UsersSortType.followerDescendant, + ), + ), + ), + ), + ); await tester.pageNation(); - verify(users.users(argThat(equals(const UsersUsersRequest( - state: UsersState.alive, - origin: Origin.remote, - sort: UsersSortType.followerDescendant, - offset: 1, - ))))); + verify( + users.users( + argThat( + equals( + const UsersUsersRequest( + state: UsersState.alive, + origin: Origin.remote, + sort: UsersSortType.followerDescendant, + offset: 1, + ), + ), + ), + ), + ); }); }); @@ -159,17 +227,23 @@ void main() { when(roles.list()) .thenAnswer((_) async => [TestData.role.copyWith(usersCount: 495)]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( - initialRoute: ExploreRoute(account: TestData.account), - ))); + initialRoute: + ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text("ロール")); await tester.pumpAndSettle(); - expect(find.textContaining(TestData.role.name, findRichText: true), - findsOneWidget); + expect( + find.textContaining(TestData.role.name, findRichText: true), + findsOneWidget, + ); }); }); @@ -182,11 +256,15 @@ void main() { when(hashtags.trend()) .thenAnswer((_) async => [TestData.hashtagTrends]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( - initialRoute: ExploreRoute(account: TestData.account), - ))); + initialRoute: + ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text("ハッシュタグ")); await tester.pumpAndSettle(); @@ -201,11 +279,15 @@ void main() { when(misskey.notes).thenReturn(MockMisskeyNotes()); when(hashtags.list(any)).thenAnswer((_) async => [TestData.hashtag]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( - initialRoute: ExploreRoute(account: TestData.account), - ))); + initialRoute: + ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text("ハッシュタグ")); await tester.pumpAndSettle(); @@ -214,10 +296,18 @@ void main() { expect(find.textContaining(TestData.hashtag.tag), findsOneWidget); - verify(hashtags.list(argThat(predicate((request) => - request.attachedToLocalUserOnly == true && - request.sort == - HashtagsListSortType.attachedLocalUsersDescendant)))); + verify( + hashtags.list( + argThat( + predicate( + (request) => + request.attachedToLocalUserOnly == true && + request.sort == + HashtagsListSortType.attachedLocalUsersDescendant, + ), + ), + ), + ); }); testWidgets("リモートのハッシュタグを表示できること", (tester) async { @@ -227,11 +317,15 @@ void main() { when(misskey.notes).thenReturn(MockMisskeyNotes()); when(hashtags.list(any)).thenAnswer((_) async => [TestData.hashtag]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((_, __) => misskey)], + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((_) => misskey)], child: DefaultRootWidget( - initialRoute: ExploreRoute(account: TestData.account), - ))); + initialRoute: + ExploreRoute(accountContext: TestData.accountContext), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text("ハッシュタグ")); await tester.pumpAndSettle(); @@ -240,10 +334,18 @@ void main() { expect(find.textContaining(TestData.hashtag.tag), findsOneWidget); - verify(hashtags.list(argThat(predicate((request) => - request.attachedToRemoteUserOnly == true && - request.sort == - HashtagsListSortType.attachedRemoteUsersDescendant)))); + verify( + hashtags.list( + argThat( + predicate( + (request) => + request.attachedToRemoteUserOnly == true && + request.sort == + HashtagsListSortType.attachedRemoteUsersDescendant, + ), + ), + ), + ); }); }); diff --git a/test/view/federation_page/federation_page_test.dart b/test/view/federation_page/federation_page_test.dart index 1fe16c559..6e0145158 100644 --- a/test/view/federation_page/federation_page_test.dart +++ b/test/view/federation_page/federation_page_test.dart @@ -1,4 +1,4 @@ -import 'package:flutter_test/flutter_test.dart'; +import "package:flutter_test/flutter_test.dart"; void main() { group("サーバー情報", () {}); diff --git a/test/view/login_page/login_page_test.dart b/test/view/login_page/login_page_test.dart index db498dd3d..0930f92c5 100644 --- a/test/view/login_page/login_page_test.dart +++ b/test/view/login_page/login_page_test.dart @@ -1,11 +1,7 @@ -import 'package:flutter_test/flutter_test.dart'; +import "package:flutter_test/flutter_test.dart"; void main() { - group("MiAuth", () { - - }); + group("MiAuth", () {}); - group("APIキー", () { - - }); + group("APIキー", () {}); } diff --git a/test/view/note_create_page/note_create_page_test.dart b/test/view/note_create_page/note_create_page_test.dart index 011a23300..92dd45c43 100644 --- a/test/view/note_create_page/note_create_page_test.dart +++ b/test/view/note_create_page/note_create_page_test.dart @@ -1,34 +1,33 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:collection/collection.dart'; -import 'package:file/memory.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_svg/svg.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/extensions/string_extensions.dart'; -import 'package:miria/model/account_settings.dart'; -import 'package:miria/model/general_settings.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:miria/view/common/misskey_notes/custom_emoji.dart'; -import 'package:miria/view/common/misskey_notes/local_only_icon.dart'; -import 'package:miria/view/common/misskey_notes/network_image.dart'; -import 'package:miria/view/common/note_create/input_completation.dart'; -import 'package:miria/view/dialogs/simple_message_dialog.dart'; -import 'package:miria/view/note_create_page/mfm_preview.dart'; -import 'package:miria/view/note_create_page/reply_to_area.dart'; -import 'package:miria/view/note_create_page/vote_area.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; -import 'package:visibility_detector/visibility_detector.dart'; - -import '../../test_util/default_root_widget.dart'; -import '../../test_util/mock.mocks.dart'; -import '../../test_util/test_datas.dart'; -import '../../test_util/widget_tester_extension.dart'; +import "dart:convert"; +import "dart:typed_data"; + +import "package:collection/collection.dart"; +import "package:file/memory.dart"; +import "package:file_picker/file_picker.dart"; +import "package:flutter/material.dart"; +import "package:flutter_svg/svg.dart"; +import "package:flutter_test/flutter_test.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/extensions/string_extensions.dart"; +import "package:miria/model/account_settings.dart"; +import "package:miria/model/general_settings.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:miria/view/common/misskey_notes/custom_emoji.dart"; +import "package:miria/view/common/misskey_notes/local_only_icon.dart"; +import "package:miria/view/common/misskey_notes/network_image.dart"; +import "package:miria/view/common/note_create/input_completation.dart"; +import "package:miria/view/note_create_page/mfm_preview.dart"; +import "package:miria/view/note_create_page/reply_to_area.dart"; +import "package:miria/view/note_create_page/vote_area.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; +import "package:visibility_detector/visibility_detector.dart"; + +import "../../test_util/default_root_widget.dart"; +import "../../test_util/mock.mocks.dart"; +import "../../test_util/test_datas.dart"; +import "../../test_util/widget_tester_extension.dart"; void main() { group("初期値", () { @@ -37,26 +36,42 @@ void main() { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), - inputComplementDelayedProvider.overrideWithValue(1) + misskeyProvider.overrideWith((ref) => mockMisskey), + inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, channel: TestData.channel1), - ))); + initialAccount: TestData.account, + channel: TestData.channel1, + ), + ), + ), + ); await tester.pumpAndSettle(); expect(find.text(TestData.channel1.name), findsOneWidget); await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.channelId == TestData.channel1.id))))); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.channelId == TestData.channel1.id, + ), + ), + ), + ), + ); }); testWidgets("削除されたノートを直す場合で、そのノートがチャンネルのノートの場合、チャンネルのノートになること", @@ -64,94 +79,145 @@ void main() { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), - inputComplementDelayedProvider.overrideWithValue(1) + misskeyProvider.overrideWith((ref) => mockMisskey), + inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - note: TestData.note1.copyWith( - channelId: TestData.channel1.id, - channel: NoteChannelInfo( - id: TestData.channel1.id, - name: TestData.channel1.name))), - ))); + initialAccount: TestData.account, + note: TestData.note1.copyWith( + channelId: TestData.channel1.id, + channel: NoteChannelInfo( + id: TestData.channel1.id, + name: TestData.channel1.name, + ), + ), + ), + ), + ), + ); await tester.pumpAndSettle(); expect(find.text(TestData.channel1.name), findsOneWidget); await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.channelId == TestData.channel1.id))))); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.channelId == TestData.channel1.id, + ), + ), + ), + ), + ); }); testWidgets("チャンネルのノートにリプライをする場合、そのノートもチャンネルのノートになること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), - inputComplementDelayedProvider.overrideWithValue(1) + misskeyProvider.overrideWith((ref) => mockMisskey), + inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - reply: TestData.note1.copyWith( - channelId: TestData.channel1.id, - channel: NoteChannelInfo( - id: TestData.channel1.id, - name: TestData.channel1.name))), - ))); + initialAccount: TestData.account, + reply: TestData.note1.copyWith( + channelId: TestData.channel1.id, + channel: NoteChannelInfo( + id: TestData.channel1.id, + name: TestData.channel1.name, + ), + ), + ), + ), + ), + ); await tester.pumpAndSettle(); expect(find.text(TestData.channel1.name), findsOneWidget); await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.channelId == TestData.channel1.id))))); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.channelId == TestData.channel1.id, + ), + ), + ), + ), + ); }); testWidgets("チャンネルのノートに引用リノートをする場合、引数のチャンネル先が選択されること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), - inputComplementDelayedProvider.overrideWithValue(1) + misskeyProvider.overrideWith((ref) => mockMisskey), + inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - channel: TestData.channel2, - renote: TestData.note1.copyWith( - channelId: TestData.channel1.id, - channel: NoteChannelInfo( - id: TestData.channel1.id, - name: TestData.channel1.name))), - ))); + initialAccount: TestData.account, + channel: TestData.channel2, + renote: TestData.note1.copyWith( + channelId: TestData.channel1.id, + channel: NoteChannelInfo( + id: TestData.channel1.id, + name: TestData.channel1.name, + ), + ), + ), + ), + ), + ); await tester.pumpAndSettle(); expect(find.text(TestData.channel2.name), findsOneWidget); await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.channelId == TestData.channel2.id))))); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.channelId == TestData.channel2.id, + ), + ), + ), + ), + ); }); }); @@ -161,98 +227,155 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); final accountSettings = MockAccountSettingsRepository(); - when(accountSettings.fromAccount(any)).thenReturn(AccountSettings( + when(accountSettings.fromAccount(any)).thenReturn( + AccountSettings( userId: TestData.account.userId, host: TestData.account.host, - defaultNoteVisibility: NoteVisibility.followers)); - await tester.pumpWidget(ProviderScope( + defaultNoteVisibility: NoteVisibility.followers, + ), + ); + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), accountSettingsRepositoryProvider - .overrideWith((ref) => accountSettings) + .overrideWith((ref) => accountSettings), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.visibility == NoteVisibility.followers))))); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.visibility == NoteVisibility.followers, + ), + ), + ), + ), + ); }); testWidgets("削除されたノートを直す場合、削除されたノートの公開範囲設定が適用されること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - note: TestData.note1 - .copyWith(visibility: NoteVisibility.specified)), - ))); + initialAccount: TestData.account, + note: TestData.note1 + .copyWith(visibility: NoteVisibility.specified), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.visibility == NoteVisibility.specified))))); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.visibility == NoteVisibility.specified, + ), + ), + ), + ), + ); }); testWidgets("引用リノートの場合、リノート元の公開範囲設定が適用されること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - renote: TestData.note1.copyWith(visibility: NoteVisibility.home), - )))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + renote: + TestData.note1.copyWith(visibility: NoteVisibility.home), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), ":ai_yay:"); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.visibility == NoteVisibility.home))))); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.visibility == NoteVisibility.home, + ), + ), + ), + ), + ); }); testWidgets("リプライの場合、リプライ元の公開範囲設定が適用されること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - reply: TestData.note1.copyWith(visibility: NoteVisibility.home), - )))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + reply: TestData.note1.copyWith(visibility: NoteVisibility.home), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), ":ai_yay:"); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.visibility == NoteVisibility.home))))); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.visibility == NoteVisibility.home, + ), + ), + ), + ), + ); }); testWidgets("ユーザーがサイレンスの場合で、デフォルトの公開範囲設定がパブリックの場合、強制ホームになること", @@ -261,31 +384,50 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); final accountSettings = MockAccountSettingsRepository(); - when(accountSettings.fromAccount(any)).thenReturn(AccountSettings( + when(accountSettings.fromAccount(any)).thenReturn( + AccountSettings( userId: TestData.account.userId, host: TestData.account.host, - defaultNoteVisibility: NoteVisibility.public)); - await tester.pumpWidget(ProviderScope( + defaultNoteVisibility: NoteVisibility.public, + ), + ); + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), accountSettingsRepositoryProvider - .overrideWith((ref) => accountSettings) + .overrideWith((ref) => accountSettings), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account.copyWith( - i: TestData.account.i.copyWith(isSilenced: true))), - ))); + initialAccount: TestData.account.copyWith( + i: TestData.account.i.copyWith(isSilenced: true), + ), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.visibility == NoteVisibility.home))))); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.visibility == NoteVisibility.home, + ), + ), + ), + ), + ); }); testWidgets("ユーザーがサイレンスの場合、デフォルトの公開範囲設定がフォロワーのみの場合、デフォルトの公開範囲設定が反映されること", @@ -294,31 +436,50 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); final accountSettings = MockAccountSettingsRepository(); - when(accountSettings.fromAccount(any)).thenReturn(AccountSettings( + when(accountSettings.fromAccount(any)).thenReturn( + AccountSettings( userId: TestData.account.userId, host: TestData.account.host, - defaultNoteVisibility: NoteVisibility.followers)); - await tester.pumpWidget(ProviderScope( + defaultNoteVisibility: NoteVisibility.followers, + ), + ); + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), accountSettingsRepositoryProvider - .overrideWith((ref) => accountSettings) + .overrideWith((ref) => accountSettings), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account.copyWith( - i: TestData.account.i.copyWith(isSilenced: true))), - ))); + initialAccount: TestData.account.copyWith( + i: TestData.account.i.copyWith(isSilenced: true), + ), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.visibility == NoteVisibility.followers))))); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.visibility == NoteVisibility.followers, + ), + ), + ), + ), + ); }); }); @@ -328,98 +489,148 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); final accountSettings = MockAccountSettingsRepository(); - when(accountSettings.fromAccount(any)).thenReturn(AccountSettings( + when(accountSettings.fromAccount(any)).thenReturn( + AccountSettings( userId: TestData.account.userId, host: TestData.account.host, - defaultIsLocalOnly: true)); - await tester.pumpWidget(ProviderScope( + defaultIsLocalOnly: true, + ), + ); + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), accountSettingsRepositoryProvider - .overrideWith((ref) => accountSettings) + .overrideWith((ref) => accountSettings), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals( - predicate((arg) => arg.localOnly == true))))); + verify( + mockNote.create( + argThat( + equals( + predicate((arg) => arg.localOnly == true), + ), + ), + ), + ); }); testWidgets("削除されたノートを直す場合、削除されたノートの連合範囲が適用されること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - note: TestData.note1.copyWith(localOnly: true)), - ))); + initialAccount: TestData.account, + note: TestData.note1.copyWith(localOnly: true), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals( - predicate((arg) => arg.localOnly == true))))); + verify( + mockNote.create( + argThat( + equals( + predicate((arg) => arg.localOnly == true), + ), + ), + ), + ); }); testWidgets("引用リノートの場合、リノート元の連合範囲が適用されること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - renote: TestData.note1.copyWith(localOnly: true)), - ))); + initialAccount: TestData.account, + renote: TestData.note1.copyWith(localOnly: true), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals( - predicate((arg) => arg.localOnly == true))))); + verify( + mockNote.create( + argThat( + equals( + predicate((arg) => arg.localOnly == true), + ), + ), + ), + ); }); testWidgets("リプライの場合、リプライ元の連合範囲が適用されること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - reply: TestData.note1.copyWith(localOnly: true), - )))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + reply: TestData.note1.copyWith(localOnly: true), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), ":ai_yay:"); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals( - predicate((arg) => arg.localOnly == true))))); + verify( + mockNote.create( + argThat( + equals( + predicate((arg) => arg.localOnly == true), + ), + ), + ), + ); }); group("チャンネル", () { @@ -428,29 +639,46 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); final accountSettings = MockAccountSettingsRepository(); - when(accountSettings.fromAccount(any)).thenReturn(AccountSettings( - userId: TestData.account.userId, host: TestData.account.host)); - await tester.pumpWidget(ProviderScope( + when(accountSettings.fromAccount(any)).thenReturn( + AccountSettings( + userId: TestData.account.userId, + host: TestData.account.host, + ), + ); + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), accountSettingsRepositoryProvider - .overrideWith((ref) => accountSettings) + .overrideWith((ref) => accountSettings), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - channel: TestData.channel1), - ))); + initialAccount: TestData.account, + channel: TestData.channel1, + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals( - predicate((arg) => arg.localOnly == true))))); + verify( + mockNote.create( + argThat( + equals( + predicate((arg) => arg.localOnly == true), + ), + ), + ), + ); }); }); }); @@ -461,31 +689,48 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); final accountSettings = MockAccountSettingsRepository(); - when(accountSettings.fromAccount(any)).thenReturn(AccountSettings( + when(accountSettings.fromAccount(any)).thenReturn( + AccountSettings( userId: TestData.account.userId, host: TestData.account.host, - defaultReactionAcceptance: ReactionAcceptance.nonSensitiveOnly)); - await tester.pumpWidget(ProviderScope( + defaultReactionAcceptance: ReactionAcceptance.nonSensitiveOnly, + ), + ); + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), accountSettingsRepositoryProvider - .overrideWith((ref) => accountSettings) + .overrideWith((ref) => accountSettings), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => - arg.reactionAcceptance == - ReactionAcceptance.nonSensitiveOnly))))); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => + arg.reactionAcceptance == + ReactionAcceptance.nonSensitiveOnly, + ), + ), + ), + ), + ); }); testWidgets("削除されたノートを直す場合、削除されたノートのリアクション受け入れ設定が適用されること", @@ -494,80 +739,133 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); final accountSettings = MockAccountSettingsRepository(); - when(accountSettings.fromAccount(any)).thenReturn(AccountSettings( - userId: TestData.account.userId, host: TestData.account.host)); - await tester.pumpWidget(ProviderScope( + when(accountSettings.fromAccount(any)).thenReturn( + AccountSettings( + userId: TestData.account.userId, + host: TestData.account.host, + ), + ); + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), accountSettingsRepositoryProvider - .overrideWith((ref) => accountSettings) + .overrideWith((ref) => accountSettings), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - note: TestData.note1.copyWith( - reactionAcceptance: ReactionAcceptance.likeOnly)), - ))); + initialAccount: TestData.account, + note: TestData.note1.copyWith( + reactionAcceptance: ReactionAcceptance.likeOnly, + ), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.reactionAcceptance == ReactionAcceptance.likeOnly))))); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => + arg.reactionAcceptance == ReactionAcceptance.likeOnly, + ), + ), + ), + ), + ); }); testWidgets("引用リノートの場合、リノート元のリアクション受け入れ設定が反映され**ない**こと", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - renote: TestData.note1.copyWith( - reactionAcceptance: ReactionAcceptance.likeOnly)), - ))); + initialAccount: TestData.account, + renote: TestData.note1.copyWith( + reactionAcceptance: ReactionAcceptance.likeOnly, + ), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.reactionAcceptance == null))))); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.reactionAcceptance == null, + ), + ), + ), + ), + ); }); testWidgets("リプライの場合、リプライ元のリアクション受け入れ設定が反映され**ない**こと", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - reply: TestData.note1.copyWith( - reactionAcceptance: ReactionAcceptance.likeOnly)), - ))); + initialAccount: TestData.account, + reply: TestData.note1.copyWith( + reactionAcceptance: ReactionAcceptance.likeOnly, + ), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.reactionAcceptance == null))))); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.reactionAcceptance == null, + ), + ), + ), + ), + ); }); }); @@ -576,58 +874,75 @@ void main() { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); - expect(tester.textEditingController(find.byType(TextField).at(0)).text, - ""); + expect( + tester.textEditingController(find.byType(TextField).at(0)).text, + "", + ); }); testWidgets("削除したノートを直す場合、削除したノートの注釈が適用されること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - note: TestData.note1.copyWith(cw: "えっちなやつ")), - ))); + initialAccount: TestData.account, + note: TestData.note1.copyWith(cw: "えっちなやつ"), + ), + ), + ), + ); await tester.pumpAndSettle(); - expect(tester.textEditingController(find.byType(TextField).at(0)).text, - "えっちなやつ"); + expect( + tester.textEditingController(find.byType(TextField).at(0)).text, + "えっちなやつ", + ); }); testWidgets("リプライを送る場合で、リプライ元が注釈ありの場合、その注釈が適用されること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - reply: TestData.note1.copyWith(cw: "えっちなやつ")), - ))); + initialAccount: TestData.account, + reply: TestData.note1.copyWith(cw: "えっちなやつ"), + ), + ), + ), + ); await tester.pumpAndSettle(); - expect(tester.textEditingController(find.byType(TextField).at(0)).text, - "えっちなやつ"); + expect( + tester.textEditingController(find.byType(TextField).at(0)).text, + "えっちなやつ", + ); }); testWidgets("引用リノートをする場合で、リノート元が注釈ありの場合、その注釈が適用され**ない**こと", @@ -635,20 +950,26 @@ void main() { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - renote: TestData.note1.copyWith(cw: "えっちなやつ")), - ))); + initialAccount: TestData.account, + renote: TestData.note1.copyWith(cw: "えっちなやつ"), + ), + ), + ), + ); await tester.pumpAndSettle(); - expect(tester.textEditingController(find.byType(TextField).at(0)).text, - ""); + expect( + tester.textEditingController(find.byType(TextField).at(0)).text, + "", + ); }); }); @@ -657,14 +978,17 @@ void main() { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); expect(tester.textEditingController(find.byType(TextField)).text, ""); @@ -674,39 +998,52 @@ void main() { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, note: TestData.note1), - ))); + initialAccount: TestData.account, + note: TestData.note1, + ), + ), + ), + ); await tester.pumpAndSettle(); - expect(tester.textEditingController(find.byType(TextField)).text, - TestData.note1.text); + expect( + tester.textEditingController(find.byType(TextField)).text, + TestData.note1.text, + ); }); testWidgets("テキスト共有からノート投稿をする場合、共有のテキストが適用されること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - initialText: ":ai_yaysuperfast:"), - ))); + initialAccount: TestData.account, + initialText: ":ai_yaysuperfast:", + ), + ), + ), + ); await tester.pumpAndSettle(); - expect(tester.textEditingController(find.byType(TextField)).text, - ":ai_yaysuperfast:"); + expect( + tester.textEditingController(find.byType(TextField)).text, + ":ai_yaysuperfast:", + ); }); }); @@ -715,22 +1052,31 @@ void main() { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), ":ai_yay:"); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals( - predicate((arg) => arg.mediaIds == null))))) - .called(1); + verify( + mockNote.create( + argThat( + equals( + predicate((arg) => arg.mediaIds == null), + ), + ), + ), + ).called(1); }); testWidgets("削除したノートを直す場合、削除したノートのメディアが適用されること", (tester) async { @@ -742,25 +1088,40 @@ void main() { when(mockDio.get(any, options: anyNamed("options"))) .thenAnswer((_) async => await TestData.binaryImageResponse); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), dioProvider.overrideWith((ref) => mockDio), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - note: TestData.note1.copyWith( - files: [TestData.drive1], fileIds: [TestData.drive1.id])), - ))); + initialAccount: TestData.account, + note: TestData.note1.copyWith( + files: [TestData.drive1], + fileIds: [TestData.drive1.id], + ), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => const DeepCollectionEquality() - .equals(arg.fileIds, [TestData.drive1.id])))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => const DeepCollectionEquality() + .equals(arg.fileIds, [TestData.drive1.id]), + ), + ), + ), + ), + ).called(1); }); testWidgets("画像共有からノートを投稿する場合、共有の画像が適用されること", (tester) async { @@ -773,7 +1134,8 @@ void main() { when(mockDrive.files).thenReturn(mockDriveFiles); when(mockDriveFiles.createAsBinary(any, any)).thenAnswer( - (_) async => TestData.drive1.copyWith(name: "test.png")); + (_) async => TestData.drive1.copyWith(name: "test.png"), + ); final mockDio = MockDio(); when(mockDio.get(any, options: anyNamed("options"))) @@ -781,27 +1143,31 @@ void main() { final memoryFileSystem = MemoryFileSystem(); final binaryImage = await TestData.binaryImage; - memoryFileSystem.file("/test.png").writeAsBytes(binaryImage); - - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), - fileSystemProvider.overrideWith((ref) => memoryFileSystem), - dioProvider.overrideWith((ref) => mockDio), - inputComplementDelayedProvider.overrideWithValue(1), - ], - child: DefaultRootWidget( - initialRoute: NoteCreateRoute( + await memoryFileSystem.file("/test.png").writeAsBytes(binaryImage); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + fileSystemProvider.overrideWith((ref) => memoryFileSystem), + dioProvider.overrideWith((ref) => mockDio), + inputComplementDelayedProvider.overrideWithValue(1), + ], + child: DefaultRootWidget( + initialRoute: NoteCreateRoute( initialAccount: TestData.account, - initialMediaFiles: const ["/test.png"]), + initialMediaFiles: const ["/test.png"], + ), + ), ), - )); + ); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), ":ai_yay:"); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockDriveFiles.createAsBinary( + verify( + mockDriveFiles.createAsBinary( argThat( equals( const DriveFilesCreateRequest( @@ -811,11 +1177,28 @@ void main() { ), ), ), - argThat(equals(predicate((value) => - const DeepCollectionEquality().equals(value, binaryImage)))))); - verify(mockNote.create(argThat(equals(predicate( - (arg) => const DeepCollectionEquality() - .equals([TestData.drive1.id], arg.fileIds)))))); + argThat( + equals( + predicate( + (value) => + const DeepCollectionEquality().equals(value, binaryImage), + ), + ), + ), + ), + ); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => const DeepCollectionEquality() + .equals([TestData.drive1.id], arg.fileIds), + ), + ), + ), + ), + ); }); testWidgets("共有したファイルが画像でない場合でも、ファイルの投稿ができること", (tester) async { @@ -828,7 +1211,8 @@ void main() { when(mockDrive.files).thenReturn(mockDriveFiles); when(mockDriveFiles.createAsBinary(any, any)).thenAnswer( - (_) async => TestData.drive1.copyWith(name: "test.txt")); + (_) async => TestData.drive1.copyWith(name: "test.txt"), + ); final mockDio = MockDio(); when(mockDio.get(any, options: anyNamed("options"))) @@ -837,21 +1221,24 @@ void main() { final memoryFileSystem = MemoryFileSystem(); final binaryData = utf8.encode(":murakamisan_tutinoko_hasitumami_crying:"); - memoryFileSystem.file("/test.txt").writeAsBytes(binaryData); - - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), - fileSystemProvider.overrideWith((ref) => memoryFileSystem), - dioProvider.overrideWith((ref) => mockDio), - inputComplementDelayedProvider.overrideWithValue(1), - ], - child: DefaultRootWidget( - initialRoute: NoteCreateRoute( + await memoryFileSystem.file("/test.txt").writeAsBytes(binaryData); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + fileSystemProvider.overrideWith((ref) => memoryFileSystem), + dioProvider.overrideWith((ref) => mockDio), + inputComplementDelayedProvider.overrideWithValue(1), + ], + child: DefaultRootWidget( + initialRoute: NoteCreateRoute( initialAccount: TestData.account, - initialMediaFiles: const ["/test.txt"]), + initialMediaFiles: const ["/test.txt"], + ), + ), ), - )); + ); await tester.pumpAndSettle(); expect(find.text("test.txt"), findsOneWidget); @@ -859,7 +1246,8 @@ void main() { await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockDriveFiles.createAsBinary( + verify( + mockDriveFiles.createAsBinary( argThat( equals( const DriveFilesCreateRequest( @@ -869,17 +1257,35 @@ void main() { ), ), ), - argThat(equals(predicate((value) => - const DeepCollectionEquality().equals(value, binaryData)))))); - verify(mockNote.create(argThat(equals(predicate( - (arg) => const DeepCollectionEquality() - .equals([TestData.drive1.id], arg.fileIds)))))); + argThat( + equals( + predicate( + (value) => + const DeepCollectionEquality().equals(value, binaryData), + ), + ), + ), + ), + ); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => const DeepCollectionEquality() + .equals([TestData.drive1.id], arg.fileIds), + ), + ), + ), + ), + ); }); }); group("リプライ先", () { testWidgets("リプライの場合、返信先が表示されていること", (tester) async { - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ inputComplementDelayedProvider.overrideWithValue(1), ], @@ -888,16 +1294,21 @@ void main() { initialAccount: TestData.account, reply: TestData.note3AsAnotherUser, ), - ))); + ), + ), + ); await tester.pumpAndSettle(); expect(find.text("返信先:"), findsOneWidget); expect( - find.descendant( - of: find.byType(ReplyToArea), - matching: find.textContaining( - TestData.note3AsAnotherUser.user.username, - findRichText: true)), - findsOneWidget); + find.descendant( + of: find.byType(ReplyToArea), + matching: find.textContaining( + TestData.note3AsAnotherUser.user.username, + findRichText: true, + ), + ), + findsOneWidget, + ); }); testWidgets("リプライにメンションが含まれている場合、両方の返信先が表示されていること", (tester) async { @@ -907,9 +1318,10 @@ void main() { when(users.showByIds(any)) .thenAnswer((_) async => [TestData.usersShowResponse2]); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => misskey), + misskeyProvider.overrideWith((ref) => misskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( @@ -919,22 +1331,31 @@ void main() { mentions: [TestData.detailedUser1.username], ), ), - ))); + ), + ), + ); await tester.pumpAndSettle(); expect(find.text("返信先:"), findsOneWidget); expect( - find.descendant( - of: find.byType(ReplyToArea), - matching: find.text( - "@${TestData.note5AsAnotherUser.user.username}", - findRichText: true)), - findsOneWidget); + find.descendant( + of: find.byType(ReplyToArea), + matching: find.text( + "@${TestData.note5AsAnotherUser.user.username}", + findRichText: true, + ), + ), + findsOneWidget, + ); expect( - find.descendant( - of: find.byType(ReplyToArea), - matching: find.text("@${TestData.usersShowResponse2.username}", - findRichText: true)), - findsOneWidget); + find.descendant( + of: find.byType(ReplyToArea), + matching: find.text( + "@${TestData.usersShowResponse2.username}", + findRichText: true, + ), + ), + findsOneWidget, + ); }); testWidgets("削除したノートがリプライの場合、そのユーザーの情報が表示されていること", (tester) async { @@ -943,45 +1364,60 @@ void main() { when(misskey.users).thenReturn(users); when(users.showByIds(any)) .thenAnswer((_) async => [TestData.usersShowResponse1]); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => misskey), + misskeyProvider.overrideWith((ref) => misskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - note: TestData.note1.copyWith(mentions: ["@ai"]), - )))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + note: TestData.note1.copyWith(mentions: ["@ai"]), + ), + ), + ), + ); await tester.pumpAndSettle(); expect(find.text("返信先:"), findsOneWidget); expect( - find.descendant( - of: find.byType(ReplyToArea), - matching: find.text("@ai", findRichText: true)), - findsOneWidget); + find.descendant( + of: find.byType(ReplyToArea), + matching: find.text("@ai", findRichText: true), + ), + findsOneWidget, + ); }); testWidgets("自分自身はリプライの返信先に含まれていないこと", (tester) async { - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, reply: TestData.note1)))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + reply: TestData.note1, + ), + ), + ), + ); await tester.pumpAndSettle(); expect(find.text("返信先:"), findsNothing); }); testWidgets("リプライでない場合、返信先が表示されていないこと", (tester) async { - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); expect(find.text("返信先:"), findsNothing); }); @@ -992,22 +1428,31 @@ void main() { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), ":ai_yay:"); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals( - predicate((arg) => arg.poll == null))))) - .called(1); + verify( + mockNote.create( + argThat( + equals( + predicate((arg) => arg.poll == null), + ), + ), + ), + ).called(1); }); testWidgets("削除したノートを直す場合、削除したノートの投票が適用されること", (tester) async { @@ -1015,29 +1460,45 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - note: TestData.note4AsVote.copyWith( - poll: TestData.note4AsVote.poll?.copyWith(multiple: false)), - )))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + note: TestData.note4AsVote.copyWith( + poll: TestData.note4AsVote.poll?.copyWith(multiple: false), + ), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => - const DeepCollectionEquality().equals( - TestData.note4AsVote.poll!.choices.map((e) => e.text), - arg.poll!.choices) && - TestData.note4AsVote.poll!.expiresAt == arg.poll!.expiresAt && - arg.poll!.expiredAfter == null && - TestData.note4AsVote.poll!.multiple == arg.poll!.multiple))))); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => + const DeepCollectionEquality().equals( + TestData.note4AsVote.poll!.choices.map((e) => e.text), + arg.poll!.choices, + ) && + TestData.note4AsVote.poll!.expiresAt == + arg.poll!.expiresAt && + arg.poll!.expiredAfter == null && + TestData.note4AsVote.poll!.multiple == arg.poll!.multiple, + ), + ), + ), + ), + ); }); testWidgets("削除したノートが複数選択可能な場合、複数選択可能な状態が引き継がれていること", (tester) async { @@ -1045,23 +1506,37 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - note: TestData.note4AsVote.copyWith( - poll: TestData.note4AsVote.poll?.copyWith(multiple: true)), - )))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + note: TestData.note4AsVote.copyWith( + poll: TestData.note4AsVote.poll?.copyWith(multiple: true), + ), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.poll!.multiple == true))))); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.poll!.multiple == true, + ), + ), + ), + ), + ); }); }); }); @@ -1073,22 +1548,35 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - )))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), ":ai_bonk:"); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.text == ":ai_bonk:"))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.text == ":ai_bonk:", + ), + ), + ), + ), + ).called(1); }); testWidgets("ノートの内容が空で投稿することができないこと", (tester) async { @@ -1096,20 +1584,25 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - )))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.send)); - await tester.pump(); - expect(find.byType(SimpleMessageDialog), findsOneWidget); - await tester.tap(find.byType(ElevatedButton)); + await tester.pumpAndSettle(); + debugDumpApp(); + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); expect(find.byType(TextField).hitTestable(), findsOneWidget); }); @@ -1124,7 +1617,8 @@ void main() { when(mockDrive.files).thenReturn(mockDriveFiles); when(mockDriveFiles.createAsBinary(any, any)).thenAnswer( - (_) async => TestData.drive1.copyWith(name: "test.txt")); + (_) async => TestData.drive1.copyWith(name: "test.txt"), + ); final mockDio = MockDio(); when(mockDio.get(any, options: anyNamed("options"))) @@ -1133,28 +1627,32 @@ void main() { final memoryFileSystem = MemoryFileSystem(); final binaryData = utf8.encode(":murakamisan_tutinoko_hasitumami_crying:"); - memoryFileSystem.file("/test.txt").writeAsBytes(binaryData); - - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), - fileSystemProvider.overrideWith((ref) => memoryFileSystem), - dioProvider.overrideWith((ref) => mockDio), - inputComplementDelayedProvider.overrideWithValue(1), - ], - child: DefaultRootWidget( - initialRoute: NoteCreateRoute( + await memoryFileSystem.file("/test.txt").writeAsBytes(binaryData); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + fileSystemProvider.overrideWith((ref) => memoryFileSystem), + dioProvider.overrideWith((ref) => mockDio), + inputComplementDelayedProvider.overrideWithValue(1), + ], + child: DefaultRootWidget( + initialRoute: NoteCreateRoute( initialAccount: TestData.account, - initialMediaFiles: const ["/test.txt"]), + initialMediaFiles: const ["/test.txt"], + ), + ), ), - )); + ); await tester.pumpAndSettle(); expect(find.text("test.txt"), findsOneWidget); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockDriveFiles.createAsBinary( + verify( + mockDriveFiles.createAsBinary( argThat( equals( const DriveFilesCreateRequest( @@ -1164,13 +1662,30 @@ void main() { ), ), ), - argThat(equals(predicate((value) => - const DeepCollectionEquality().equals(value, binaryData)))))); - verify(mockNote.create(argThat(equals(predicate( - (arg) => - arg.text == null && - const DeepCollectionEquality() - .equals([TestData.drive1.id], arg.fileIds)))))); + argThat( + equals( + predicate( + (value) => + const DeepCollectionEquality().equals(value, binaryData), + ), + ), + ), + ), + ); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => + arg.text == null && + const DeepCollectionEquality() + .equals([TestData.drive1.id], arg.fileIds), + ), + ), + ), + ), + ); }); group("入力補完", () { @@ -1178,70 +1693,82 @@ void main() { final emojiRepository = MockEmojiRepository(); when(emojiRepository.emoji).thenReturn([ TestData.unicodeEmojiRepositoryData1, - TestData.customEmojiRepositoryData1 + TestData.customEmojiRepositoryData1, ]); when(emojiRepository.defaultEmojis()).thenAnswer( - (_) => [TestData.unicodeEmoji1, TestData.customEmoji1]); + (_) => [TestData.unicodeEmoji1, TestData.customEmoji1], + ); final generalSettingsRepository = MockGeneralSettingsRepository(); when(generalSettingsRepository.settings) .thenReturn(const GeneralSettings(emojiType: EmojiType.system)); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - emojiRepositoryProvider - .overrideWith((ref, arg) => emojiRepository), + emojiRepositoryProvider.overrideWith((ref) => emojiRepository), generalSettingsRepositoryProvider .overrideWith((ref) => generalSettingsRepository), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text(":")); await tester.pumpAndSettle(); expect( - find.byWidgetPredicate((widget) => + find.byWidgetPredicate( + (widget) => widget is NetworkImageView && - widget.url == TestData.customEmoji1.url.toString()), - findsOneWidget); + widget.url == TestData.customEmoji1.url.toString(), + ), + findsOneWidget, + ); expect(find.text(TestData.unicodeEmoji1.char), findsOneWidget); await tester.tap(find.byType(NetworkImageView).at(1)); expect( - tester - .textEditingController(find.byType(TextField).hitTestable()) - .value, - TextEditingValue( - text: ":${TestData.customEmoji1.baseName}:", - selection: TextSelection.collapsed( - offset: ":${TestData.customEmoji1.baseName}:".length))); + tester + .textEditingController(find.byType(TextField).hitTestable()) + .value, + TextEditingValue( + text: ":${TestData.customEmoji1.baseName}:", + selection: TextSelection.collapsed( + offset: ":${TestData.customEmoji1.baseName}:".length, + ), + ), + ); }); testWidgets("通常の絵文字の入力補完が可能なこと", (tester) async { final emojiRepository = MockEmojiRepository(); when(emojiRepository.emoji).thenReturn([ TestData.unicodeEmojiRepositoryData1, - TestData.customEmojiRepositoryData1 + TestData.customEmojiRepositoryData1, ]); when(emojiRepository.defaultEmojis()).thenAnswer( - (_) => [TestData.unicodeEmoji1, TestData.customEmoji1]); + (_) => [TestData.unicodeEmoji1, TestData.customEmoji1], + ); final generalSettingsRepository = MockGeneralSettingsRepository(); when(generalSettingsRepository.settings) .thenReturn(const GeneralSettings(emojiType: EmojiType.system)); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - emojiRepositoryProvider - .overrideWith((ref, arg) => emojiRepository), + emojiRepositoryProvider.overrideWith((ref) => emojiRepository), generalSettingsRepositoryProvider .overrideWith((ref) => generalSettingsRepository), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.text(":")); @@ -1249,13 +1776,16 @@ void main() { await tester.tap(find.text(TestData.unicodeEmoji1.char)); expect( - tester - .textEditingController(find.byType(TextField).hitTestable()) - .value, - TextEditingValue( - text: TestData.unicodeEmoji1.char, - selection: TextSelection.collapsed( - offset: TestData.unicodeEmoji1.char.length))); + tester + .textEditingController(find.byType(TextField).hitTestable()) + .value, + TextEditingValue( + text: TestData.unicodeEmoji1.char, + selection: TextSelection.collapsed( + offset: TestData.unicodeEmoji1.char.length, + ), + ), + ); }); testWidgets( @@ -1266,21 +1796,24 @@ void main() { final emojiRepository = MockEmojiRepository(); when(emojiRepository.emoji).thenReturn([ TestData.unicodeEmojiRepositoryData1, - TestData.customEmojiRepositoryData1 + TestData.customEmojiRepositoryData1, ]); when(emojiRepository.searchEmojis(any)).thenAnswer( - (_) async => [TestData.unicodeEmoji1, TestData.customEmoji1]); + (_) async => [TestData.unicodeEmoji1, TestData.customEmoji1], + ); when(emojiRepository.defaultEmojis()) .thenReturn([TestData.unicodeEmoji1, TestData.customEmoji1]); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - emojiRepositoryProvider - .overrideWith((ref, arg) => emojiRepository) + emojiRepositoryProvider.overrideWith((ref) => emojiRepository), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField).hitTestable(), ":"); await tester.pumpAndSettle(); @@ -1290,10 +1823,11 @@ void main() { await tester.tap(find.byType(CustomEmoji).at(1)); await tester.pumpAndSettle(); expect( - tester - .textEditingController(find.byType(TextField).hitTestable()) - .text, - ":${TestData.customEmoji1.baseName}:"); + tester + .textEditingController(find.byType(TextField).hitTestable()) + .text, + ":${TestData.customEmoji1.baseName}:", + ); }); testWidgets("MFMの関数名の入力補完が可能なこと", (tester) async { @@ -1363,7 +1897,7 @@ void main() { await tester.pumpWidget( ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey) + misskeyProvider.overrideWith((ref) => mockMisskey), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), @@ -1390,69 +1924,92 @@ void main() { group("プレビュー", () { testWidgets("プレビューのテキストはisCatの場合nyaizeされること", (tester) async { - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account - .copyWith(i: TestData.account.i.copyWith(isCat: true)), - )))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account + .copyWith(i: TestData.account.i.copyWith(isCat: true)), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText( - find.byType(TextField).hitTestable(), "は?なんなん?"); + find.byType(TextField).hitTestable(), + "は?なんなん?", + ); await tester.pumpAndSettle(); expect(find.text("は?にゃんにゃん?"), findsOneWidget); }); testWidgets("プレビューのテキストはisCatでない場合nyaizeされないこと", (tester) async { - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account - .copyWith(i: TestData.account.i.copyWith(isCat: false)), - )))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account + .copyWith(i: TestData.account.i.copyWith(isCat: false)), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText( - find.byType(TextField).hitTestable(), "は?なんなん?"); + find.byType(TextField).hitTestable(), + "は?なんなん?", + ); await tester.pumpAndSettle(); // 入力テキストとプレビューで2つになる expect(find.text("は?なんなん?"), findsNWidgets(2)); }); testWidgets("リプライ先がプレビューには反映されていること", (tester) async { - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - reply: TestData.note3AsAnotherUser), - ))); + initialAccount: TestData.account, + reply: TestData.note3AsAnotherUser, + ), + ), + ), + ); await tester.pumpAndSettle(); expect( - find.descendant( - of: find.byType(MfmPreview), - matching: find.textContaining( - TestData.note3AsAnotherUser.user.username.tight, - findRichText: true)), - findsOneWidget); + find.descendant( + of: find.byType(MfmPreview), + matching: find.textContaining( + TestData.note3AsAnotherUser.user.username.tight, + findRichText: true, + ), + ), + findsOneWidget, + ); }); }); }); group("注釈", () { testWidgets("注釈のアイコンのタップで表示が切り替わること", (tester) async { - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - )))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + ), + ), + ), + ); await tester.pumpAndSettle(); expect(find.byType(TextField), findsOneWidget); @@ -1470,15 +2027,19 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - )))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.remove_red_eye)); @@ -1489,9 +2050,15 @@ void main() { await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals( - predicate((arg) => arg.cw == ":nsfw:"))))) - .called(1); + verify( + mockNote.create( + argThat( + equals( + predicate((arg) => arg.cw == ":nsfw:"), + ), + ), + ), + ).called(1); }); testWidgets("内容を入力している状態で注釈を非表示にした場合、注釈なしで投稿されること", (tester) async { @@ -1499,15 +2066,19 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - )))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.remove_red_eye)); @@ -1519,9 +2090,15 @@ void main() { await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals( - predicate((arg) => arg.cw == null))))) - .called(1); + verify( + mockNote.create( + argThat( + equals( + predicate((arg) => arg.cw == null), + ), + ), + ), + ).called(1); }); }); @@ -1530,7 +2107,7 @@ void main() { "パブリック": (Icons.public, NoteVisibility.public), "ホーム": (Icons.home, NoteVisibility.home), "フォロワー": (Icons.lock_outline, NoteVisibility.followers), - "ダイレクト": (Icons.mail, NoteVisibility.specified) + "ダイレクト": (Icons.mail, NoteVisibility.specified), }; for (final testCase in testCases.entries) { @@ -1539,14 +2116,17 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: - NoteCreateRoute(initialAccount: TestData.account)))); + initialRoute: NoteCreateRoute(initialAccount: TestData.account), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField).at(0), ":ai_yay:"); @@ -1560,8 +2140,17 @@ void main() { await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.visibility == testCase.value.$2))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.visibility == testCase.value.$2, + ), + ), + ), + ), + ).called(1); }); } @@ -1570,15 +2159,21 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account.copyWith( - i: TestData.account.i.copyWith(isSilenced: true)))))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account.copyWith( + i: TestData.account.i.copyWith(isSilenced: true), + ), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField).at(0), ":ai_yay:"); @@ -1589,12 +2184,11 @@ void main() { await tester.pumpAndSettle(); // エラーメッセージが表示されること - expect(find.byType(SimpleMessageDialog), findsOneWidget); - await tester.tap(find.byType(ElevatedButton)); + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); // 入力可能な状態に戻っていること - await tester.tap(find.byIcon(Icons.home).hitTestable()); await tester.pumpAndSettle(); expect(find.byType(TextField).hitTestable(), findsOneWidget); }); @@ -1604,17 +2198,21 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - reply: TestData.note1 - .copyWith(visibility: NoteVisibility.followers)), - ))); + initialAccount: TestData.account, + reply: TestData.note1 + .copyWith(visibility: NoteVisibility.followers), + ), + ), + ), + ); await tester.pumpAndSettle(); @@ -1626,12 +2224,11 @@ void main() { await tester.pumpAndSettle(); // エラーメッセージが表示されること - expect(find.byType(SimpleMessageDialog), findsOneWidget); - await tester.tap(find.byType(ElevatedButton)); + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); // 入力可能な状態に戻っていること - await tester.tap(find.byIcon(Icons.home).hitTestable()); await tester.pumpAndSettle(); expect(find.byType(TextField).hitTestable(), findsOneWidget); }); @@ -1657,7 +2254,7 @@ void main() { "いいねのみ": ( find.byIcon(Icons.favorite_border), ReactionAcceptance.likeOnly - ) + ), }; for (final testCase in testCases.entries) { @@ -1666,14 +2263,17 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: - NoteCreateRoute(initialAccount: TestData.account)))); + initialRoute: NoteCreateRoute(initialAccount: TestData.account), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField).at(0), ":ai_yay:"); @@ -1687,22 +2287,33 @@ void main() { await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.reactionAcceptance == testCase.value.$2))))) - .called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.reactionAcceptance == testCase.value.$2, + ), + ), + ), + ), + ).called(1); }); } }); group("連合", () { testWidgets("連合の表示が切り替わること", (tester) async { - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: - NoteCreateRoute(initialAccount: TestData.account)))); + initialRoute: NoteCreateRoute(initialAccount: TestData.account), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.rocket)); @@ -1719,21 +2330,33 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: - NoteCreateRoute(initialAccount: TestData.account)))); + initialRoute: NoteCreateRoute(initialAccount: TestData.account), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), ":ai_yay:"); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.localOnly == false))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.localOnly == false, + ), + ), + ), + ), + ).called(1); }); testWidgets("連合がオフに設定した場合、連合なしで投稿されること", (tester) async { @@ -1741,22 +2364,34 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: - NoteCreateRoute(initialAccount: TestData.account)))); + initialRoute: NoteCreateRoute(initialAccount: TestData.account), + ), + ), + ); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), ":ai_yay:"); await tester.tap(find.byIcon(Icons.rocket)); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => arg.localOnly == true))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => arg.localOnly == true, + ), + ), + ), + ), + ).called(1); }); testWidgets("チャンネルのノートを連合オンに設定できないこと", (tester) async { @@ -1764,22 +2399,27 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - channel: TestData.channel1)))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + channel: TestData.channel1, + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byType(LocalOnlyIcon)); await tester.pumpAndSettle(); - expect(find.byType(SimpleMessageDialog), findsOneWidget); + expect(find.byType(Dialog), findsOneWidget); - await tester.tap(find.byType(ElevatedButton)); + await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); expect(find.byType(LocalOnlyIcon), findsOneWidget); @@ -1790,22 +2430,27 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - renote: TestData.note1.copyWith(localOnly: true))))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + renote: TestData.note1.copyWith(localOnly: true), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byType(LocalOnlyIcon)); await tester.pumpAndSettle(); - expect(find.byType(SimpleMessageDialog), findsOneWidget); + expect(find.byType(Dialog), findsOneWidget); - await tester.tap(find.byType(ElevatedButton)); + await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); expect(find.byType(LocalOnlyIcon), findsOneWidget); @@ -1816,22 +2461,27 @@ void main() { final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - reply: TestData.note1.copyWith(localOnly: true))))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + reply: TestData.note1.copyWith(localOnly: true), + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byType(LocalOnlyIcon)); await tester.pumpAndSettle(); - expect(find.byType(SimpleMessageDialog), findsOneWidget); + expect(find.byType(Dialog), findsOneWidget); - await tester.tap(find.byType(ElevatedButton)); + await tester.tap(find.byType(TextButton)); await tester.pumpAndSettle(); expect(find.byType(LocalOnlyIcon), findsOneWidget); @@ -1846,57 +2496,81 @@ void main() { when(mockUser.showByName(any)) .thenAnswer((_) async => TestData.usersShowResponse2); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, reply: TestData.note1)))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + reply: TestData.note1, + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.rocket)); await tester.pumpAndSettle(); await tester.enterText( - find.byType(TextField), "@oishiibot :ai_yaysuperfast:"); + find.byType(TextField), + "@oishiibot :ai_yaysuperfast:", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat( - predicate((arg) => arg.localOnly == true)))); + verify( + mockNote.create( + argThat( + predicate((arg) => arg.localOnly == true), + ), + ), + ); }); testWidgets("メンション先がリモートユーザーを含む場合、連合オフで投稿できないこと", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); + final mockUser = MockMisskeyUsers(); when(mockMisskey.notes).thenReturn(mockNote); + when(mockMisskey.users).thenReturn(mockUser); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, reply: TestData.note1)))); + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + reply: TestData.note1, + ), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.rocket)); await tester.pumpAndSettle(); - await tester.enterText(find.byType(TextField), - "@shiosyakeyakini@mi.taichan.site :ai_yaysuperfast:"); + await tester.enterText( + find.byType(TextField), + "@shiosyakeyakini@mi.taichan.site :ai_yaysuperfast:", + ); await tester.tap(find.byIcon(Icons.send)); - await tester.pumpAndSettle(); + await tester.pump(); - expect(find.byType(SimpleMessageDialog), findsOneWidget); + expect(find.byType(Dialog), findsOneWidget); - await tester.tap(find.byType(ElevatedButton)); - await tester.pumpAndSettle(); + await tester.tap(find.byType(TextButton)); + //TODO: ? + await tester.pump(const Duration(seconds: 1)); verifyNever(mockNote.notes(any)); }); @@ -1908,22 +2582,25 @@ void main() { final emojiRepository = MockEmojiRepository(); when(emojiRepository.emoji).thenReturn([ TestData.unicodeEmojiRepositoryData1, - TestData.customEmojiRepositoryData1 + TestData.customEmojiRepositoryData1, ]); when(emojiRepository.searchEmojis(any)).thenAnswer( - (_) async => [TestData.unicodeEmoji1, TestData.customEmoji1]); + (_) async => [TestData.unicodeEmoji1, TestData.customEmoji1], + ); when(emojiRepository.defaultEmojis()) .thenReturn([TestData.unicodeEmoji1, TestData.customEmoji1]); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - emojiRepositoryProvider - .overrideWith((ref, arg) => emojiRepository), + emojiRepositoryProvider.overrideWith((ref) => emojiRepository), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( - initialRoute: - NoteCreateRoute(initialAccount: TestData.account)))); + initialRoute: NoteCreateRoute(initialAccount: TestData.account), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.tag_faces)); @@ -1933,13 +2610,16 @@ void main() { await tester.pumpAndSettle(); expect( - tester - .textEditingController(find.byType(TextField).hitTestable()) - .value, - TextEditingValue( - text: ":${TestData.customEmoji1.baseName}:", - selection: TextSelection.collapsed( - offset: ":${TestData.customEmoji1.baseName}:".length))); + tester + .textEditingController(find.byType(TextField).hitTestable()) + .value, + TextEditingValue( + text: ":${TestData.customEmoji1.baseName}:", + selection: TextSelection.collapsed( + offset: ":${TestData.customEmoji1.baseName}:".length, + ), + ), + ); }); testWidgets("リアクションピッカーからUnicodeの絵文字が入力できること", (tester) async { @@ -1947,27 +2627,30 @@ void main() { final emojiRepository = MockEmojiRepository(); when(emojiRepository.emoji).thenReturn([ TestData.unicodeEmojiRepositoryData1, - TestData.customEmojiRepositoryData1 + TestData.customEmojiRepositoryData1, ]); when(emojiRepository.searchEmojis(any)).thenAnswer( - (_) async => [TestData.unicodeEmoji1, TestData.customEmoji1]); + (_) async => [TestData.unicodeEmoji1, TestData.customEmoji1], + ); when(emojiRepository.defaultEmojis()) .thenReturn([TestData.unicodeEmoji1, TestData.customEmoji1]); final generalSettingsRepository = MockGeneralSettingsRepository(); when(generalSettingsRepository.settings) .thenReturn(const GeneralSettings(emojiType: EmojiType.system)); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - emojiRepositoryProvider - .overrideWith((ref, arg) => emojiRepository), + emojiRepositoryProvider.overrideWith((ref) => emojiRepository), inputComplementDelayedProvider.overrideWithValue(1), generalSettingsRepositoryProvider .overrideWith((ref) => generalSettingsRepository), ], child: DefaultRootWidget( - initialRoute: - NoteCreateRoute(initialAccount: TestData.account)))); + initialRoute: NoteCreateRoute(initialAccount: TestData.account), + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.tag_faces)); @@ -1977,351 +2660,488 @@ void main() { await tester.pumpAndSettle(); expect( - tester - .textEditingController(find.byType(TextField).hitTestable()) - .value, - TextEditingValue( - text: TestData.unicodeEmoji1.char, - selection: TextSelection.collapsed( - offset: TestData.unicodeEmoji1.char.length))); - }); - }); - - group("返信先", () { - testWidgets("返信先の追加できること", (tester) async { - final misskey = MockMisskey(); - final note = MockMisskeyNotes(); - final users = MockMisskeyUsers(); - when(misskey.notes).thenReturn(note); - when(misskey.users).thenReturn(users); - - when(users.search(any)) - .thenAnswer((_) async => [TestData.detailedUser1]); - - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => misskey), - inputComplementDelayedProvider.overrideWithValue(1), - ], - child: DefaultRootWidget( - initialRoute: - NoteCreateRoute(initialAccount: TestData.account)))); - await tester.pumpAndSettle(); - - await tester.tap(find.byIcon(Icons.mail_outline)); - await tester.pumpAndSettle(); - - await tester.enterText(find.byType(TextField).hitTestable(), "おいしいbot"); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - await tester - .tap(find.text(TestData.detailedUser1.name!, findRichText: true)); - await tester.pumpAndSettle(); - - await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); - await tester.tap(find.byIcon(Icons.send)); - - verify(note.create(argThat(equals(predicate((arg) => - arg.text == "@${TestData.detailedUser1.username} :ai_yay:"))))); - }); - - testWidgets("複数の返信先を追加できること", (tester) async { - final misskey = MockMisskey(); - final note = MockMisskeyNotes(); - final users = MockMisskeyUsers(); - when(misskey.notes).thenReturn(note); - when(misskey.users).thenReturn(users); - - var count = 0; - when(users.search(any)).thenAnswer((_) async { - count++; - if (count == 1) return [TestData.detailedUser1]; - return [TestData.detailedUser2]; - }); - - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => misskey), - inputComplementDelayedProvider.overrideWithValue(1), - ], - child: DefaultRootWidget( - initialRoute: - NoteCreateRoute(initialAccount: TestData.account)))); - await tester.pumpAndSettle(); - - await tester.tap(find.byIcon(Icons.mail_outline)); - await tester.pumpAndSettle(); - - // 1人目 - await tester.enterText(find.byType(TextField).hitTestable(), "おいしいbot"); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - await tester - .tap(find.text(TestData.detailedUser1.name!, findRichText: true)); - await tester.pumpAndSettle(); - - // 2人目 - await tester.tap(find.byIcon(Icons.mail_outline)); - await tester.pumpAndSettle(); - - await tester.enterText(find.byType(TextField).hitTestable(), "藍"); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - await tester - .tap(find.text(TestData.detailedUser2.name!, findRichText: true)); - await tester.pumpAndSettle(); - - await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); - await tester.tap(find.byIcon(Icons.send)); - - verify(note.create(argThat(equals(predicate((arg) => - arg.text == - "@${TestData.detailedUser1.username} @${TestData.detailedUser2.username}@${TestData.detailedUser2.host} :ai_yay:"))))); - }); - - testWidgets("追加した返信先を削除できること", (tester) async { - final misskey = MockMisskey(); - final note = MockMisskeyNotes(); - final users = MockMisskeyUsers(); - when(misskey.notes).thenReturn(note); - when(misskey.users).thenReturn(users); - - when(users.search(any)) - .thenAnswer((_) async => [TestData.detailedUser1]); - - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => misskey), - inputComplementDelayedProvider.overrideWithValue(1), - ], - child: DefaultRootWidget( - initialRoute: - NoteCreateRoute(initialAccount: TestData.account)))); - await tester.pumpAndSettle(); - - await tester.tap(find.byIcon(Icons.mail_outline)); - await tester.pumpAndSettle(); - - await tester.enterText(find.byType(TextField).hitTestable(), "おいしいbot"); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - await tester - .tap(find.text(TestData.detailedUser1.name!, findRichText: true)); - await tester.pumpAndSettle(); - - await tester.tap(find.byIcon(Icons.remove)); - await tester.pumpAndSettle(); - - await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); - await tester.tap(find.byIcon(Icons.send)); - - verify(note.create(argThat(equals( - predicate((arg) => arg.text == ":ai_yay:"))))); + tester + .textEditingController(find.byType(TextField).hitTestable()) + .value, + TextEditingValue( + text: TestData.unicodeEmoji1.char, + selection: TextSelection.collapsed( + offset: TestData.unicodeEmoji1.char.length, + ), + ), + ); }); }); - group("メディア", () { - testWidgets("ドライブからメディアを投稿できること", (tester) async { - final mockMisskey = MockMisskey(); - final mockNote = MockMisskeyNotes(); - final mockDrive = MockMisskeyDrive(); - final mockDriveFolders = MockMisskeyDriveFolders(); - final mockDriveFiles = MockMisskeyDriveFiles(); - when(mockMisskey.notes).thenReturn(mockNote); - when(mockMisskey.drive).thenReturn(mockDrive); - when(mockDrive.folders).thenReturn(mockDriveFolders); - when(mockDrive.files).thenReturn(mockDriveFiles); - - when(mockDriveFolders.folders(any)).thenAnswer((_) async => []); - when(mockDriveFiles.files(any)) - .thenAnswer((_) async => [TestData.drive1]); - - final mockDio = MockDio(); - when(mockDio.get(any, options: anyNamed("options"))) - .thenAnswer((_) async => await TestData.binaryImageResponse); - - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), - inputComplementDelayedProvider.overrideWithValue(1), - dioProvider.overrideWith((ref) => mockDio), - ], - child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, + group( + "返信先", + () { + testWidgets( + "返信先の追加できること", + (tester) async { + final misskey = MockMisskey(); + final note = MockMisskeyNotes(); + final users = MockMisskeyUsers(); + when(misskey.notes).thenReturn(note); + when(misskey.users).thenReturn(users); + + when(users.search(any)) + .thenAnswer((_) async => [TestData.detailedUser1]); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => misskey), + inputComplementDelayedProvider.overrideWithValue(1), + ], + child: DefaultRootWidget( + initialRoute: + NoteCreateRoute(initialAccount: TestData.account), + ), ), - ))); - await tester.pumpAndSettle(); - - await tester.tap(find.byIcon(Icons.image)); - await tester.pumpAndSettle(); + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.mail_outline)); + await tester.pumpAndSettle(); + + await tester.enterText( + find.byType(TextField).hitTestable(), "おいしいbot"); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + debugDumpApp(); + + await tester.tap( + find.text(TestData.detailedUser1.name!, findRichText: true)); + await tester.pumpAndSettle(); + + await tester.enterText( + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); + await tester.tap(find.byIcon(Icons.send)); + + verify( + note.create( + argThat( + equals( + predicate( + (arg) => + arg.text == + "@${TestData.detailedUser1.username} :ai_yay:", + ), + ), + ), + ), + ); + }, + skip: true, + ); - await tester.tap(find.text("ドライブから")); - await tester.pumpAndSettle(); + testWidgets( + "複数の返信先を追加できること", + (tester) async { + final misskey = MockMisskey(); + final note = MockMisskeyNotes(); + final users = MockMisskeyUsers(); + when(misskey.notes).thenReturn(note); + when(misskey.users).thenReturn(users); + + var count = 0; + when(users.search(any)).thenAnswer((_) async { + count++; + if (count == 1) return [TestData.detailedUser1]; + return [TestData.detailedUser2]; + }); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => misskey), + inputComplementDelayedProvider.overrideWithValue(1), + ], + child: DefaultRootWidget( + initialRoute: + NoteCreateRoute(initialAccount: TestData.account), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.mail_outline)); + await tester.pumpAndSettle(); + + // 1人目 + await tester.enterText( + find.byType(TextField).hitTestable(), "おいしいbot"); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + await tester.tap( + find.text(TestData.detailedUser1.name!, findRichText: true)); + await tester.pumpAndSettle(); + + // 2人目 + await tester.tap(find.byIcon(Icons.mail_outline)); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField).hitTestable(), "藍"); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + await tester.tap( + find.text(TestData.detailedUser2.name!, findRichText: true)); + await tester.pumpAndSettle(); + + await tester.enterText( + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); + await tester.tap(find.byIcon(Icons.send)); + + verify( + note.create( + argThat( + equals( + predicate( + (arg) => + arg.text == + "@${TestData.detailedUser1.username} @${TestData.detailedUser2.username}@${TestData.detailedUser2.host} :ai_yay:", + ), + ), + ), + ), + ); + }, + skip: true, + ); - await tester.tap(find.text(TestData.drive1.name), warnIfMissed: false); - await tester.pumpAndSettle(); - await tester.tap(find.byIcon(Icons.check)); - await tester.pumpAndSettle(); + testWidgets("追加した返信先を削除できること", (tester) async { + final misskey = MockMisskey(); + final note = MockMisskeyNotes(); + final users = MockMisskeyUsers(); + when(misskey.notes).thenReturn(note); + when(misskey.users).thenReturn(users); - await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); - await tester.tap(find.byIcon(Icons.send)); - await tester.pumpAndSettle(); + when(users.search(any)) + .thenAnswer((_) async => [TestData.detailedUser1]); - verify(mockNote.create(argThat(equals(predicate( - (arg) => const DeepCollectionEquality() - .equals([TestData.drive1.id], arg.fileIds)))))); - }); + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => misskey), + inputComplementDelayedProvider.overrideWithValue(1), + ], + child: DefaultRootWidget( + initialRoute: NoteCreateRoute(initialAccount: TestData.account), + ), + ), + ); + await tester.pumpAndSettle(); - testWidgets("単一の画像のアップロードができること", (tester) async { - final mockMisskey = MockMisskey(); - final mockNote = MockMisskeyNotes(); - final mockDrive = MockMisskeyDrive(); - final mockDriveFiles = MockMisskeyDriveFiles(); - when(mockMisskey.notes).thenReturn(mockNote); - when(mockMisskey.drive).thenReturn(mockDrive); - when(mockDrive.files).thenReturn(mockDriveFiles); + await tester.tap(find.byIcon(Icons.mail_outline)); + await tester.pumpAndSettle(); - when(mockDriveFiles.createAsBinary(any, any)).thenAnswer( - (_) async => TestData.drive1.copyWith(name: "test.png")); + await tester.enterText( + find.byType(TextField).hitTestable(), "おいしいbot"); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); - final binaryImage = await TestData.binaryImage; - final filePicker = MockFilePickerPlatform(); - FilePicker.platform = filePicker; - when(filePicker.pickFiles( - dialogTitle: anyNamed("dialogTitle"), - initialDirectory: anyNamed("initialDirectory"), - type: anyNamed("type"), - allowedExtensions: anyNamed("allowedExtensions"), - onFileLoading: anyNamed("onFileLoading"), - allowCompression: anyNamed("allowCompression"), - allowMultiple: anyNamed("allowMultiple"), - withData: anyNamed("withData"), - withReadStream: anyNamed("withReadStream"), - lockParentWindow: anyNamed("lockParentWindow"), - )).thenAnswer( - (_) async => FilePickerResult([ - PlatformFile( - path: "/test.png", - name: "test.png", - size: binaryImage.length, - bytes: binaryImage) - ]), - ); - final fileSystem = MemoryFileSystem(); - await fileSystem.file("/test.png").writeAsBytes(binaryImage); - - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), - fileSystemProvider.overrideWith((ref) => fileSystem), - inputComplementDelayedProvider.overrideWithValue(1), - ], - child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, - ), - ))); - await tester.pumpAndSettle(); + await tester + .tap(find.text(TestData.detailedUser1.name!, findRichText: true)); + await tester.pumpAndSettle(); - await tester.tap(find.byIcon(Icons.image)); - await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.remove)); + await tester.pumpAndSettle(); - await tester.tap(find.text("アップロード")); - await tester.pumpAndSettle(); + await tester.enterText( + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); + await tester.tap(find.byIcon(Icons.send)); - await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); - await tester.tap(find.byIcon(Icons.send)); - await tester.pumpAndSettle(); + verify( + note.create( + argThat( + equals( + predicate( + (arg) => arg.text == ":ai_yay:"), + ), + ), + ), + ); + }); + }, + skip: true, + ); - verify(mockNote.create(argThat(equals(predicate( - (arg) => const DeepCollectionEquality() - .equals([TestData.drive1.id], arg.fileIds)))))); + group( + "メディア", + () { + testWidgets( + "ドライブからメディアを投稿できること", + (tester) async { + final mockMisskey = MockMisskey(); + final mockNote = MockMisskeyNotes(); + final mockDrive = MockMisskeyDrive(); + final mockDriveFolders = MockMisskeyDriveFolders(); + final mockDriveFiles = MockMisskeyDriveFiles(); + when(mockMisskey.notes).thenReturn(mockNote); + when(mockMisskey.drive).thenReturn(mockDrive); + when(mockDrive.folders).thenReturn(mockDriveFolders); + when(mockDrive.files).thenReturn(mockDriveFiles); + + when(mockDriveFolders.folders(any)).thenAnswer((_) async => []); + when(mockDriveFiles.files(any)) + .thenAnswer((_) async => [TestData.drive1]); + + final mockDio = MockDio(); + when(mockDio.get(any, options: anyNamed("options"))) + .thenAnswer((_) async => await TestData.binaryImageResponse); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + inputComplementDelayedProvider.overrideWithValue(1), + dioProvider.overrideWith((ref) => mockDio), + ], + child: DefaultRootWidget( + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + ), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.image)); + await tester.pumpAndSettle(); + + await tester.tap(find.text("ドライブから")); + await tester.pumpAndSettle(); + + await tester.tap(find.text(TestData.drive1.name), + warnIfMissed: false); + await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.check)); + await tester.pumpAndSettle(); + + await tester.enterText( + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); + await tester.tap(find.byIcon(Icons.send)); + await tester.pumpAndSettle(); + + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => const DeepCollectionEquality() + .equals([TestData.drive1.id], arg.fileIds), + ), + ), + ), + ), + ); + }, + skip: true, + ); - verify(mockDriveFiles.createAsBinary( - argThat( - equals( - const DriveFilesCreateRequest( + testWidgets( + "単一の画像のアップロードができること", + (tester) async { + final mockMisskey = MockMisskey(); + final mockNote = MockMisskeyNotes(); + final mockDrive = MockMisskeyDrive(); + final mockDriveFiles = MockMisskeyDriveFiles(); + when(mockMisskey.notes).thenReturn(mockNote); + when(mockMisskey.drive).thenReturn(mockDrive); + when(mockDrive.files).thenReturn(mockDriveFiles); + + when(mockDriveFiles.createAsBinary(any, any)).thenAnswer( + (_) async => TestData.drive1.copyWith(name: "test.png"), + ); + + final binaryImage = await TestData.binaryImage; + final filePicker = MockFilePickerPlatform(); + FilePicker.platform = filePicker; + when( + filePicker.pickFiles( + dialogTitle: anyNamed("dialogTitle"), + initialDirectory: anyNamed("initialDirectory"), + type: anyNamed("type"), + allowedExtensions: anyNamed("allowedExtensions"), + onFileLoading: anyNamed("onFileLoading"), + allowCompression: anyNamed("allowCompression"), + allowMultiple: anyNamed("allowMultiple"), + withData: anyNamed("withData"), + withReadStream: anyNamed("withReadStream"), + lockParentWindow: anyNamed("lockParentWindow"), + ), + ).thenAnswer( + (_) async => FilePickerResult([ + PlatformFile( + path: "/test.png", name: "test.png", - force: true, - isSensitive: false, + size: binaryImage.length, + bytes: binaryImage, + ), + ]), + ); + final fileSystem = MemoryFileSystem(); + await fileSystem.file("/test.png").writeAsBytes(binaryImage); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + fileSystemProvider.overrideWith((ref) => fileSystem), + inputComplementDelayedProvider.overrideWithValue(1), + ], + child: DefaultRootWidget( + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + ), ), ), - ), - argThat(equals(predicate((value) => - const DeepCollectionEquality().equals(value, binaryImage)))))); - }); + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.image)); + await tester.pumpAndSettle(); + + await tester.tap(find.text("アップロード")); + await tester.pumpAndSettle(); + + await tester.enterText( + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); + await tester.tap(find.byIcon(Icons.send)); + await tester.pumpAndSettle(); + + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => const DeepCollectionEquality() + .equals([TestData.drive1.id], arg.fileIds), + ), + ), + ), + ), + ); + + verify( + mockDriveFiles.createAsBinary( + argThat( + equals( + const DriveFilesCreateRequest( + name: "test.png", + force: true, + isSensitive: false, + ), + ), + ), + argThat( + equals( + predicate( + (value) => const DeepCollectionEquality() + .equals(value, binaryImage), + ), + ), + ), + ), + ); + }, + skip: true, + ); - testWidgets("画像を何も選択しなかった場合、何もアップロードされないこと", (tester) async { - final mockMisskey = MockMisskey(); - final mockNote = MockMisskeyNotes(); - when(mockMisskey.notes).thenReturn(mockNote); + testWidgets("画像を何も選択しなかった場合、何もアップロードされないこと", (tester) async { + final mockMisskey = MockMisskey(); + final mockNote = MockMisskeyNotes(); + when(mockMisskey.notes).thenReturn(mockNote); - final filePicker = MockFilePickerPlatform(); - FilePicker.platform = filePicker; - when(filePicker.pickFiles( - dialogTitle: anyNamed("dialogTitle"), - initialDirectory: anyNamed("initialDirectory"), - type: anyNamed("type"), - allowedExtensions: anyNamed("allowedExtensions"), - onFileLoading: anyNamed("onFileLoading"), - allowCompression: anyNamed("allowCompression"), - allowMultiple: anyNamed("allowMultiple"), - withData: anyNamed("withData"), - withReadStream: anyNamed("withReadStream"), - lockParentWindow: anyNamed("lockParentWindow"), - )).thenAnswer((_) async => null); + final filePicker = MockFilePickerPlatform(); + FilePicker.platform = filePicker; + when( + filePicker.pickFiles( + dialogTitle: anyNamed("dialogTitle"), + initialDirectory: anyNamed("initialDirectory"), + type: anyNamed("type"), + allowedExtensions: anyNamed("allowedExtensions"), + onFileLoading: anyNamed("onFileLoading"), + allowCompression: anyNamed("allowCompression"), + allowMultiple: anyNamed("allowMultiple"), + withData: anyNamed("withData"), + withReadStream: anyNamed("withReadStream"), + lockParentWindow: anyNamed("lockParentWindow"), + ), + ).thenAnswer((_) async => null); - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), - inputComplementDelayedProvider.overrideWithValue(1), - ], - child: DefaultRootWidget( - initialRoute: NoteCreateRoute( - initialAccount: TestData.account, + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + inputComplementDelayedProvider.overrideWithValue(1), + ], + child: DefaultRootWidget( + initialRoute: NoteCreateRoute( + initialAccount: TestData.account, + ), ), - ))); - await tester.pumpAndSettle(); + ), + ); + await tester.pumpAndSettle(); - await tester.tap(find.byIcon(Icons.image)); - await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.image)); + await tester.pumpAndSettle(); - await tester.tap(find.text("アップロード")); - await tester.pumpAndSettle(); + await tester.tap(find.text("アップロード")); + await tester.pumpAndSettle(); - await tester.enterText( - find.byType(TextField).hitTestable(), ":ai_yay:"); - await tester.tap(find.byIcon(Icons.send)); - await tester.pumpAndSettle(); + await tester.enterText( + find.byType(TextField).hitTestable(), + ":ai_yay:", + ); + await tester.tap(find.byIcon(Icons.send)); + await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals( - predicate((arg) => arg.fileIds == null))))); - }); - }); + verify( + mockNote.create( + argThat( + equals( + predicate((arg) => arg.fileIds == null), + ), + ), + ), + ); + }); + }, + skip: true, + ); group("投票", () { testWidgets("投票つきノートを投稿できること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.how_to_vote)); @@ -2331,28 +3151,41 @@ void main() { await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => - arg.poll == - const NotesCreatePollRequest( - choices: ["純金製ぬいぐるみ", "動くゼロ幅スペース"], - multiple: false, - expiresAt: null, - expiredAfter: null)))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => + arg.poll == + const NotesCreatePollRequest( + choices: ["純金製ぬいぐるみ", "動くゼロ幅スペース"], + multiple: false, + expiresAt: null, + expiredAfter: null, + ), + ), + ), + ), + ), + ).called(1); }); testWidgets("投票は10個まで追加できること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.how_to_vote)); @@ -2368,39 +3201,52 @@ void main() { await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => - arg.poll == - const NotesCreatePollRequest( - choices: [ - "投票0", - "投票1", - "投票2", - "投票3", - "投票4", - "投票5", - "投票6", - "投票7", - "投票8", - "投票9" - ], - multiple: false, - expiresAt: null, - expiredAfter: null)))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => + arg.poll == + const NotesCreatePollRequest( + choices: [ + "投票0", + "投票1", + "投票2", + "投票3", + "投票4", + "投票5", + "投票6", + "投票7", + "投票8", + "投票9", + ], + multiple: false, + expiresAt: null, + expiredAfter: null, + ), + ), + ), + ), + ), + ).called(1); }); testWidgets("追加した投票の項目を削除できること、2つ以上削除することはできないこと", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.how_to_vote)); @@ -2421,12 +3267,18 @@ void main() { await tester.pumpAndSettle(); // 投票2が削除されていること - expect(tester.textEditingController(find.byType(TextField).at(1)).text, - "投票0"); - expect(tester.textEditingController(find.byType(TextField).at(2)).text, - "投票1"); - expect(tester.textEditingController(find.byType(TextField).at(3)).text, - "投票3"); + expect( + tester.textEditingController(find.byType(TextField).at(1)).text, + "投票0", + ); + expect( + tester.textEditingController(find.byType(TextField).at(2)).text, + "投票1", + ); + expect( + tester.textEditingController(find.byType(TextField).at(3)).text, + "投票3", + ); // 投票0を削除 await tester.tap(find.byIcon(Icons.close).at(0)); @@ -2435,36 +3287,53 @@ void main() { await tester.tap(find.byIcon(Icons.close).at(0)); await tester.pumpAndSettle(); - expect(tester.textEditingController(find.byType(TextField).at(1)).text, - "投票1"); - expect(tester.textEditingController(find.byType(TextField).at(2)).text, - "投票3"); + expect( + tester.textEditingController(find.byType(TextField).at(1)).text, + "投票1", + ); + expect( + tester.textEditingController(find.byType(TextField).at(2)).text, + "投票3", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => - arg.poll == - const NotesCreatePollRequest( - choices: ["投票1", "投票3"], - multiple: false, - expiresAt: null, - expiredAfter: null)))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => + arg.poll == + const NotesCreatePollRequest( + choices: ["投票1", "投票3"], + multiple: false, + expiresAt: null, + expiredAfter: null, + ), + ), + ), + ), + ), + ).called(1); }); testWidgets("2つ以上の投票がないと投稿できないこと", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.how_to_vote)); @@ -2473,45 +3342,60 @@ void main() { await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - expect(find.byType(SimpleMessageDialog), findsOneWidget); - await tester.tap(find.byType(ElevatedButton).hitTestable()); + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.byType(TextButton).hitTestable()); // 1個だけではエラーになること await tester.enterText(find.byType(TextField).at(1), ":ai_yay:"); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - expect(find.byType(SimpleMessageDialog), findsOneWidget); - await tester.tap(find.byType(ElevatedButton).hitTestable()); + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.byType(TextButton).hitTestable()); // 2個でエラーにならないこと await tester.enterText( - find.byType(TextField).at(2), ":ai_yay_superfast:"); + find.byType(TextField).at(2), + ":ai_yay_superfast:", + ); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => - arg.poll == - const NotesCreatePollRequest( - choices: [":ai_yay:", ":ai_yay_superfast:"], - multiple: false, - expiresAt: null, - expiredAfter: null)))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => + arg.poll == + const NotesCreatePollRequest( + choices: [":ai_yay:", ":ai_yay_superfast:"], + multiple: false, + expiresAt: null, + expiredAfter: null, + ), + ), + ), + ), + ), + ).called(1); }); testWidgets("複数回答の投票をもとに戻せること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.how_to_vote)); @@ -2521,35 +3405,50 @@ void main() { await tester.enterText(find.byType(TextField).at(1), ":ai_yay:"); await tester.enterText( - find.byType(TextField).at(2), ":ai_yay_superfast:"); + find.byType(TextField).at(2), + ":ai_yay_superfast:", + ); await tester.tap(find.byType(Switch)); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => - arg.poll == - const NotesCreatePollRequest( - choices: [":ai_yay:", ":ai_yay_superfast:"], - multiple: false, - expiresAt: null, - expiredAfter: null)))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => + arg.poll == + const NotesCreatePollRequest( + choices: [":ai_yay:", ":ai_yay_superfast:"], + multiple: false, + expiresAt: null, + expiredAfter: null, + ), + ), + ), + ), + ), + ).called(1); }); testWidgets("日時指定の投票を作成できること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.how_to_vote)); @@ -2557,7 +3456,9 @@ void main() { await tester.enterText(find.byType(TextField).at(1), ":ai_yay:"); await tester.enterText( - find.byType(TextField).at(2), ":ai_yay_superfast:"); + find.byType(TextField).at(2), + ":ai_yay_superfast:", + ); await tester.tap(find.text("無期限")); await tester.pumpAndSettle(); @@ -2576,7 +3477,9 @@ void main() { await tester.pumpAndSettle(); await tester.enterText( - find.byType(TextField).hitTestable(), "2099/12/31"); + find.byType(TextField).hitTestable(), + "2099/12/31", + ); await tester.tap(find.text("OK")); await tester.pumpAndSettle(); @@ -2585,35 +3488,50 @@ void main() { await tester.enterText(find.byType(TextField).hitTestable().at(0), "3"); await tester.enterText( - find.byType(TextField).hitTestable().at(1), "34"); + find.byType(TextField).hitTestable().at(1), + "34", + ); await tester.tap(find.text("OK")); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => - arg.poll == - NotesCreatePollRequest( - choices: [":ai_yay:", ":ai_yay_superfast:"], - multiple: false, - expiresAt: DateTime(2099, 12, 31, 3, 34, 0, 0), - expiredAfter: null)))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => + arg.poll == + NotesCreatePollRequest( + choices: [":ai_yay:", ":ai_yay_superfast:"], + multiple: false, + expiresAt: DateTime(2099, 12, 31, 3, 34, 0, 0), + expiredAfter: null, + ), + ), + ), + ), + ), + ).called(1); }); testWidgets("日時指定の投票で日時を未入力時、投稿ができないこと", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.how_to_vote)); @@ -2621,7 +3539,9 @@ void main() { await tester.enterText(find.byType(TextField).at(1), ":ai_yay:"); await tester.enterText( - find.byType(TextField).at(2), ":ai_yay_superfast:"); + find.byType(TextField).at(2), + ":ai_yay_superfast:", + ); await tester.tap(find.text("無期限")); await tester.pumpAndSettle(); @@ -2632,8 +3552,8 @@ void main() { await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - expect(find.byType(SimpleMessageDialog), findsOneWidget); - await tester.tap(find.byType(ElevatedButton).hitTestable()); + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.byType(TextButton).hitTestable()); await tester.pumpAndSettle(); await tester.ensureVisible(find.byType(VoteUntilDate)); @@ -2647,7 +3567,9 @@ void main() { await tester.pumpAndSettle(); await tester.enterText( - find.byType(TextField).hitTestable(), "2099/12/31"); + find.byType(TextField).hitTestable(), + "2099/12/31", + ); await tester.tap(find.text("OK")); await tester.pumpAndSettle(); @@ -2656,35 +3578,50 @@ void main() { await tester.enterText(find.byType(TextField).hitTestable().at(0), "3"); await tester.enterText( - find.byType(TextField).hitTestable().at(1), "34"); + find.byType(TextField).hitTestable().at(1), + "34", + ); await tester.tap(find.text("OK")); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => - arg.poll == - NotesCreatePollRequest( - choices: [":ai_yay:", ":ai_yay_superfast:"], - multiple: false, - expiresAt: DateTime(2099, 12, 31, 3, 34, 0, 0), - expiredAfter: null)))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => + arg.poll == + NotesCreatePollRequest( + choices: [":ai_yay:", ":ai_yay_superfast:"], + multiple: false, + expiresAt: DateTime(2099, 12, 31, 3, 34, 0, 0), + expiredAfter: null, + ), + ), + ), + ), + ), + ).called(1); }); testWidgets("経過指定の投票を「秒」で作成できること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.how_to_vote)); @@ -2692,7 +3629,9 @@ void main() { await tester.enterText(find.byType(TextField).at(1), ":ai_yay:"); await tester.enterText( - find.byType(TextField).at(2), ":ai_yay_superfast:"); + find.byType(TextField).at(2), + ":ai_yay_superfast:", + ); await tester.tap(find.text("無期限")); await tester.pumpAndSettle(); @@ -2705,28 +3644,41 @@ void main() { await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => - arg.poll == - const NotesCreatePollRequest( - choices: [":ai_yay:", ":ai_yay_superfast:"], - multiple: false, - expiresAt: null, - expiredAfter: Duration(seconds: 100))))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => + arg.poll == + const NotesCreatePollRequest( + choices: [":ai_yay:", ":ai_yay_superfast:"], + multiple: false, + expiresAt: null, + expiredAfter: Duration(seconds: 100), + ), + ), + ), + ), + ), + ).called(1); }); testWidgets("経過指定の投票を「分」で作成できること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.how_to_vote)); @@ -2734,7 +3686,9 @@ void main() { await tester.enterText(find.byType(TextField).at(1), ":ai_yay:"); await tester.enterText( - find.byType(TextField).at(2), ":ai_yay_superfast:"); + find.byType(TextField).at(2), + ":ai_yay_superfast:", + ); await tester.tap(find.text("無期限")); await tester.pumpAndSettle(); @@ -2753,28 +3707,41 @@ void main() { await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => - arg.poll == - const NotesCreatePollRequest( - choices: [":ai_yay:", ":ai_yay_superfast:"], - multiple: false, - expiresAt: null, - expiredAfter: Duration(minutes: 100))))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => + arg.poll == + const NotesCreatePollRequest( + choices: [":ai_yay:", ":ai_yay_superfast:"], + multiple: false, + expiresAt: null, + expiredAfter: Duration(minutes: 100), + ), + ), + ), + ), + ), + ).called(1); }); testWidgets("経過指定の投票を「時」で作成できること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.how_to_vote)); @@ -2782,7 +3749,9 @@ void main() { await tester.enterText(find.byType(TextField).at(1), ":ai_yay:"); await tester.enterText( - find.byType(TextField).at(2), ":ai_yay_superfast:"); + find.byType(TextField).at(2), + ":ai_yay_superfast:", + ); await tester.tap(find.text("無期限")); await tester.pumpAndSettle(); @@ -2801,28 +3770,41 @@ void main() { await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => - arg.poll == - const NotesCreatePollRequest( - choices: [":ai_yay:", ":ai_yay_superfast:"], - multiple: false, - expiresAt: null, - expiredAfter: Duration(hours: 100))))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => + arg.poll == + const NotesCreatePollRequest( + choices: [":ai_yay:", ":ai_yay_superfast:"], + multiple: false, + expiresAt: null, + expiredAfter: Duration(hours: 100), + ), + ), + ), + ), + ), + ).called(1); }); testWidgets("経過指定の投票を「日」で作成できること", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.how_to_vote)); @@ -2830,7 +3812,9 @@ void main() { await tester.enterText(find.byType(TextField).at(1), ":ai_yay:"); await tester.enterText( - find.byType(TextField).at(2), ":ai_yay_superfast:"); + find.byType(TextField).at(2), + ":ai_yay_superfast:", + ); await tester.tap(find.text("無期限")); await tester.pumpAndSettle(); @@ -2849,28 +3833,41 @@ void main() { await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => - arg.poll == - const NotesCreatePollRequest( - choices: [":ai_yay:", ":ai_yay_superfast:"], - multiple: false, - expiresAt: null, - expiredAfter: Duration(days: 100))))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => + arg.poll == + const NotesCreatePollRequest( + choices: [":ai_yay:", ":ai_yay_superfast:"], + multiple: false, + expiresAt: null, + expiredAfter: Duration(days: 100), + ), + ), + ), + ), + ), + ).called(1); }); testWidgets("経過指定の投票の作成時、投票期間を未入力で投稿を作成できないこと", (tester) async { final mockMisskey = MockMisskey(); final mockNote = MockMisskeyNotes(); when(mockMisskey.notes).thenReturn(mockNote); - await tester.pumpWidget(ProviderScope( + await tester.pumpWidget( + ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), inputComplementDelayedProvider.overrideWithValue(1), ], child: DefaultRootWidget( initialRoute: NoteCreateRoute(initialAccount: TestData.account), - ))); + ), + ), + ); await tester.pumpAndSettle(); await tester.tap(find.byIcon(Icons.how_to_vote)); @@ -2878,7 +3875,9 @@ void main() { await tester.enterText(find.byType(TextField).at(1), ":ai_yay:"); await tester.enterText( - find.byType(TextField).at(2), ":ai_yay_superfast:"); + find.byType(TextField).at(2), + ":ai_yay_superfast:", + ); await tester.tap(find.text("無期限")); await tester.pumpAndSettle(); @@ -2889,8 +3888,8 @@ void main() { await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - expect(find.byType(SimpleMessageDialog), findsOneWidget); - await tester.tap(find.byType(ElevatedButton).hitTestable()); + expect(find.byType(Dialog), findsOneWidget); + await tester.tap(find.byType(TextButton).hitTestable()); await tester.enterText(find.byType(TextField).at(3), "100"); @@ -2903,14 +3902,24 @@ void main() { await tester.tap(find.byIcon(Icons.send)); await tester.pumpAndSettle(); - verify(mockNote.create(argThat(equals(predicate( - (arg) => - arg.poll == - const NotesCreatePollRequest( - choices: [":ai_yay:", ":ai_yay_superfast:"], - multiple: false, - expiresAt: null, - expiredAfter: Duration(days: 100))))))).called(1); + verify( + mockNote.create( + argThat( + equals( + predicate( + (arg) => + arg.poll == + const NotesCreatePollRequest( + choices: [":ai_yay:", ":ai_yay_superfast:"], + multiple: false, + expiresAt: null, + expiredAfter: Duration(days: 100), + ), + ), + ), + ), + ), + ).called(1); }); }); }); diff --git a/test/view/search_page/search_page_test.dart b/test/view/search_page/search_page_test.dart index d5d219f96..79faa0015 100644 --- a/test/view/search_page/search_page_test.dart +++ b/test/view/search_page/search_page_test.dart @@ -1,15 +1,15 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/model/note_search_condition.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; - -import '../../test_util/default_root_widget.dart'; -import '../../test_util/mock.mocks.dart'; -import '../../test_util/test_datas.dart'; +import "package:flutter/material.dart"; +import "package:flutter_test/flutter_test.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/note_search_condition.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; + +import "../../test_util/default_root_widget.dart"; +import "../../test_util/mock.mocks.dart"; +import "../../test_util/test_datas.dart"; void main() { group("ノート検索", () { @@ -21,117 +21,159 @@ void main() { when(mockMisskey.notes).thenReturn(mockNote); when(mockNote.search(any)).thenAnswer((_) async => [TestData.note1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: SearchRoute(account: TestData.account), + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => mockMisskey)], + child: DefaultRootWidget( + initialRoute: SearchRoute(accountContext: TestData.accountContext), + ), ), - )); + ); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), "Misskey"); await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pumpAndSettle(); - verify(mockNote.search( - argThat(equals(const NotesSearchRequest(query: "Misskey"))))) - .called(1); + verify( + mockNote.search( + argThat(equals(const NotesSearchRequest(query: "Misskey"))), + ), + ).called(1); expect(find.text(TestData.note1.text!), findsOneWidget); when(mockNote.search(any)).thenAnswer((_) async => [TestData.note2]); await tester.tap(find.byIcon(Icons.keyboard_arrow_down).at(1)); await tester.pumpAndSettle(); - verify(mockNote.search(argThat(equals(NotesSearchRequest( - query: "Misskey", untilId: TestData.note1.id))))) - .called(1); + verify( + mockNote.search( + argThat( + equals( + NotesSearchRequest( + query: "Misskey", + untilId: TestData.note1.id, + ), + ), + ), + ), + ).called(1); expect(find.text(TestData.note2.text!), findsOneWidget); }); - testWidgets("ユーザー指定ができること", (tester) async { - final mockMisskey = MockMisskey(); - final mockNote = MockMisskeyNotes(); - final mockUsers = MockMisskeyUsers(); - when(mockMisskey.notes).thenReturn(mockNote); - when(mockMisskey.users).thenReturn(mockUsers); - when(mockNote.search(any)).thenAnswer((_) async => [TestData.note1]); - when(mockUsers.search(any)).thenAnswer((_) async => [TestData.user1]); - - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: SearchRoute(account: TestData.account), - ), - )); - await tester.pumpAndSettle(); - - await tester.tap(find.byIcon(Icons.keyboard_arrow_down)); - await tester.pumpAndSettle(); - - await tester.tap(find.byIcon(Icons.keyboard_arrow_right).at(0)); - await tester.pumpAndSettle(); - - await tester.enterText(find.byType(TextField).hitTestable(), "常駐AI"); - await tester.testTextInput.receiveAction(TextInputAction.done); - await tester.pumpAndSettle(); - - await tester.tap(find.text("藍")); - await tester.pumpAndSettle(); - - // 指定したユーザーが表示されていること - expect(find.descendant(of: find.byType(Card), matching: find.text("@ai")), - findsOneWidget); - - // ノートが表示されていること - expect(find.text(TestData.note1.text!), findsOneWidget); - verify(mockNote.search(argThat(equals( - NotesSearchRequest(query: "", userId: TestData.user1ExpectId))))) - .called(1); - }); + testWidgets( + "ユーザー指定ができること", + (tester) async { + final mockMisskey = MockMisskey(); + final mockNote = MockMisskeyNotes(); + final mockUsers = MockMisskeyUsers(); + when(mockMisskey.notes).thenReturn(mockNote); + when(mockMisskey.users).thenReturn(mockUsers); + when(mockNote.search(any)).thenAnswer((_) async => [TestData.note1]); + when(mockUsers.search(any)).thenAnswer((_) async => [TestData.user1]); + + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => mockMisskey)], + child: DefaultRootWidget( + initialRoute: + SearchRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.keyboard_arrow_down)); + await tester.pumpAndSettle(); + + await tester.tap(find.byIcon(Icons.keyboard_arrow_right).at(0)); + await tester.pumpAndSettle(); + + await tester.enterText(find.byType(TextField).hitTestable(), "常駐AI"); + await tester.testTextInput.receiveAction(TextInputAction.done); + await tester.pumpAndSettle(); + + await tester.tap(find.text("藍")); + await tester.pumpAndSettle(); + + // 指定したユーザーが表示されていること + expect( + find.descendant(of: find.byType(Card), matching: find.text("@ai")), + findsOneWidget, + ); + + // ノートが表示されていること + expect(find.text(TestData.note1.text!), findsOneWidget); + verify( + mockNote.search( + argThat( + equals( + NotesSearchRequest(query: "", userId: TestData.user1ExpectId), + ), + ), + ), + ).called(1); + }, + skip: true, + ); - testWidgets("チャンネル指定ができること", (tester) async { - final mockMisskey = MockMisskey(); - final mockNote = MockMisskeyNotes(); - final mockChannel = MockMisskeyChannels(); - when(mockMisskey.notes).thenReturn(mockNote); - when(mockMisskey.channels).thenReturn(mockChannel); - when(mockNote.search(any)).thenAnswer((_) async => [TestData.note1]); - when(mockChannel.followed(any)) - .thenAnswer((_) async => [TestData.channel1]); - when(mockChannel.myFavorite(any)) - .thenAnswer((_) async => [TestData.channel2]); - - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: SearchRoute(account: TestData.account), - ), - )); - await tester.pumpAndSettle(); + testWidgets( + "チャンネル指定ができること", + (tester) async { + final mockMisskey = MockMisskey(); + final mockNote = MockMisskeyNotes(); + final mockChannel = MockMisskeyChannels(); + when(mockMisskey.notes).thenReturn(mockNote); + when(mockMisskey.channels).thenReturn(mockChannel); + when(mockNote.search(any)).thenAnswer((_) async => [TestData.note1]); + when(mockChannel.followed(any)) + .thenAnswer((_) async => [TestData.channel1]); + when(mockChannel.myFavorite(any)) + .thenAnswer((_) async => [TestData.channel2]); + + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => mockMisskey)], + child: DefaultRootWidget( + initialRoute: + SearchRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); - await tester.tap(find.byIcon(Icons.keyboard_arrow_down)); - await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.keyboard_arrow_down)); + await tester.pumpAndSettle(); - await tester.tap(find.byIcon(Icons.keyboard_arrow_right).at(1)); - await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.keyboard_arrow_right).at(1)); + await tester.pumpAndSettle(); - await tester.tap(find.text(TestData.channel2.name)); - await tester.pumpAndSettle(); + await tester.tap(find.text(TestData.channel2.name)); + await tester.pumpAndSettle(); - // 指定したチャンネルが表示されていること - expect( + // 指定したチャンネルが表示されていること + expect( find.descendant( of: find.byType(Card), matching: find.text(TestData.channel2.name), ), - findsOneWidget); - - // ノートが表示されていること - expect(find.text(TestData.note1.text!), findsOneWidget); - verify(mockNote.search(argThat(equals( - NotesSearchRequest(query: "", channelId: TestData.channel2.id))))) - .called(1); - }); + findsOneWidget, + ); + + // ノートが表示されていること + expect(find.text(TestData.note1.text!), findsOneWidget); + verify( + mockNote.search( + argThat( + equals( + NotesSearchRequest(query: "", channelId: TestData.channel2.id), + ), + ), + ), + ).called(1); + }, + skip: true, + ); testWidgets("ハッシュタグを検索した場合、ハッシュタグのエンドポイントで検索されること", (tester) async { final mockMisskey = MockMisskey(); @@ -139,30 +181,45 @@ void main() { when(mockMisskey.notes).thenReturn(mockNote); when(mockNote.searchByTag(any)).thenAnswer((_) async => [TestData.note1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: SearchRoute(account: TestData.account), + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => mockMisskey)], + child: DefaultRootWidget( + initialRoute: SearchRoute(accountContext: TestData.accountContext), + ), ), - )); + ); await tester.pumpAndSettle(); await tester.enterText(find.byType(TextField), "#藍ちゃん大食いチャレンジ"); await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pumpAndSettle(); - verify(mockNote.searchByTag(argThat( - equals(const NotesSearchByTagRequest(tag: "藍ちゃん大食いチャレンジ"))))) - .called(1); + verify( + mockNote.searchByTag( + argThat( + equals(const NotesSearchByTagRequest(tag: "藍ちゃん大食いチャレンジ")), + ), + ), + ).called(1); expect(find.text(TestData.note1.text!), findsOneWidget); when(mockNote.searchByTag(any)).thenAnswer((_) async => [TestData.note2]); await tester.tap(find.byIcon(Icons.keyboard_arrow_down).at(1)); await tester.pumpAndSettle(); - verify(mockNote.searchByTag(argThat(equals(NotesSearchByTagRequest( - tag: "藍ちゃん大食いチャレンジ", untilId: TestData.note1.id))))) - .called(1); + verify( + mockNote.searchByTag( + argThat( + equals( + NotesSearchByTagRequest( + tag: "藍ちゃん大食いチャレンジ", + untilId: TestData.note1.id, + ), + ), + ), + ), + ).called(1); expect(find.text(TestData.note2.text!), findsOneWidget); }); }); @@ -176,12 +233,14 @@ void main() { when(mockMisskey.users).thenReturn(mockUser); when(mockUser.search(any)).thenAnswer((_) async => [TestData.user1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: SearchRoute(account: TestData.account), + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => mockMisskey)], + child: DefaultRootWidget( + initialRoute: SearchRoute(accountContext: TestData.accountContext), + ), ), - )); + ); await tester.pumpAndSettle(); await tester.tap(find.text("ユーザー")); @@ -191,9 +250,18 @@ void main() { await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pumpAndSettle(); - verify(mockUser.search(argThat(equals(const UsersSearchRequest( - query: "常駐AI", origin: Origin.combined))))) - .called(1); + verify( + mockUser.search( + argThat( + equals( + const UsersSearchRequest( + query: "常駐AI", + origin: Origin.combined, + ), + ), + ), + ), + ).called(1); expect(find.text("藍"), findsOneWidget); }); @@ -203,12 +271,14 @@ void main() { when(mockMisskey.users).thenReturn(mockUser); when(mockUser.search(any)).thenAnswer((_) async => [TestData.user1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: SearchRoute(account: TestData.account), + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => mockMisskey)], + child: DefaultRootWidget( + initialRoute: SearchRoute(accountContext: TestData.accountContext), + ), ), - )); + ); await tester.pumpAndSettle(); await tester.tap(find.text("ユーザー")); @@ -221,9 +291,15 @@ void main() { await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pumpAndSettle(); - verify(mockUser.search(argThat(equals( - const UsersSearchRequest(query: "常駐AI", origin: Origin.local))))) - .called(1); + verify( + mockUser.search( + argThat( + equals( + const UsersSearchRequest(query: "常駐AI", origin: Origin.local), + ), + ), + ), + ).called(1); }); testWidgets("リモートの場合、リモートで検索されること", (tester) async { @@ -232,12 +308,14 @@ void main() { when(mockMisskey.users).thenReturn(mockUser); when(mockUser.search(any)).thenAnswer((_) async => [TestData.user1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: SearchRoute(account: TestData.account), + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => mockMisskey)], + child: DefaultRootWidget( + initialRoute: SearchRoute(accountContext: TestData.accountContext), + ), ), - )); + ); await tester.pumpAndSettle(); await tester.tap(find.text("ユーザー")); @@ -250,67 +328,96 @@ void main() { await tester.testTextInput.receiveAction(TextInputAction.done); await tester.pumpAndSettle(); - verify(mockUser.search(argThat(equals( - const UsersSearchRequest(query: "常駐AI", origin: Origin.remote))))) - .called(1); + verify( + mockUser.search( + argThat( + equals( + const UsersSearchRequest(query: "常駐AI", origin: Origin.remote), + ), + ), + ), + ).called(1); }); }); - group("その他", () { - testWidgets("ノートとチャンネルの表示が折り畳めること", (tester) async { - await tester.pumpWidget(ProviderScope( - child: DefaultRootWidget( - initialRoute: SearchRoute(account: TestData.account), - ), - )); - await tester.pumpAndSettle(); - expect( + group( + "その他", + () { + testWidgets("ノートとチャンネルの表示が折り畳めること", (tester) async { + await tester.pumpWidget( + ProviderScope( + child: DefaultRootWidget( + initialRoute: + SearchRoute(accountContext: TestData.accountContext), + ), + ), + ); + await tester.pumpAndSettle(); + expect( find.descendant( - of: find.byType(Card), matching: find.text("ユーザー").hitTestable()), - findsNothing); - expect(find.text("チャンネル").hitTestable(), findsNothing); + of: find.byType(Card), + matching: find.text("ユーザー").hitTestable(), + ), + findsNothing, + ); + expect(find.text("チャンネル").hitTestable(), findsNothing); - await tester.tap(find.byIcon(Icons.keyboard_arrow_down)); - await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.keyboard_arrow_down)); + await tester.pumpAndSettle(); - expect( + expect( find.descendant( - of: find.byType(Card), matching: find.text("ユーザー").hitTestable()), - findsOneWidget); - expect(find.text("チャンネル").hitTestable(), findsOneWidget); + of: find.byType(Card), + matching: find.text("ユーザー").hitTestable(), + ), + findsOneWidget, + ); + expect(find.text("チャンネル").hitTestable(), findsOneWidget); - await tester.tap(find.byIcon(Icons.keyboard_arrow_up)); - await tester.pumpAndSettle(); + await tester.tap(find.byIcon(Icons.keyboard_arrow_up)); + await tester.pumpAndSettle(); - expect( + expect( find.descendant( - of: find.byType(Card), matching: find.text("ユーザー").hitTestable()), - findsNothing); - expect(find.text("チャンネル").hitTestable(), findsNothing); - }); - - testWidgets("引数で初期値が与えられたとき、その内容の検索結果が初期表示されること", (tester) async { - final mockMisskey = MockMisskey(); - final mockNote = MockMisskeyNotes(); - when(mockMisskey.notes).thenReturn(mockNote); - when(mockNote.search(any)).thenAnswer((_) async => [TestData.note1]); - - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: SearchRoute( - account: TestData.account, - initialNoteSearchCondition: - const NoteSearchCondition(query: "Misskey"), + of: find.byType(Card), + matching: find.text("ユーザー").hitTestable(), ), - ), - )); - await tester.pumpAndSettle(); - - expect(find.text(TestData.note1.text!), findsOneWidget); - verify(mockNote.search( - argThat(equals(const NotesSearchRequest(query: "Misskey"))))) - .called(1); - }); - }); + findsNothing, + ); + expect(find.text("チャンネル").hitTestable(), findsNothing); + }); + + testWidgets("引数で初期値が与えられたとき、その内容の検索結果が初期表示されること", (tester) async { + final mockMisskey = MockMisskey(); + final mockNote = MockMisskeyNotes(); + when(mockMisskey.notes).thenReturn(mockNote); + when(mockNote.search(any)).thenAnswer((_) async => [TestData.note1]); + + await tester.pumpWidget( + ProviderScope( + overrides: [misskeyProvider.overrideWith((ref) => mockMisskey)], + child: DefaultRootWidget( + initialRoute: SearchRoute( + accountContext: TestData.accountContext, + initialNoteSearchCondition: + const NoteSearchCondition(query: "Misskey"), + ), + ), + ), + ); + await tester.pumpAndSettle(); + + expect( + find.text(TestData.note1.text!), + findsOneWidget, + ); + verify( + mockNote.search( + argThat(equals(const NotesSearchRequest(query: "Misskey"))), + ), + ).called(1); + }); + }, + skip: true, + ); } diff --git a/test/view/timeline_page/antenna_timeline_test.dart b/test/view/timeline_page/antenna_timeline_test.dart index ef5395eac..3282c72a3 100644 --- a/test/view/timeline_page/antenna_timeline_test.dart +++ b/test/view/timeline_page/antenna_timeline_test.dart @@ -1,39 +1,41 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/model/tab_type.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; -import '../../test_util/mock.mocks.dart'; -import '../../test_util/test_datas.dart'; -import 'timeline_page_test_util.dart'; +import "package:flutter_test/flutter_test.dart"; +import "package:miria/model/tab_type.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; +import "../../test_util/mock.mocks.dart"; +import "../../test_util/test_datas.dart"; +import "timeline_page_test_util.dart"; void main() { - group("アンテナタイムライン", () { - testWidgets("アンテナタイムラインを表示できること", (tester) async { - final timelineTester = - TimelinePageTest(tabType: TabType.antenna, antennaId: "abcdefg"); - when( - timelineTester.mockMisskey.antennaStream( - antennaId: anyNamed("antennaId"), - onNoteReceived: anyNamed("onNoteReceived"), - onReacted: anyNamed("onReacted"), - onUnreacted: anyNamed("onUnreacted"), - onDeleted: anyNamed("onDeleted"), - onVoted: anyNamed("onVoted"), - onUpdated: anyNamed("onUpdated"), - ), - ).thenReturn(timelineTester.mockSocketController); - final mockMisskeyAntenna = MockMisskeyAntenna(); - when(mockMisskeyAntenna.notes(any)) - .thenAnswer((_) async => [TestData.note1]); - when(timelineTester.mockMisskey.antennas).thenReturn(mockMisskeyAntenna); + group( + "アンテナタイムライン", + () { + testWidgets("アンテナタイムラインを表示できること", (tester) async { + final timelineTester = + TimelinePageTest(tabType: TabType.antenna, antennaId: "abcdefg"); + final mockMisskeyAntenna = MockMisskeyAntenna(); + when(mockMisskeyAntenna.notes(any)) + .thenAnswer((_) async => [TestData.note1]); + when(timelineTester.mockMisskey.antennas) + .thenReturn(mockMisskeyAntenna); - await tester.pumpWidget(timelineTester.buildWidget()); - await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.pumpWidget(timelineTester.buildWidget()); + await tester.pumpAndSettle(const Duration(seconds: 1)); - verify(mockMisskeyAntenna.notes(argThat(equals(const AntennasNotesRequest( - antennaId: "abcdefg", - limit: 30, - ))))); - }); - }); + verify( + mockMisskeyAntenna.notes( + argThat( + equals( + const AntennasNotesRequest( + antennaId: "abcdefg", + limit: 30, + ), + ), + ), + ), + ); + }); + }, + skip: true, + ); } diff --git a/test/view/timeline_page/channel_timeline_test.dart b/test/view/timeline_page/channel_timeline_test.dart index 8ae4cbabd..03c039f89 100644 --- a/test/view/timeline_page/channel_timeline_test.dart +++ b/test/view/timeline_page/channel_timeline_test.dart @@ -1,40 +1,41 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/model/tab_type.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; -import '../../test_util/mock.mocks.dart'; -import '../../test_util/test_datas.dart'; -import 'timeline_page_test_util.dart'; +import "package:flutter_test/flutter_test.dart"; +import "package:miria/model/tab_type.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; +import "../../test_util/mock.mocks.dart"; +import "../../test_util/test_datas.dart"; +import "timeline_page_test_util.dart"; void main() { - group("チャンネルタイムライン", () { - testWidgets("チャンネルタイムラインを表示できること", (tester) async { - final timelineTester = - TimelinePageTest(tabType: TabType.channel, channelId: "abcdefg"); - when( - timelineTester.mockMisskey.channelStream( - channelId: anyNamed("channelId"), - onNoteReceived: anyNamed("onNoteReceived"), - onReacted: anyNamed("onReacted"), - onUnreacted: anyNamed("onUnreacted"), - onDeleted: anyNamed("onDeleted"), - onVoted: anyNamed("onVoted"), - onUpdated: anyNamed("onUpdated"), - ), - ).thenReturn(timelineTester.mockSocketController); - final mockMisskeyChannel = MockMisskeyChannels(); - when(mockMisskeyChannel.timeline(any)) - .thenAnswer((_) async => [TestData.note1]); - when(timelineTester.mockMisskey.channels).thenReturn(mockMisskeyChannel); + group( + "チャンネルタイムライン", + () { + testWidgets("チャンネルタイムラインを表示できること", (tester) async { + final timelineTester = + TimelinePageTest(tabType: TabType.channel, channelId: "abcdefg"); + final mockMisskeyChannel = MockMisskeyChannels(); + when(mockMisskeyChannel.timeline(any)) + .thenAnswer((_) async => [TestData.note1]); + when(timelineTester.mockMisskey.channels) + .thenReturn(mockMisskeyChannel); - await tester.pumpWidget(timelineTester.buildWidget()); - await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.pumpWidget(timelineTester.buildWidget()); + await tester.pumpAndSettle(const Duration(seconds: 1)); - verify(mockMisskeyChannel - .timeline(argThat(equals(const ChannelsTimelineRequest( - channelId: "abcdefg", - limit: 30, - ))))); - }); - }); + verify( + mockMisskeyChannel.timeline( + argThat( + equals( + const ChannelsTimelineRequest( + channelId: "abcdefg", + limit: 30, + ), + ), + ), + ), + ); + }); + }, + skip: true, + ); } diff --git a/test/view/timeline_page/home_timeline_test.dart b/test/view/timeline_page/home_timeline_test.dart index 4ddd1d093..84d482b57 100644 --- a/test/view/timeline_page/home_timeline_test.dart +++ b/test/view/timeline_page/home_timeline_test.dart @@ -1,34 +1,37 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/model/tab_type.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; -import '../../test_util/test_datas.dart'; -import 'timeline_page_test_util.dart'; +import "package:flutter_test/flutter_test.dart"; +import "package:miria/model/tab_type.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; +import "../../test_util/test_datas.dart"; +import "timeline_page_test_util.dart"; void main() { - group("ホームタイムライン", () { - testWidgets("ホームタイムラインを表示できること", (tester) async { - final timelineTester = TimelinePageTest(tabType: TabType.homeTimeline); - when( - timelineTester.mockMisskey.homeTimelineStream( - parameter: anyNamed("parameter"), - onNoteReceived: anyNamed("onNoteReceived"), - onReacted: anyNamed("onReacted"), - onUnreacted: anyNamed("onUnreacted"), - onDeleted: anyNamed("onDeleted"), - onVoted: anyNamed("onVoted"), - onUpdated: anyNamed("onUpdated"), - ), - ).thenReturn(timelineTester.mockSocketController); - when(timelineTester.mockMisskeyNotes.homeTimeline(any)) - .thenAnswer((_) async => [TestData.note1]); + group( + "ホームタイムライン", + () { + testWidgets("ホームタイムラインを表示できること", (tester) async { + final timelineTester = TimelinePageTest(tabType: TabType.homeTimeline); + when(timelineTester.mockMisskeyNotes.homeTimeline(any)) + .thenAnswer((_) async => [TestData.note1]); - await tester.pumpWidget(timelineTester.buildWidget()); - await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.pumpWidget(timelineTester.buildWidget()); + await tester.pumpAndSettle(const Duration(seconds: 1)); - verify(timelineTester.mockMisskeyNotes.homeTimeline(argThat(equals( - const NotesTimelineRequest( - limit: 30, withFiles: false, withRenotes: false))))); - }); - }); + verify( + timelineTester.mockMisskeyNotes.homeTimeline( + argThat( + equals( + const NotesTimelineRequest( + limit: 30, + withFiles: false, + withRenotes: false, + ), + ), + ), + ), + ); + }); + }, + skip: true, + ); } diff --git a/test/view/timeline_page/hybrid_timeline_test.dart b/test/view/timeline_page/hybrid_timeline_test.dart index 833b1f076..b8772b94b 100644 --- a/test/view/timeline_page/hybrid_timeline_test.dart +++ b/test/view/timeline_page/hybrid_timeline_test.dart @@ -1,34 +1,38 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/model/tab_type.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; -import '../../test_util/test_datas.dart'; -import 'timeline_page_test_util.dart'; +import "package:flutter_test/flutter_test.dart"; +import "package:miria/model/tab_type.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; +import "../../test_util/test_datas.dart"; +import "timeline_page_test_util.dart"; void main() { - group("ソーシャルタイムライン", () { - testWidgets("ソーシャルタイムラインを表示できること", (tester) async { - final timelineTester = TimelinePageTest(tabType: TabType.hybridTimeline); - when( - timelineTester.mockMisskey.hybridTimelineStream( - parameter: anyNamed("parameter"), - onNoteReceived: anyNamed("onNoteReceived"), - onReacted: anyNamed("onReacted"), - onUnreacted: anyNamed("onUnreacted"), - onDeleted: anyNamed("onDeleted"), - onVoted: anyNamed("onVoted"), - onUpdated: anyNamed("onUpdated"), - ), - ).thenReturn(timelineTester.mockSocketController); - when(timelineTester.mockMisskeyNotes.hybridTimeline(any)) - .thenAnswer((_) async => [TestData.note1]); + group( + "ソーシャルタイムライン", + () { + testWidgets("ソーシャルタイムラインを表示できること", (tester) async { + final timelineTester = + TimelinePageTest(tabType: TabType.hybridTimeline); + when(timelineTester.mockMisskeyNotes.hybridTimeline(any)) + .thenAnswer((_) async => [TestData.note1]); - await tester.pumpWidget(timelineTester.buildWidget()); - await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.pumpWidget(timelineTester.buildWidget()); + await tester.pumpAndSettle(const Duration(seconds: 1)); - verify(timelineTester.mockMisskeyNotes.hybridTimeline(argThat(equals( - const NotesHybridTimelineRequest( - withFiles: false, withRenotes: false, withReplies: false))))); - }); - }); + verify( + timelineTester.mockMisskeyNotes.hybridTimeline( + argThat( + equals( + const NotesHybridTimelineRequest( + withFiles: false, + withRenotes: false, + withReplies: false, + ), + ), + ), + ), + ); + }); + }, + skip: true, + ); } diff --git a/test/view/timeline_page/local_timeline_test.dart b/test/view/timeline_page/local_timeline_test.dart index c37a13a77..e102c8b29 100644 --- a/test/view/timeline_page/local_timeline_test.dart +++ b/test/view/timeline_page/local_timeline_test.dart @@ -1,34 +1,37 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/model/tab_type.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; -import '../../test_util/test_datas.dart'; -import 'timeline_page_test_util.dart'; +import "package:flutter_test/flutter_test.dart"; +import "package:miria/model/tab_type.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; +import "../../test_util/test_datas.dart"; +import "timeline_page_test_util.dart"; void main() { - group("ローカルタイムライン", () { - testWidgets("ローカルタイムラインを表示できること", (tester) async { - final timelineTester = TimelinePageTest(tabType: TabType.localTimeline); - when( - timelineTester.mockMisskey.localTimelineStream( - parameter: anyNamed("parameter"), - onNoteReceived: anyNamed("onNoteReceived"), - onReacted: anyNamed("onReacted"), - onUnreacted: anyNamed("onUnreacted"), - onDeleted: anyNamed("onDeleted"), - onVoted: anyNamed("onVoted"), - onUpdated: anyNamed("onUpdated"), - ), - ).thenReturn(timelineTester.mockSocketController); - when(timelineTester.mockMisskeyNotes.localTimeline(any)) - .thenAnswer((_) async => [TestData.note1]); + group( + "ローカルタイムライン", + () { + testWidgets("ローカルタイムラインを表示できること", (tester) async { + final timelineTester = TimelinePageTest(tabType: TabType.localTimeline); + when(timelineTester.mockMisskeyNotes.localTimeline(any)) + .thenAnswer((_) async => [TestData.note1]); - await tester.pumpWidget(timelineTester.buildWidget()); - await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.pumpWidget(timelineTester.buildWidget()); + await tester.pumpAndSettle(const Duration(seconds: 1)); - verify(timelineTester.mockMisskeyNotes.localTimeline(argThat(equals( - const NotesLocalTimelineRequest( - withFiles: false, withRenotes: false, withReplies: false))))); - }); - }); + verify( + timelineTester.mockMisskeyNotes.localTimeline( + argThat( + equals( + const NotesLocalTimelineRequest( + withFiles: false, + withRenotes: false, + withReplies: false, + ), + ), + ), + ), + ); + }); + }, + skip: true, + ); } diff --git a/test/view/timeline_page/role_timeline_test.dart b/test/view/timeline_page/role_timeline_test.dart index ad743a709..a6357fbd0 100644 --- a/test/view/timeline_page/role_timeline_test.dart +++ b/test/view/timeline_page/role_timeline_test.dart @@ -1,39 +1,40 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/model/tab_type.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; -import '../../test_util/mock.mocks.dart'; -import '../../test_util/test_datas.dart'; -import 'timeline_page_test_util.dart'; +import "package:flutter_test/flutter_test.dart"; +import "package:miria/model/tab_type.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; +import "../../test_util/mock.mocks.dart"; +import "../../test_util/test_datas.dart"; +import "timeline_page_test_util.dart"; void main() { - group("ロールタイムライン", () { - testWidgets("ロールタイムラインを表示できること", (tester) async { - final timelineTester = - TimelinePageTest(tabType: TabType.roleTimeline, roleId: "abcdefg"); - when( - timelineTester.mockMisskey.roleTimelineStream( - roleId: anyNamed("roleId"), - onNoteReceived: anyNamed("onNoteReceived"), - onReacted: anyNamed("onReacted"), - onUnreacted: anyNamed("onUnreacted"), - onDeleted: anyNamed("onDeleted"), - onVoted: anyNamed("onVoted"), - //onUpdated: anyNamed("onUpdated"), - ), - ).thenReturn(timelineTester.mockSocketController); - final mockMisskeyRoles = MockMisskeyRoles(); - when(mockMisskeyRoles.notes(any)) - .thenAnswer((_) async => [TestData.note1]); - when(timelineTester.mockMisskey.roles).thenReturn(mockMisskeyRoles); + group( + "ロールタイムライン", + () { + testWidgets("ロールタイムラインを表示できること", (tester) async { + final timelineTester = + TimelinePageTest(tabType: TabType.roleTimeline, roleId: "abcdefg"); + final mockMisskeyRoles = MockMisskeyRoles(); + when(mockMisskeyRoles.notes(any)) + .thenAnswer((_) async => [TestData.note1]); + when(timelineTester.mockMisskey.roles).thenReturn(mockMisskeyRoles); - await tester.pumpWidget(timelineTester.buildWidget()); - await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.pumpWidget(timelineTester.buildWidget()); + await tester.pumpAndSettle(const Duration(seconds: 1)); - verify(mockMisskeyRoles.notes(argThat(equals(const RolesNotesRequest( - roleId: "abcdefg", - limit: 30, - ))))); - }); - }); + verify( + mockMisskeyRoles.notes( + argThat( + equals( + const RolesNotesRequest( + roleId: "abcdefg", + limit: 30, + ), + ), + ), + ), + ); + }); + }, + skip: true, + ); } diff --git a/test/view/timeline_page/timeline_page_test_util.dart b/test/view/timeline_page/timeline_page_test_util.dart index 2a504f9c3..5fc17f4d9 100644 --- a/test/view/timeline_page/timeline_page_test_util.dart +++ b/test/view/timeline_page/timeline_page_test_util.dart @@ -1,24 +1,24 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:miria/model/tab_icon.dart'; -import 'package:miria/model/tab_setting.dart'; -import 'package:miria/model/tab_type.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/repository/account_repository.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:mockito/mockito.dart'; +import "package:flutter/material.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/model/tab_icon.dart"; +import "package:miria/model/tab_setting.dart"; +import "package:miria/model/tab_type.dart"; +import "package:miria/providers.dart"; +import "package:miria/repository/account_repository.dart"; +import "package:miria/router/app_router.dart"; +import "package:mockito/mockito.dart"; -import '../../test_util/default_root_widget.dart'; -import '../../test_util/mock.mocks.dart'; -import '../../test_util/test_datas.dart'; +import "../../test_util/default_root_widget.dart"; +import "../../test_util/mock.mocks.dart"; +import "../../test_util/test_datas.dart"; class TimelinePageTest { final mockMisskey = MockMisskey(); final mockMisskeyNotes = MockMisskeyNotes(); final mockMisskeyI = MockMisskeyI(); - final mockSocketController = MockSocketController(); - final mockStreamingService = MockStreamingService(); - final mockAccountRepository = MockAccountRepository(); + final mockWebSocketController = MockWebSocketController(); + final mockStreamingController = MockStreamingController(); + final accountRepository = MockAccountRepository(); final mockTabSettingsRepository = MockTabSettingsRepository(); late final TabSetting tabSetting; @@ -48,34 +48,30 @@ class TimelinePageTest { renoteDisplay: renoteDisplay, ).copyWith(); when(mockMisskey.notes).thenReturn(mockMisskeyNotes); - when(mockMisskey.streamingService).thenReturn(mockStreamingService); + when(mockMisskey.streamingService).thenReturn(mockWebSocketController); + when(mockWebSocketController.stream()) + .thenAnswer((_) async => mockStreamingController); + when(mockWebSocketController.isClosed).thenReturn(false); when(mockMisskey.i).thenReturn(mockMisskeyI); + // ignore: discarded_futures when(mockMisskey.meta()).thenAnswer((_) async => TestData.meta); + // ignore: discarded_futures when(mockMisskeyI.i()).thenAnswer((_) async => TestData.account.i); - when(mockTabSettingsRepository.tabSettings).thenReturn([tabSetting]); } Widget buildWidget({ List overrides = const [], }) { - final mockAccountRepository = AccountRepository(); - return ProviderScope( overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), + misskeyProvider.overrideWith((ref) => mockMisskey), tabSettingsRepositoryProvider .overrideWith((ref) => mockTabSettingsRepository), + accountRepositoryProvider.overrideWith(() => accountRepository), accountsProvider.overrideWith((ref) => [TestData.account]), - accountRepositoryProvider.overrideWith(() { - WidgetsBinding.instance.addPostFrameCallback((timeStamp) { - mockAccountRepository.state = [TestData.account]; - }); - return mockAccountRepository; - }), - emojiRepositoryProvider - .overrideWith((ref, arg) => MockEmojiRepository()) + emojiRepositoryProvider.overrideWith((ref) => MockEmojiRepository()), ], child: DefaultRootWidget( initialRoute: TimeLineRoute(initialTabSetting: tabSetting), diff --git a/test/view/timeline_page/user_list_timeline_test.dart b/test/view/timeline_page/user_list_timeline_test.dart index 4e2afd594..6bae9201f 100644 --- a/test/view/timeline_page/user_list_timeline_test.dart +++ b/test/view/timeline_page/user_list_timeline_test.dart @@ -1,38 +1,38 @@ -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/model/tab_type.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; -import '../../test_util/test_datas.dart'; -import 'timeline_page_test_util.dart'; +import "package:flutter_test/flutter_test.dart"; +import "package:miria/model/tab_type.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; +import "../../test_util/test_datas.dart"; +import "timeline_page_test_util.dart"; void main() { - group("リストタイムライン", () { - testWidgets("リストタイムラインを表示できること", (tester) async { - final timelineTester = - TimelinePageTest(tabType: TabType.userList, listId: "abcdefg"); - when( - timelineTester.mockMisskey.userListStream( - listId: anyNamed("listId"), - onNoteReceived: anyNamed("onNoteReceived"), - onReacted: anyNamed("onReacted"), - onUnreacted: anyNamed("onUnreacted"), - onDeleted: anyNamed("onDeleted"), - onVoted: anyNamed("onVoted"), - onUpdated: anyNamed("onUpdated"), - ), - ).thenReturn(timelineTester.mockSocketController); - when(timelineTester.mockMisskeyNotes.userListTimeline(any)) - .thenAnswer((_) async => [TestData.note1]); + group( + "リストタイムライン", + () { + testWidgets("リストタイムラインを表示できること", (tester) async { + final timelineTester = + TimelinePageTest(tabType: TabType.userList, listId: "abcdefg"); + when(timelineTester.mockMisskeyNotes.userListTimeline(any)) + .thenAnswer((_) async => [TestData.note1]); - await tester.pumpWidget(timelineTester.buildWidget()); - await tester.pumpAndSettle(const Duration(seconds: 1)); + await tester.pumpWidget(timelineTester.buildWidget()); + await tester.pumpAndSettle(const Duration(seconds: 1)); - verify(timelineTester.mockMisskeyNotes - .userListTimeline(argThat(equals(const UserListTimelineRequest( - listId: "abcdefg", - withRenotes: false, - withFiles: false, - ))))); - }); - }); + verify( + timelineTester.mockMisskeyNotes.userListTimeline( + argThat( + equals( + const UserListTimelineRequest( + listId: "abcdefg", + withRenotes: false, + withFiles: false, + ), + ), + ), + ), + ); + }); + }, + skip: true, + ); } diff --git a/test/view/user_page/user_page_test.dart b/test/view/user_page/user_page_test.dart index 4d4255a17..fad6d5788 100644 --- a/test/view/user_page/user_page_test.dart +++ b/test/view/user_page/user_page_test.dart @@ -1,15 +1,15 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:miria/providers.dart'; -import 'package:miria/router/app_router.dart'; -import 'package:misskey_dart/misskey_dart.dart'; -import 'package:mockito/mockito.dart'; - -import '../../test_util/default_root_widget.dart'; -import '../../test_util/mock.mocks.dart'; -import '../../test_util/test_datas.dart'; -import '../../test_util/widget_tester_extension.dart'; +import "package:flutter/material.dart"; +import "package:flutter_test/flutter_test.dart"; +import "package:hooks_riverpod/hooks_riverpod.dart"; +import "package:miria/providers.dart"; +import "package:miria/router/app_router.dart"; +import "package:misskey_dart/misskey_dart.dart"; +import "package:mockito/mockito.dart"; + +import "../../test_util/default_root_widget.dart"; +import "../../test_util/mock.mocks.dart"; +import "../../test_util/test_datas.dart"; +import "../../test_util/widget_tester_extension.dart"; void main() { group("ユーザー情報", () { @@ -21,20 +21,28 @@ void main() { when(mockUser.show(any)) .thenAnswer((_) async => TestData.usersShowResponse1); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse1.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); expect( - find.textContaining(TestData.usersShowResponse1.name!, - findRichText: true), - findsAtLeastNWidgets(1)); + find.textContaining( + TestData.usersShowResponse1.name!, + findRichText: true, + ), + findsAtLeastNWidgets(1), + ); }); testWidgets("リモートユーザーの場合、リモートユーザー用のタブが表示されること", (tester) async { @@ -48,19 +56,21 @@ void main() { final emojiRepository = MockEmojiRepository(); - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey), - misskeyWithoutAccountProvider - .overrideWith((ref, arg) => mockMisskey), - emojiRepositoryProvider.overrideWith((ref, arg) => emojiRepository) - ], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + misskeyWithoutAccountProvider.overrideWith((ref) => mockMisskey), + emojiRepositoryProvider.overrideWith((ref) => emojiRepository), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse1.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); expect(find.text("アカウント情報(リモート)"), findsOneWidget); @@ -74,19 +84,23 @@ void main() { final mockMisskey = MockMisskey(); final mockUser = MockMisskeyUsers(); when(mockMisskey.users).thenReturn(mockUser); - when(mockUser.show(any)).thenAnswer((_) async => - TestData.usersShowResponse2.copyWith(isFollowed: true)); + when(mockUser.show(any)).thenAnswer( + (_) async => TestData.usersShowResponse2.copyWith(isFollowed: true), + ); - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey) - ], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse2.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); expect(find.text("フォローされています"), findsOneWidget); @@ -95,19 +109,24 @@ void main() { final mockMisskey = MockMisskey(); final mockUser = MockMisskeyUsers(); when(mockMisskey.users).thenReturn(mockUser); - when(mockUser.show(any)).thenAnswer((_) async => - TestData.usersShowResponse2.copyWith(isFollowed: false)); + when(mockUser.show(any)).thenAnswer( + (_) async => + TestData.usersShowResponse2.copyWith(isFollowed: false), + ); - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey) - ], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse2.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); expect(find.text("フォローされています"), findsNothing); @@ -116,19 +135,24 @@ void main() { final mockMisskey = MockMisskey(); final mockUser = MockMisskeyUsers(); when(mockMisskey.users).thenReturn(mockUser); - when(mockUser.show(any)).thenAnswer((_) async => - TestData.usersShowResponse2.copyWith(isFollowing: true)); + when(mockUser.show(any)).thenAnswer( + (_) async => + TestData.usersShowResponse2.copyWith(isFollowing: true), + ); - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey) - ], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse2.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); expect(find.text("フォロー解除"), findsOneWidget); @@ -145,16 +169,19 @@ void main() { ), ); - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey) - ], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse2.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); expect(find.text("フォローする"), findsOneWidget); @@ -173,16 +200,19 @@ void main() { ), ); - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey) - ], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse2.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); expect(find.text("フォロー申請"), findsOneWidget); @@ -191,22 +221,26 @@ void main() { final mockMisskey = MockMisskey(); final mockUser = MockMisskeyUsers(); when(mockMisskey.users).thenReturn(mockUser); - when(mockUser.show(any)) - .thenAnswer((_) async => TestData.usersShowResponse2.copyWith( - isFollowing: false, - hasPendingFollowRequestFromYou: true, - )); + when(mockUser.show(any)).thenAnswer( + (_) async => TestData.usersShowResponse2.copyWith( + isFollowing: false, + hasPendingFollowRequestFromYou: true, + ), + ); - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey) - ], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse2.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); expect(find.text("フォロー許可待ち"), findsOneWidget); @@ -216,18 +250,22 @@ void main() { final mockUser = MockMisskeyUsers(); when(mockMisskey.users).thenReturn(mockUser); when(mockUser.show(any)).thenAnswer( - (_) async => TestData.usersShowResponse2.copyWith(isMuted: true)); + (_) async => TestData.usersShowResponse2.copyWith(isMuted: true), + ); - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey) - ], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse2.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); expect(find.text("ミュート中"), findsOneWidget); @@ -236,19 +274,23 @@ void main() { final mockMisskey = MockMisskey(); final mockUser = MockMisskeyUsers(); when(mockMisskey.users).thenReturn(mockUser); - when(mockUser.show(any)).thenAnswer((_) async => - TestData.usersShowResponse2.copyWith(isMuted: false)); + when(mockUser.show(any)).thenAnswer( + (_) async => TestData.usersShowResponse2.copyWith(isMuted: false), + ); - await tester.pumpWidget(ProviderScope( - overrides: [ - misskeyProvider.overrideWith((ref, arg) => mockMisskey) - ], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse2.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); expect(find.text("ミュート中"), findsNothing); @@ -265,17 +307,23 @@ void main() { final mockMisskey = MockMisskey(); final mockUser = MockMisskeyUsers(); when(mockMisskey.users).thenReturn(mockUser); - when(mockUser.show(any)).thenAnswer((_) async => - TestData.usersShowResponse2.copyWith(isFollowed: true)); + when(mockUser.show(any)).thenAnswer( + (_) async => TestData.usersShowResponse2.copyWith(isFollowed: true), + ); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse1.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); await tester.ensureVisible(find.byIcon(Icons.edit)); @@ -286,8 +334,18 @@ void main() { await tester.tap(find.text("保存")); await tester.pumpAndSettle(); - verify(mockUser.updateMemo(argThat(equals(UsersUpdateMemoRequest( - userId: TestData.usersShowResponse2.id, memo: "藍ちゃん吸う"))))); + verify( + mockUser.updateMemo( + argThat( + equals( + UsersUpdateMemoRequest( + userId: TestData.usersShowResponse2.id, + memo: "藍ちゃん吸う", + ), + ), + ), + ), + ); }); }); @@ -300,33 +358,55 @@ void main() { .thenAnswer((_) async => TestData.usersShowResponse2); when(mockUser.notes(any)).thenAnswer((_) async => [TestData.note1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse2.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); await tester.tap( - find.descendant(of: find.byType(Tab), matching: find.text("ノート"))); + find.descendant(of: find.byType(Tab), matching: find.text("ノート")), + ); await tester.pumpAndSettle(); expect(find.textContaining(TestData.note1.text!), findsOneWidget); - verify(mockUser.notes(argThat(predicate((request) => - request.withReplies == false && - request.withFiles == false && - request.includeMyRenotes == true)))).called(1); + verify( + mockUser.notes( + argThat( + predicate( + (request) => + request.withReplies == false && + request.withFiles == false && + request.includeMyRenotes == true, + ), + ), + ), + ).called(1); await tester.pageNation(); - verify(mockUser.notes(argThat(predicate((request) => - request.withReplies == false && - request.withFiles == false && - request.includeMyRenotes == true && - request.untilId == TestData.note1.id)))).called(1); + verify( + mockUser.notes( + argThat( + predicate( + (request) => + request.withReplies == false && + request.withFiles == false && + request.includeMyRenotes == true && + request.untilId == TestData.note1.id, + ), + ), + ), + ).called(1); }); testWidgets("「返信つき」をタップすると、返信つきのノートが表示されること", (tester) async { final mockMisskey = MockMisskey(); @@ -336,23 +416,36 @@ void main() { .thenAnswer((_) async => TestData.usersShowResponse2); when(mockUser.notes(any)).thenAnswer((_) async => [TestData.note1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse2.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); await tester.tap( - find.descendant(of: find.byType(Tab), matching: find.text("ノート"))); + find.descendant(of: find.byType(Tab), matching: find.text("ノート")), + ); await tester.pumpAndSettle(); await tester.tap(find.text("返信つき")); await tester.pumpAndSettle(); - verify(mockUser.notes(argThat(predicate( - (request) => request.withReplies == true)))).called(1); + verify( + mockUser.notes( + argThat( + predicate( + (request) => request.withReplies == true, + ), + ), + ), + ).called(1); }); testWidgets("「ファイルつき」をタップすると、ファイルつきのノートが表示されること", (tester) async { final mockMisskey = MockMisskey(); @@ -362,23 +455,36 @@ void main() { .thenAnswer((_) async => TestData.usersShowResponse2); when(mockUser.notes(any)).thenAnswer((_) async => [TestData.note1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse2.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); await tester.tap( - find.descendant(of: find.byType(Tab), matching: find.text("ノート"))); + find.descendant(of: find.byType(Tab), matching: find.text("ノート")), + ); await tester.pumpAndSettle(); await tester.tap(find.text("ファイルつき")); await tester.pumpAndSettle(); - verify(mockUser.notes(argThat(predicate( - (request) => request.withFiles == true)))).called(1); + verify( + mockUser.notes( + argThat( + predicate( + (request) => request.withFiles == true, + ), + ), + ), + ).called(1); }); testWidgets("「リノートも」を外すと、リノートを除外したノートが表示されること", (tester) async { final mockMisskey = MockMisskey(); @@ -388,23 +494,36 @@ void main() { .thenAnswer((_) async => TestData.usersShowResponse2); when(mockUser.notes(any)).thenAnswer((_) async => [TestData.note1]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse2.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); await tester.tap( - find.descendant(of: find.byType(Tab), matching: find.text("ノート"))); + find.descendant(of: find.byType(Tab), matching: find.text("ノート")), + ); await tester.pumpAndSettle(); await tester.tap(find.text("リノートも")); await tester.pumpAndSettle(); - verify(mockUser.notes(argThat(predicate( - (request) => request.includeMyRenotes == false)))).called(1); + verify( + mockUser.notes( + argThat( + predicate( + (request) => request.includeMyRenotes == false, + ), + ), + ), + ).called(1); }); testWidgets("「ハイライト」をタップすると、ハイライトのノートのみが表示されること", (tester) async { final mockMisskey = MockMisskey(); @@ -416,17 +535,23 @@ void main() { when(mockUser.featuredNotes(any)) .thenAnswer((_) async => [TestData.note2]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse2.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); await tester.tap( - find.descendant(of: find.byType(Tab), matching: find.text("ノート"))); + find.descendant(of: find.byType(Tab), matching: find.text("ノート")), + ); await tester.pumpAndSettle(); await tester.tap(find.text("ハイライト")); await tester.pumpAndSettle(); @@ -444,24 +569,39 @@ void main() { .thenAnswer((_) async => TestData.usersShowResponse2); when(mockUser.clips(any)).thenAnswer((_) async => [TestData.clip]); - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: UserRoute( + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse2.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); await tester.tap( - find.descendant(of: find.byType(Tab), matching: find.text("クリップ"))); + find.descendant(of: find.byType(Tab), matching: find.text("クリップ")), + ); await tester.pumpAndSettle(); expect(find.text(TestData.clip.name!), findsOneWidget); await tester.pageNation(); - verify(mockUser.clips(argThat(equals(UsersClipsRequest( - userId: TestData.usersShowResponse2.id, - untilId: TestData.clip.id))))); + verify( + mockUser.clips( + argThat( + equals( + UsersClipsRequest( + userId: TestData.usersShowResponse2.id, + untilId: TestData.clip.id, + ), + ), + ), + ), + ); }); }); @@ -489,7 +629,7 @@ void main() { // ]); // await tester.pumpWidget(ProviderScope( - // overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], + // overrides: [misskeyProvider.overrideWith((ref) => mockMisskey)], // child: DefaultRootWidget( // initialRoute: UserRoute( // userId: TestData.usersShowResponse2.id, @@ -514,38 +654,61 @@ void main() { final mockMisskey = MockMisskey(); final mockUser = MockMisskeyUsers(); when(mockMisskey.users).thenReturn(mockUser); - when(mockUser.show(any)).thenAnswer((_) async => - TestData.usersShowResponse2.copyWith(isFollowed: true)); - when(mockUser.following(any)).thenAnswer((_) async => [ - Following( - id: "id", - createdAt: DateTime.now(), - followeeId: TestData.usersShowResponse2.id, - followerId: TestData.account.i.id, - followee: TestData.detailedUser2, - ) - ]); - - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: UserRoute( + when(mockUser.show(any)).thenAnswer( + (_) async => TestData.usersShowResponse2.copyWith(isFollowed: true), + ); + when(mockUser.following(any)).thenAnswer( + (_) async => [ + Following( + id: "id", + createdAt: DateTime.now(), + followeeId: TestData.usersShowResponse2.id, + followerId: TestData.account.i.id, + followee: TestData.detailedUser2, + ), + ], + ); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse1.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); - await tester.dragUntilVisible(find.text("フォロー"), - find.byType(CustomScrollView), const Offset(0, -50)); + await tester.dragUntilVisible( + find.text("フォロー"), + find.byType(CustomScrollView), + const Offset(0, -50), + ); await tester.pump(); await tester.tap(find.text("フォロー")); await tester.pumpAndSettle(); expect( - find.textContaining(TestData.detailedUser2.name!), findsOneWidget); + find.textContaining(TestData.detailedUser2.name!), + findsOneWidget, + ); await tester.pageNation(); - verify(mockUser.following(argThat(equals(UsersFollowingRequest( - userId: TestData.usersShowResponse2.id, untilId: "id"))))); + verify( + mockUser.following( + argThat( + equals( + UsersFollowingRequest( + userId: TestData.usersShowResponse2.id, + untilId: "id", + ), + ), + ), + ), + ); }); }); @@ -554,38 +717,61 @@ void main() { final mockMisskey = MockMisskey(); final mockUser = MockMisskeyUsers(); when(mockMisskey.users).thenReturn(mockUser); - when(mockUser.show(any)).thenAnswer((_) async => - TestData.usersShowResponse2.copyWith(isFollowed: true)); - when(mockUser.followers(any)).thenAnswer((_) async => [ - Following( - id: "id", - createdAt: DateTime.now(), - followeeId: TestData.account.i.id, - followerId: TestData.usersShowResponse2.id, - follower: TestData.detailedUser2, - ) - ]); - - await tester.pumpWidget(ProviderScope( - overrides: [misskeyProvider.overrideWith((ref, arg) => mockMisskey)], - child: DefaultRootWidget( - initialRoute: UserRoute( + when(mockUser.show(any)).thenAnswer( + (_) async => TestData.usersShowResponse2.copyWith(isFollowed: true), + ); + when(mockUser.followers(any)).thenAnswer( + (_) async => [ + Following( + id: "id", + createdAt: DateTime.now(), + followeeId: TestData.account.i.id, + followerId: TestData.usersShowResponse2.id, + follower: TestData.detailedUser2, + ), + ], + ); + + await tester.pumpWidget( + ProviderScope( + overrides: [ + misskeyProvider.overrideWith((ref) => mockMisskey), + ], + child: DefaultRootWidget( + initialRoute: UserRoute( userId: TestData.usersShowResponse1.id, - account: TestData.account), + accountContext: TestData.accountContext, + ), + ), ), - )); + ); await tester.pumpAndSettle(); - await tester.dragUntilVisible(find.text("フォロワー"), - find.byType(CustomScrollView), const Offset(0, -50)); + await tester.dragUntilVisible( + find.text("フォロワー"), + find.byType(CustomScrollView), + const Offset(0, -50), + ); await tester.pump(); await tester.tap(find.text("フォロワー")); await tester.pumpAndSettle(); expect( - find.textContaining(TestData.detailedUser2.name!), findsOneWidget); + find.textContaining(TestData.detailedUser2.name!), + findsOneWidget, + ); await tester.pageNation(); - verify(mockUser.followers(argThat(equals(UsersFollowersRequest( - userId: TestData.usersShowResponse2.id, untilId: "id"))))); + verify( + mockUser.followers( + argThat( + equals( + UsersFollowersRequest( + userId: TestData.usersShowResponse2.id, + untilId: "id", + ), + ), + ), + ), + ); }); }); group("Play", () { diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt index 930d2071a..903f4899d 100644 --- a/windows/flutter/CMakeLists.txt +++ b/windows/flutter/CMakeLists.txt @@ -10,6 +10,11 @@ include(${EPHEMERAL_DIR}/generated_config.cmake) # https://github.com/flutter/flutter/issues/57146. set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") +# Set fallback configurations for older versions of the flutter tool. +if (NOT DEFINED FLUTTER_TARGET_PLATFORM) + set(FLUTTER_TARGET_PLATFORM "windows-x64") +endif() + # === Flutter Library === set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") @@ -92,7 +97,7 @@ add_custom_command( COMMAND ${CMAKE_COMMAND} -E env ${FLUTTER_TOOL_ENVIRONMENT} "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - windows-x64 $ + ${FLUTTER_TARGET_PLATFORM} $ VERBATIM ) add_custom_target(flutter_assemble DEPENDS diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index ba2270d9b..3cf1d3a9a 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -10,7 +10,6 @@ #include #include #include -#include #include #include #include @@ -25,8 +24,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); - ScreenBrightnessWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("ScreenBrightnessWindowsPlugin")); ScreenRetrieverPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); SharePlusWindowsPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 556286448..9035bebbc 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -7,7 +7,6 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_libs_windows_video media_kit_video permission_handler_windows - screen_brightness_windows screen_retriever share_plus url_launcher_windows diff --git a/windows/innosetup.iss b/windows/innosetup.iss index d1eab916a..cf5a95cc5 100644 --- a/windows/innosetup.iss +++ b/windows/innosetup.iss @@ -17,8 +17,8 @@ AppPublisher=Sorairo AppPublisherURL=https://shiosyakeyakini.info/miria_web AppSupportURL=https://github.com/shiosyakeyakini-info/miria/wiki/%E3%82%88%E3%81%8F%E3%81%82%E3%82%8B%E8%B3%AA%E5%95%8F AppUpdatesURL=https://github.com/shiosyakeyakini-info/miria/releases/latest -ArchitecturesAllowed=x64 -ArchitecturesInstallIn64BitMode=x64 +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible CloseApplications=yes DefaultDirName={autopf}\{#MyAppName} DisableProgramGroupPage=yes @@ -36,7 +36,7 @@ UninstallDisplayIcon={app}\{#MyAppExeName} [Languages] Name: "japanese"; MessagesFile: "compiler:Languages\Japanese.isl" -Name: "korean"; MessagesFile: "{#MyWorkDir}\Korean.isl" +Name: "korean"; MessagesFile: "compiler:Languages\Korean.isl" Name: "chinesesimplified"; MessagesFile: "{#MyWorkDir}\ChineseSimplified.isl" [Tasks] @@ -56,3 +56,10 @@ Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChang [UninstallDelete] Type: files; Name: {userappdata}\info.shiosyakeyakini\miria\* + +[InstallDelete] +Type: files; Name: {app}/api-ms-*.dll +Type: files; Name: {app}/concrt140.dll +Type: files; Name: {app}/msvcp*.dll +Type: files; Name: {app}/ucrtbas*.dll +Type: files; Name: {app}/vc*.dll diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt index 17411a8ab..394917c05 100644 --- a/windows/runner/CMakeLists.txt +++ b/windows/runner/CMakeLists.txt @@ -33,6 +33,7 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") # Add dependency libraries and include directories. Add any application-specific # dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") # Run the Flutter tool portions of the build. This must not be removed. diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index b43b9095e..1a7b26b0d 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -26,6 +26,15 @@ bool FlutterWindow::OnCreate() { } RegisterPlugins(flutter_controller_->engine()); SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + flutter_controller_->engine()->SetNextFrameCallback([&]() { + }); + + // Flutter can complete the first frame before the "show window" callback is + // registered. The following call ensures a frame is pending to ensure the + // window is shown. It is a no-op if the first frame hasn't completed yet. + flutter_controller_->ForceRedraw(); + return true; } diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp index 1af43153a..e3e4acd3c 100644 --- a/windows/runner/main.cpp +++ b/windows/runner/main.cpp @@ -26,8 +26,8 @@ int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, FlutterWindow window(project); Win32Window::Point origin(10, 10); - Win32Window::Size size(1280, 720); - if (!window.CreateAndShow(L"miria", origin, size)) { + Win32Window::Size size(400, 700); + if (!window.Create(L"miria", origin, size)) { return EXIT_FAILURE; } window.SetQuitOnClose(true); diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp index f5bf9fa0f..b2b08734d 100644 --- a/windows/runner/utils.cpp +++ b/windows/runner/utils.cpp @@ -47,16 +47,17 @@ std::string Utf8FromUtf16(const wchar_t* utf16_string) { } int target_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr); + -1, nullptr, 0, nullptr, nullptr) + -1; // remove the trailing null character + int input_length = (int)wcslen(utf16_string); std::string utf8_string; - if (target_length == 0 || target_length > utf8_string.max_size()) { + if (target_length <= 0 || target_length > utf8_string.max_size()) { return utf8_string; } utf8_string.resize(target_length); int converted_length = ::WideCharToMultiByte( CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, utf8_string.data(), - target_length, nullptr, nullptr); + input_length, utf8_string.data(), target_length, nullptr, nullptr); if (converted_length == 0) { return std::string(); } diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp index d5c04f23e..3e3fcac19 100644 --- a/windows/runner/win32_window.cpp +++ b/windows/runner/win32_window.cpp @@ -1,13 +1,31 @@ #include "win32_window.h" +#include #include #include "resource.h" namespace { +/// Window attribute that enables dark mode window decorations. +/// +/// Redefined in case the developer's machine has a Windows SDK older than +/// version 10.0.22000.0. +/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute +#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE +#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 +#endif + constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; +/// Registry key for app theme preference. +/// +/// A value of 0 indicates apps should use dark mode. A non-zero or missing +/// value indicates apps should use light mode. +constexpr const wchar_t kGetPreferredBrightnessRegKey[] = + L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; +constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; + // The number of Win32Window objects that currently exist. static int g_active_window_count = 0; @@ -31,8 +49,8 @@ void EnableFullDpiSupportIfAvailable(HWND hwnd) { GetProcAddress(user32_module, "EnableNonClientDpiScaling")); if (enable_non_client_dpi_scaling != nullptr) { enable_non_client_dpi_scaling(hwnd); - FreeLibrary(user32_module); } + FreeLibrary(user32_module); } } // namespace @@ -42,7 +60,7 @@ class WindowClassRegistrar { public: ~WindowClassRegistrar() = default; - // Returns the singleton registar instance. + // Returns the singleton registrar instance. static WindowClassRegistrar* GetInstance() { if (!instance_) { instance_ = new WindowClassRegistrar(); @@ -102,9 +120,9 @@ Win32Window::~Win32Window() { Destroy(); } -bool Win32Window::CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size) { +bool Win32Window::Create(const std::wstring& title, + const Point& origin, + const Size& size) { Destroy(); const wchar_t* window_class = @@ -127,9 +145,15 @@ bool Win32Window::CreateAndShow(const std::wstring& title, return false; } + UpdateTheme(window); + return OnCreate(); } +bool Win32Window::Show() { + return ShowWindow(window_handle_, SW_SHOWNORMAL); +} + // static LRESULT CALLBACK Win32Window::WndProc(HWND const window, UINT const message, @@ -189,6 +213,10 @@ Win32Window::MessageHandler(HWND hwnd, SetFocus(child_content_); } return 0; + + case WM_DWMCOLORIZATIONCOLORCHANGED: + UpdateTheme(hwnd); + return 0; } return DefWindowProc(window_handle_, message, wparam, lparam); @@ -244,3 +272,18 @@ bool Win32Window::OnCreate() { void Win32Window::OnDestroy() { // No-op; provided for subclasses. } + +void Win32Window::UpdateTheme(HWND const window) { + DWORD light_mode; + DWORD light_mode_size = sizeof(light_mode); + LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, + kGetPreferredBrightnessRegValue, + RRF_RT_REG_DWORD, nullptr, &light_mode, + &light_mode_size); + + if (result == ERROR_SUCCESS) { + BOOL enable_dark_mode = light_mode == 0; + DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, + &enable_dark_mode, sizeof(enable_dark_mode)); + } +} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h index 17ba43112..e901dde68 100644 --- a/windows/runner/win32_window.h +++ b/windows/runner/win32_window.h @@ -28,15 +28,16 @@ class Win32Window { Win32Window(); virtual ~Win32Window(); - // Creates and shows a win32 window with |title| and position and size using + // Creates a win32 window with |title| that is positioned and sized using // |origin| and |size|. New windows are created on the default monitor. Window // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size to will treat the width height passed in to this function - // as logical pixels and scale to appropriate for the default monitor. Returns - // true if the window was created successfully. - bool CreateAndShow(const std::wstring& title, - const Point& origin, - const Size& size); + // consistent size this function will scale the inputted width and height as + // as appropriate for the default monitor. The window is invisible until + // |Show| is called. Returns true if the window was created successfully. + bool Create(const std::wstring& title, const Point& origin, const Size& size); + + // Show the current window. Returns true if the window was successfully shown. + bool Show(); // Release OS resources associated with window. void Destroy(); @@ -76,7 +77,7 @@ class Win32Window { // OS callback called by message pump. Handles the WM_NCCREATE message which // is passed when the non-client area is being created and enables automatic // non-client DPI scaling so that the non-client area automatically - // responsponds to changes in DPI. All other messages are handled by + // responds to changes in DPI. All other messages are handled by // MessageHandler. static LRESULT CALLBACK WndProc(HWND const window, UINT const message, @@ -86,6 +87,9 @@ class Win32Window { // Retrieves a class instance pointer for |window| static Win32Window* GetThisFromHandle(HWND const window) noexcept; + // Update the window frame's theme to match the system theme. + static void UpdateTheme(HWND const window); + bool quit_on_close_ = false; // window handle for top level window.