diff --git a/.fvmrc b/.fvmrc index 906bbb349..691c22dd1 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.24.3" -} \ No newline at end of file + "flutter": "3.24.5" +} diff --git a/.github/workflows/linux_deploy.yml b/.github/workflows/linux_deploy.yml new file mode 100644 index 000000000..f70b119eb --- /dev/null +++ b/.github/workflows/linux_deploy.yml @@ -0,0 +1,140 @@ + +name: デプロイ(linux) +on: + workflow_dispatch: + release: + types: [published] + +env: + APP_NAME: 'miria' + MAINTAINER: 'sorairo ' + +permissions: + contents: write + +jobs: + build-snap: + 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 + + + build-debs: + name: ビルド(Deb) + runs-on: ubuntu-latest + strategy: + matrix: + platform: [amd64] + steps: + - name: Clone repository + 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: + flutter-version: ${{ env.FLUTTER_FVM_VERSION }} + cache: true + + - name: Patch for linux build + run: | + sudo apt-get update -y + sudo apt-get install -y ninja-build libgtk-3-dev libsecret-1-dev libstdc++-12-dev nasm + sudo pip3 install meson + + - name: Build libmpv + run: | + git clone https://github.com/mpv-player/mpv-build.git + cd mpv-build + sed -i 's|checkout_ffmpeg=master|checkout_ffmpeg=@dcdfd7fb62464beeeb03c24f21713bf3914b9ea4|g' update + sed -i 's|checkout_libplacebo=master|checkout_libplacebo=@ed29e541a55acf28022738440b2a925386292551|g' update + sed -i 's|checkout_mpv=master|checkout_mpv=@140ec21c89d671d392877a7f3b91d67e7d7b9239|g' update + sed -i 's|OPTIONS="--enable-static --disable-shared"|OPTIONS="--enable-shared"|g' scripts/libass-config + sed -i 's|--prefix="$BUILD/build_libs" --libdir="$BUILD/build_libs/lib"||g' scripts/libass-config + sed -i 's|^meson setup build.*||g' scripts/mpv-config + echo -e "\npython3 ./bootstrap.py\n./waf configure --disable-alsa --enable-libmpv-shared\n./waf build" >> scripts/mpv-config + ./update + ./scripts/libplacebo-config + ./scripts/libplacebo-build -j$(nproc) + ./scripts/libass-config + sudo make -C libass install -j$(nproc) + ./scripts/ffmpeg-config + ./scripts/ffmpeg-build -j$(nproc) + ./scripts/mpv-config + cd mpv + sudo ./waf install + + - run: flutter pub get +# - run: flutter test + - run: flutter build linux + - name: Get Build Version + run: | + echo "VERSION=$(yq -r '.version' pubspec.yaml)" >> $GITHUB_ENV + + - name: Prepare to build DEB + run: | + echo "DESC=$(awk -F '=' '/^Comment=/{print $2}' ./snap/gui/miria.desktop)" >> $GITHUB_ENV + sed -i -E 's|^Version=.*|Version=${{ env.VERSION }}|g' ./snap/gui/miria.desktop + sed -i -E 's|^Icon=.*|Icon=/usr/share/pixmaps/miria.png|g' ./snap/gui/miria.desktop + sed -i -E 's|^Exec=.*|Exec=/opt/miria/miria|g' ./snap/gui/miria.desktop + mkdir -p .debpkg/opt/miria .debpkg/usr/share/applications .debpkg/usr/share/pixmaps + cp -rp ./build/linux/x64/release/bundle/* .debpkg/opt/miria/ + cp ./snap/gui/miria.desktop .debpkg/usr/share/applications/ + cp ./assets/images/icon.png .debpkg/usr/share/pixmaps/miria.png + + - name: Build DEB + uses: jiro4989/build-deb-action@v3 + with: + desc: '${{ env.DESC }}' + package: ${{ env.APP_NAME }} + maintainer: ${{ env.MAINTAINER }} + version: ${{ env.VERSION }} + arch: "amd64" + package_root: ".debpkg" + depends: "libgtk-3-0, libstdc++6, libx11-6, libmpv2, libsecret-1-0" + + - name: Upload DEB + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload v$VERSION ./miria_${{ env.VERSION }}_amd64.deb \ No newline at end of file diff --git a/.github/workflows/snap_deploy.yml b/.github/workflows/snap_deploy.yml deleted file mode 100644 index f956e9571..000000000 --- a/.github/workflows/snap_deploy.yml +++ /dev/null @@ -1,54 +0,0 @@ - -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 cf75e9a65..740ed8bbb 100644 --- a/.github/workflows/windows_deploy.yml +++ b/.github/workflows/windows_deploy.yml @@ -41,10 +41,6 @@ jobs: - 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: | diff --git a/README-ja.md b/README-ja.md index 3fc5e32a2..1ca27146b 100644 --- a/README-ja.md +++ b/README-ja.md @@ -10,7 +10,8 @@ MiriaはiOS, Android向けMisskeyクライアントです。Windowsでも動作 - [Play Store](https://play.google.com/store/apps/details?id=info.shiosyakeyakini.miria) - [App Store](https://apps.apple.com/jp/app/miria/id6449201469) - 最新版をGitHubからインストール - - [GitHub最新版のリリース(Windows版もあります)](https://github.com/shiosyakeyakini-info/miria/releases/latest) + - [GitHub最新版のリリース(Windows版とSnapパッケージもあります)](https://github.com/shiosyakeyakini-info/miria/releases/latest) + - [Obtainium(APKマネージャー)](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22info.shiosyakeyakini.miria%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fshiosyakeyakini-info%2Fmiria%22%2C%22author%22%3A%22shiosyakeyakini-info%22%2C%22name%22%3A%22miria%22%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22dontSortReleasesList%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionDetection%5C%22%3Atrue%7D%22%2C%22overrideSource%22%3Anull%7D) - 最新版をTestFlightからインストール - [TestFlight](https://testflight.apple.com/join/X6Q7pE98) @@ -37,7 +38,7 @@ MiriaはiOS, Android向けMisskeyクライアントです。Windowsでも動作 - ただし、一部の機能を独自に取り込んだり、特定のサーバー向けの実装を行っている場合があります。 - MiriaはMisskey v13以上とそれらのフォークされたサーバーでのみ動作します。 - [Sharkey](https://joinsharkey.org/)、[CherryPick](https://github.com/kokonect-link/cherrypick)、[もこきー](https://mkkey.net/)では動作するかもしれませんが、テストは行われていません。 - - [Firefish](https://joinfirefish.org/ja/)、[Catodon](https://catodon.social/)およびMastodonへは今後もサポートする予定はありません。 + - [Iceshrimp](https://iceshrimp.dev/)、[Catodon](https://catodon.social/)およびMastodonへは今後もサポートする予定はありません。 - MiriaはAiScript関連の機能(プラグイン、Play)は実装されていません。難しすぎるためです。 - Miriaはブラウザの機能に依存していません。 - MFMの見た目がブラウザと異なる場合があります。 diff --git a/README.md b/README.md index b810356e0..839b4bb5d 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,11 @@ Miria is Misskey Client App for iOS, Android and many targets which made by Flut ## Install -- [Play Store](https://play.google.com/store/apps/details?id=info.shiosyakeyakini.miria) - [App Store](https://apps.apple.com/jp/app/miria/id6449201469) -- [Download latest release from GitHub(Include Windows Version)](https://github.com/shiosyakeyakini-info/miria/releases/latest) +- [Play Store](https://play.google.com/store/apps/details?id=info.shiosyakeyakini.miria) - [TestFlight](https://testflight.apple.com/join/X6Q7pE98) +- [Obtainium](https://apps.obtainium.imranr.dev/redirect?r=obtainium://app/%7B%22id%22%3A%22info.shiosyakeyakini.miria%22%2C%22url%22%3A%22https%3A%2F%2Fgithub.com%2Fshiosyakeyakini-info%2Fmiria%22%2C%22author%22%3A%22shiosyakeyakini-info%22%2C%22name%22%3A%22miria%22%2C%22additionalSettings%22%3A%22%7B%5C%22includePrereleases%5C%22%3Atrue%2C%5C%22fallbackToOlderReleases%5C%22%3Atrue%2C%5C%22verifyLatestTag%5C%22%3Afalse%2C%5C%22dontSortReleasesList%5C%22%3Afalse%2C%5C%22releaseTitleAsVersion%5C%22%3Afalse%2C%5C%22trackOnly%5C%22%3Afalse%2C%5C%22versionDetection%5C%22%3Atrue%7D%22%2C%22overrideSource%22%3Anull%7D) +- [Download latest release from GitHub(Include Windows Version & Snap packages)](https://github.com/shiosyakeyakini-info/miria/releases/latest) I'm planning to deploy from F-Droid. @@ -33,12 +34,12 @@ 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. + - Available in [Sharkey](https://joinsharkey.org/), [CherryPick](https://github.com/kokonect-link/cherrypick) and [mkkey.net](https://mkkey.net/), but not tested. + - [Iceshrimp](https://iceshrimp.dev/), [Catodon](https://catodon.social/), Mastodon will not be supported in the future. - Miria does not support AiScript related features. (Plugin, Play) -- Miria does not depend browser features. ¥ +- Miria does not depend on browser features. - There are cases in which Miria's MFM appearance is different from browsers. - - Custom CSS didn't support. + - Custom CSS not supported. ## Contribute diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 2dd1fa0d1..907199eed 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -215,6 +215,6 @@ SPEC CHECKSUMS: wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 webview_flutter_wkwebview: 0982481e3d9c78fd5c6f62a002fcd24fc791f1e4 -PODFILE CHECKSUM: 9a5aff7d2a4fe2dcc914618b99f9ff063b14915b +PODFILE CHECKSUM: fc9f5883117ffc5dbdfedd53e44b13710e7871f2 COCOAPODS: 1.14.3 diff --git a/lib/l10n/app_ja-oj.arb b/lib/l10n/app_ja-oj.arb index bac68a8d4..e10be7c48 100644 --- a/lib/l10n/app_ja-oj.arb +++ b/lib/l10n/app_ja-oj.arb @@ -8,7 +8,6 @@ "noneAction": "なにもいたしませんわ", "pleaseInput": "お入れなさって", "pleaseSelect": "お選びになさって", - "clipDescription": "説明(なくてもよろしくてよ)", "serverRules": "サーバーの定め", "antennaSourceUserHintText": "ユーザーネームを改行で区切って指定いたしますわ", @@ -22,9 +21,6 @@ "confirmDeletingAntenna": "アンテナを削除いたしますこと?", "channelJoinningCounts": "{usersCount}人が参加なされてますわ", - "willFavorite": "お気に入りに入れますわ", - "willFollow": "フォローいたしますわ", - "confirmDeleteClip": "クリップを削除いたしますこと?", "clipDescription": "説明(省略されてもよろしくてよ)", "alreadyAddedClip": "こちらのノートすでにクリップに追加されていますわ", @@ -58,6 +54,8 @@ "openAsOtherAccount": "開くアカウントを選びなさって", "pickColor": "色を選びなさって", "decideColor": "これにいたしますわ", + "replyNotePlaceholder": "何と送りますの?", + "defaultNotePlaceholder": "ごきげんよう", "followedNotification": "{userName}からフォローされましてよ", "followRequestAcceptedNotification": "{userName}がフォローしてもよくってよとお聞きいたしましたわ", @@ -106,10 +104,14 @@ "cannotMentionToRemoteInLocalOnlyNote": "連合切られているのに他のサーバーの人がメンションに含まれているようですわ", "cannotPublicReplyToPrivateNote": "リプライが{visibility}のようでして……パブリックにはできませんこと", - "unsupportedFile": "対応してないファイルのようですわ", - "failedFileSave": "ファイルの保存に失敗したようですわね…" + "memoDescription": "メモしたいことをお書きくださいまし", + "confirmCreateBlock": "ブロックなさりますの?", + "unsupportedFile": "対応してないファイルのようですわ", + "unsupportedFileWithFilename": "{filename}は対応してないファイルのようですわ", + "failedFileSave": "ファイルの保存に失敗したようですわね…", + "nothingHere": "ここには何もありませんわ" diff --git a/lib/l10n/app_ja.arb b/lib/l10n/app_ja.arb index bb3497262..a8482059e 100644 --- a/lib/l10n/app_ja.arb +++ b/lib/l10n/app_ja.arb @@ -990,6 +990,7 @@ "updatedAtDescendingOrder": "更新された順", "unsupportedFile": "対応してないファイルやわ", + "unsupportedFileWithFilename": "{filename}は対応してないファイルやわ", "failedFileSave": "ファイルの保存に失敗したみたいや", "misskeyGames": "Misskey Games", @@ -1006,6 +1007,12 @@ } } }, - "nonInvitedReversi": "招待はされとらへんみたいや" + "nonInvitedReversi": "招待はされとらへんみたいや", + + "remoteServerWithoutLogin": "相手先のサーバー(ログインなし)", + "nothingHere": "なんもないで", + + "deckMode": "デッキモード", + "enableDeckMode": "デッキモードにする" } diff --git a/lib/main.dart b/lib/main.dart index e6aeb47cd..cdce35502 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -56,6 +56,7 @@ class Miria extends HookConsumerWidget with WidgetsBindingObserver { } Future _initWindow(WidgetRef ref) async { + if (!isDesktop) return; await windowManager.setPreventClose(true); final config = ref.read(desktopSettingsRepositoryProvider).settings; @@ -187,7 +188,8 @@ class MiriaWindowListener with WindowListener { } catch (e) { if (kDebugMode) print(e); } finally { - await windowManager.destroy(); + await windowManager.setPreventClose(false); + await windowManager.close(); } } } diff --git a/lib/repository/general_settings_repository.dart b/lib/repository/general_settings_repository.dart index 3edd9c163..a8e67b378 100644 --- a/lib/repository/general_settings_repository.dart +++ b/lib/repository/general_settings_repository.dart @@ -25,8 +25,8 @@ class GeneralSettingsRepository extends ChangeNotifier { Future update(GeneralSettings settings) async { _settings = settings; - notifyListeners(); final prefs = await SharedPreferences.getInstance(); await prefs.setString("general_settings", jsonEncode(settings.toJson())); + notifyListeners(); } } diff --git a/lib/repository/import_export_repository.dart b/lib/repository/import_export_repository.dart index 9966b1765..6ef46dbd2 100644 --- a/lib/repository/import_export_repository.dart +++ b/lib/repository/import_export_repository.dart @@ -108,8 +108,10 @@ class ImportExportRepository extends ChangeNotifier { await SimpleMessageDialog.show(context, S.of(context).importCompleted); if (!context.mounted) return; - context.router.removeWhere((route) => true); - await context.router.push(const SplashRoute()); + await context.router.pushAndPopUntil( + const SplashRoute(), + predicate: (_) => false, + ); } Future export(BuildContext context, Account account) async { diff --git a/lib/router/app_router.gr.dart b/lib/router/app_router.gr.dart index 2477e2e3a..bda93569c 100644 --- a/lib/router/app_router.gr.dart +++ b/lib/router/app_router.gr.dart @@ -514,11 +514,12 @@ abstract class _$AppRouter extends RootStackRouter { final args = routeData.argsAs(); return AutoRoutePage( routeData: routeData, - child: RenoteModalSheet( + child: WrappedRoute( + child: RenoteModalSheet( note: args.note, account: args.account, key: args.key, - ), + )), ); }, RenoteUserRoute.name: (routeData) { 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 32da9539d..d102596b3 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,12 +1,16 @@ +import "dart:io"; 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:freezed_annotation/freezed_annotation.dart"; +import "package:image/image.dart"; import "package:mfm_parser/mfm_parser.dart"; +import "package:mime/mime.dart"; import "package:miria/extensions/note_visibility_extension.dart"; import "package:miria/log.dart"; import "package:miria/model/image_file.dart"; @@ -155,25 +159,32 @@ class NoteCreateNotifier extends _$NoteCreateNotifier { } if (initialMediaFiles != null && initialMediaFiles.isNotEmpty) { resultState = resultState.copyWith( - files: await Future.wait( + files: (await Future.wait( initialMediaFiles.map((media) async { final file = _fileSystem.file(media); - final contents = await file.readAsBytes(); final fileName = file.basename; final extension = fileName.split(".").last.toLowerCase(); - if (["jpg", "png", "gif", "webp"].contains(extension)) { - return ImageFile( - data: contents, - fileName: fileName, - ); + if (["jpg", "jpeg", "png", "gif", "webp", "heic", "tif", "tiff"] + .contains(extension)) { + final d = await loadImage(file); + if (d.data.isEmpty) { + await _dialogNotifier.showSimpleDialog( + message: (context) => + S.of(context).unsupportedFileWithFilename(fileName), + ); + return null; + } + return d; } else { return UnknownFile( - data: contents, + data: await file.readAsBytes(), fileName: fileName, ); } }), - ), + )) + .nonNulls + .toList(), ); } @@ -592,8 +603,8 @@ class NoteCreateNotifier extends _$NoteCreateNotifier { final result = await FilePicker.platform.pickFiles( type: FileType.image, allowMultiple: true, - allowCompression: false, - compressionQuality: 0, + allowCompression: Platform.isIOS, // v8.1.3ではiOS以外でこの値を使用していない + compressionQuality: 0, // Androidでは0にすることで圧縮パススルー ); if (result == null || result.files.isEmpty) return; @@ -606,14 +617,87 @@ class NoteCreateNotifier extends _$NoteCreateNotifier { }).nonNulls; final files = await Future.wait( fsFiles.map( - (file) async => ImageFile( - data: await file.readAsBytes(), - fileName: file.basename, - ), + (file) async { + final d = await loadImage(file); + if (d.data.isEmpty) { + await _dialogNotifier.showSimpleDialog( + message: (context) => + S.of(context).unsupportedFileWithFilename(file.basename), + ); + return null; + } + return d; + }, ), ); - state = state.copyWith(files: [...state.files, ...files]); + state = state.copyWith( + files: [ + ...state.files, + ...files.nonNulls, + ], + ); + } + } + + Future loadImage(File file) async { + try { + final imageBytes = await file.readAsBytes(); + final mime = lookupMimeType(file.path, headerBytes: imageBytes); + var basename = file.basename; + + switch (mime) { + case "image/jpeg": + if (!RegExp(r"\.jpe?g$", caseSensitive: false).hasMatch(basename)) { + basename = "$basename.jpg"; + } + + final origExif = decodeJpgExif(imageBytes); + if (origExif == null || origExif.isEmpty) { + return ImageFile(fileName: basename, data: imageBytes); + } + + final exif = ExifData(); + exif.imageIfd.orientation = (origExif.imageIfd.hasOrientation) + ? origExif.imageIfd.orientation + : 1; + + return ImageFile( + fileName: basename, + data: injectJpgExif(imageBytes, exif) ?? Uint8List(0), + ); + + case "image/heic": + return ImageFile( + fileName: "$basename.jpg", + data: await FlutterImageCompress.compressWithList( + imageBytes, + quality: 95, + format: CompressFormat.jpeg, + keepExif: false, + ), + ); + + case "image/tiff": + final tiff = decodeTiff(imageBytes); + if (tiff == null) { + throw const FormatException("Decoded TIFF image is null"); + } + + final exif = ExifData(); + if (tiff.exif.imageIfd.hasOrientation) { + exif.imageIfd.orientation = tiff.exif.imageIfd.orientation; + } + tiff.exif = exif; + + return ImageFile( + fileName: "$basename.jpg", data: encodeJpg(tiff, quality: 95)); + + default: + return ImageFile(fileName: basename, data: imageBytes); + } + } catch (e) { + return ImageFile(fileName: file.basename, data: Uint8List(0)); } } diff --git a/lib/view/antenna_page/antenna_settings_dialog.dart b/lib/view/antenna_page/antenna_settings_dialog.dart index 612ae5d24..5d1a1713d 100644 --- a/lib/view/antenna_page/antenna_settings_dialog.dart +++ b/lib/view/antenna_page/antenna_settings_dialog.dart @@ -152,9 +152,12 @@ class AntennaSettingsForm extends HookConsumerWidget { final settings = ref.watch(_antennaSettingsNotifierProvider); final list = ref.watch(_usersListListProvider); final controller = useTextEditingController(); - ref.listen( - _initialSettingsProvider.select((settings) => settings.users.join("\n")), - (_, next) => controller.text = next, + useEffect( + () { + controller.text = settings.users.join("\n"); + return null; + }, + const [], ); return Form( diff --git a/lib/view/channels_page/channel_detail_info.dart b/lib/view/channels_page/channel_detail_info.dart index 281f7328d..2c9a796b7 100644 --- a/lib/view/channels_page/channel_detail_info.dart +++ b/lib/view/channels_page/channel_detail_info.dart @@ -1,4 +1,5 @@ import "package:flutter/material.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"; @@ -189,21 +190,37 @@ class ChannelDetailArea extends ConsumerWidget { ), ), const Padding(padding: EdgeInsets.only(top: 5)), - Align( - 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, - ), - ], - ), + Wrap( + spacing: 5, + alignment: WrapAlignment.end, + children: [ + ChannelFavoriteButton( + channelId: channel.id, + isFavorite: channel.isFavorited, + ), + ChannelFollowingButton( + isFollowing: channel.isFollowing, + channelId: channel.id, + ), + OutlinedButton( + onPressed: () async { + await Clipboard.setData( + ClipboardData( + text: + "https://${ref.read(accountContextProvider).getAccount.host}/channels/${channel.id}", + ), + ); + if (!context.mounted) return; + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(S.of(context).doneCopy), + duration: const Duration(seconds: 1), + ), + ); + }, + child: const Icon(Icons.copy), + ), + ], ), MfmText(mfmText: channel.description ?? ""), for (final pinnedNote in channel.pinnedNotes ?? []) diff --git a/lib/view/common/account_select_dialog.dart b/lib/view/common/account_select_dialog.dart index 35bf2f9eb..9690447fa 100644 --- a/lib/view/common/account_select_dialog.dart +++ b/lib/view/common/account_select_dialog.dart @@ -48,7 +48,7 @@ class AccountSelectDialog extends HookConsumerWidget { ), _ => ListTile( leading: const Icon(Icons.language), - title: const Text("相手先のサーバー(ログインなし)"), + title: Text(S.of(context).remoteServerWithoutLogin), onTap: navigateAsRemote.executeOrNull, ), }, diff --git a/lib/view/common/misskey_notes/misskey_file_view.dart b/lib/view/common/misskey_notes/misskey_file_view.dart index 263706420..19431cd69 100644 --- a/lib/view/common/misskey_notes/misskey_file_view.dart +++ b/lib/view/common/misskey_notes/misskey_file_view.dart @@ -70,7 +70,9 @@ class MisskeyFileView extends HookConsumerWidget { width: double.infinity, child: MisskeyImage( isSensitive: targetFile.element.isSensitive, - thumbnailUrl: targetFile.element.thumbnailUrl, + thumbnailUrl: (targetFile.element.type.startsWith("audio")) + ? null + : targetFile.element.thumbnailUrl, targetFiles: targetFiles, fileType: targetFile.element.type, name: targetFile.element.name, diff --git a/lib/view/common/misskey_notes/misskey_note.dart b/lib/view/common/misskey_notes/misskey_note.dart index 6de90fb40..4236884ce 100644 --- a/lib/view/common/misskey_notes/misskey_note.dart +++ b/lib/view/common/misskey_notes/misskey_note.dart @@ -133,6 +133,7 @@ class MisskeyNote extends HookConsumerWidget { } await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + final notesRepository = ref.read(notesProvider(account)); await ref .read(misskeyPostContextProvider) .notes @@ -143,7 +144,7 @@ class MisskeyNote extends HookConsumerWidget { const Duration(milliseconds: misskeyHQReactionDelay), ); } - await ref.read(notesProvider(account)).refresh(displayNote.id); + await notesRepository.refresh(displayNote.id); return; }); } @@ -349,6 +350,7 @@ class MisskeyNote extends HookConsumerWidget { if (dialogValue != 0) return; await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + final notesRepository = ref.read(notesWithProvider); await ref .read(misskeyPostContextProvider) .notes @@ -361,7 +363,7 @@ class MisskeyNote extends HookConsumerWidget { ); } - await ref.read(notesWithProvider).refresh(displayNote.id); + await notesRepository.refresh(displayNote.id); }); return; @@ -387,6 +389,7 @@ class MisskeyNote extends HookConsumerWidget { true) { return; } + final notesRepository = ref.read(notesProvider(account)); await ref .read(misskeyPostContextProvider) @@ -398,7 +401,7 @@ class MisskeyNote extends HookConsumerWidget { const Duration(milliseconds: misskeyHQReactionDelay), ); } - await ref.read(notesProvider(account)).refresh(displayNote.id); + await notesRepository.refresh(displayNote.id); return; } final misskey = ref.read(misskeyPostContextProvider); diff --git a/lib/view/common/misskey_notes/network_image.dart b/lib/view/common/misskey_notes/network_image.dart index 6b00385ff..b09771b4b 100644 --- a/lib/view/common/misskey_notes/network_image.dart +++ b/lib/view/common/misskey_notes/network_image.dart @@ -61,22 +61,25 @@ class NetworkImageView extends ConsumerWidget { fit: fit, errorWidget: (context, url, error) => errorBuilder?.call(context, error, StackTrace.current) ?? - Stack( + Container( 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, - ), - ], + decoration: type == ImageType.avatarDecoration + ? null + : BoxDecoration( + borderRadius: BorderRadius.circular(5), + color: const Color.fromARGB(255, 224, 224, 224), + ), + child: type == ImageType.avatarDecoration + ? null + : 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, diff --git a/lib/view/common/misskey_notes/reaction_button.dart b/lib/view/common/misskey_notes/reaction_button.dart index 732a760d4..aa5b3ae35 100644 --- a/lib/view/common/misskey_notes/reaction_button.dart +++ b/lib/view/common/misskey_notes/reaction_button.dart @@ -65,6 +65,7 @@ class ReactionButton extends HookConsumerWidget { if (dialogValue != 0) return; await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + final notesRepository = ref.read(notesWithProvider); await ref .read(misskeyPostContextProvider) .notes @@ -76,7 +77,7 @@ class ReactionButton extends HookConsumerWidget { ); } - await ref.read(notesWithProvider).refresh(noteId); + await notesRepository.refresh(noteId); }); return; @@ -96,6 +97,7 @@ class ReactionButton extends HookConsumerWidget { return; } await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + final notesRepository = ref.read(notesWithProvider); await ref.read(misskeyPostContextProvider).notes.reactions.create( NotesReactionsCreateRequest( noteId: noteId, @@ -110,7 +112,7 @@ class ReactionButton extends HookConsumerWidget { ); } - await ref.read(notesWithProvider).refresh(noteId); + await notesRepository.refresh(noteId); }); }); diff --git a/lib/view/common/misskey_notes/renote_modal_sheet.dart b/lib/view/common/misskey_notes/renote_modal_sheet.dart index 5c15b3a0b..ae9f4f7a6 100644 --- a/lib/view/common/misskey_notes/renote_modal_sheet.dart +++ b/lib/view/common/misskey_notes/renote_modal_sheet.dart @@ -9,8 +9,12 @@ 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/state_notifier/common/misskey_notes/misskey_note_notifier.dart"; +import "package:miria/view/common/account_scope.dart"; +import "package:miria/view/common/avatar_icon.dart"; import "package:miria/view/common/dialog/dialog_state.dart"; import "package:miria/view/common/misskey_notes/local_only_icon.dart"; +import "package:miria/view/common/misskey_notes/mfm_text.dart"; import "package:misskey_dart/misskey_dart.dart"; import "package:riverpod_annotation/riverpod_annotation.dart"; @@ -84,8 +88,48 @@ class RenoteChannelNotifier extends _$RenoteChannelNotifier { } } +@Riverpod(dependencies: [accountContext], keepAlive: false) +class RenoteOtherAccountNotifier extends _$RenoteOtherAccountNotifier { + @override + AsyncValue<(Account, Note)>? build(Account account, Note note) => null; + + Future renoteOtherAccount() async { + final selectedAccount = await ref.read(appRouterProvider).push( + AccountSelectRoute( + host: note.localOnly ? this.account.host : null, + remoteHost: + note.user.host != this.account.host && note.user.host != null + ? note.user.host + : null, + ), + ); + if (selectedAccount == null) return; + await ref.read(dialogStateNotifierProvider.notifier).guard(() async { + final accountContext = AccountContext( + getAccount: selectedAccount, + postAccount: selectedAccount.isDemoAccount + ? ref.read(accountContextProvider).postAccount + : selectedAccount, + ); + state = const AsyncLoading(); + final foundNote = note.user.host == null && + note.uri?.host == accountContext.getAccount.host + ? note + : await ref + .read(misskeyNoteNotifierProvider.notifier) + .lookupNote(note: note, accountContext: accountContext); + if (foundNote == null) { + state = null; + return; + } + ref.read(notesProvider(selectedAccount)).registerNote(foundNote); + state = AsyncValue.data((selectedAccount, foundNote)); + }); + } +} + @RoutePage() -class RenoteModalSheet extends HookConsumerWidget { +class RenoteModalSheet extends HookConsumerWidget implements AutoRouteWrapper { final Note note; final Account account; @@ -95,6 +139,12 @@ class RenoteModalSheet extends HookConsumerWidget { super.key, }); + @override + Widget wrappedRoute(BuildContext context) => AccountContextScope.as( + account: account, + child: this, + ); + @override Widget build(BuildContext context, WidgetRef ref) { final channel = note.channel; @@ -121,12 +171,26 @@ class RenoteModalSheet extends HookConsumerWidget { initialAccount: account, ), ); + }) + ..listen(renoteOtherAccountNotifierProvider(account, note), + (_, next) async { + if (next is! AsyncData<(Account, Note)>) return; + unawaited(context.maybePop()); + await context.pushRoute( + RenoteModalRoute( + account: next.value.$1, + note: next.value.$2, + ), + ); }); final renoteState = ref.watch(renoteNotifierProvider(account, note)); final renoteChannelState = ref.watch(renoteChannelNotifierProvider(account)); + final renoteOtherAccountState = + ref.watch(renoteOtherAccountNotifierProvider(account, note)); + final isLocalOnly = useState(false); final visibility = useState(NoteVisibility.public); useEffect( @@ -145,12 +209,34 @@ class RenoteModalSheet extends HookConsumerWidget { if (renoteState is AsyncLoading || renoteChannelState is AsyncLoading || + renoteOtherAccountState is AsyncLoading || renoteState is AsyncData || - renoteChannelState is AsyncData) { + renoteChannelState is AsyncData || + renoteOtherAccountState is AsyncData) { return const Center(child: CircularProgressIndicator.adaptive()); } return ListView( children: [ + 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, + ), + trailing: IconButton( + onPressed: () async => await ref + .read( + renoteOtherAccountNotifierProvider(account, note).notifier, + ) + .renoteOtherAccount(), + icon: const Icon(Icons.keyboard_arrow_down), + ), + ), + Divider(color: Theme.of(context).primaryColor, thickness: 2), if (channel != null) ...[ ListTile( onTap: () async => diff --git a/lib/view/common/misskey_notes/renote_modal_sheet.g.dart b/lib/view/common/misskey_notes/renote_modal_sheet.g.dart index b406c57bb..ba6e0aa48 100644 --- a/lib/view/common/misskey_notes/renote_modal_sheet.g.dart +++ b/lib/view/common/misskey_notes/renote_modal_sheet.g.dart @@ -429,5 +429,232 @@ class _RenoteChannelNotifierProviderElement @override Account get account => (origin as RenoteChannelNotifierProvider).account; } + +String _$renoteOtherAccountNotifierHash() => + r'6ce80b97d6f49aa3302dfd27b2b1b8242dc559cb'; + +abstract class _$RenoteOtherAccountNotifier + extends BuildlessAutoDisposeNotifier?> { + late final Account account; + late final Note note; + + AsyncValue<(Account, Note)>? build( + Account account, + Note note, + ); +} + +/// See also [RenoteOtherAccountNotifier]. +@ProviderFor(RenoteOtherAccountNotifier) +const renoteOtherAccountNotifierProvider = RenoteOtherAccountNotifierFamily(); + +/// See also [RenoteOtherAccountNotifier]. +class RenoteOtherAccountNotifierFamily extends Family { + /// See also [RenoteOtherAccountNotifier]. + const RenoteOtherAccountNotifierFamily(); + + 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'renoteOtherAccountNotifierProvider'; + + /// See also [RenoteOtherAccountNotifier]. + RenoteOtherAccountNotifierProvider call( + Account account, + Note note, + ) { + return RenoteOtherAccountNotifierProvider( + account, + note, + ); + } + + @visibleForOverriding + @override + RenoteOtherAccountNotifierProvider getProviderOverride( + covariant RenoteOtherAccountNotifierProvider provider, + ) { + return call( + provider.account, + provider.note, + ); + } + + /// Enables overriding the behavior of this provider, no matter the parameters. + Override overrideWith(RenoteOtherAccountNotifier Function() create) { + return _$RenoteOtherAccountNotifierFamilyOverride(this, create); + } +} + +class _$RenoteOtherAccountNotifierFamilyOverride implements FamilyOverride { + _$RenoteOtherAccountNotifierFamilyOverride( + this.overriddenFamily, this.create); + + final RenoteOtherAccountNotifier Function() create; + + @override + final RenoteOtherAccountNotifierFamily overriddenFamily; + + @override + RenoteOtherAccountNotifierProvider getProviderOverride( + covariant RenoteOtherAccountNotifierProvider provider, + ) { + return provider._copyWith(create); + } +} + +/// See also [RenoteOtherAccountNotifier]. +class RenoteOtherAccountNotifierProvider + extends AutoDisposeNotifierProviderImpl?> { + /// See also [RenoteOtherAccountNotifier]. + RenoteOtherAccountNotifierProvider( + Account account, + Note note, + ) : this._internal( + () => RenoteOtherAccountNotifier() + ..account = account + ..note = note, + from: renoteOtherAccountNotifierProvider, + name: r'renoteOtherAccountNotifierProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$renoteOtherAccountNotifierHash, + dependencies: RenoteOtherAccountNotifierFamily._dependencies, + allTransitiveDependencies: + RenoteOtherAccountNotifierFamily._allTransitiveDependencies, + account: account, + note: note, + ); + + RenoteOtherAccountNotifierProvider._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<(Account, Note)>? runNotifierBuild( + covariant RenoteOtherAccountNotifier notifier, + ) { + return notifier.build( + account, + note, + ); + } + + @override + Override overrideWith(RenoteOtherAccountNotifier Function() create) { + return ProviderOverride( + origin: this, + override: RenoteOtherAccountNotifierProvider._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 _RenoteOtherAccountNotifierProviderElement(this); + } + + RenoteOtherAccountNotifierProvider _copyWith( + RenoteOtherAccountNotifier Function() create, + ) { + return RenoteOtherAccountNotifierProvider._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 RenoteOtherAccountNotifierProvider && + 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 RenoteOtherAccountNotifierRef + on AutoDisposeNotifierProviderRef?> { + /// The parameter `account` of this provider. + Account get account; + + /// The parameter `note` of this provider. + Note get note; +} + +class _RenoteOtherAccountNotifierProviderElement + extends AutoDisposeNotifierProviderElement?> with RenoteOtherAccountNotifierRef { + _RenoteOtherAccountNotifierProviderElement(super.provider); + + @override + Account get account => (origin as RenoteOtherAccountNotifierProvider).account; + @override + Note get note => (origin as RenoteOtherAccountNotifierProvider).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/note_file_dialog/media_viewer.dart b/lib/view/common/note_file_dialog/media_viewer.dart index a8d17752c..44cd54f13 100644 --- a/lib/view/common/note_file_dialog/media_viewer.dart +++ b/lib/view/common/note_file_dialog/media_viewer.dart @@ -36,11 +36,12 @@ class MediaViewer extends HookConsumerWidget { fit: StackFit.passthrough, alignment: Alignment.center, children: [ - NetworkImageView( - url: file.thumbnailUrl.toString(), - type: ImageType.imageThumbnail, - fit: BoxFit.contain, - ), + if (!file.type.startsWith("audio")) + NetworkImageView( + url: file.thumbnailUrl.toString(), + type: ImageType.imageThumbnail, + fit: BoxFit.contain, + ), Icon( Icons.play_circle, size: 100, diff --git a/lib/view/common/pushable_listview.dart b/lib/view/common/pushable_listview.dart index 8ca154c84..13f282c54 100644 --- a/lib/view/common/pushable_listview.dart +++ b/lib/view/common/pushable_listview.dart @@ -2,6 +2,7 @@ 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_hooks/flutter_hooks.dart"; import "package:hooks_riverpod/hooks_riverpod.dart"; import "package:miria/model/general_settings.dart"; @@ -106,7 +107,10 @@ class PushableListView extends HookConsumerWidget { return const Center( child: Padding( padding: EdgeInsets.all(20), - child: CircularProgressIndicator.adaptive(), + child: SizedBox.square( + dimension: 100, + child: CircularProgressIndicator.adaptive(), + ), ), ); } @@ -119,10 +123,10 @@ class PushableListView extends HookConsumerWidget { } if (items.value.isEmpty && !hideIsEmpty) { - return const Center( + return Center( child: Padding( - padding: EdgeInsets.all(10), - child: Text("なんもないで"), + padding: const EdgeInsets.all(10), + child: Text(S.of(context).nothingHere), ), ); } @@ -150,13 +154,12 @@ class PushableListView extends HookConsumerWidget { const SizedBox.shrink(), ], ), - Center( - child: Padding( - padding: const EdgeInsets.only(top: 10, bottom: 10), - child: IconButton( - onPressed: nextLoad, - icon: const Icon(Icons.keyboard_arrow_down), - ), + InkWell( + onTap: nextLoad, + child: Container( + alignment: Alignment.center, + height: 60, + child: const Icon(Icons.keyboard_arrow_down), ), ), ], diff --git a/lib/view/note_create_page/vote_area.dart b/lib/view/note_create_page/vote_area.dart index c302dbca9..d1fed557f 100644 --- a/lib/view/note_create_page/vote_area.dart +++ b/lib/view/note_create_page/vote_area.dart @@ -66,31 +66,34 @@ class VoteContentListItem extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final onVoteContentChange = ref.watch( + noteCreateNotifierProvider + .select((notifier) => notifier.voteContentCount), + ); + final initial = useMemoized( () => ref.read(noteCreateNotifierProvider).voteContent[index], + [onVoteContentChange], ); final controller = useTextEditingController(text: initial); - ref.listen( - noteCreateNotifierProvider.select( - (value) => - value.voteContent.length <= index ? "" : value.voteContent[index], - ), (_, next) { - controller.text = next; - }); useEffect( () { - controller.addListener(() { - if (ref.read(noteCreateNotifierProvider).voteContent.length <= - index) { - return; - } - ref - .read(noteCreateNotifierProvider.notifier) - .setVoteContent(index, controller.text); - }); + controller + ..text = initial + ..addListener(() { + final voteContent = + ref.read(noteCreateNotifierProvider).voteContent; + if (voteContent.length <= index || + voteContent[index] == controller.text) { + return; + } + ref + .read(noteCreateNotifierProvider.notifier) + .setVoteContent(index, controller.text); + }); return null; }, - [index], + [index, onVoteContentChange], ); return Padding( padding: const EdgeInsets.only(bottom: 5), diff --git a/lib/view/note_modal_sheet/note_modal_sheet.dart b/lib/view/note_modal_sheet/note_modal_sheet.dart index 2550b9f53..09bdffa5f 100644 --- a/lib/view/note_modal_sheet/note_modal_sheet.dart +++ b/lib/view/note_modal_sheet/note_modal_sheet.dart @@ -140,14 +140,18 @@ class NoteModalSheetNotifier extends _$NoteModalSheetNotifier { } Future unRenote() async { + if (note.renoteId == null) 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)), - ), + () async { + await ref + .read(misskeyPostContextProvider) + .notes + .delete(NotesDeleteRequest(noteId: note.id)); + ref.read(notesWithProvider).delete(note.id); + }, + ), ); } @@ -224,9 +228,12 @@ class NoteModalSheet extends ConsumerWidget implements AutoRouteWrapper { @override Widget build(BuildContext context, WidgetRef ref) { final accounts = ref.watch(accountRepositoryProvider); - final notifierProvider = noteModalSheetNotifierProvider(targetNote); + final targetNoteNotifierProvider = + noteModalSheetNotifierProvider(targetNote); + final baseNoteNotiferProvider = noteModalSheetNotifierProvider(baseNote); - ref.listen(notifierProvider.select((value) => value.user), (_, next) async { + ref.listen(targetNoteNotifierProvider.select((value) => value.user), + (_, next) async { switch (next) { case AsyncData(:final value): await context.pushRoute( @@ -240,10 +247,11 @@ class NoteModalSheet extends ConsumerWidget implements AutoRouteWrapper { case AsyncError(): } }); - final noteStatus = - ref.watch(notifierProvider.select((value) => value.noteState)); + final noteStatus = ref + .watch(targetNoteNotifierProvider.select((value) => value.noteState)); - if (ref.read(notifierProvider).isLoading) { + if (ref.watch(targetNoteNotifierProvider).isLoading || + ref.watch(baseNoteNotiferProvider).isLoading) { return const Center( child: SizedBox( width: 100, @@ -317,7 +325,8 @@ class NoteModalSheet extends ConsumerWidget implements AutoRouteWrapper { 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(), + onTap: () async => + ref.read(targetNoteNotifierProvider.notifier).user(), ), ListTile( leading: const Icon(Icons.open_in_browser), @@ -339,7 +348,7 @@ class NoteModalSheet extends ConsumerWidget implements AutoRouteWrapper { onTap: () async { final uri = targetNote.url ?? targetNote.uri; if (uri == null) return; - launchUrl(uri, mode: LaunchMode.externalApplication); + await launchUrl(uri, mode: LaunchMode.externalApplication); if (!context.mounted) return; Navigator.of(context).pop(); }, @@ -363,11 +372,12 @@ class NoteModalSheet extends ConsumerWidget implements AutoRouteWrapper { onTap: () { WidgetsBinding.instance.addPostFrameCallback((timeStamp) { Future(() async { + if (!context.mounted) return; final box = context.findRenderObject() as RenderBox?; if (box == null) return; final boundary = noteBoundaryKey.currentContext! .findRenderObject()! as RenderRepaintBoundary; - await ref.read(notifierProvider.notifier).copyAsImage( + await ref.read(targetNoteNotifierProvider.notifier).copyAsImage( box, boundary, View.of(context).devicePixelRatio, @@ -386,7 +396,9 @@ class NoteModalSheet extends ConsumerWidget implements AutoRouteWrapper { AsyncData(:final value) => ListTile( leading: const Icon(Icons.star_rounded), onTap: () async { - await ref.read(notifierProvider.notifier).favorite(); + await ref + .read(targetNoteNotifierProvider.notifier) + .favorite(); if (!context.mounted) return; Navigator.of(context).pop(); }, @@ -448,7 +460,7 @@ class NoteModalSheet extends ConsumerWidget implements AutoRouteWrapper { leading: const Icon(Icons.delete), title: Text(S.of(context).delete), onTap: () async { - await ref.read(notifierProvider.notifier).delete(); + await ref.read(targetNoteNotifierProvider.notifier).delete(); if (!context.mounted) return; Navigator.of(context).pop(); }, @@ -457,8 +469,9 @@ class NoteModalSheet extends ConsumerWidget implements AutoRouteWrapper { leading: const Icon(Icons.edit_outlined), title: Text(S.of(context).deletedRecreate), onTap: () async { - final result = - await ref.read(notifierProvider.notifier).deleteRecreate(); + final result = await ref + .read(targetNoteNotifierProvider.notifier) + .deleteRecreate(); if (!result || !context.mounted) return; Navigator.of(context).pop(); await context.pushRoute( @@ -482,7 +495,11 @@ class NoteModalSheet extends ConsumerWidget implements AutoRouteWrapper { ListTile( leading: const Icon(Icons.delete), title: Text(S.of(context).deleteRenote), - onTap: () async => ref.read(notifierProvider.notifier).unRenote(), + onTap: () async { + await ref.read(baseNoteNotiferProvider.notifier).unRenote(); + if (!context.mounted) return; + Navigator.of(context).pop(); + }, ), ], if (accountContext.isSame && baseNote.user.host != null || 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 a25e477f2..628ad9777 100644 --- a/lib/view/settings_page/account_settings_page/account_list.dart +++ b/lib/view/settings_page/account_settings_page/account_list.dart @@ -59,14 +59,10 @@ class AccountListPage extends ConsumerWidget { child: Padding( padding: const EdgeInsets.all(10), child: ElevatedButton( - 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()); - }); - }, + onPressed: () async => await context.router.pushAndPopUntil( + const SplashRoute(), + predicate: (_) => false, + ), child: Text(S.of(context).quitAccountSettings), ), ), 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 b71a61677..0acb89650 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 @@ -68,7 +68,8 @@ class GeneralSettingsPage extends HookConsumerWidget { enableLongTextElipsed.value, tabPosition.value, emojiType.value, - textScaleFactor.value, + // 変更ボタンを押すまで反映しない + // textScaleFactor.value, defaultFontName.value, serifFontName.value, monospaceFontName.value, @@ -178,9 +179,9 @@ class GeneralSettingsPage extends HookConsumerWidget { onChanged: (value) async => automaticPush.value = value ?? AutomaticPush.none, ), - const Text("デッキモード"), //TODO: localize + Text(S.of(context).deckMode), CheckboxListTile( - title: const Text("デッキモードにします。"), + title: Text(S.of(context).enableDeckMode), value: isDeckMode.value, onChanged: (value) => isDeckMode.value = value ?? false, ), 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 f5fc4ae52..850f3a1f3 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 @@ -78,10 +78,10 @@ class TabSettingsListPage extends ConsumerWidget { child: Padding( padding: const EdgeInsets.all(10), child: ElevatedButton( - onPressed: () async { - context.router.removeWhere((route) => true); - await context.router.push(const SplashRoute()); - }, + onPressed: () async => await context.router.pushAndPopUntil( + const SplashRoute(), + predicate: (_) => false, + ), child: Text(S.of(context).apply), ), ), 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 2ae06bbd7..676cdd927 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 @@ -57,31 +57,6 @@ class ReactionDeckPageState extends ConsumerState { return Scaffold( appBar: AppBar( title: Text(S.of(context).reactionDeck), - actions: [ - PopupMenuButton( - onSelected: (type) => switch (type) { - ReactionDeckPageMenuType.addMany => - showAddReactionsDialog(context: context), - ReactionDeckPageMenuType.copy => copyReactions(context: context), - ReactionDeckPageMenuType.clear => - clearReactions(context: context), - }, - itemBuilder: (context) => [ - PopupMenuItem( - value: ReactionDeckPageMenuType.addMany, - child: Text(S.of(context).bulkAddReactions), - ), - PopupMenuItem( - value: ReactionDeckPageMenuType.clear, - child: Text(S.of(context).clear), - ), - PopupMenuItem( - value: ReactionDeckPageMenuType.copy, - child: Text(S.of(context).copy), - ), - ], - ), - ], ), body: Padding( padding: const EdgeInsets.all(10), @@ -152,6 +127,34 @@ class ReactionDeckPageState extends ConsumerState { ), ], ), + const Padding(padding: EdgeInsets.only(top: 10)), + Wrap( + direction: Axis.horizontal, + alignment: WrapAlignment.center, + spacing: 10, + children: [ + ElevatedButton.icon( + onPressed: () async { + await showAddReactionsDialog(context: context); + }, + icon: const Icon(Icons.file_download_outlined), + label: Text(S.of(context).bulkAddReactions), + ), + ElevatedButton.icon( + onPressed: () async { + await copyReactions(context: context); + }, + icon: const Icon(Icons.copy_all), + label: Text(S.of(context).copy), + ), + OutlinedButton( + onPressed: () async { + await clearReactions(context: context); + }, + child: const Icon(Icons.clear), + ), + ], + ), ], ), ), diff --git a/lib/view/time_line_page/misskey_time_line.dart b/lib/view/time_line_page/misskey_time_line.dart index b154909fc..bd6fd606c 100644 --- a/lib/view/time_line_page/misskey_time_line.dart +++ b/lib/view/time_line_page/misskey_time_line.dart @@ -125,9 +125,14 @@ class MisskeyTimeline extends HookConsumerWidget { } return Center( - child: IconButton( - onPressed: downDirectionLoad, - icon: const Icon(Icons.keyboard_arrow_down), + child: InkWell( + onTap: downDirectionLoad, + child: Container( + alignment: Alignment.center, + constraints: const BoxConstraints(maxWidth: 800), + height: 60, + child: const Icon(Icons.keyboard_arrow_down), + ), ), ); } diff --git a/lib/view/user_page/user_control_dialog.dart b/lib/view/user_page/user_control_dialog.dart index 9c6b6f4f5..48ec542ff 100644 --- a/lib/view/user_page/user_control_dialog.dart +++ b/lib/view/user_page/user_control_dialog.dart @@ -96,7 +96,7 @@ class UserControlDialog extends HookConsumerWidget implements AutoRouteWrapper { Navigator.of(context).pop(); }); final openBrowserAsRemote = useAsync(() async { - final uri = response.uri ?? response.url; + final uri = response.url ?? response.uri; if (uri == null) return; await launchUrl(uri, mode: LaunchMode.externalApplication); if (!context.mounted) return; diff --git a/lib/view/user_page/user_detail.dart b/lib/view/user_page/user_detail.dart index cb18c02a6..8ead69bb1 100644 --- a/lib/view/user_page/user_detail.dart +++ b/lib/view/user_page/user_detail.dart @@ -234,6 +234,7 @@ class UserDetail extends ConsumerWidget { S .of(context) .messageForFollower(response.followedMessage ?? ""), + emojis: response.emojis, ), ), const Padding(padding: EdgeInsets.only(top: 5)), diff --git a/lib/view/user_page/user_info_notifier.dart b/lib/view/user_page/user_info_notifier.dart index 78be30fe8..505bfe241 100644 --- a/lib/view/user_page/user_info_notifier.dart +++ b/lib/view/user_page/user_info_notifier.dart @@ -202,7 +202,10 @@ class UserInfoNotifier extends _$UserInfoNotifier { final expires = await ref.read(appRouterProvider).push( const ExpireSelectRoute(), ); - if (expires == null) return null; + if (expires == null) { + await ref.read(appRouterProvider).maybePop(); + return null; + } final expiresDate = expires == Expire.indefinite ? null : DateTime.now().add(expires.expires!); @@ -292,7 +295,10 @@ class UserInfoNotifier extends _$UserInfoNotifier { S.of(context).cancel, ], ); - if (confirm == 1) return null; + if (confirm == 1) { + await ref.read(appRouterProvider).maybePop(); + return null; + } return await _dialog.guard(() async { await _postMisskey.blocking.create(BlockCreateRequest(userId: userId)); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index dd37f1256..276b93631 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -9,7 +9,7 @@ #include #include #include -#include +#include #include #include @@ -23,9 +23,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) media_kit_video_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "MediaKitVideoPlugin"); media_kit_video_plugin_register_with_registrar(media_kit_video_registrar); - g_autoptr(FlPluginRegistrar) screen_retriever_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverPlugin"); - screen_retriever_plugin_register_with_registrar(screen_retriever_registrar); + g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); + screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 1a4e0cd6b..d06ff596e 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -6,7 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_linux media_kit_libs_linux media_kit_video - screen_retriever + screen_retriever_linux url_launcher_linux window_manager ) diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 83e14241c..9bf4f69c0 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -13,7 +13,7 @@ import media_kit_libs_macos_video import media_kit_video import package_info_plus import path_provider_foundation -import screen_retriever +import screen_retriever_macos import share_plus import shared_preferences_foundation import sqflite_darwin @@ -31,7 +31,7 @@ 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")) - ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) + ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index a50cdd8fc..589257cc3 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -20,9 +20,7 @@ PODS: - path_provider_foundation (0.0.1): - Flutter - FlutterMacOS - - screen_brightness_macos (0.1.0): - - FlutterMacOS - - screen_retriever (0.0.1): + - screen_retriever_macos (0.0.1): - FlutterMacOS - share_plus (0.0.1): - FlutterMacOS @@ -53,8 +51,7 @@ DEPENDENCIES: - media_kit_video (from `Flutter/ephemeral/.symlinks/plugins/media_kit_video/macos`) - package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`) - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - - screen_brightness_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos`) - - screen_retriever (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos`) + - screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`) - share_plus (from `Flutter/ephemeral/.symlinks/plugins/share_plus/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite_darwin (from `Flutter/ephemeral/.symlinks/plugins/sqflite_darwin/darwin`) @@ -84,10 +81,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos path_provider_foundation: :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin - screen_brightness_macos: - :path: Flutter/ephemeral/.symlinks/plugins/screen_brightness_macos/macos - screen_retriever: - :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever/macos + screen_retriever_macos: + :path: Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos share_plus: :path: Flutter/ephemeral/.symlinks/plugins/share_plus/macos shared_preferences_foundation: @@ -114,8 +109,7 @@ SPEC CHECKSUMS: media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5 package_info_plus: f5790acc797bf17c3e959e9d6cf162cc68ff7523 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda - screen_retriever: 59634572a57080243dd1bf715e55b6c54f241a38 + screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 share_plus: 36537c04ce0c3e3f5bd297ce4318b6d5ee5fd6cf shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 sqflite_darwin: a553b1fd6fe66f53bbb0fe5b4f5bab93f08d7a13 diff --git a/pubspec.lock b/pubspec.lock index 3af88c6bb..d8d4dd9a4 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1033,7 +1033,7 @@ packages: source: hosted version: "1.0.5" mime: - dependency: transitive + dependency: "direct main" description: name: mime sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" @@ -1357,10 +1357,42 @@ packages: dependency: transitive description: name: screen_retriever - sha256: "6ee02c8a1158e6dae7ca430da79436e3b1c9563c8cf02f524af997c201ac2b90" + sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_linux: + dependency: transitive + description: + name: screen_retriever_linux + sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_macos: + dependency: transitive + description: + name: screen_retriever_macos + sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" url: "https://pub.dev" source: hosted - version: "0.1.9" + version: "0.2.0" + screen_retriever_platform_interface: + dependency: transitive + description: + name: screen_retriever_platform_interface + sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 + url: "https://pub.dev" + source: hosted + version: "0.2.0" + screen_retriever_windows: + dependency: transitive + description: + name: screen_retriever_windows + sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" + url: "https://pub.dev" + source: hosted + version: "0.2.0" scrollable_positioned_list: dependency: "direct main" description: @@ -1890,10 +1922,10 @@ packages: dependency: "direct main" description: name: window_manager - sha256: "8699323b30da4cdbe2aa2e7c9de567a6abd8a97d9a5c850a3c86dcd0b34bbfbf" + sha256: "732896e1416297c63c9e3fb95aea72d0355f61390263982a47fd519169dc5059" url: "https://pub.dev" source: hosted - version: "0.3.9" + version: "0.4.3" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c1ae7d786..fe3564788 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: 2.0.0+114 +version: 2.0.2+117 environment: sdk: '>=3.0.0 <4.0.0' @@ -67,7 +67,7 @@ dependencies: media_kit_libs_video: ^1.0.5 flutter_colorpicker: ^1.1.0 volume_controller: ^2.0.7 - window_manager: ^0.3.8 + window_manager: ^0.4.3 shared_preference_app_group: ^1.0.0+1 stack_trace: ^1.11.1 flutter_cache_manager: ^3.3.2 @@ -77,6 +77,7 @@ dependencies: flutter_hooks: ^0.20.5 punycode: ^1.0.0 image: ^4.3.0 + mime: ^1.0.6 dependency_overrides: image_editor: diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml index d36acbed0..d04debc64 100644 --- a/snap/snapcraft.yaml +++ b/snap/snapcraft.yaml @@ -100,7 +100,12 @@ parts: 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/{}" \; + cd "/snap/${snap}/current" + find . -type f,l ! -xtype d -print0 > "/tmp/${snap}.log" + cd "${SNAPCRAFT_PRIME}" + cat "/tmp/${snap}.log" | xargs -0 rm -f + cd "${SNAPCRAFT_PRIME}/usr" + cat "/tmp/${snap}.log" | xargs -0 rm -f done layout: diff --git a/test/assets/images/exif_ifd_only.jpg b/test/assets/images/exif_ifd_only.jpg new file mode 100644 index 000000000..c23bf4db2 Binary files /dev/null and b/test/assets/images/exif_ifd_only.jpg differ diff --git a/test/assets/images/exif_no_data.jpg b/test/assets/images/exif_no_data.jpg new file mode 100644 index 000000000..ecbbcbc75 Binary files /dev/null and b/test/assets/images/exif_no_data.jpg differ diff --git a/test/assets/images/exif_test.jpg b/test/assets/images/exif_test.jpg new file mode 100644 index 000000000..f5f9ab08f Binary files /dev/null and b/test/assets/images/exif_test.jpg differ diff --git a/test/assets/images/test.gif b/test/assets/images/test.gif new file mode 100644 index 000000000..77e999f13 Binary files /dev/null and b/test/assets/images/test.gif differ diff --git a/test/assets/images/test.png b/test/assets/images/test.png new file mode 100644 index 000000000..c89ffa1e8 Binary files /dev/null and b/test/assets/images/test.png differ diff --git a/test/assets/images/test.tiff b/test/assets/images/test.tiff new file mode 100644 index 000000000..2417375c2 Binary files /dev/null and b/test/assets/images/test.tiff differ diff --git a/test/state_notifier/note_create_page/image_load_test.dart b/test/state_notifier/note_create_page/image_load_test.dart new file mode 100644 index 000000000..1986fa81b --- /dev/null +++ b/test/state_notifier/note_create_page/image_load_test.dart @@ -0,0 +1,91 @@ +import "package:file/file.dart"; +import "package:file/local.dart"; +import "package:flutter_test/flutter_test.dart"; +import "package:image/image.dart"; +import "package:miria/state_notifier/note_create_page/note_create_state_notifier.dart"; + +void main() { + const FileSystem fs = LocalFileSystem(); + group("画像読込", () { + test("PNG画像が読み込めること", () async { + final file = fs.file("test/assets/images/test.png"); + final d = await NoteCreateNotifier().loadImage(file); + expect(decodePng(d.data), isNotNull); + }); + + test("GIF画像が読み込めること", () async { + final file = fs.file("test/assets/images/test.gif"); + final d = await NoteCreateNotifier().loadImage(file); + expect(decodeGif(d.data), isNotNull); + }); + + test("JPEG画像が読み込めること", () async { + final file = fs.file("test/assets/images/exif_test.jpg"); + final d = await NoteCreateNotifier().loadImage(file); + expect(decodeJpg(d.data), isNotNull); + }); + + test("TIFFはJPEGに変換されること", () async { + final file = fs.file("test/assets/images/test.tiff"); + final d = await NoteCreateNotifier().loadImage(file); + expect(decodeTiff(d.data), isNull); + expect(decodeJpg(d.data), isNotNull); + }); + + test("入力JPEG画像にgpsIfdのキーがあること", () async { + final file = fs.file("test/assets/images/exif_test.jpg"); + final exif = decodeJpgExif(await file.readAsBytes()); + expect(exif?.gpsIfd.values.length, 5); + }); + + test("出力JPEG画像にgpsIfdのキーが存在しないこと", () async { + final file = fs.file("test/assets/images/exif_test.jpg"); + final d = await NoteCreateNotifier().loadImage(file); + final exif = decodeJpgExif(d.data); + expect(exif, isNotNull); + expect(exif?.gpsIfd.keys.length, 0); + }); + + test("出力JPEG画像のEXIFに向き以外存在しないこと", () async { + final file = fs.file("test/assets/images/exif_test.jpg"); + final d = await NoteCreateNotifier().loadImage(file); + final exif = decodeJpgExif(d.data); + expect(exif, isNotNull); + expect(exif?.imageIfd.keys.length, 1); + expect(exif?.imageIfd.keys.first, 0x112); + }); + + test("出力JPEG画像のEXIFのOrientation値が6であること", () async { + final file = fs.file("test/assets/images/exif_test.jpg"); + final d = await NoteCreateNotifier().loadImage(file); + final exif = decodeJpgExif(d.data); + expect(exif, isNotNull); + expect(exif?.imageIfd.keys.length, 1); + expect(exif?.imageIfd.orientation, 6); + }); + + test("EXIFがIFDだけのJPEG画像が読み込めること", () async { + final file = fs.file("test/assets/images/exif_ifd_only.jpg"); + final d = await NoteCreateNotifier().loadImage(file); + final img = decodeJpg(d.data); + expect(img, isNotNull); + }); + + test("EXIFがIFDだけのJPEG画像はOrientation値が1になること", () async { + final file = fs.file("test/assets/images/exif_ifd_only.jpg"); + final d = await NoteCreateNotifier().loadImage(file); + final exif = decodeJpgExif(d.data); + expect(exif, isNotNull); + expect(exif?.imageIfd.keys.length, 1); + expect(exif?.imageIfd.orientation, 1); + }); + + test("EXIFがないJPEG画像の場合はEXIFをつけないこと", () async { + final file = fs.file("test/assets/images/exif_no_data.jpg"); + final d = await NoteCreateNotifier().loadImage(file); + expect(decodeJpg(d.data), isNotNull); + final exif = decodeJpgExif(d.data); + expect(exif, isNull); + }); + }); +} diff --git a/test/test_util/widget_tester_extension.dart b/test/test_util/widget_tester_extension.dart index f3d15a833..fc8b3a3d0 100644 --- a/test/test_util/widget_tester_extension.dart +++ b/test/test_util/widget_tester_extension.dart @@ -15,8 +15,8 @@ extension WidgetTestExtension on WidgetTester { await tap( find.descendant( of: find.descendant( - of: find.byType(Center), - matching: find.byType(IconButton).hitTestable(), + of: find.byType(InkWell), + matching: find.byType(Container).hitTestable(), ), matching: find.byIcon(Icons.keyboard_arrow_down), ), 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 086d849e7..d03f4b561 100644 --- a/test/view/common/misskey_notes/note_modal_sheet_test.dart +++ b/test/view/common/misskey_notes/note_modal_sheet_test.dart @@ -371,8 +371,13 @@ void main() { ); when(misskey.notes).thenReturn(misskeyNotes); when(misskeyNotes.featured(any)).thenAnswer( - (e) async => - [TestData.note1.copyWith(text: null, renote: TestData.note2)], + (e) async => [ + TestData.note1.copyWith( + text: null, + renoteId: TestData.note2.id, + renote: TestData.note2, + ) + ], ); await tester.pumpWidget( ProviderScope( diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 3cf1d3a9a..5b11de801 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -10,7 +10,7 @@ #include #include #include -#include +#include #include #include #include @@ -24,8 +24,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("MediaKitVideoPluginCApi")); PermissionHandlerWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); - ScreenRetrieverPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); + ScreenRetrieverWindowsPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); UrlLauncherWindowsRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 9035bebbc..b32875e97 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -7,7 +7,7 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_libs_windows_video media_kit_video permission_handler_windows - screen_retriever + screen_retriever_windows share_plus url_launcher_windows window_manager diff --git a/windows/innosetup.iss b/windows/innosetup.iss index cf5a95cc5..5810ec51d 100644 --- a/windows/innosetup.iss +++ b/windows/innosetup.iss @@ -59,7 +59,3 @@ 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