diff --git a/.fvmrc b/.fvmrc index 19e8577e95..5913bec0cf 100644 --- a/.fvmrc +++ b/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.41.6" + "flutter": "3.44.4" } \ No newline at end of file diff --git "a/.github/ISSUE_TEMPLATE/bug-\345\217\215\351\246\210.yml" "b/.github/ISSUE_TEMPLATE/bug-\345\217\215\351\246\210.yml" index 3be86597cd..7dd70e4ffa 100644 --- "a/.github/ISSUE_TEMPLATE/bug-\345\217\215\351\246\210.yml" +++ "b/.github/ISSUE_TEMPLATE/bug-\345\217\215\351\246\210.yml" @@ -11,18 +11,18 @@ body: options: - label: 搜索了 [历史 issue](https://github.com/FRBLanApps/PiliSuper/issues?q=is%3Aissue) ,并未发现相同问题 required: true - - label: 搜索了 [上游的历史 issue](https://github.com/bggRGjQaUbCoE/PiliPlus/issues?q=is%3Aissue) ,并未发现相同问题 + - label: 搜索了 [上游的历史 issue](https://github.com/bggRGjQaUbCoE/PiliPlus/issues?q=is%3Aissue) ,404 not found required: true - label: 正在使用最新版本。 required: true - label: 已排除网络问题 required: true + - label: 我并没有详细看这些选项,我只是机械化的选了这些,请关闭本issue。 + required: false - label: 已排除账号问题 required: true - label: 已排除设置问题 required: true - - label: 我并没有详细看这些选项,我只是机械化的选了这些,请关闭本issue。 - required: false - type: checkboxes id: assign diff --git "a/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\350\257\267\346\261\202.yml" "b/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\350\257\267\346\261\202.yml" index b9f3b4eb4a..47b367099f 100644 --- "a/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\350\257\267\346\261\202.yml" +++ "b/.github/ISSUE_TEMPLATE/\345\212\237\350\203\275\350\257\267\346\261\202.yml" @@ -11,14 +11,15 @@ body: options: - label: 搜索了 [历史 issue](https://github.com/FRBLanApps/PiliSuper/issues?q=is%3Aissue) ,并未发现相同功能请求 required: true - - label: 搜索了 [上游的历史 issue](https://github.com/bggRGjQaUbCoE/PiliPlus/issues?q=is%3Aissue) ,并未发现相同功能请求 + - label: 搜索了 [上游的历史 issue](https://github.com/bggRGjQaUbCoE/PiliPlus/issues?q=is%3Aissue) ,404 not found required: true - label: 正在使用最新版本。 required: true - - label: 设置中未搜索到该功能 - required: true - label: 我并没有详细看这些选项,我只是机械化的选了这些,请关闭本issue。 required: false + - label: 设置中未搜索到该功能 + required: true + - type: checkboxes id: assign diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a117f00324..f7dbb71f7f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,6 +26,10 @@ on: description: "Build Android" default: true type: boolean + build_android_dev: + description: "Build Android Dev version" + default: true + type: boolean build_ios: description: "Build iOS" default: true @@ -77,7 +81,7 @@ jobs: run: | dart pub global activate rename - - name: Build + - name: Build Release env: KEYSTORE_B64: ${{ secrets.SIGN_KEYSTORE_BASE64 }} KEY_ALIAS: ${{ secrets.KEY_ALIAS }} @@ -115,13 +119,45 @@ jobs: "${SIGN_ARGS[@]}" \ "${NAME_ARGS[@]}" \ + - name: Build Dev + env: + KEYSTORE_B64: ${{ secrets.SIGN_KEYSTORE_BASE64 }} + KEY_ALIAS: ${{ secrets.KEY_ALIAS }} + KEY_PASS: ${{ secrets.KEY_PASSWORD }} + STORE_PASS: ${{ secrets.KEYSTORE_PASSWORD }} + if: ${{ github.event_name == 'pull_request' || inputs.build_android_dev == true }} + run: | + SIGN_ARGS=() + if [[ "${{ github.event_name }}" == "workflow_dispatch" && -n "$KEYSTORE_B64" ]]; then + SIGN_ARGS+=( + --sign + --keystore-base64 "$KEYSTORE_B64" + --key-alias "$KEY_ALIAS" + --key-password "$KEY_PASS" + --store-password "$STORE_PASS" + --clean-keys + ) + fi + + python3 build.py android \ + --output-prefix PiliSuper \ + --output dist \ + --dev \ + --skip_rename \ + --no-patches \ + --no-pub-get \ + "${SIGN_ARGS[@]}" \ + - name: Release if: ${{ inputs.tag != '' }} uses: softprops/action-gh-release@v2 with: tag_name: ${{ inputs.tag }} name: ${{ inputs.tag }} - files: dist/PiliSuper_android_*.apk + files: | + dist/PiliSuper_android_*_arm64-v8a.apk + dist/PiliSuper_android_*_armeabi-v7a.apk + dist/PiliSuper_android_*_x86_64.apk - uses: actions/upload-artifact@v7 with: @@ -140,6 +176,24 @@ jobs: name: Android_x86_64 path: dist/PiliSuper_android_*_x86_64.apk archive: false + + - uses: actions/upload-artifact@v7 + with: + name: Android_arm64-v8a + path: dist/PiliSuper_android_*_arm64-v8a_dev.apk + archive: false + + - uses: actions/upload-artifact@v7 + with: + name: Android_armeabi-v7a + path: dist/PiliSuper_android_*_armeabi-v7a_dev.apk + archive: false + + - uses: actions/upload-artifact@v7 + with: + name: Android_x86_64 + path: dist/PiliSuper_android_*_x86_64_dev.apk + archive: false # ───────────── iOS ───────────── ios: diff --git a/.gitignore b/.gitignore index 8bb30a4efc..edb399e53e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,10 +5,13 @@ *.swp .DS_Store .atom/ +.build/ .buildlog/ .history .svn/ +.swiftpm/ migrate_working_dir/ +Package.resolved # IntelliJ related *.iml @@ -146,4 +149,4 @@ pili_release.json dist -test*.dart \ No newline at end of file +test* \ No newline at end of file diff --git a/README.md b/README.md index 079d8daf18..248e9065f8 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,10 @@ 直接在关于那里找导入/导出登录信息/设置即可。 产生的文本既适用于本项目,也适用于上游的PiliPlus。 +## 致歉 + +抱歉各位用户,近两个月团队成员都在忙于中考高考与竞赛等人生大事,无暇顾及本软件的开发工作,对此,我们深表歉意,我们将会在未来给大家带来更好的功能更新,敬请期待。 + ## 适配平台 - [x] Android diff --git a/analysis_options.yaml b/analysis_options.yaml index 76b0839624..143015c4c5 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -12,7 +12,6 @@ include: package:flutter_lints/flutter.yaml analyzer: exclude: - lib/grpc/bilibili/** - # - lib/grpc/google/** # - lib/common/widgets/flutter/** formatter: diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts index 8128334d62..a101e55c93 100644 --- a/android/app/build.gradle.kts +++ b/android/app/build.gradle.kts @@ -3,11 +3,20 @@ import org.jetbrains.kotlin.konan.properties.Properties plugins { id("com.android.application") - id("kotlin-android") // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. id("dev.flutter.flutter-gradle-plugin") } +val agpMajorVersion = com.android.Version.ANDROID_GRADLE_PLUGIN_VERSION + .substringBefore('.') + .toInt() +val builtInKotlinProperty = providers.gradleProperty("android.builtInKotlin").orNull +val isBuiltInKotlinEnabled = agpMajorVersion >= 9 && + (builtInKotlinProperty == null || builtInKotlinProperty.toBoolean()) +if (!isBuiltInKotlinEnabled) { + apply(plugin = "org.jetbrains.kotlin.android") +} + android { namespace = "com.example.piliplus" compileSdk = flutter.compileSdkVersion @@ -18,12 +27,6 @@ android { targetCompatibility = JavaVersion.VERSION_17 } - kotlin { - compilerOptions { - jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17) - } - } - defaultConfig { applicationId = "com.example.piliplus" minSdk = flutter.minSdkVersion @@ -51,6 +54,12 @@ android { } } + buildFeatures { + if (project.hasProperty("dev")) { + resValues = true + } + } + buildTypes { all { signingConfig = config ?: signingConfigs["debug"] @@ -82,6 +91,12 @@ android { } } +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + flutter { source = "../.." } diff --git a/android/app/src/main/java/com/example/piliplus/AndroidHelper.java b/android/app/src/main/java/com/example/piliplus/AndroidHelper.java new file mode 100644 index 0000000000..ad199bc20a --- /dev/null +++ b/android/app/src/main/java/com/example/piliplus/AndroidHelper.java @@ -0,0 +1,272 @@ +package com.example.piliplus; + +import android.app.Activity; +import android.app.PendingIntent; +import android.app.PictureInPictureParams; +import android.app.RemoteAction; +import android.app.SearchManager; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ShortcutInfo; +import android.content.pm.ShortcutManager; +import android.graphics.Bitmap; +import android.graphics.BitmapFactory; +import android.graphics.Point; +import android.graphics.Rect; +import android.graphics.drawable.Icon; +import android.media.session.PlaybackState; +import android.net.Uri; +import android.os.Build; +import android.provider.MediaStore; +import android.provider.Settings; +import android.util.Rational; +import android.view.WindowManager; + +import androidx.annotation.DrawableRes; +import androidx.annotation.Keep; +import androidx.annotation.NonNull; +import androidx.annotation.RequiresApi; + +import com.github.dart_lang.jni_flutter.JniFlutterPlugin; + +import java.util.ArrayList; +import java.util.Objects; + +@Keep +public final class AndroidHelper { + public static final boolean isFoldable; + + public static final boolean isPipAvailable; + + public static volatile boolean isPipMode = false; + + static { + PackageManager pm = getContext().getPackageManager(); + isFoldable = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && pm.hasSystemFeature(PackageManager.FEATURE_SENSOR_HINGE_ANGLE); + isPipAvailable = pm.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE); + } + + private AndroidHelper() { + } + + private static Context getContext() { + return JniFlutterPlugin.getApplicationContext(); + } + + public static int sdkInt() { + return Build.VERSION.SDK_INT; + } + + public static void back() { + Intent intent = new Intent(Intent.ACTION_MAIN); + intent.addCategory(Intent.CATEGORY_HOME); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + getContext().startActivity(intent); + } + + public static void biliSendCommAntifraud( + int action, long oid, int type, long rpId, long root, long parent, long ctime, @NonNull String commentText, + String pictures, @NonNull String sourceId, long uid, @NonNull String cookie + ) { + Intent intent = new Intent(); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.setComponent(new ComponentName( + "icu.freedomIntrovert.biliSendCommAntifraud", + "icu.freedomIntrovert.biliSendCommAntifraud.ByXposedLaunchedActivity" + )); + intent.putExtra("action", action); + intent.putExtra("oid", oid); + intent.putExtra("type", type); + intent.putExtra("rpid", rpId); + intent.putExtra("root", root); + intent.putExtra("parent", parent); + intent.putExtra("ctime", ctime); + intent.putExtra("comment_text", commentText); + if (pictures != null) { + intent.putExtra("pictures", pictures); + } + intent.putExtra("source_id", sourceId); + intent.putExtra("uid", uid); + ArrayList cookiesList = new ArrayList<>(1); + cookiesList.add(cookie); + intent.putStringArrayListExtra("cookies", cookiesList); + getContext().startActivity(intent); + } + + public static void openLinkVerifySettings() { + Context context = getContext(); + Uri uri = Uri.parse("package:" + context.getPackageName()); + try { + Intent intent; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + intent = new Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri); + } else { + intent = new Intent(Intent.ACTION_MAIN, uri); + intent.setClassName( + "com.android.settings", + "com.android.settings.applications.InstalledAppOpenByDefaultActivity" + ); + } + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } catch (Exception ignored) { + Intent intent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + context.startActivity(intent); + } + } + + public static boolean openMusic(@NonNull String title, String artist, String album) { + Intent intent = new Intent(MediaStore.INTENT_ACTION_MEDIA_SEARCH); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + intent.putExtra(SearchManager.QUERY, title); + intent.putExtra(MediaStore.EXTRA_MEDIA_TITLE, title); + if (artist != null) { + intent.putExtra(MediaStore.EXTRA_MEDIA_ARTIST, artist); + } + if (album != null) { + intent.putExtra(MediaStore.EXTRA_MEDIA_ALBUM, album); + } + intent.addCategory(Intent.CATEGORY_DEFAULT); + + Context context = getContext(); + PackageManager pm = context.getPackageManager(); + + try { + if (pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) { + context.startActivity(intent); + return true; + } + } catch (Exception ignored) { + } + + try { + intent.setAction(MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH); + if (pm.resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY) != null) { + context.startActivity(intent); + return true; + } + } catch (Exception ignored) { + } + + return false; + } + + public static void enterPip(long engineId, int width, int height, boolean autoEnter, boolean isLive, boolean isPlaying) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Activity activity = JniFlutterPlugin.getActivity(engineId); + assert activity != null; + PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder() + .setAspectRatio(new Rational(width, height)); + setPipActions(activity, builder, isLive, isPlaying); + if (autoEnter) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + builder.setAutoEnterEnabled(true); + activity.setPictureInPictureParams(builder.build()); + } + } else { + activity.enterPictureInPictureMode(builder.build()); + } + } + } + + @RequiresApi(api = Build.VERSION_CODES.O) + public static void updatePipActions(long engineId, boolean isLive, boolean isPlaying) { + Activity activity = JniFlutterPlugin.getActivity(engineId); + assert activity != null; + PictureInPictureParams.Builder builder = new PictureInPictureParams.Builder(); + setPipActions(activity, builder, isLive, isPlaying); + activity.setPictureInPictureParams(builder.build()); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private static void setPipActions(Activity activity, PictureInPictureParams.Builder builder, boolean isLive, boolean isPlaying) { + ComponentName mbrComponent = MediaHelper.getMediaButtonReceiverComponent(activity); + if (mbrComponent == null) return; + ArrayList actionList = new ArrayList<>(3); + if (!isLive) { + actionList.add(getRemoteAction(mbrComponent, activity, R.drawable.ic_player_rewind_10s, "ACTION_REWIND", (int) PlaybackState.ACTION_REWIND)); + } + if (isPlaying) { + actionList.add(getRemoteAction(mbrComponent, activity, R.drawable.ic_player_pause, "ACTION_PAUSE", (int) PlaybackState.ACTION_PAUSE)); + } else { + actionList.add(getRemoteAction(mbrComponent, activity, R.drawable.ic_player_play, "ACTION_PLAY", (int) PlaybackState.ACTION_PLAY)); + } + if (!isLive) { + actionList.add(getRemoteAction(mbrComponent, activity, R.drawable.ic_player_fast_forward_10s, "ACTION_FAST_FORWARD", (int) PlaybackState.ACTION_FAST_FORWARD)); + } + builder.setActions(actionList); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private static RemoteAction getRemoteAction(@NonNull ComponentName mbrComponent, Activity activity, @DrawableRes int resId, String title, int action) { + return new RemoteAction( + Icon.createWithResource(activity, resId), + title, + title, + Objects.requireNonNull(MediaHelper.buildMediaButtonPendingIntent(activity, mbrComponent, action)) + ); + } + + public static void disableAutoEnterPip(long engineId) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Activity activity = JniFlutterPlugin.getActivity(engineId); + if (activity != null) { + activity.setPictureInPictureParams(new PictureInPictureParams.Builder() + .setAutoEnterEnabled(false) + .build() + ); + } + } + } + + public static int[] maxScreenSize() { + Context context = getContext(); + WindowManager wm = context.getSystemService(WindowManager.class); + try { + float density = context.getResources().getDisplayMetrics().density; + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + Rect maxBounds = wm.getMaximumWindowMetrics().getBounds(); + return new int[]{Math.round(maxBounds.width() / density), Math.round(maxBounds.height() / density)}; + } else { + Point realSize = new Point(); + wm.getDefaultDisplay().getRealSize(realSize); + return new int[]{Math.round(realSize.x / density), Math.round(realSize.y / density)}; + } + } catch (Exception ignored) { + return null; + } + } + + public static void createShortcut(@NonNull String id, @NonNull String uri, @NonNull String label, @NonNull String icon) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + Context context = getContext(); + ShortcutManager shortcutManager = context.getSystemService(ShortcutManager.class); + if (shortcutManager != null && shortcutManager.isRequestPinShortcutSupported()) { + Bitmap bitmap = BitmapFactory.decodeFile(icon); + ShortcutInfo shortcut = new ShortcutInfo.Builder(context, id) + .setShortLabel(label) + .setIcon(Icon.createWithAdaptiveBitmap(bitmap)) + .setIntent(new Intent(Intent.ACTION_VIEW, Uri.parse(uri))) + .build(); + // TODO: WorkerThread + Intent pinIntent = shortcutManager.createShortcutResultIntent(shortcut); + PendingIntent pendingIntent = PendingIntent.getBroadcast( + context, 0, pinIntent, PendingIntent.FLAG_IMMUTABLE + ); + shortcutManager.requestPinShortcut(shortcut, pendingIntent.getIntentSender()); + } + } + } + + @Keep + public static final class ToDart { + public static volatile Runnable onUserLeaveHint; + public static Runnable onConfigurationChanged; + + private ToDart() { + } + } +} diff --git a/android/app/src/main/java/com/example/piliplus/MediaHelper.java b/android/app/src/main/java/com/example/piliplus/MediaHelper.java new file mode 100644 index 0000000000..a3befb3e99 --- /dev/null +++ b/android/app/src/main/java/com/example/piliplus/MediaHelper.java @@ -0,0 +1,83 @@ +/* + * Copyright 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.example.piliplus; + +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.media.session.PlaybackState; +import android.os.Build; +import android.util.Log; +import android.view.KeyEvent; + +import java.util.List; + +class MediaHelper { + private static final String TAG = "MediaButtonReceiver"; + + static PendingIntent buildMediaButtonPendingIntent(Context context, ComponentName mbrComponent, int action) { + if (mbrComponent == null) { + Log.w(TAG, "The component name of media button receiver should be provided."); + return null; + } + int keyCode = PlaybackStateCompat_toKeyCode(action); + if (keyCode == KeyEvent.KEYCODE_UNKNOWN) { + Log.w(TAG, + "Cannot build a media button pending intent with the given action: " + action); + return null; + } + Intent intent = new Intent(Intent.ACTION_MEDIA_BUTTON); + intent.setComponent(mbrComponent); + intent.putExtra(Intent.EXTRA_KEY_EVENT, new KeyEvent(KeyEvent.ACTION_DOWN, keyCode)); + intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND); + return PendingIntent.getBroadcast(context, keyCode, intent, + Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ? PendingIntent.FLAG_MUTABLE : 0); + } + + private static int PlaybackStateCompat_toKeyCode(int action) { + return switch (action) { + case (int) PlaybackState.ACTION_STOP -> KeyEvent.KEYCODE_MEDIA_STOP; + case (int) PlaybackState.ACTION_PAUSE -> KeyEvent.KEYCODE_MEDIA_PAUSE; + case (int) PlaybackState.ACTION_PLAY -> KeyEvent.KEYCODE_MEDIA_PLAY; + case (int) PlaybackState.ACTION_REWIND -> KeyEvent.KEYCODE_MEDIA_REWIND; + case (int) PlaybackState.ACTION_SKIP_TO_PREVIOUS -> KeyEvent.KEYCODE_MEDIA_PREVIOUS; + case (int) PlaybackState.ACTION_SKIP_TO_NEXT -> KeyEvent.KEYCODE_MEDIA_NEXT; + case (int) PlaybackState.ACTION_FAST_FORWARD -> KeyEvent.KEYCODE_MEDIA_FAST_FORWARD; + case (int) PlaybackState.ACTION_PLAY_PAUSE -> KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE; + default -> KeyEvent.KEYCODE_UNKNOWN; + }; + } + + static ComponentName getMediaButtonReceiverComponent(Context context) { + Intent queryIntent = new Intent(Intent.ACTION_MEDIA_BUTTON); + queryIntent.setPackage(context.getPackageName()); + PackageManager pm = context.getPackageManager(); + List resolveInfos = pm.queryBroadcastReceivers(queryIntent, 0); + if (resolveInfos.size() == 1) { + ResolveInfo resolveInfo = resolveInfos.get(0); + return new ComponentName(resolveInfo.activityInfo.packageName, + resolveInfo.activityInfo.name); + } else if (resolveInfos.size() > 1) { + Log.w(TAG, "More than one BroadcastReceiver that handles " + + Intent.ACTION_MEDIA_BUTTON + " was found, returning null."); + } + return null; + } +} diff --git a/android/app/src/main/kotlin/com/example/piliplus/MainActivity.kt b/android/app/src/main/kotlin/com/example/piliplus/MainActivity.kt index 21d46b4707..d3b27cc9e6 100644 --- a/android/app/src/main/kotlin/com/example/piliplus/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/piliplus/MainActivity.kt @@ -1,187 +1,18 @@ package com.example.piliplus -import android.app.PendingIntent -import android.app.PictureInPictureParams -import android.app.SearchManager -import android.content.ComponentName import android.content.Intent -import android.content.pm.PackageManager -import android.content.pm.ShortcutInfo -import android.content.pm.ShortcutManager import android.content.res.Configuration -import android.graphics.BitmapFactory -import android.graphics.drawable.Icon import android.os.Build import android.os.Bundle -import android.provider.MediaStore -import android.provider.Settings import android.view.WindowManager.LayoutParams -import androidx.core.net.toUri import com.ryanheise.audioservice.AudioServiceActivity -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugin.common.MethodChannel -import kotlin.system.exitProcess -import java.io.File class MainActivity : AudioServiceActivity() { - private lateinit var methodChannel: MethodChannel - - override fun configureFlutterEngine(flutterEngine: FlutterEngine) { - super.configureFlutterEngine(flutterEngine) - - methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, "PiliPlus") - methodChannel.setMethodCallHandler { call, result -> - when (call.method) { - "back" -> back(); - "biliSendCommAntifraud" -> { - try { - val action = call.argument("action") ?: 0 - val oid = call.argument("oid") ?: 0L - val type = call.argument("type") ?: 0 - val rpid = call.argument("rpid") ?: 0L - val root = call.argument("root") ?: 0L - val parent = call.argument("parent") ?: 0L - val ctime = call.argument("ctime") ?: 0L - val commentText = call.argument("comment_text") ?: "" - val pictures = call.argument("pictures") - val sourceId = call.argument("source_id") ?: "" - val uid = call.argument("uid") ?: 0L - val cookies = call.argument>("cookies") ?: emptyList() - - val intent = Intent().apply { - component = ComponentName( - "icu.freedomIntrovert.biliSendCommAntifraud", - "icu.freedomIntrovert.biliSendCommAntifraud.ByXposedLaunchedActivity" - ) - putExtra("action", action) - putExtra("oid", oid.toLong()) - putExtra("type", type) - putExtra("rpid", rpid.toLong()) - putExtra("root", root.toLong()) - putExtra("parent", parent.toLong()) - putExtra("ctime", ctime.toLong()) - putExtra("comment_text", commentText) - if (pictures != null) - putExtra("pictures", pictures) - putExtra("source_id", sourceId) - putExtra("uid", uid.toLong()) - putStringArrayListExtra("cookies", ArrayList(cookies)) - } - startActivity(intent) - } catch (_: Exception) { - } - } - - "linkVerifySettings" -> { - val uri = ("package:" + context.packageName).toUri() - try { - val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - Intent(Settings.ACTION_APP_OPEN_BY_DEFAULT_SETTINGS, uri) - } else { - Intent("android.intent.action.MAIN", uri).setClassName( - "com.android.settings", - "com.android.settings.applications.InstalledAppOpenByDefaultActivity" - ) - } - context.startActivity(intent) - } catch (_: Throwable) { - val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS, uri) - context.startActivity(intent) - } - } - - "music" -> { - val title = call.argument("title") - val intent = Intent(MediaStore.INTENT_ACTION_MEDIA_SEARCH).apply { - putExtra(SearchManager.QUERY, title) - putExtra(MediaStore.EXTRA_MEDIA_TITLE, title) - call.argument("artist") - ?.let { putExtra(MediaStore.EXTRA_MEDIA_ARTIST, it) } - call.argument("album") - ?.let { putExtra(MediaStore.EXTRA_MEDIA_ALBUM, it) } - - addCategory(Intent.CATEGORY_DEFAULT) - } - try { - if (packageManager.resolveActivity( - intent, - PackageManager.MATCH_DEFAULT_ONLY - ) != null - ) { - startActivity(intent) - result.success(true) - return@setMethodCallHandler - } - } catch (_: Throwable) { - } - try { - intent.action = MediaStore.INTENT_ACTION_MEDIA_PLAY_FROM_SEARCH - if (packageManager.resolveActivity( - intent, - PackageManager.MATCH_DEFAULT_ONLY - ) != null - ) { - startActivity(intent) - result.success(true) - return@setMethodCallHandler - } - } catch (_: Throwable) { - } - result.success(false) - } - - "setPipAutoEnterEnabled" -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - val params = PictureInPictureParams.Builder() - .setAutoEnterEnabled(call.argument("autoEnable") ?: false) - .build() - setPictureInPictureParams(params) - } - } - - "createShortcut" -> { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - try { - val shortcutManager = - context.getSystemService(ShortcutManager::class.java) - if (shortcutManager.isRequestPinShortcutSupported) { - val id = call.argument("id")!! - val uri = call.argument("uri")!! - val label = call.argument("label")!! - val icon = call.argument("icon")!! - val bitmap = BitmapFactory.decodeFile(icon) - val shortcut = - ShortcutInfo.Builder(context, id) - .setShortLabel(label) - .setIcon(Icon.createWithAdaptiveBitmap(bitmap)) - .setIntent(Intent(Intent.ACTION_VIEW, uri.toUri())) - .build() - val pinIntent = - shortcutManager.createShortcutResultIntent(shortcut) - val pendingIntent = PendingIntent.getBroadcast( - context, 0, pinIntent, PendingIntent.FLAG_IMMUTABLE - ) - shortcutManager.requestPinShortcut( - shortcut, - pendingIntent.intentSender - ) - } - } catch (e: Exception) { - } - } - } - - else -> result.notImplemented() - } - } - } - - private fun back() { - val intent = Intent(Intent.ACTION_MAIN).apply { - addCategory(Intent.CATEGORY_HOME) - flags = Intent.FLAG_ACTIVITY_NEW_TASK + override fun onConfigurationChanged(newConfig: Configuration) { + super.onConfigurationChanged(newConfig) + if (AndroidHelper.isFoldable) { + AndroidHelper.ToDart.onConfigurationChanged?.run() } - startActivity(intent) } override fun onCreate(savedInstanceState: Bundle?) { @@ -195,23 +26,15 @@ class MainActivity : AudioServiceActivity() { override fun onDestroy() { stopService(Intent(this, com.ryanheise.audioservice.AudioService::class.java)) super.onDestroy() - android.os.Process.killProcess(android.os.Process.myPid()) - exitProcess(0) } override fun onUserLeaveHint() { super.onUserLeaveHint() - methodChannel.invokeMethod("onUserLeaveHint", null) + AndroidHelper.ToDart.onUserLeaveHint?.run() } - override fun onPictureInPictureModeChanged( - isInPictureInPictureMode: Boolean, - newConfig: Configuration? - ) { + override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) { super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) - MethodChannel( - flutterEngine!!.dartExecutor.binaryMessenger, - "floating" - ).invokeMethod("onPipChanged", isInPictureInPictureMode) + AndroidHelper.isPipMode = isInPictureInPictureMode } } diff --git a/android/app/src/main/res/drawable/ic_baseline_forward_10_24.xml b/android/app/src/main/res/drawable/ic_baseline_forward_10_24.xml deleted file mode 100644 index f6a6c06497..0000000000 --- a/android/app/src/main/res/drawable/ic_baseline_forward_10_24.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_baseline_replay_10_24.xml b/android/app/src/main/res/drawable/ic_baseline_replay_10_24.xml deleted file mode 100644 index 06db412b17..0000000000 --- a/android/app/src/main/res/drawable/ic_baseline_replay_10_24.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/android/app/src/main/res/drawable/ic_notification_icon.xml b/android/app/src/main/res/drawable/ic_notification_icon.xml index 697e5cbdaa..ed3c75aaed 100644 --- a/android/app/src/main/res/drawable/ic_notification_icon.xml +++ b/android/app/src/main/res/drawable/ic_notification_icon.xml @@ -6,7 +6,6 @@ android:viewportHeight="108.0"> diff --git a/android/app/src/main/res/drawable/ic_player_fast_forward_10s.xml b/android/app/src/main/res/drawable/ic_player_fast_forward_10s.xml new file mode 100644 index 0000000000..7afb5605ee --- /dev/null +++ b/android/app/src/main/res/drawable/ic_player_fast_forward_10s.xml @@ -0,0 +1,11 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_player_pause.xml b/android/app/src/main/res/drawable/ic_player_pause.xml new file mode 100644 index 0000000000..e5a6faaadf --- /dev/null +++ b/android/app/src/main/res/drawable/ic_player_pause.xml @@ -0,0 +1,11 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_player_play.xml b/android/app/src/main/res/drawable/ic_player_play.xml new file mode 100644 index 0000000000..e185c82f6a --- /dev/null +++ b/android/app/src/main/res/drawable/ic_player_play.xml @@ -0,0 +1,10 @@ + + + + diff --git a/android/app/src/main/res/drawable/ic_player_rewind_10s.xml b/android/app/src/main/res/drawable/ic_player_rewind_10s.xml new file mode 100644 index 0000000000..55cdc09fff --- /dev/null +++ b/android/app/src/main/res/drawable/ic_player_rewind_10s.xml @@ -0,0 +1,11 @@ + + + + diff --git a/android/build.gradle.kts b/android/build.gradle.kts index be844ccc8e..59b31be82c 100644 --- a/android/build.gradle.kts +++ b/android/build.gradle.kts @@ -43,14 +43,14 @@ subprojects { val pluginCompileSdk = pluginCompileSdkStr ?.removePrefix("android-") ?.toIntOrNull() - if (pluginCompileSdk != null && pluginCompileSdk < 31) { + if (pluginCompileSdk != null && pluginCompileSdk < 36) { project.logger.error( "Warning: Overriding compileSdk version in Flutter plugin: ${project.name} " + - "from $pluginCompileSdk to 31 (to work around https://issuetracker.google.com/issues/199180389).\n" + + "from $pluginCompileSdk to 36 (to work around https://issuetracker.google.com/issues/199180389).\n" + "If there is not a new version of ${project.name}, consider filing an issue against ${project.name} " + "to increase their compileSdk to the latest (otherwise try updating to the latest version)." ) - androidExtension.setCompileSdkVersion(31) + androidExtension.setCompileSdkVersion(36) } } diff --git a/android/gradle.properties b/android/gradle.properties index f3dfe33489..2f0b4caddd 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,3 +1,5 @@ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true -android.enableJetifier=true \ No newline at end of file +android.enableJetifier=true +android.builtInKotlin=false +android.newDsl=false diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 82282519db..497e99979a 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.5.0-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts index 8de0af82b9..c21f0c5b49 100644 --- a/android/settings.gradle.kts +++ b/android/settings.gradle.kts @@ -19,8 +19,8 @@ pluginManagement { plugins { id("dev.flutter.flutter-plugin-loader") version "1.0.0" - id("com.android.application") version "8.12.1" apply false - id("org.jetbrains.kotlin.android") version "2.2.20" apply false + id("com.android.application") version "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false } include(":app") diff --git a/assets/fonts/custom_icon.ttf b/assets/fonts/custom_icon.ttf index 88e7f64fce..f7e4a4eaca 100644 Binary files a/assets/fonts/custom_icon.ttf and b/assets/fonts/custom_icon.ttf differ diff --git a/assets/images/big-vip.png b/assets/images/big-vip.png deleted file mode 100644 index bb00915466..0000000000 Binary files a/assets/images/big-vip.png and /dev/null differ diff --git a/assets/images/big-vip.svg b/assets/images/big-vip.svg new file mode 100644 index 0000000000..089eeeae41 --- /dev/null +++ b/assets/images/big-vip.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/dm.svg b/assets/images/dm.svg deleted file mode 100644 index 2690acd204..0000000000 --- a/assets/images/dm.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/assets/images/dm_gray.png b/assets/images/dm_gray.png deleted file mode 100644 index 438cffc023..0000000000 Binary files a/assets/images/dm_gray.png and /dev/null differ diff --git a/assets/images/dm_white.png b/assets/images/dm_white.png deleted file mode 100644 index 71fd28f94e..0000000000 Binary files a/assets/images/dm_white.png and /dev/null differ diff --git a/assets/images/logo/logo_3.png b/assets/images/logo/logo_3.png deleted file mode 100644 index ce5fed6b70..0000000000 Binary files a/assets/images/logo/logo_3.png and /dev/null differ diff --git a/assets/images/lv/lv0.png b/assets/images/lv/lv0.png deleted file mode 100644 index f9ed49b620..0000000000 Binary files a/assets/images/lv/lv0.png and /dev/null differ diff --git a/assets/images/lv/lv1.png b/assets/images/lv/lv1.png deleted file mode 100644 index ad8b70ce0a..0000000000 Binary files a/assets/images/lv/lv1.png and /dev/null differ diff --git a/assets/images/lv/lv2.png b/assets/images/lv/lv2.png deleted file mode 100644 index c369f5ada8..0000000000 Binary files a/assets/images/lv/lv2.png and /dev/null differ diff --git a/assets/images/lv/lv3.png b/assets/images/lv/lv3.png deleted file mode 100644 index 80a0db3fad..0000000000 Binary files a/assets/images/lv/lv3.png and /dev/null differ diff --git a/assets/images/lv/lv4.png b/assets/images/lv/lv4.png deleted file mode 100644 index 5441967b20..0000000000 Binary files a/assets/images/lv/lv4.png and /dev/null differ diff --git a/assets/images/lv/lv5.png b/assets/images/lv/lv5.png deleted file mode 100644 index dedd1309c0..0000000000 Binary files a/assets/images/lv/lv5.png and /dev/null differ diff --git a/assets/images/lv/lv6.png b/assets/images/lv/lv6.png deleted file mode 100644 index 399e585a70..0000000000 Binary files a/assets/images/lv/lv6.png and /dev/null differ diff --git a/assets/images/lv/lv6_s.png b/assets/images/lv/lv6_s.png deleted file mode 100644 index 4b867500c7..0000000000 Binary files a/assets/images/lv/lv6_s.png and /dev/null differ diff --git a/assets/images/play.png b/assets/images/play.png deleted file mode 100644 index b5d0299c58..0000000000 Binary files a/assets/images/play.png and /dev/null differ diff --git a/assets/images/play.svg b/assets/images/play.svg deleted file mode 100644 index 0032f069f6..0000000000 --- a/assets/images/play.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/tv.svg b/assets/images/tv.svg deleted file mode 100644 index fdb077b169..0000000000 --- a/assets/images/tv.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/up.svg b/assets/images/up.svg deleted file mode 100644 index c63989c590..0000000000 --- a/assets/images/up.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/up_gray.png b/assets/images/up_gray.png deleted file mode 100644 index c6d7f4ab7e..0000000000 Binary files a/assets/images/up_gray.png and /dev/null differ diff --git a/assets/images/video/danmu_close.svg b/assets/images/video/danmu_close.svg deleted file mode 100644 index 9f48027b05..0000000000 --- a/assets/images/video/danmu_close.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/assets/images/video/danmu_open.svg b/assets/images/video/danmu_open.svg deleted file mode 100644 index 24e8d7a99c..0000000000 --- a/assets/images/video/danmu_open.svg +++ /dev/null @@ -1 +0,0 @@ -Layer 1 \ No newline at end of file diff --git a/assets/images/view.svg b/assets/images/view.svg deleted file mode 100644 index 88fe609c7b..0000000000 --- a/assets/images/view.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/assets/images/view_gray.png b/assets/images/view_gray.png deleted file mode 100644 index fe2b34825a..0000000000 Binary files a/assets/images/view_gray.png and /dev/null differ diff --git a/assets/images/view_white.png b/assets/images/view_white.png deleted file mode 100644 index d97b0e937d..0000000000 Binary files a/assets/images/view_white.png and /dev/null differ diff --git a/assets/linux/DEBIAN/control b/assets/linux/DEBIAN/control deleted file mode 100644 index 484ad25d69..0000000000 --- a/assets/linux/DEBIAN/control +++ /dev/null @@ -1,16 +0,0 @@ -Package: PiliPlus -Version: version_need_change -Maintainer: gh-MzA4Nzk -Original-Maintainer: bggRGjQaUbCoE -Section: x11 -Priority: optional -Architecture: amd64 -Essential: no -Installed-Size: size_need_change -Description: third-party Bilibili client developed in Flutter -Homepage: https://github.com/bggRGjQaUbCoE/PiliPlus -Depends: libgtk-3-0t64, - libmpv2, - gir1.2-ayatanaappindicator3-0.1, - libayatana-appindicator3-1 - diff --git a/build.py b/build.py index f769e38534..6a0875b1b8 100644 --- a/build.py +++ b/build.py @@ -14,7 +14,6 @@ from __future__ import annotations import argparse -from dataclasses import dataclass import io import json import logging @@ -26,6 +25,7 @@ import sys import textwrap import time +from dataclasses import dataclass from pathlib import Path from typing import Any, Callable, Sequence from urllib.request import urlopen @@ -71,6 +71,7 @@ class BuildOptions: version: str | None no_prebuild: bool no_split: bool + dev: bool sign: bool keystore_file: Path | None keystore_base64: str | None @@ -115,6 +116,7 @@ def from_namespace( version=namespace.version, no_prebuild=namespace.no_prebuild, no_split=namespace.no_split, + dev=namespace.dev, sign=namespace.sign, keystore_file=( Path(namespace.keystore_file) if namespace.keystore_file else None @@ -216,6 +218,7 @@ def configure_logging() -> tuple[logging.Logger, Any | None]: LOGGER, RICH_CONSOLE = configure_logging() SKIP_RENAME_DIR_NAMES = {".git", ".dart_tool", "build", "Pods", "ephemeral"} +#定义日志格式 def _format_log_message(label: str, message: str, style: str) -> str: if RICH_CONSOLE is not None: @@ -260,11 +263,10 @@ def _resolve_command(command: Sequence[str]) -> list[str]: subprocess shell=False 找不到,用 shutil.which 解析完整路径。 """ command_parts = [str(part) for part in command] - if not IS_WINDOWS: - return command_parts - resolved = shutil.which(command_parts[0]) - if resolved: - command_parts = [resolved] + command_parts[1:] + if IS_WINDOWS: + resolved = shutil.which(command_parts[0]) + if resolved: + command_parts = [resolved] + command_parts[1:] return command_parts @@ -452,7 +454,7 @@ def rename_with_search_replace(options: BuildOptions) -> None: orig_pkg = options.original_pkg_id new_pkg = options.pkg_id - dart_globs = ["**/*.dart", "pubspec.yaml"] + dart_globs = ["**/*.dart", "pubspec.yaml", "**/*.patch"] native_globs = [ "android/**/*.kt", "android/**/*.java", @@ -470,8 +472,7 @@ def rename_with_search_replace(options: BuildOptions) -> None: "windows/runner/*.h", "windows/runner/*.cpp", "windows/CMakeLists.txt", - "assets/linux/com.example.piliplus.desktop", - "assets/linux/DEBIAN/*", + "assets/linux/**/*" ] if new_name and orig_name and new_name != orig_name: @@ -673,19 +674,8 @@ def make_flutter_patch_action(name: str) -> FlutterPatchAction: FLUTTER_PATCH_ACTION_PRIORITY = {"cherry-pick": 0, "revert": 1, "patch": 2} FLUTTER_PATCHES_DIR = Path("lib/scripts") -TOOLTIP_FIX_COMMIT = "56956c33ef102ac0b5fc46b62bd2dd9f50a86616" NEW_OVERSCROLL_INDICATOR_COMMIT = "362b1de29974ffc1ed6faa826e1df870d7bec75f" -TOOLTIP_FIX_ACTION = FlutterPatchAction( - kind="cherry-pick", - target=TOOLTIP_FIX_COMMIT, - success_message="Add RawTooltip.ignorePointer 应用完成", - failure_message="Add RawTooltip.ignorePointer 应用失败(已忽略)", - step_message=( - "checkout Add RawTooltip.ignorePointer commit " - f"({TOOLTIP_FIX_COMMIT[:9]}…)" - ), -) OVERSCROLL_INDICATOR_REVERT_ACTION = FlutterPatchAction( kind="revert", target=NEW_OVERSCROLL_INDICATOR_COMMIT, @@ -697,23 +687,40 @@ def make_flutter_patch_action(name: str) -> FlutterPatchAction: ), ) +# 对所有平台都生效的 patch(对应 patch.ps1 中的 $patches 基础数组) COMMON_FLUTTER_PATCH_ACTIONS = ( make_flutter_patch_action("modal_barrier.patch"), make_flutter_patch_action("text_selection.patch"), make_flutter_patch_action("mouse_cursor.patch"), + make_flutter_patch_action("image_anim.patch"), + make_flutter_patch_action("layout_builder.patch"), + make_flutter_patch_action("navigation_drawer.patch"), ) +# 按平台追加的 patch(对应 patch.ps1 中 switch ($platform) 各分支) PLATFORM_FLUTTER_PATCH_ACTIONS = { "android": ( OVERSCROLL_INDICATOR_REVERT_ACTION, - make_flutter_patch_action("bottom_sheet.patch"), + make_flutter_patch_action("bottom_sheet_android.patch"), make_flutter_patch_action("scroll_view.patch"), + make_flutter_patch_action("navigator.patch"), ), - "ios": (make_flutter_patch_action("scroll_view.patch"),), - "linux": (TOOLTIP_FIX_ACTION,), - "macos": (TOOLTIP_FIX_ACTION,), - "windows": (TOOLTIP_FIX_ACTION,), + "ios": ( + make_flutter_patch_action("scroll_view.patch"), + make_flutter_patch_action("bottom_sheet_ios_flutter.patch"), + make_flutter_patch_action("navigator.patch"), + ), + "linux": (), + "macos": (), + "windows": (), } +# 直接作用于项目仓库(而非 Flutter SDK)的 iOS patch,对应 patch.ps1 中 +# `if ($platform.ToLower() -eq "ios")` 分支,须在 Set-Location 到 Flutter SDK 之前应用 +IOS_PROJECT_PATCH_ACTIONS = ( + make_flutter_patch_action("bottom_sheet_ios_piliplus.patch"), + make_flutter_patch_action("geetest_ios.patch"), +) + def dedupe_flutter_patch_actions( actions: Sequence[FlutterPatchAction], @@ -751,7 +758,7 @@ def build_flutter_patch_actions(platform_name: str) -> tuple[FlutterPatchAction, return dedupe_flutter_patch_actions(actions) -def apply_flutter_patch_action(root: str, action: FlutterPatchAction) -> None: +def apply_flutter_patch_action(root: str | None, action: FlutterPatchAction) -> None: if action.step_message: log_step(action.step_message) @@ -788,10 +795,24 @@ def apply_flutter_patch_action(root: str, action: FlutterPatchAction) -> None: def apply_flutter_patches(root: str, platform_name: str = "") -> None: log_step("应用 Flutter patches") + run_command(["git", "reset", "--hard", "HEAD"], cwd=root) for action in build_flutter_patch_actions(platform_name): apply_flutter_patch_action(root, action) + +def apply_ios_project_patches() -> None: + """对应 patch.ps1 中 `if ($platform -eq "ios")` 分支: + 在 Set-Location 到 Flutter SDK 之前,直接对当前项目仓库应用的 patch。""" + log_step("应用 iOS 项目 patches") + for action in IOS_PROJECT_PATCH_ACTIONS: + apply_flutter_patch_action(None, action) + + def run_common_setup(options: BuildOptions) -> str | None: + normalized_platform = options.platform.strip().lower() + if options.apply_patches and normalized_platform in ("ios", "all"): + apply_ios_project_patches() + flutter_root_dir = find_flutter_root() if options.apply_patches and flutter_root_dir: apply_flutter_patches(flutter_root_dir, platform_name=options.platform) @@ -921,6 +942,7 @@ def build_android(context: BuildContext) -> None: [ "--pub", *([] if options.no_split else ["--split-per-abi"]), + *(["--android-project-arg", "dev=1"] if options.dev else []), ], ) ) @@ -936,7 +958,8 @@ def build_android(context: BuildContext) -> None: for apk in apks: match = re.search(r"app-(.+)-release\.apk", apk.name) abi = match.group(1) if match else "universal" - destination = context.platform_output_path("android", abi, suffix=".apk") + name_parts = [abi, "dev"] if options.dev else [abi] + destination = context.platform_output_path("android", *name_parts, suffix=".apk") shutil.copy2(apk, destination) log_success(f"输出: {destination}") @@ -1259,33 +1282,60 @@ def package_deb(context: BuildContext, arch: str, bundle: Path) -> None: ctrl_src = Path("assets/linux/DEBIAN") (root / "DEBIAN").mkdir(exist_ok=True) + installed_size_kb = ( + sum( + f.stat().st_size + for f in (root / "opt/app").rglob("*") + if f.is_file() + ) + // 1024 + + 1 + ) + depends = ", ".join( + [ + "libgtk-3-0t64", + "libmpv2", + "gir1.2-ayatanaappindicator3-0.1", + "libayatana-appindicator3-1", + ] + ) + control_content = textwrap.dedent(f"""\ + Package: {context.options.app_name} + Version: {context.version} + Maintainer: FRBLanApps Members + Original-Maintainer: bggRGjQaUbCoE + Section: x11 + Priority: optional + Architecture: {deb_arch} + Essential: no + Installed-Size: {installed_size_kb} + Description: third-party Bilibili client developed in Flutter + Homepage: https://github.com/bggRGjQaUbCoE/PiliPlus + Depends: {depends} + """) + (root / "DEBIAN" / "control").write_text(control_content) if ctrl_src.exists(): shutil.copytree(ctrl_src, root / "DEBIAN", dirs_exist_ok=True) - ctrl = root / "DEBIAN/control" - if ctrl.exists(): - txt = ctrl.read_text() - txt = txt.replace("version_need_change", context.version) - txt = re.sub(r"^Architecture:\s+\S+", f"Architecture: {deb_arch}", txt, flags=re.MULTILINE) - size_kb = ( - sum( - f.stat().st_size - for f in (root / "opt/app").rglob("*") - if f.is_file() - ) - // 1024 - + 1 - ) - txt = txt.replace("size_need_change", str(size_kb)) - ctrl.write_text(txt, encoding="utf-8") + # ctrl = root / "DEBIAN/control" + # if ctrl.exists(): + # txt = ctrl.read_text() + # txt = txt.replace("version_need_change", context.version) + # txt = re.sub(r"^Architecture:\s+\S+", f"Architecture: {deb_arch}", txt, flags=re.MULTILINE) + # size_kb = ( + # sum( + # f.stat().st_size + # for f in (root / "opt/app").rglob("*") + # if f.is_file() + # ) + # // 1024 + # + 1 + # ) + # txt = txt.replace("size_need_change", str(size_kb)) + # ctrl.write_text(txt, encoding="utf-8") for s in ["postinst", "postrm", "prerm"]: sp = root / "DEBIAN" / s if sp.exists(): sp.chmod(0o755) - else: - (root / "DEBIAN/control").write_text( - f"Package: {app_name}\nVersion: {context.version}\nArchitecture: {deb_arch}\n" - f"Maintainer: Unknown\nInstalled-Size: 0\nDescription: Flutter App\n" - ) if desktop: shutil.copy2(desktop, root / "usr/share/applications" / desktop.name) @@ -1580,6 +1630,13 @@ def parse_arguments() -> BuildOptions: android_group = parser.add_argument_group("Android") android_group.add_argument("--no-split", action="store_true") + android_group.add_argument( + "--dev", + action="store_true", + help="构建 dev APK:附加 --android-project-arg dev=1," + "对应上游 build_android.yml 中 PR 触发的 Dev Apk 步骤;" + "输出文件名会额外带 dev 标记,不会覆盖正式 release APK", + ) android_signing_group = parser.add_argument_group("Android 签名") android_signing_group.add_argument("--sign", action="store_true") diff --git a/ios/Podfile b/ios/Podfile index a1de6446bb..29e3d9b8b1 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '13.0' +platform :ios, '14.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. ENV['COCOAPODS_DISABLE_STATS'] = 'true' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index fe233303a8..ebeafc0c84 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -14,40 +14,9 @@ PODS: - Flutter - device_info_plus (0.0.1): - Flutter - - DKImagePickerController/Core (4.3.9): - - DKImagePickerController/ImageDataManager - - DKImagePickerController/Resource - - DKImagePickerController/ImageDataManager (4.3.9) - - DKImagePickerController/PhotoGallery (4.3.9): - - DKImagePickerController/Core - - DKPhotoGallery - - DKImagePickerController/Resource (4.3.9) - - DKPhotoGallery (0.0.19): - - DKPhotoGallery/Core (= 0.0.19) - - DKPhotoGallery/Model (= 0.0.19) - - DKPhotoGallery/Preview (= 0.0.19) - - DKPhotoGallery/Resource (= 0.0.19) - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Core (0.0.19): - - DKPhotoGallery/Model - - DKPhotoGallery/Preview - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Model (0.0.19): - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Preview (0.0.19): - - DKPhotoGallery/Model - - DKPhotoGallery/Resource - - SDWebImage - - SwiftyGif - - DKPhotoGallery/Resource (0.0.19): - - SDWebImage - - SwiftyGif - file_picker (0.0.1): - - DKImagePickerController/PhotoGallery - Flutter + - FlutterMacOS - Flutter (1.0.0) - flutter_inappwebview_ios (0.0.1): - Flutter @@ -67,7 +36,7 @@ PODS: - gt3_flutter_plugin (0.0.9): - Flutter - GT3Captcha-iOS - - GT3Captcha-iOS (0.15.8.3) + - GT3Captcha-iOS (0.15.9) - image_cropper (0.0.5): - Flutter - TOCropViewController (~> 3.1.2) @@ -92,9 +61,6 @@ PODS: - Flutter - screen_brightness_ios (0.1.0): - Flutter - - SDWebImage (5.21.3): - - SDWebImage/Core (= 5.21.3) - - SDWebImage/Core (5.21.3) - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -103,7 +69,6 @@ PODS: - sqflite_darwin (0.0.4): - Flutter - FlutterMacOS - - SwiftyGif (5.4.5) - TOCropViewController (3.1.2) - url_launcher_ios (0.0.1): - Flutter @@ -118,7 +83,7 @@ DEPENDENCIES: - chat_bottom_container (from `.symlinks/plugins/chat_bottom_container/ios`) - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - - file_picker (from `.symlinks/plugins/file_picker/ios`) + - file_picker (from `.symlinks/plugins/file_picker/darwin`) - Flutter (from `Flutter`) - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) @@ -145,12 +110,8 @@ DEPENDENCIES: SPEC REPOS: trunk: - - DKImagePickerController - - DKPhotoGallery - GT3Captcha-iOS - OrderedSet - - SDWebImage - - SwiftyGif - TOCropViewController EXTERNAL SOURCES: @@ -169,7 +130,7 @@ EXTERNAL SOURCES: device_info_plus: :path: ".symlinks/plugins/device_info_plus/ios" file_picker: - :path: ".symlinks/plugins/file_picker/ios" + :path: ".symlinks/plugins/file_picker/darwin" Flutter: :path: Flutter flutter_inappwebview_ios: @@ -225,9 +186,7 @@ SPEC CHECKSUMS: chat_bottom_container: f1eb8323db77a87db50f361142c679f11e892d1b connectivity_plus: cb623214f4e1f6ef8fe7403d580fdad517d2f7dd device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe - DKImagePickerController: 946cec48c7873164274ecc4624d19e3da4c1ef3c - DKPhotoGallery: b3834fecb755ee09a593d7c9e389d8b5d6deed60 - file_picker: a0560bc09d61de87f12d246fc47d2119e6ef37be + file_picker: 70164d9778c42c47218d6cd79ce435de0856b11a Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467 flutter_inappwebview_ios: b89ba3482b96fb25e00c967aae065701b66e9b99 flutter_mailer: 3a8cd4f36c960fb04528d5471097270c19fec1c4 @@ -235,7 +194,7 @@ SPEC CHECKSUMS: flutter_volume_controller: c2be490cb0487e8b88d0d9fc2b7e1c139a4ebccb fluttertoast: 2c67e14dce98bbdb200df9e1acf610d7a6264ea1 gt3_flutter_plugin: 37090e5fa66ff2a52939eb9d208fc36fa49d36e5 - GT3Captcha-iOS: 5e3b1077834d8a9d6f4d64a447a30af3e14affe6 + GT3Captcha-iOS: aeb6fed2e8594099821430a89208679e5a55b740 image_cropper: fca51f94982730acae168c4b5d691e0f11aeb259 image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326 live_photo_maker: 29280ca88323bd5a33aafd00d98624d5cf522176 @@ -248,15 +207,13 @@ SPEC CHECKSUMS: permission_handler_apple: 4ed2196e43d0651e8ff7ca3483a069d469701f2d saver_gallery: af2d0c762dafda254e0ad025ef0dabd6506cd490 screen_brightness_ios: 9953fd7da5bd480f1a93990daeec2eb42d4f3b52 - SDWebImage: 16309af6d214ba3f77a7c6f6fdda888cb313a50a share_plus: 50da8cb520a8f0f65671c6c6a99b3617ed10a58a shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 - SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 TOCropViewController: a916930c465b5d9445a74d95e0c0da931771b4df url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556 -PODFILE CHECKSUM: f62db4fb414ebdecb264109948f76dfef35fdc3d +PODFILE CHECKSUM: 5e755568c318fde60f7b59d132a4ba634d53bf27 COCOAPODS: 1.16.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e316c85e16..eba4fe1dbe 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -360,7 +360,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -438,7 +438,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -487,7 +487,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/lib/common/assets.dart b/lib/common/assets.dart index 97996d3640..ed429dc808 100644 --- a/lib/common/assets.dart +++ b/lib/common/assets.dart @@ -6,14 +6,14 @@ abstract final class Assets { static const logoIco = 'assets/images/logo/ico/app_icon.ico'; static const logoLarge = 'assets/images/logo/desktop/logo_large.png'; - static const vipIcon = 'assets/images/big-vip.png'; + static const vipIcon = 'assets/images/big-vip.svg'; static const avatarPlaceHolder = 'assets/images/noface.jpeg'; static const loading = 'assets/images/loading.png'; static const buffering = 'assets/images/loading.webp'; - static const play = 'assets/images/play.png'; static const topicHeader = 'assets/images/topic-header-bg.png'; static const trendingBanner = 'assets/images/trending_banner.png'; static const ai = 'assets/images/ai.png'; + static const error = 'assets/images/error.svg'; static const livingChart = 'assets/images/live.gif'; static const livingStatic = 'assets/images/live.png'; diff --git a/lib/common/skeleton/fav_pgc_item.dart b/lib/common/skeleton/fav_pgc_item.dart index 2704b3ee00..e139d70c48 100644 --- a/lib/common/skeleton/fav_pgc_item.dart +++ b/lib/common/skeleton/fav_pgc_item.dart @@ -1,7 +1,6 @@ import 'package:PiliPlus/common/skeleton/skeleton.dart'; import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; class FavPgcItemSkeleton extends StatelessWidget { const FavPgcItemSkeleton({super.key}); diff --git a/lib/common/skeleton/skeleton.dart b/lib/common/skeleton/skeleton.dart index a2bcf8c75d..d8b75fd810 100644 --- a/lib/common/skeleton/skeleton.dart +++ b/lib/common/skeleton/skeleton.dart @@ -1,188 +1,62 @@ +import 'dart:ui' as ui; + import 'package:flutter/material.dart'; -class Skeleton extends StatelessWidget { +class Skeleton extends StatefulWidget { final Widget child; - const Skeleton({ - required this.child, - super.key, - }); - - @override - Widget build(BuildContext context) { - final color = Theme.of(context).colorScheme.surface.withAlpha(10); - final shimmerGradient = LinearGradient( - colors: [ - Colors.transparent, - color, - color, - Colors.transparent, - ], - stops: const [ - 0.1, - 0.3, - 0.5, - 0.7, - ], - begin: const Alignment(-1.0, -0.3), - end: const Alignment(1.0, 0.9), - tileMode: TileMode.clamp, - ); - return Shimmer( - linearGradient: shimmerGradient, - child: ShimmerLoading( - isLoading: true, - child: child, - ), - ); - } -} - -class Shimmer extends StatefulWidget { - static ShimmerState? of(BuildContext context) { - return context.findAncestorStateOfType(); - } - - const Shimmer({ - super.key, - required this.linearGradient, - this.child, - }); - - final LinearGradient linearGradient; - final Widget? child; + const Skeleton({super.key, required this.child}); @override - ShimmerState createState() => ShimmerState(); + State createState() => _SkeletonState(); } -class ShimmerState extends State with SingleTickerProviderStateMixin { - late AnimationController _shimmerController; +class _SkeletonState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late Color color; + final matrix = Matrix4.identity(); @override void initState() { super.initState(); - _shimmerController = AnimationController.unbounded(vsync: this) - ..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000)); + _controller = AnimationController.unbounded(vsync: this) + ..repeat(min: -0.5, max: 1.5, period: const Duration(milliseconds: 1000)) + ..addListener(_setState); } @override void dispose() { - _shimmerController.dispose(); + _controller.dispose(); super.dispose(); } - LinearGradient get gradient => LinearGradient( - colors: widget.linearGradient.colors, - stops: widget.linearGradient.stops, - begin: widget.linearGradient.begin, - end: widget.linearGradient.end, - transform: _SlidingGradientTransform( - slidePercent: _shimmerController.value, - ), - ); - - bool get isSized => - (context.findRenderObject() as RenderBox?)?.hasSize ?? false; - - Size get size => (context.findRenderObject() as RenderBox).size; - - Offset getDescendantOffset({ - required RenderBox descendant, - Offset offset = Offset.zero, - }) { - final shimmerBox = context.findRenderObject() as RenderBox; - return descendant.localToGlobal(offset, ancestor: shimmerBox); - } - - Listenable get shimmerChanges => _shimmerController; - - @override - Widget build(BuildContext context) { - return widget.child ?? const SizedBox.shrink(); - } -} - -class _SlidingGradientTransform extends GradientTransform { - const _SlidingGradientTransform({ - required this.slidePercent, - }); - - final double slidePercent; - - @override - Matrix4? transform(Rect bounds, {TextDirection? textDirection}) { - return Matrix4.translationValues(bounds.width * slidePercent, 0.0, 0.0); + void _setState() { + setState(() {}); } -} - -class ShimmerLoading extends StatefulWidget { - const ShimmerLoading({ - super.key, - required this.isLoading, - required this.child, - }); - - final bool isLoading; - final Widget child; - - @override - State createState() => _ShimmerLoadingState(); -} - -class _ShimmerLoadingState extends State { - Listenable? _shimmerChanges; @override void didChangeDependencies() { super.didChangeDependencies(); - if (_shimmerChanges != null) { - _shimmerChanges!.removeListener(_onShimmerChange); - } - _shimmerChanges = Shimmer.of(context)?.shimmerChanges; - if (_shimmerChanges != null) { - _shimmerChanges!.addListener(_onShimmerChange); - } - } - - @override - void dispose() { - _shimmerChanges?.removeListener(_onShimmerChange); - super.dispose(); - } - - void _onShimmerChange() { - if (widget.isLoading) { - setState(() {}); - } + color = ColorScheme.of(context).surface.withAlpha(10); } @override Widget build(BuildContext context) { - if (!widget.isLoading) { - return widget.child; - } - - final shimmer = Shimmer.of(context)!; - if (!shimmer.isSized) { - return const SizedBox.shrink(); - } - final shimmerSize = shimmer.size; - final gradient = shimmer.gradient; - final offsetWithinShimmer = shimmer.getDescendantOffset( - descendant: context.findRenderObject() as RenderBox, - ); - + final colors = [Colors.transparent, color, color, Colors.transparent]; return ShaderMask( blendMode: BlendMode.srcATop, - shaderCallback: (bounds) { - return gradient.createShader( - Rect.fromLTWH( - -offsetWithinShimmer.dx, - -offsetWithinShimmer.dy, - shimmerSize.width, - shimmerSize.height, - ), + shaderCallback: (Rect bounds) { + final width = bounds.width; + final height = bounds.height; + matrix[12] = width * _controller.value; + return ui.Gradient.linear( + Offset(0, 0.35 * height), + Offset(width, 0.95 * height), + colors, + const [0.1, 0.3, 0.5, 0.7], + TileMode.clamp, + matrix.storage, ); }, child: widget.child, diff --git a/lib/common/skeleton/space_opus.dart b/lib/common/skeleton/space_opus.dart index 2d34fea8e1..e62e15c423 100644 --- a/lib/common/skeleton/space_opus.dart +++ b/lib/common/skeleton/space_opus.dart @@ -1,7 +1,6 @@ import 'package:PiliPlus/common/skeleton/skeleton.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; class SpaceOpusSkeleton extends StatelessWidget { const SpaceOpusSkeleton({super.key}); diff --git a/lib/common/style.dart b/lib/common/style.dart index a50f36f798..09b83ffa51 100644 --- a/lib/common/style.dart +++ b/lib/common/style.dart @@ -9,13 +9,8 @@ abstract final class Style { static const aspectRatio = 16 / 10; static const aspectRatio16x9 = 16 / 9; static const imgMaxRatio = 2.6; - static const bottomSheetRadius = BorderRadius.vertical( - top: Radius.circular(18), - ); - static const dialogFixedConstraints = BoxConstraints( - minWidth: 420, - maxWidth: 420, - ); + static const bottomSheetRadius = BorderRadius.vertical(top: .circular(18)); + static const dialogFixedConstraints = BoxConstraints.tightFor(width: 420); static const topBarHeight = 52.0; static const buttonStyle = ButtonStyle( visualDensity: VisualDensity(horizontal: -2, vertical: -1.25), diff --git a/lib/common/widgets/avatars.dart b/lib/common/widgets/avatars.dart index c49db91af9..4fc165d185 100644 --- a/lib/common/widgets/avatars.dart +++ b/lib/common/widgets/avatars.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/model_owner.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; Widget avatars({ @@ -28,19 +29,19 @@ Widget avatars({ width: offset * users.length + gap, child: Stack( clipBehavior: .none, - children: users.indexed - .map( - (e) => Positioned( + children: users + .mapIndexed( + (i, e) => Positioned( top: 0, bottom: 0, width: size, - left: e.$1 * offset, + left: i * offset, child: DecoratedBox( decoration: decoration, child: Padding( padding: const .all(padding), child: NetworkImgLayer( - src: e.$2.face, + src: e.face, width: imgSize, height: imgSize, type: .avatar, diff --git a/lib/common/widgets/badge.dart b/lib/common/widgets/badge.dart index 63f3a3027c..826ee0fcd2 100644 --- a/lib/common/widgets/badge.dart +++ b/lib/common/widgets/badge.dart @@ -82,26 +82,21 @@ class PBadge extends StatelessWidget { color = theme.onSurfaceVariant; } - late EdgeInsets paddingStyle = const EdgeInsets.symmetric( - vertical: 2, - horizontal: 3, - ); - BorderRadius br = size == PBadgeSize.small - ? const BorderRadius.all(Radius.circular(3)) - : const BorderRadius.all(Radius.circular(4)); + late EdgeInsets paddingStyle = const .symmetric(vertical: 2, horizontal: 3); + final BorderRadius br = size == .small + ? const .all(.circular(3)) + : const .all(.circular(4)); Widget content = Container( padding: padding ?? paddingStyle, decoration: BoxDecoration( borderRadius: br, color: bgColor, - border: Border.all(color: borderColor), + border: .all(color: borderColor), ), child: Text( text!, - textScaler: textScaleFactor != null - ? TextScaler.linear(textScaleFactor!) - : null, + textScaler: textScaleFactor != null ? .linear(textScaleFactor!) : null, style: TextStyle( height: 1, fontSize: fontSize, diff --git a/lib/common/widgets/button/toolbar_icon_button.dart b/lib/common/widgets/button/toolbar_icon_button.dart index e9c78cc82b..ae033e8076 100644 --- a/lib/common/widgets/button/toolbar_icon_button.dart +++ b/lib/common/widgets/button/toolbar_icon_button.dart @@ -16,7 +16,7 @@ class ToolbarIconButton extends StatelessWidget { @override Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); + final colorScheme = ColorScheme.of(context); return SizedBox( width: 36, height: 36, @@ -24,14 +24,14 @@ class ToolbarIconButton extends StatelessWidget { tooltip: tooltip, onPressed: onPressed, icon: icon, - highlightColor: theme.colorScheme.secondaryContainer, + highlightColor: colorScheme.secondaryContainer, color: selected - ? theme.colorScheme.onSecondaryContainer - : theme.colorScheme.outline, + ? colorScheme.onSecondaryContainer + : colorScheme.outline, style: ButtonStyle( padding: const WidgetStatePropertyAll(EdgeInsets.zero), backgroundColor: WidgetStatePropertyAll( - selected ? theme.colorScheme.secondaryContainer : null, + selected ? colorScheme.secondaryContainer : null, ), ), ), diff --git a/lib/common/widgets/cropped_image.dart b/lib/common/widgets/cropped_image.dart index 46ae1ed101..46123930b3 100644 --- a/lib/common/widgets/cropped_image.dart +++ b/lib/common/widgets/cropped_image.dart @@ -70,20 +70,14 @@ class CroppedImage extends LeafRenderObjectWidget { class RenderCroppedImage extends RenderBox { RenderCroppedImage({ - required Size preferredSize, - required ui.Image image, - required Rect srcRect, - required Rect dstRect, - required RRect rrect, - required Paint imgPaint, - required Paint borderPaint, - }) : _preferredSize = preferredSize, - _image = image, - _srcRect = srcRect, - _dstRect = dstRect, - _rrect = rrect, - _imgPaint = imgPaint, - _borderPaint = borderPaint; + required this._preferredSize, + required this._image, + required this._srcRect, + required this._dstRect, + required this._rrect, + required this._imgPaint, + required this._borderPaint, + }); Size _preferredSize; Size get preferredSize => _preferredSize; diff --git a/lib/common/widgets/custom_arc.dart b/lib/common/widgets/custom_arc.dart index 98c5e2f341..2f728c5614 100644 --- a/lib/common/widgets/custom_arc.dart +++ b/lib/common/widgets/custom_arc.dart @@ -41,14 +41,11 @@ class Arc extends LeafRenderObjectWidget { class RenderArc extends RenderBox { RenderArc({ - required double preferredSize, - required Color color, - required double progress, - required double strokeWidth, - }) : _preferredSize = preferredSize, - _color = color, - _progress = progress, - _strokeWidth = strokeWidth; + required this._preferredSize, + required this._color, + required this._progress, + required this._strokeWidth, + }); Color _color; Color get color => _color; diff --git a/lib/common/widgets/custom_height_widget.dart b/lib/common/widgets/custom_height_widget.dart index b6958c7edd..64bc5effe1 100644 --- a/lib/common/widgets/custom_height_widget.dart +++ b/lib/common/widgets/custom_height_widget.dart @@ -34,10 +34,9 @@ class CustomHeightWidget extends SingleChildRenderObjectWidget { class RenderCustomHeightWidget extends RenderProxyBox { RenderCustomHeightWidget({ - double? height, - required Offset offset, - }) : _height = height, - _offset = offset; + this._height, + required this._offset, + }); double? _height; double? get height => _height; diff --git a/lib/common/widgets/custom_icon.dart b/lib/common/widgets/custom_icon.dart index 69d1c8931f..00117db15a 100644 --- a/lib/common/widgets/custom_icon.dart +++ b/lib/common/widgets/custom_icon.dart @@ -1,35 +1,46 @@ // ignore_for_file: constant_identifier_names -import 'package:flutter/widgets.dart'; +import 'package:flutter/widgets.dart' show IconData; -class CustomIcons { - static const IconData coin = _CustomIconData(0xe800); - static const IconData dm_off = _CustomIconData(0xe801); - static const IconData dm_on = _CustomIconData(0xe802); - static const IconData dm_settings = _CustomIconData(0xe803); - static const IconData dyn = _CustomIconData(0xe804); - static const IconData fav = _CustomIconData(0xe805); - static const IconData live_reserve = _CustomIconData(0xe806); - static const IconData player_dm_tip_back = _CustomIconData(0xe807); - static const IconData player_dm_tip_copy = _CustomIconData(0xe808); - static const IconData player_dm_tip_like = _CustomIconData(0xe809); - static const IconData player_dm_tip_like_solid = _CustomIconData(0xe80a); - static const IconData player_dm_tip_recall = _CustomIconData(0xe80b); - static const IconData share = _CustomIconData(0xe80c); - static const IconData share_line = _CustomIconData(0xe80d); - static const IconData share_node = _CustomIconData(0xe80e); - static const IconData star_favorite_line = _CustomIconData(0xe80f); - static const IconData star_favorite_solid = _CustomIconData(0xe810); - static const IconData thumbs_down = _CustomIconData(0xe811); - static const IconData thumbs_down_outline = _CustomIconData(0xe812); - static const IconData thumbs_up = _CustomIconData(0xe813); - static const IconData thumbs_up_fill = _CustomIconData(0xe814); - static const IconData thumbs_up_line = _CustomIconData(0xe815); - static const IconData thumbs_up_outline = _CustomIconData(0xe816); - static const IconData topic_tag = _CustomIconData(0xe817); - static const IconData watch_later = _CustomIconData(0xe818); -} +// dart format off +abstract final class CustomIcons { + static const _kFontFam = 'custom_icon'; -class _CustomIconData extends IconData { - const _CustomIconData(super.codePoint) : super(fontFamily: 'custom_icon'); + static const IconData ai_circle = IconData(0xe800, fontFamily: _kFontFam); + static const IconData coin = IconData(0xe801, fontFamily: _kFontFam); + static const IconData dm_off = IconData(0xe802, fontFamily: _kFontFam); + static const IconData dm_on = IconData(0xe803, fontFamily: _kFontFam); + static const IconData dm_settings = IconData(0xe804, fontFamily: _kFontFam); + static const IconData dyn = IconData(0xe805, fontFamily: _kFontFam); + static const IconData fav = IconData(0xe806, fontFamily: _kFontFam); + static const IconData flip_rotate_90 = IconData(0xe807, fontFamily: _kFontFam); + static const IconData identifier_circle = IconData(0xe808, fontFamily: _kFontFam); + static const IconData live_reserve = IconData(0xe809, fontFamily: _kFontFam); + static const IconData open_in_full_rotate_45 = IconData(0xe80a, fontFamily: _kFontFam); + static const IconData player_dm_tip_back = IconData(0xe80b, fontFamily: _kFontFam); + static const IconData player_dm_tip_copy = IconData(0xe80c, fontFamily: _kFontFam); + static const IconData player_dm_tip_like = IconData(0xe80d, fontFamily: _kFontFam); + static const IconData player_dm_tip_like_solid = IconData(0xe80e, fontFamily: _kFontFam); + static const IconData player_dm_tip_recall = IconData(0xe80f, fontFamily: _kFontFam); + static const IconData repeat_rounded_rotate_90 = IconData(0xe810, fontFamily: _kFontFam); + static const IconData share = IconData(0xe811, fontFamily: _kFontFam); + static const IconData share_line = IconData(0xe812, fontFamily: _kFontFam); + static const IconData share_node = IconData(0xe813, fontFamily: _kFontFam); + static const IconData shield_play_arrow = IconData(0xe814, fontFamily: _kFontFam); + static const IconData shield_published = IconData(0xe815, fontFamily: _kFontFam); + static const IconData shield_reply = IconData(0xe816, fontFamily: _kFontFam); + static const IconData shopping_bag_not_interested = IconData(0xe817, fontFamily: _kFontFam); + static const IconData splitscreen_rotate_90 = IconData(0xe818, fontFamily: _kFontFam); + static const IconData star_favorite_line = IconData(0xe819, fontFamily: _kFontFam); + static const IconData star_favorite_solid = IconData(0xe81a, fontFamily: _kFontFam); + static const IconData thumbs_down = IconData(0xe81b, fontFamily: _kFontFam); + static const IconData thumbs_down_outline = IconData(0xe81c, fontFamily: _kFontFam); + static const IconData thumbs_up = IconData(0xe81d, fontFamily: _kFontFam); + static const IconData thumbs_up_fill = IconData(0xe81e, fontFamily: _kFontFam); + static const IconData thumbs_up_line = IconData(0xe81f, fontFamily: _kFontFam); + static const IconData thumbs_up_outline = IconData(0xe820, fontFamily: _kFontFam); + static const IconData topic_tag = IconData(0xe821, fontFamily: _kFontFam); + static const IconData touch_app_rotate_270 = IconData(0xe822, fontFamily: _kFontFam); + static const IconData view_headline_rotate_90 = IconData(0xe823, fontFamily: _kFontFam); + static const IconData watch_later = IconData(0xe824, fontFamily: _kFontFam); } diff --git a/lib/common/widgets/custom_toast.dart b/lib/common/widgets/custom_toast.dart index edc416fb84..2546455f07 100644 --- a/lib/common/widgets/custom_toast.dart +++ b/lib/common/widgets/custom_toast.dart @@ -2,7 +2,7 @@ import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/material.dart'; class CustomToast extends StatelessWidget { - const CustomToast({super.key, required this.msg}); + const CustomToast(this.msg, {super.key}); final String msg; @@ -12,13 +12,13 @@ class CustomToast extends StatelessWidget { Widget build(BuildContext context) { final colorScheme = ColorScheme.of(context); return Container( - margin: EdgeInsets.only( + margin: .only( bottom: MediaQuery.viewPaddingOf(context).bottom + 30, ), - padding: const EdgeInsets.symmetric(horizontal: 17, vertical: 10), + padding: const .symmetric(horizontal: 17, vertical: 10), decoration: BoxDecoration( color: colorScheme.primaryContainer.withValues(alpha: toastOpacity), - borderRadius: const BorderRadius.all(Radius.circular(20)), + borderRadius: const .all(.circular(20)), ), child: Text( msg, @@ -32,7 +32,7 @@ class CustomToast extends StatelessWidget { } class LoadingWidget extends StatelessWidget { - const LoadingWidget({super.key, required this.msg}); + const LoadingWidget(this.msg, {super.key}); ///loading msg final String msg; @@ -42,14 +42,14 @@ class LoadingWidget extends StatelessWidget { final theme = Theme.of(context); final onSurfaceVariant = theme.colorScheme.onSurfaceVariant; return Container( - padding: const EdgeInsets.symmetric(horizontal: 30, vertical: 20), + padding: const .symmetric(horizontal: 30, vertical: 20), decoration: BoxDecoration( color: theme.dialogTheme.backgroundColor, - borderRadius: const BorderRadius.all(Radius.circular(15)), + borderRadius: const .all(.circular(15)), ), child: Column( spacing: 20, - mainAxisSize: MainAxisSize.min, + mainAxisSize: .min, children: [ //loading animation CircularProgressIndicator( @@ -63,3 +63,34 @@ class LoadingWidget extends StatelessWidget { ); } } + +class NotifyWarning extends StatelessWidget { + const NotifyWarning(this.msg, {super.key}); + + final String msg; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final onSurfaceVariant = theme.colorScheme.onSurfaceVariant; + return Container( + decoration: BoxDecoration( + borderRadius: const .all(.circular(8)), + color: theme.dialogTheme.backgroundColor, + ), + padding: const .symmetric(horizontal: 20, vertical: 10), + child: Column( + spacing: 5, + mainAxisSize: .min, + children: [ + Icon( + Icons.warning_amber_rounded, + size: 22, + color: onSurfaceVariant, + ), + Text(msg, style: TextStyle(color: onSurfaceVariant)), + ], + ), + ); + } +} diff --git a/lib/common/widgets/custom_tooltip.dart b/lib/common/widgets/custom_tooltip.dart index b5d22914b7..e5b81b8a72 100644 --- a/lib/common/widgets/custom_tooltip.dart +++ b/lib/common/widgets/custom_tooltip.dart @@ -162,11 +162,9 @@ class _RenderToolTip extends RenderBox RenderBoxContainerDefaultsMixin { _RenderToolTip({ VoidCallback? onTap, - required Offset target, - required bool preferBelow, - }) : _target = target, - _preferBelow = preferBelow, - _hitTestSelf = onTap != null { + required this._target, + required this._preferBelow, + }) : _hitTestSelf = onTap != null { if (onTap != null) { _tapGestureRecognizer = TapGestureRecognizer()..onTap = onTap; } @@ -287,10 +285,9 @@ class Triangle extends LeafRenderObjectWidget { class RenderTriangle extends RenderBox { RenderTriangle({ - required Color color, - required Size preferredSize, - }) : _color = color, - _preferredSize = preferredSize; + required this._color, + required this._preferredSize, + }); Color _color; Color get color => _color; diff --git a/lib/common/widgets/dialog/dialog.dart b/lib/common/widgets/dialog/dialog.dart index 9d9ff7b227..db4b717f51 100644 --- a/lib/common/widgets/dialog/dialog.dart +++ b/lib/common/widgets/dialog/dialog.dart @@ -64,42 +64,39 @@ void showPgcFollowDialog({ showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ...const [ - (followStatus: 3, title: '看过'), - (followStatus: 2, title: '在看'), - (followStatus: 1, title: '想看'), - ].map( - (item) => statusItem( - enabled: followStatus != item.followStatus, - text: item.title, - onTap: () { - Get.back(); - onUpdateStatus(item.followStatus); - }, - ), - ), - ListTile( - dense: true, - title: Padding( - padding: const EdgeInsets.only(left: 10), - child: Text( - '取消$type', - style: const TextStyle(fontSize: 14), - ), - ), + children: [ + ...const [ + (followStatus: 3, title: '看过'), + (followStatus: 2, title: '在看'), + (followStatus: 1, title: '想看'), + ].map( + (item) => statusItem( + enabled: followStatus != item.followStatus, + text: item.title, onTap: () { Get.back(); - onUpdateStatus(-1); + onUpdateStatus(item.followStatus); }, ), - ], - ), + ), + ListTile( + dense: true, + title: Padding( + padding: const EdgeInsets.only(left: 10), + child: Text( + '取消$type', + style: const TextStyle(fontSize: 14), + ), + ), + onTap: () { + Get.back(); + onUpdateStatus(-1); + }, + ), + ], ), ); } diff --git a/lib/common/widgets/dialog/export_import.dart b/lib/common/widgets/dialog/export_import.dart index fc7c9a0907..4b80766861 100644 --- a/lib/common/widgets/dialog/export_import.dart +++ b/lib/common/widgets/dialog/export_import.dart @@ -1,9 +1,10 @@ import 'dart:async' show FutureOr; import 'dart:convert' show utf8, jsonDecode; -import 'dart:io' show File; import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/utils/extension/context_ext.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; +import 'package:PiliPlus/utils/extension/theme_ext.dart'; +import 'package:PiliPlus/utils/storage_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; @@ -28,7 +29,7 @@ void exportToLocalFile({ required ValueGetter localFileName, }) { final res = utf8.encode(onExport()); - Utils.saveBytes2File( + StorageUtils.saveBytes2File( name: 'piliplus_${localFileName()}_' '${DateFormat('yyyyMMddHHmmss').format(DateTime.now())}.json', @@ -45,99 +46,95 @@ Future importFromClipBoard( bool showConfirmDialog = true, }) async { final data = await Clipboard.getData('text/plain'); - if (data?.text?.isNotEmpty != true) { - SmartDialog.showToast('剪贴板无数据'); - return; - } - if (!context.mounted) return; - final text = data!.text!; - late final T json; - late final String formatText; - try { - json = jsonDecode(text); - formatText = Utils.jsonEncoder.convert(json); - } catch (e) { - SmartDialog.showToast('解析json失败:$e'); - return; - } - bool? executeImport; - if (showConfirmDialog) { - final highlight = Highlight()..registerLanguage('json', langJson); - final result = highlight.highlight( - code: formatText, - language: 'json', - ); - late TextSpanRenderer renderer; - bool? isDarkMode; - executeImport = await showDialog( - context: context, - builder: (context) { - final isDark = context.isDarkMode; - if (isDark != isDarkMode) { - isDarkMode = isDark; - renderer = TextSpanRenderer( - const TextStyle(), - isDark ? githubDarkTheme : githubTheme, - ); - result.render(renderer); - } - return AlertDialog( - title: Text('是否导入如下$title?'), - content: SingleChildScrollView( - child: Text.rich(renderer.span!), - ), - actions: [ - TextButton( - onPressed: Get.back, - child: Text( - '取消', - style: TextStyle( - color: Theme.of(context).colorScheme.outline, - ), - ), - ), - TextButton( - onPressed: () => Get.back(result: true), - child: const Text('确定'), - ), - ], - ); - }, - ); - } else { - executeImport = true; - } - if (executeImport ?? false) { + if (data?.text case final text? when (text.isNotEmpty)) { + if (!context.mounted) return; + final T json; + final String formatText; try { - await onImport(json); - SmartDialog.showToast('导入成功'); + json = jsonDecode(text); + formatText = Utils.jsonEncoder.convert(json); } catch (e) { - SmartDialog.showToast('导入失败:$e'); + SmartDialog.showToast('解析json失败:$e'); + return; } + bool? executeImport; + if (showConfirmDialog) { + final highlight = Highlight()..registerLanguage('json', langJson); + final result = highlight.highlight( + code: formatText, + language: 'json', + ); + late TextSpanRenderer renderer; + bool? isDarkMode; + executeImport = await showDialog( + context: context, + builder: (context) { + final colorScheme = ColorScheme.of(context); + final isDark = colorScheme.isDark; + if (isDark != isDarkMode) { + isDarkMode = isDark; + renderer = TextSpanRenderer( + null, + isDark ? githubDarkTheme : githubTheme, + ); + result.render(renderer); + } + return AlertDialog( + title: Text('是否导入如下$title?'), + content: SingleChildScrollView( + child: Text.rich(renderer.span!), + ), + actions: [ + TextButton( + onPressed: Get.back, + child: Text('取消', style: TextStyle(color: colorScheme.outline)), + ), + TextButton( + onPressed: () => Get.back(result: true), + child: const Text('确定'), + ), + ], + ); + }, + ); + } else { + executeImport = true; + } + if (executeImport ?? false) { + try { + await onImport(json); + SmartDialog.showToast('导入成功'); + } catch (e) { + SmartDialog.showToast('导入失败:$e'); + } + } + } else { + SmartDialog.showToast('剪贴板无数据'); + return; } } Future importFromLocalFile({ required FutureOr Function(T json) onImport, }) async { - final result = await FilePicker.pickFiles(); + final result = await FilePicker.pickFile( + type: .custom, + allowedExtensions: const ['json', 'txt'], + ); if (result != null) { - final path = result.files.first.path; - if (path != null) { - final data = await File(path).readAsString(); - late final T json; - try { - json = jsonDecode(data); - } catch (e) { - SmartDialog.showToast('解析json失败:$e'); - return; - } - try { - await onImport(json); - SmartDialog.showToast('导入成功'); - } catch (e) { - SmartDialog.showToast('导入失败:$e'); - } + final data = await result.xFile.readAsString(); + final T json; + try { + json = jsonDecode(data); + } catch (e) { + SmartDialog.showToast('解析json失败:$e'); + return; + } + try { + await onImport(json); + SmartDialog.showToast('导入成功'); + } catch (e) { + SmartDialog.showToast('导入失败:$e'); } } } @@ -171,7 +168,6 @@ void importFromInput( json = jsonDecode(value!) as T; return null; } catch (e) { - if (e is FormatException) {} return '解析json失败:$e'; } }, @@ -182,7 +178,7 @@ void importFromInput( child: Text( '取消', style: TextStyle( - color: Theme.of(context).colorScheme.outline, + color: ColorScheme.of(context).outline, ), ), ), @@ -219,21 +215,19 @@ Future showImportExportDialog( builder: (context) { const style = TextStyle(fontSize: 15); return SimpleDialog( - clipBehavior: Clip.hardEdge, + clipBehavior: .hardEdge, title: Text('导入/导出$title'), children: [ - ListTile( - dense: true, - title: const Text('导出至剪贴板', style: style), - onTap: () { + DialogOption( + child: const Text('导出至剪贴板', style: style), + onPressed: () { Get.back(); exportToClipBoard(onExport: onExport); }, ), - ListTile( - dense: true, - title: const Text('导出文件至本地', style: style), - onTap: () { + DialogOption( + child: const Text('导出文件至本地', style: style), + onPressed: () { Get.back(); exportToLocalFile(onExport: onExport, localFileName: localFileName); }, @@ -242,18 +236,16 @@ Future showImportExportDialog( height: 1, color: ColorScheme.of(context).outline.withValues(alpha: 0.1), ), - ListTile( - dense: true, - title: const Text('输入', style: style), - onTap: () { + DialogOption( + child: const Text('输入', style: style), + onPressed: () { Get.back(); importFromInput(context, title: title, onImport: onImport); }, ), - ListTile( - dense: true, - title: const Text('从剪贴板导入', style: style), - onTap: () { + DialogOption( + child: const Text('从剪贴板导入', style: style), + onPressed: () { Get.back(); importFromClipBoard( context, @@ -263,10 +255,9 @@ Future showImportExportDialog( ); }, ), - ListTile( - dense: true, - title: const Text('从本地文件导入', style: style), - onTap: () { + DialogOption( + child: const Text('从本地文件导入', style: style), + onPressed: () { Get.back(); importFromLocalFile(onImport: onImport); }, diff --git a/lib/common/widgets/dialog/simple_dialog_option.dart b/lib/common/widgets/dialog/simple_dialog_option.dart new file mode 100644 index 0000000000..26a8d907e7 --- /dev/null +++ b/lib/common/widgets/dialog/simple_dialog_option.dart @@ -0,0 +1,29 @@ +import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:flutter/material.dart'; + +final EdgeInsets _padding = PlatformUtils.isMobile + ? const .symmetric(horizontal: 16, vertical: 14) + : const .symmetric(horizontal: 16, vertical: 10); + +class DialogOption extends StatelessWidget { + const DialogOption({ + super.key, + this.onPressed, + this.child, + }); + + final VoidCallback? onPressed; + + final Widget? child; + + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onPressed, + child: Padding( + padding: _padding, + child: child, + ), + ); + } +} diff --git a/lib/common/widgets/disabled_icon.dart b/lib/common/widgets/disabled_icon.dart index 3f38bc2225..29d8fe359a 100644 --- a/lib/common/widgets/disabled_icon.dart +++ b/lib/common/widgets/disabled_icon.dart @@ -50,16 +50,12 @@ class DisabledIcon extends SingleChildRenderObjectWidget { class RenderMaskedIcon extends RenderProxyBox { RenderMaskedIcon({ - required bool disable, - required double iconSize, - required Color color, - required StrokeCap strokeCap, - required double lineLengthScale, - }) : _disable = disable, - _iconSize = iconSize, - _color = color, - _strokeCap = strokeCap, - _lineLengthScale = lineLengthScale; + required this._disable, + required this._iconSize, + required this._color, + required this._strokeCap, + required this._lineLengthScale, + }); bool _disable; bool get disable => _disable; diff --git a/lib/common/widgets/draggable_sheet/topic.dart b/lib/common/widgets/draggable_sheet/topic.dart index 9e627b3b2d..bb3693a4e7 100644 --- a/lib/common/widgets/draggable_sheet/topic.dart +++ b/lib/common/widgets/draggable_sheet/topic.dart @@ -50,8 +50,8 @@ class _TopicDraggableScrollableSheetScrollController extends _DraggableScrollableSheetScrollController { _TopicDraggableScrollableSheetScrollController({ required super.extent, - double initialScrollOffset = 0.0, - }) : _initialScrollOffset = initialScrollOffset; + this._initialScrollOffset = 0.0, + }); @override double get initialScrollOffset => _initialScrollOffset; diff --git a/lib/common/widgets/extra_hit_test_widget.dart b/lib/common/widgets/extra_hit_test_widget.dart index 356510f756..c1e30612f8 100644 --- a/lib/common/widgets/extra_hit_test_widget.dart +++ b/lib/common/widgets/extra_hit_test_widget.dart @@ -18,8 +18,8 @@ class ExtraHitTestWidget extends SingleChildRenderObjectWidget { class RenderExtraHitTestWidget extends RenderProxyBox { RenderExtraHitTestWidget({ - required double width, - }) : _width = width; + required this._width, + }); final double _width; diff --git a/lib/common/widgets/floating_navigation_bar.dart b/lib/common/widgets/floating_navigation_bar.dart index 267757bd70..46838ca14e 100644 --- a/lib/common/widgets/floating_navigation_bar.dart +++ b/lib/common/widgets/floating_navigation_bar.dart @@ -724,7 +724,7 @@ class _NavigationBarDefaultsM3 extends NavigationBarThemeData { late final _colors = Theme.of(context).colorScheme; late final _textTheme = Theme.of(context).textTheme; - BorderSide get borderSide => _colors.brightness.isDark + BorderSide get borderSide => _colors.isDark ? const BorderSide(color: Color(0x08FFFFFF)) : const BorderSide(color: Color(0x08000000)); @@ -753,7 +753,7 @@ class _NavigationBarDefaultsM3 extends NavigationBarThemeData { @override Color? get indicatorColor => - _colors.brightness.isDark ? _indicatorDark : _indicatorLight; + _colors.isDark ? _indicatorDark : _indicatorLight; @override ShapeBorder? get indicatorShape => const StadiumBorder(); diff --git a/lib/common/widgets/flutter/chat_list_view.dart b/lib/common/widgets/flutter/chat_list_view.dart index 540aaef0cc..2d820f9d41 100644 --- a/lib/common/widgets/flutter/chat_list_view.dart +++ b/lib/common/widgets/flutter/chat_list_view.dart @@ -2,11 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + import 'dart:math' as math; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter/rendering.dart'; +import 'package:flutter/material.dart' hide ListView; +import 'package:flutter/rendering.dart' hide RenderSliverList; class ChatListView extends BoxScrollView { ChatListView.separated({ @@ -32,7 +34,12 @@ class ChatListView extends BoxScrollView { bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, bool addSemanticIndexes = true, + @Deprecated( + 'Use scrollCacheExtent instead. ' + 'This feature was deprecated after v3.41.0-0.0.pre.', + ) super.cacheExtent, + super.scrollCacheExtent, super.dragStartBehavior, super.keyboardDismissBehavior, super.restorationId, diff --git a/lib/common/widgets/flutter/draggable_scrollable_sheet.dart b/lib/common/widgets/flutter/draggable_scrollable_sheet.dart index 9ff5370e57..a020b227b1 100644 --- a/lib/common/widgets/flutter/draggable_scrollable_sheet.dart +++ b/lib/common/widgets/flutter/draggable_scrollable_sheet.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + import 'dart:math' as math; import 'package:collection/collection.dart'; diff --git a/lib/common/widgets/flutter/layout_builder.dart b/lib/common/widgets/flutter/layout_builder.dart deleted file mode 100644 index af7a7a7352..0000000000 --- a/lib/common/widgets/flutter/layout_builder.dart +++ /dev/null @@ -1,526 +0,0 @@ -// 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 'package:flutter/foundation.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart'; - -/// An abstract superclass for widgets that defer their building until layout. -/// -/// Similar to the [Builder] widget except that the implementation calls the [builder] -/// function at layout time and provides the [LayoutInfoType] that is required to -/// configure the child widget subtree. -/// -/// This is useful when the child widget tree relies on information that are only -/// available during layout, and doesn't depend on the child's intrinsic size. -/// -/// The [LayoutInfoType] should typically be immutable. The equality of the -/// [LayoutInfoType] type is used by the implementation to avoid unnecessary -/// rebuilds: if the new [LayoutInfoType] computed during layout is the same as -/// (defined by `LayoutInfoType.==`) the previous [LayoutInfoType], the -/// implementation will try to avoid calling the [builder] again unless -/// [updateShouldRebuild] returns true. The corresponding [RenderObject] produced -/// by this widget retains the most up-to-date [LayoutInfoType] for this purpose, -/// which may keep a [LayoutInfoType] object in memory until the widget is removed -/// from the tree. -/// -/// Subclasses must return a [RenderObject] that mixes in [RenderAbstractLayoutBuilderMixin]. -abstract class AbstractLayoutBuilder - extends RenderObjectWidget { - /// Creates a widget that defers its building until layout. - const AbstractLayoutBuilder({super.key}); - - /// Called at layout time to construct the widget tree. - /// - /// The builder must not return null. - Widget Function(BuildContext context, LayoutInfoType layoutInfo) get builder; - - @override - RenderObjectElement createElement() => - _LayoutBuilderElement(this); - - /// Whether [builder] needs to be called again even if the layout constraints - /// are the same. - /// - /// When this widget's configuration is updated, the [builder] callback most - /// likely needs to be called to build this widget's child. However, - /// subclasses may provide ways in which the widget can be updated without - /// needing to rebuild the child. Such subclasses can use this method to tell - /// the framework when the child widget should be rebuilt. - /// - /// When this method is called by the framework, the newly configured widget - /// is asked if it requires a rebuild, and it is passed the old widget as a - /// parameter. - /// - /// See also: - /// - /// * [State.setState] and [State.didUpdateWidget], which talk about widget - /// configuration changes and how they're triggered. - /// * [Element.update], the method that actually updates the widget's - /// configuration. - @protected - bool updateShouldRebuild( - covariant AbstractLayoutBuilder oldWidget, - ) => true; - - @override - RenderAbstractLayoutBuilderMixin - createRenderObject( - BuildContext context, - ); - - // updateRenderObject is redundant with the logic in the LayoutBuilderElement below. -} - -/// A specialized [AbstractLayoutBuilder] whose widget subtree depends on the -/// incoming [ConstraintType] that will be imposed on the widget. -/// -/// {@template flutter.widgets.ConstrainedLayoutBuilder} -/// The [builder] function is called in the following situations: -/// -/// * The first time the widget is laid out. -/// * When the parent widget passes different layout constraints. -/// * When the parent widget updates this widget and [updateShouldRebuild] returns `true`. -/// * When the dependencies that the [builder] function subscribes to change. -/// -/// The [builder] function is _not_ called during layout if the parent passes -/// the same constraints repeatedly. -/// -/// In the event that an ancestor skips the layout of this subtree so the -/// constraints become outdated, the `builder` rebuilds with the last known -/// constraints. -/// {@endtemplate} -abstract class ConstrainedLayoutBuilder - extends AbstractLayoutBuilder { - /// Creates a widget that defers its building until layout. - const ConstrainedLayoutBuilder({super.key, required this.builder}); - - @override - final Widget Function(BuildContext context, ConstraintType constraints) - builder; -} - -class _LayoutBuilderElement extends RenderObjectElement { - _LayoutBuilderElement(AbstractLayoutBuilder super.widget); - - @override - RenderAbstractLayoutBuilderMixin - get renderObject => - super.renderObject - as RenderAbstractLayoutBuilderMixin; - - Element? _child; - - // @override - // BuildScope get buildScope => _buildScope; - - // late final BuildScope _buildScope = BuildScope( - // scheduleRebuild: _scheduleRebuild, - // ); - - // To schedule a rebuild, markNeedsLayout needs to be called on this Element's - // render object (as the rebuilding is done in its performLayout call). However, - // the render tree should typically be kept clean during the postFrameCallbacks - // and the idle phase, so the layout data can be safely read. - // bool _deferredCallbackScheduled = false; - // void _scheduleRebuild() { - // if (_deferredCallbackScheduled) { - // return; - // } - - // final bool deferMarkNeedsLayout = - // switch (SchedulerBinding.instance.schedulerPhase) { - // SchedulerPhase.idle || SchedulerPhase.postFrameCallbacks => true, - // SchedulerPhase.transientCallbacks || - // SchedulerPhase.midFrameMicrotasks || - // SchedulerPhase.persistentCallbacks => false, - // }; - // if (!deferMarkNeedsLayout) { - // renderObject.scheduleLayoutCallback(); - // return; - // } - // _deferredCallbackScheduled = true; - // SchedulerBinding.instance.scheduleFrameCallback(_frameCallback); - // } - - // void _frameCallback(Duration timestamp) { - // _deferredCallbackScheduled = false; - // // This method is only called when the render tree is stable, if the Element - // // is deactivated it will never be reincorporated back to the tree. - // if (mounted) { - // renderObject.scheduleLayoutCallback(); - // } - // } - - @override - void visitChildren(ElementVisitor visitor) { - if (_child != null) { - visitor(_child!); - } - } - - @override - void forgetChild(Element child) { - assert(child == _child); - _child = null; - super.forgetChild(child); - } - - @override - void mount(Element? parent, Object? newSlot) { - super.mount(parent, newSlot); // Creates the renderObject. - renderObject._updateCallback(_rebuildWithConstraints); - } - - @override - void update(AbstractLayoutBuilder newWidget) { - assert(widget != newWidget); - final oldWidget = widget as AbstractLayoutBuilder; - super.update(newWidget); - assert(widget == newWidget); - - renderObject._updateCallback(_rebuildWithConstraints); - if (newWidget.updateShouldRebuild(oldWidget)) { - _needsBuild = true; - renderObject.scheduleLayoutCallback(); - } - } - - @override - void markNeedsBuild() { - // Calling super.markNeedsBuild is not needed. This Element does not need - // to performRebuild since this call already does what performRebuild does, - // So the element is clean as soon as this method returns and does not have - // to be added to the dirty list or marked as dirty. - renderObject.scheduleLayoutCallback(); - _needsBuild = true; - } - - @override - void performRebuild() { - // This gets called if markNeedsBuild() is called on us. - // That might happen if, e.g., our builder uses Inherited widgets. - - // Force the callback to be called, even if the layout constraints are the - // same. This is because that callback may depend on the updated widget - // configuration, or an inherited widget. - renderObject.scheduleLayoutCallback(); - _needsBuild = true; - super - .performRebuild(); // Calls widget.updateRenderObject (a no-op in this case). - } - - @override - void unmount() { - renderObject._callback = null; - super.unmount(); - } - - // The LayoutInfoType that was used to invoke the layout callback with last time, - // during layout. The `_previousLayoutInfo` value is compared to the new one - // to determine whether [LayoutBuilderBase.builder] needs to be called. - LayoutInfoType? _previousLayoutInfo; - bool _needsBuild = true; - - void _rebuildWithConstraints(Constraints _) { - final LayoutInfoType layoutInfo = renderObject.layoutInfo; - @pragma('vm:notify-debugger-on-exception') - void updateChildCallback() { - Widget built; - try { - assert(layoutInfo == renderObject.layoutInfo); - built = (widget as AbstractLayoutBuilder).builder( - this, - layoutInfo, - ); - debugWidgetBuilderValue(widget, built); - } catch (e, stack) { - built = ErrorWidget.builder( - _reportException( - ErrorDescription('building $widget'), - e, - stack, - informationCollector: () => [ - if (kDebugMode) DiagnosticsDebugCreator(DebugCreator(this)), - ], - ), - ); - } - try { - _child = updateChild(_child, built, null); - assert(_child != null); - } catch (e, stack) { - built = ErrorWidget.builder( - _reportException( - ErrorDescription('building $widget'), - e, - stack, - informationCollector: () => [ - if (kDebugMode) DiagnosticsDebugCreator(DebugCreator(this)), - ], - ), - ); - _child = updateChild(null, built, slot); - } finally { - _needsBuild = false; - _previousLayoutInfo = layoutInfo; - } - } - - final VoidCallback? callback = - _needsBuild || (layoutInfo != _previousLayoutInfo) - ? updateChildCallback - : null; - owner!.buildScope(this, callback); - } - - @override - void insertRenderObjectChild(RenderObject child, Object? slot) { - final RenderObjectWithChildMixin renderObject = - this.renderObject; - assert(slot == null); - assert(renderObject.debugValidateChild(child)); - renderObject.child = child; - assert(renderObject == this.renderObject); - } - - @override - void moveRenderObjectChild( - RenderObject child, - Object? oldSlot, - Object? newSlot, - ) { - assert(false); - } - - @override - void removeRenderObjectChild(RenderObject child, Object? slot) { - final RenderAbstractLayoutBuilderMixin - renderObject = this.renderObject; - assert(renderObject.child == child); - renderObject.child = null; - assert(renderObject == this.renderObject); - } -} - -/// Generic mixin for [RenderObject]s created by an [AbstractLayoutBuilder] with -/// the the same `LayoutInfoType`. -/// -/// Provides a [layoutCallback] implementation which, if needed, invokes -/// [AbstractLayoutBuilder]'s builder callback. -/// -/// Implementers can override the [layoutInfo] implementation with a value -/// that is safe to access in [layoutCallback], which is called in -/// [performLayout]. The default [layoutInfo] returns the incoming -/// [Constraints]. -/// -/// This mixin replaces [RenderConstrainedLayoutBuilder]. -mixin RenderAbstractLayoutBuilderMixin< - LayoutInfoType, - ChildType extends RenderObject -> - on - RenderObjectWithChildMixin, - RenderObjectWithLayoutCallbackMixin { - LayoutCallback? _callback; - - /// Change the layout callback. - void _updateCallback(LayoutCallback value) { - if (value == _callback) { - return; - } - _callback = value; - scheduleLayoutCallback(); - } - - /// Invokes the builder callback supplied via [AbstractLayoutBuilder] and - /// rebuilds the [AbstractLayoutBuilder]'s widget tree, if needed. - /// - /// No further work will be done if [layoutInfo] has not changed since the last - /// time this method was called, and [AbstractLayoutBuilder.updateShouldRebuild] - /// returned `false` when the widget was rebuilt. - /// - /// This method should typically be called as soon as possible in the class's - /// [performLayout] implementation, before any layout work is done. - @visibleForOverriding - @override - void layoutCallback() => _callback!(constraints); - - /// The information to invoke the [AbstractLayoutBuilder.builder] callback with. - /// - /// This is typically the information that are only made available in - /// [performLayout], which is inaccessible for regular [Builder] widget, - /// such as the incoming [Constraints], which are the default value. - @protected - LayoutInfoType get layoutInfo => constraints as LayoutInfoType; -} - -/// Generic mixin for [RenderObject]s created by an [AbstractLayoutBuilder] with -/// the the same `LayoutInfoType`. -/// -/// Use [RenderAbstractLayoutBuilderMixin] instead, which replaces this mixin. -typedef RenderConstrainedLayoutBuilder< - LayoutInfoType, - ChildType extends RenderObject -> = RenderAbstractLayoutBuilderMixin; - -/// Builds a widget tree that can depend on the parent widget's size. -/// -/// Similar to the [Builder] widget except that the framework calls the [builder] -/// function at layout time and provides the parent widget's constraints. This -/// is useful when the parent constrains the child's size and doesn't depend on -/// the child's intrinsic size. The [LayoutBuilder]'s final size will match its -/// child's size. -/// -/// {@macro flutter.widgets.ConstrainedLayoutBuilder} -/// -/// {@youtube 560 315 https://www.youtube.com/watch?v=IYDVcriKjsw} -/// -/// If the child should be smaller than the parent, consider wrapping the child -/// in an [Align] widget. If the child might want to be bigger, consider -/// wrapping it in a [SingleChildScrollView] or [OverflowBox]. -/// -/// {@tool dartpad} -/// This example uses a [LayoutBuilder] to build a different widget depending on the available width. Resize the -/// DartPad window to see [LayoutBuilder] in action! -/// -/// ** See code in examples/api/lib/widgets/layout_builder/layout_builder.0.dart ** -/// {@end-tool} -/// -/// See also: -/// -/// * [SliverLayoutBuilder], the sliver counterpart of this widget. -/// * [Builder], which calls a `builder` function at build time. -/// * [StatefulBuilder], which passes its `builder` function a `setState` callback. -/// * [CustomSingleChildLayout], which positions its child during layout. -/// * The [catalog of layout widgets](https://flutter.dev/widgets/layout/). -class LayoutBuilder extends ConstrainedLayoutBuilder { - /// Creates a widget that defers its building until layout. - const LayoutBuilder({super.key, required super.builder}); - - @override - RenderAbstractLayoutBuilderMixin - createRenderObject( - BuildContext context, - ) => _RenderLayoutBuilder(); -} - -class _RenderLayoutBuilder extends RenderBox - with - RenderObjectWithChildMixin, - RenderObjectWithLayoutCallbackMixin, - RenderAbstractLayoutBuilderMixin { - @override - double computeMinIntrinsicWidth(double height) { - assert(_debugThrowIfNotCheckingIntrinsics()); - return 0.0; - } - - @override - double computeMaxIntrinsicWidth(double height) { - assert(_debugThrowIfNotCheckingIntrinsics()); - return 0.0; - } - - @override - double computeMinIntrinsicHeight(double width) { - assert(_debugThrowIfNotCheckingIntrinsics()); - return 0.0; - } - - @override - double computeMaxIntrinsicHeight(double width) { - assert(_debugThrowIfNotCheckingIntrinsics()); - return 0.0; - } - - @override - Size computeDryLayout(BoxConstraints constraints) { - assert( - debugCannotComputeDryLayout( - reason: - 'Calculating the dry layout would require running the layout callback ' - 'speculatively, which might mutate the live render object tree.', - ), - ); - return Size.zero; - } - - @override - double? computeDryBaseline( - BoxConstraints constraints, - TextBaseline baseline, - ) { - assert( - debugCannotComputeDryLayout( - reason: - 'Calculating the dry baseline would require running the layout callback ' - 'speculatively, which might mutate the live render object tree.', - ), - ); - return null; - } - - @override - void performLayout() { - final BoxConstraints constraints = this.constraints; - runLayoutCallback(); - if (child != null) { - child!.layout(constraints, parentUsesSize: true); - size = constraints.constrain(child!.size); - } else { - size = constraints.biggest; - } - } - - @override - double? computeDistanceToActualBaseline(TextBaseline baseline) { - return child?.getDistanceToActualBaseline(baseline) ?? - super.computeDistanceToActualBaseline(baseline); - } - - @override - bool hitTestChildren(BoxHitTestResult result, {required Offset position}) { - return child?.hitTest(result, position: position) ?? false; - } - - @override - void paint(PaintingContext context, Offset offset) { - if (child != null) { - context.paintChild(child!, offset); - } - } - - bool _debugThrowIfNotCheckingIntrinsics() { - assert(() { - if (!RenderObject.debugCheckingIntrinsics) { - throw FlutterError( - 'LayoutBuilder does not support returning intrinsic dimensions.\n' - 'Calculating the intrinsic dimensions would require running the layout ' - 'callback speculatively, which might mutate the live render object tree.', - ); - } - return true; - }()); - - return true; - } -} - -FlutterErrorDetails _reportException( - DiagnosticsNode context, - Object exception, - StackTrace stack, { - InformationCollector? informationCollector, -}) { - final details = FlutterErrorDetails( - exception: exception, - stack: stack, - library: 'widgets library', - context: context, - informationCollector: informationCollector, - ); - FlutterError.reportError(details); - return details; -} diff --git a/lib/common/widgets/flutter/list_tile.dart b/lib/common/widgets/flutter/list_tile.dart index 048fe14005..79e1aece0f 100644 --- a/lib/common/widgets/flutter/list_tile.dart +++ b/lib/common/widgets/flutter/list_tile.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: uri_does_not_exist_in_doc_import +// ignore_for_file: prefer_initializing_formals, uri_does_not_exist_in_doc_import /// @docImport 'card.dart'; /// @docImport 'checkbox.dart'; @@ -742,19 +742,6 @@ class ListTile extends StatelessWidget { return dense ?? tileTheme.dense ?? theme.listTileTheme.dense ?? false; } - Color _tileBackgroundColor( - ThemeData theme, - ListTileThemeData tileTheme, - ListTileThemeData defaults, - ) { - final Color? color = selected - ? selectedTileColor ?? - tileTheme.selectedTileColor ?? - theme.listTileTheme.selectedTileColor - : tileColor ?? tileTheme.tileColor ?? theme.listTileTheme.tileColor; - return color ?? defaults.tileColor!; - } - @override Widget build(BuildContext context) { assert(debugCheckHasMaterial(context)); @@ -769,6 +756,25 @@ class ListTile extends StatelessWidget { final ListTileThemeData defaults = theme.useMaterial3 ? _LisTileDefaultsM3(context) : _LisTileDefaultsM2(context, listTileStyle); + + final Color backgroundColor = + tileColor ?? + tileTheme.tileColor ?? + theme.listTileTheme.tileColor ?? + defaults.tileColor!; + final Color selectedBackgroundColor = + selectedTileColor ?? + tileTheme.selectedTileColor ?? + theme.listTileTheme.selectedTileColor ?? + defaults.tileColor!; + final effectiveTileColor = selected + ? selectedBackgroundColor + : backgroundColor; + final bool hasOpaqueBackground = + backgroundColor.alpha > 0 || selectedBackgroundColor.alpha > 0; + if (onTap != null || onLongPress != null || hasOpaqueBackground) { + assert(_debugCheckBackgroundIsHidden(context)); + } final Set states = { if (!enabled) WidgetState.disabled, if (selected) WidgetState.selected, @@ -1015,7 +1021,7 @@ class ListTile extends StatelessWidget { child: Ink( decoration: ShapeDecoration( shape: shape ?? tileTheme.shape ?? const Border(), - color: _tileBackgroundColor(theme, tileTheme, defaults), + color: effectiveTileColor, ), child: child, ), @@ -1189,6 +1195,68 @@ class ListTile extends StatelessWidget { ), ); } + + bool _debugCheckBackgroundIsHidden(BuildContext context) { + assert(() { + final Widget? intermediateWidget = _findIntermediateWidget(context); + if (intermediateWidget != null) { + FlutterError.reportError( + FlutterErrorDetails( + exception: FlutterError.fromParts([ + ErrorSummary( + 'ListTile background color or ink splashes may be invisible.', + ), + ErrorDescription( + 'The ListTile is wrapped in a ${intermediateWidget.runtimeType} that has a background color. ' + 'Because ListTile paints its background and ink splashes on the nearest Material ancestor, ' + 'this ${intermediateWidget.runtimeType} will hide those effects.', + ), + ErrorHint( + 'To fix this, wrap the ListTile in its own Material widget, ' + 'or remove the background color from the intermediate ${intermediateWidget.runtimeType}.', + ), + ]), + informationCollector: () => [ + DiagnosticsProperty( + 'ListTile', + this, + expandableValue: true, + ), + DiagnosticsProperty( + '${intermediateWidget.runtimeType}', + intermediateWidget, + expandableValue: true, + ), + ], + ), + ); + } + return true; + }()); + return true; + } + + Widget? _findIntermediateWidget(BuildContext context) { + Widget? intermediateWidget; + (context as Element).visitAncestorElements((Element ancestor) { + if (ancestor.widget is Material) { + return false; + } + final Widget widget = ancestor.widget; + final Color? color = switch (widget) { + ColoredBox(:final Color color) => color, + DecoratedBox(decoration: BoxDecoration(:final Color? color)) => color, + DecoratedBox(decoration: ShapeDecoration(:final Color? color)) => color, + _ => null, + }; + if (color != null && color.a > 0) { + intermediateWidget = widget; + return false; + } + return true; + }); + return intermediateWidget; + } } class _IndividualOverrides extends WidgetStateProperty { diff --git a/lib/common/widgets/flutter/page/page_view.dart b/lib/common/widgets/flutter/page/page_view.dart index b61ef905db..ae23e49fb5 100644 --- a/lib/common/widgets/flutter/page/page_view.dart +++ b/lib/common/widgets/flutter/page/page_view.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart'; import 'package:flutter/gestures.dart' show DragStartBehavior, HorizontalDragGestureRecognizer; @@ -99,13 +101,24 @@ class PageView List children = const [], this.dragStartBehavior = DragStartBehavior.start, this.allowImplicitScrolling = false, + ScrollCacheExtent? scrollCacheExtent, this.restorationId, this.clipBehavior = Clip.hardEdge, this.hitTestBehavior = HitTestBehavior.opaque, this.scrollBehavior, this.padEnds = true, required this.horizontalDragGestureRecognizer, - }) : childrenDelegate = SliverChildListDelegate(children); + }) : assert( + scrollCacheExtent == null || + (scrollCacheExtent.value > 0.0) == allowImplicitScrolling, + 'scrollCacheExtent and allowImplicitScrolling must be consistent: ' + 'scrollCacheExtent must be greater than 0.0 when allowImplicitScrolling is true, ' + 'and must be 0.0 when allowImplicitScrolling is false.', + ), + scrollCacheExtent = + scrollCacheExtent ?? + ScrollCacheExtent.viewport(allowImplicitScrolling ? 1.0 : 0.0), + childrenDelegate = SliverChildListDelegate(children); final GestureRecognizerFactoryConstructor horizontalDragGestureRecognizer; @@ -147,13 +160,24 @@ class PageView int? itemCount, this.dragStartBehavior = DragStartBehavior.start, this.allowImplicitScrolling = false, + ScrollCacheExtent? scrollCacheExtent, this.restorationId, this.clipBehavior = Clip.hardEdge, this.hitTestBehavior = HitTestBehavior.opaque, this.scrollBehavior, this.padEnds = true, required this.horizontalDragGestureRecognizer, - }) : childrenDelegate = SliverChildBuilderDelegate( + }) : assert( + scrollCacheExtent == null || + (scrollCacheExtent.value > 0.0) == allowImplicitScrolling, + 'scrollCacheExtent and allowImplicitScrolling must be consistent: ' + 'scrollCacheExtent must be greater than 0.0 when allowImplicitScrolling is true, ' + 'and must be 0.0 when allowImplicitScrolling is false.', + ), + scrollCacheExtent = + scrollCacheExtent ?? + ScrollCacheExtent.viewport(allowImplicitScrolling ? 1.0 : 0.0), + childrenDelegate = SliverChildBuilderDelegate( itemBuilder, findChildIndexCallback: findChildIndexCallback, childCount: itemCount, @@ -170,7 +194,7 @@ class PageView /// {@end-tool} /// /// {@macro flutter.widgets.PageView.allowImplicitScrolling} - const PageView.custom({ + PageView.custom({ super.key, this.scrollDirection = Axis.horizontal, this.reverse = false, @@ -181,14 +205,25 @@ class PageView required this.childrenDelegate, this.dragStartBehavior = DragStartBehavior.start, this.allowImplicitScrolling = false, + ScrollCacheExtent? scrollCacheExtent, this.restorationId, this.clipBehavior = Clip.hardEdge, this.hitTestBehavior = HitTestBehavior.opaque, this.scrollBehavior, this.padEnds = true, required this.horizontalDragGestureRecognizer, - }); + }) : assert( + scrollCacheExtent == null || + (scrollCacheExtent.value > 0.0) == allowImplicitScrolling, + 'scrollCacheExtent and allowImplicitScrolling must be consistent: ' + 'scrollCacheExtent must be greater than 0.0 when allowImplicitScrolling is true, ' + 'and must be 0.0 when allowImplicitScrolling is false.', + ), + scrollCacheExtent = + scrollCacheExtent ?? + ScrollCacheExtent.viewport(allowImplicitScrolling ? 1.0 : 0.0); + /// {@template flutter.widgets.PageView.allowImplicitScrolling} /// Controls whether the widget's pages will respond to /// [RenderObject.showOnScreen], which will allow for implicit accessibility /// scrolling. @@ -200,8 +235,37 @@ class PageView /// With this flag set to true, when accessibility focus reaches the end of /// the current page and user attempts to move it to the next element, focus /// will traverse to the next page in the page view. + /// {@endtemplate} final bool allowImplicitScrolling; + /// {@macro flutter.rendering.RenderViewportBase.scrollCacheExtent} + /// + /// In [PageView], the default [scrollCacheExtent] uses + /// [ScrollCacheExtent.viewport], where the value represents the number of + /// viewport lengths to cache beyond the visible area. + /// + /// When [PageController.viewportFraction] is 1.0 (the default), this is + /// equivalent to the number of pages. For example, + /// `ScrollCacheExtent.viewport(2.0)` caches 2 pages before and after the + /// visible page. + /// + /// When [PageController.viewportFraction] is less than 1.0, multiple pages + /// may be visible in a single viewport, so `ScrollCacheExtent.viewport(1.0)` + /// may cache more than one additional page in each direction. + /// + /// [ScrollCacheExtent.pixels] can also be used to specify the cache extent + /// in logical pixels instead of viewport sizes. + /// + /// If [scrollCacheExtent] is specified, its value must be consistent with + /// [allowImplicitScrolling]: the value must be greater than 0.0 when + /// [allowImplicitScrolling] is true, and must be 0.0 when + /// [allowImplicitScrolling] is false. + /// + /// Defaults to `ScrollCacheExtent.viewport(1.0)` if + /// [allowImplicitScrolling] is true, and `ScrollCacheExtent.viewport(0.0)` if + /// [allowImplicitScrolling] is false. + final ScrollCacheExtent scrollCacheExtent; + /// {@macro flutter.widgets.scrollable.restorationId} final String? restorationId; @@ -392,11 +456,7 @@ class _PageViewState ScrollConfiguration.of(context).copyWith(scrollbars: false), viewportBuilder: (BuildContext context, ViewportOffset position) { return Viewport( - // TODO(dnfield): we should provide a way to set cacheExtent - // independent of implicit scrolling: - // https://github.com/flutter/flutter/issues/45632 - cacheExtent: widget.allowImplicitScrolling ? 1.0 : 0.0, - cacheExtentStyle: CacheExtentStyle.viewport, + scrollCacheExtent: widget.scrollCacheExtent, axisDirection: axisDirection, offset: position, clipBehavior: widget.clipBehavior, @@ -405,6 +465,7 @@ class _PageViewState viewportFraction: _controller.viewportFraction, delegate: widget.childrenDelegate, padEnds: widget.padEnds, + allowImplicitScrolling: widget.allowImplicitScrolling, ), ], ); @@ -447,6 +508,15 @@ class _PageViewState value: widget.allowImplicitScrolling, ifTrue: 'allow implicit scrolling', ), + ) + ..add( + DiagnosticsProperty( + 'scrollCacheExtent', + widget.scrollCacheExtent, + defaultValue: ScrollCacheExtent.viewport( + widget.allowImplicitScrolling ? 1.0 : 0.0, + ), + ), ); } } diff --git a/lib/common/widgets/flutter/page/scrollable.dart b/lib/common/widgets/flutter/page/scrollable.dart index f1876e5f3b..1abd487d0a 100644 --- a/lib/common/widgets/flutter/page/scrollable.dart +++ b/lib/common/widgets/flutter/page/scrollable.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + import 'dart:async'; import 'dart:math' as math; @@ -948,9 +950,6 @@ class ScrollableState void _receivedPointerSignal(PointerSignalEvent event) { if (event is PointerScrollEvent && _position != null) { if (_physics != null && !_physics!.shouldAcceptUserOffset(position)) { - // The handler won't use the `event`, so allow the platform to trigger - // any default native actions. - event.respond(allowPlatformDefault: true); return; } final double delta = _pointerSignalEventDelta(event); @@ -965,9 +964,6 @@ class ScrollableState ); return; } - // The `event` won't result in a scroll, so allow the platform to trigger - // any default native actions. - event.respond(allowPlatformDefault: true); } else if (event is PointerScrollInertiaCancelEvent) { position.pointerScroll(0); // Don't use the pointer signal resolver, all hit-tested scrollables should stop. @@ -976,12 +972,16 @@ class ScrollableState void _handlePointerScroll(PointerEvent event) { assert(event is PointerScrollEvent); - final double delta = _pointerSignalEventDelta(event as PointerScrollEvent); + final scrollEvent = event as PointerScrollEvent; + final double delta = _pointerSignalEventDelta(scrollEvent); final double targetScrollOffset = _targetScrollOffsetForPointerScroll( delta, ); if (delta != 0.0 && targetScrollOffset != position.pixels) { position.pointerScroll(delta); + // Tell engine this scrollable handled the event. + // This prevents parent page from scrolling when nested scrollables exist. + scrollEvent.respond(allowPlatformDefault: false); } } diff --git a/lib/common/widgets/flutter/page/scrollable_helpers.dart b/lib/common/widgets/flutter/page/scrollable_helpers.dart index 53a2b2bb75..b68809be38 100644 --- a/lib/common/widgets/flutter/page/scrollable_helpers.dart +++ b/lib/common/widgets/flutter/page/scrollable_helpers.dart @@ -2,13 +2,13 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: dangling_library_doc_comments, prefer_initializing_formals + /// @docImport 'package:flutter/material.dart'; /// /// @docImport 'overscroll_indicator.dart'; /// @docImport 'viewport.dart'; -// ignore_for_file: dangling_library_doc_comments - import 'dart:math' as math; import 'package:PiliPlus/common/widgets/flutter/page/scrollable.dart'; diff --git a/lib/common/widgets/flutter/page/tabs.dart b/lib/common/widgets/flutter/page/tabs.dart index 24be4d0845..cd71c0ee4b 100644 --- a/lib/common/widgets/flutter/page/tabs.dart +++ b/lib/common/widgets/flutter/page/tabs.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart'; import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/gestures.dart' diff --git a/lib/common/widgets/flutter/pop_scope.dart b/lib/common/widgets/flutter/pop_scope.dart index 33cffe2e1c..ec0010f628 100644 --- a/lib/common/widgets/flutter/pop_scope.dart +++ b/lib/common/widgets/flutter/pop_scope.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + import 'package:flutter/material.dart' hide PopScope; import 'package:get/get_core/src/get_main.dart'; import 'package:get/get_navigation/src/extension_navigation.dart'; diff --git a/lib/common/widgets/flutter/popup_menu.dart b/lib/common/widgets/flutter/popup_menu.dart index 1e05da9cc4..c8e8e8e74e 100644 --- a/lib/common/widgets/flutter/popup_menu.dart +++ b/lib/common/widgets/flutter/popup_menu.dart @@ -2,9 +2,9 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -library; +// ignore_for_file: prefer_initializing_formals -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide PopupMenuItem; class CustomPopupMenuItem extends PopupMenuEntry { const CustomPopupMenuItem({ @@ -114,7 +114,7 @@ class _CustomPopupMenuDividerState extends State { // dart format off class _PopupMenuDefaultsM3 extends PopupMenuThemeData { _PopupMenuDefaultsM3(this.context) - : super(elevation: 3.0); + : super(elevation: 3.0); final BuildContext context; late final ThemeData _theme = Theme.of(context); @@ -123,8 +123,8 @@ class _PopupMenuDefaultsM3 extends PopupMenuThemeData { @override WidgetStateProperty? get labelTextStyle { return WidgetStateProperty.resolveWith((Set states) { - // TODO(quncheng): Update this hard-coded value to use the latest tokens. - final TextStyle style = _textTheme.labelLarge!; + // TODO(quncheng): Update this hard-coded value to use the latest tokens. + final TextStyle style = _textTheme.labelLarge!; if (states.contains(WidgetState.disabled)) { return style.apply(color: _colors.onSurface.withValues(alpha: 0.38)); } diff --git a/lib/common/widgets/flutter/refresh_indicator.dart b/lib/common/widgets/flutter/refresh_indicator.dart index d1a5fe9440..c350875a72 100644 --- a/lib/common/widgets/flutter/refresh_indicator.dart +++ b/lib/common/widgets/flutter/refresh_indicator.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + import 'dart:async' show Completer; import 'dart:io' show Platform; @@ -517,7 +519,7 @@ class RefreshIndicatorState extends State left: 0.0, right: 0.0, child: SizeTransition( - axisAlignment: 1.0, + alignment: .bottomStart, sizeFactor: _positionFactor, // This is what brings it down. child: Padding( padding: EdgeInsets.only(top: displacement), diff --git a/lib/common/widgets/flutter/sliver_layout_builder.dart b/lib/common/widgets/flutter/sliver_layout_builder.dart deleted file mode 100644 index f3f35b773b..0000000000 --- a/lib/common/widgets/flutter/sliver_layout_builder.dart +++ /dev/null @@ -1,83 +0,0 @@ -// 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 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; -import 'package:flutter/rendering.dart'; -import 'package:flutter/widgets.dart' - hide - ConstrainedLayoutBuilder, - LayoutBuilder, - RenderConstrainedLayoutBuilder; - -/// Builds a sliver widget tree that can depend on its own [SliverConstraints]. -/// -/// Similar to the [LayoutBuilder] widget except its builder should return a sliver -/// widget, and [SliverLayoutBuilder] is itself a sliver. The framework calls the -/// [builder] function at layout time and provides the current [SliverConstraints]. -/// The [SliverLayoutBuilder]'s final [SliverGeometry] will match the [SliverGeometry] -/// of its child. -/// -/// {@macro flutter.widgets.ConstrainedLayoutBuilder} -/// -/// See also: -/// -/// * [LayoutBuilder], the non-sliver version of this widget. -class SliverLayoutBuilder extends ConstrainedLayoutBuilder { - /// Creates a sliver widget that defers its building until layout. - const SliverLayoutBuilder({super.key, required super.builder}); - - @override - RenderConstrainedLayoutBuilder - createRenderObject( - BuildContext context, - ) => _RenderSliverLayoutBuilder(); -} - -class _RenderSliverLayoutBuilder extends RenderSliver - with - RenderObjectWithChildMixin, - RenderObjectWithLayoutCallbackMixin, - RenderConstrainedLayoutBuilder { - @override - double childMainAxisPosition(RenderObject child) { - assert(child == this.child); - return 0; - } - - @override - void performLayout() { - runLayoutCallback(); - child?.layout(constraints, parentUsesSize: true); - geometry = child?.geometry ?? SliverGeometry.zero; - } - - @override - void applyPaintTransform(RenderObject child, Matrix4 transform) { - assert(child == this.child); - // child's offset is always (0, 0), transform.translate(0, 0) does not mutate the transform. - } - - @override - void paint(PaintingContext context, Offset offset) { - // This renderObject does not introduce additional offset to child's position. - if (child?.geometry?.visible ?? false) { - context.paintChild(child!, offset); - } - } - - @override - bool hitTestChildren( - SliverHitTestResult result, { - required double mainAxisPosition, - required double crossAxisPosition, - }) { - return child != null && - child!.geometry!.hitTestExtent > 0 && - child!.hitTest( - result, - mainAxisPosition: mainAxisPosition, - crossAxisPosition: crossAxisPosition, - ); - } -} diff --git a/lib/common/widgets/flutter/text/paragraph.dart b/lib/common/widgets/flutter/text/paragraph.dart index ff60f8c77d..4743ddedeb 100644 --- a/lib/common/widgets/flutter/text/paragraph.dart +++ b/lib/common/widgets/flutter/text/paragraph.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: uri_does_not_exist_in_doc_import +// ignore_for_file: prefer_initializing_formals, uri_does_not_exist_in_doc_import /// @docImport 'package:flutter/widgets.dart'; /// @@ -47,6 +47,7 @@ const String _kEllipsis = '\u2026'; class _UnspecifiedTextScaler extends TextScaler { const _UnspecifiedTextScaler(); + @override Never get textScaleFactor => throw UnimplementedError(); @@ -130,6 +131,7 @@ class RenderParagraph extends RenderBox // TODO(abarth): Make computing the min/max intrinsic width/height a // non-destructive operation. TextPainter? _textIntrinsicsCache; + TextPainter get _textIntrinsics { return (_textIntrinsicsCache ??= TextPainter()) ..text = _textPainter.text @@ -150,6 +152,7 @@ class RenderParagraph extends RenderBox /// The text to display. InlineSpan get text => _textPainter.text!; + set text(({InlineSpan text, Color primary}) params) { final value = params.text; _primary = params.primary; @@ -223,6 +226,7 @@ class RenderParagraph extends RenderBox /// The [SelectionRegistrar] this paragraph will be, or is, registered to. SelectionRegistrar? get registrar => _registrar; SelectionRegistrar? _registrar; + set registrar(SelectionRegistrar? value) { if (value == _registrar) { return; @@ -324,6 +328,7 @@ class RenderParagraph extends RenderBox /// How the text should be aligned horizontally. TextAlign get textAlign => _textPainter.textAlign; + set textAlign(TextAlign value) { if (_textPainter.textAlign == value) { return; @@ -344,6 +349,7 @@ class RenderParagraph extends RenderBox /// context, the English phrase will be on the right and the Hebrew phrase on /// its left. TextDirection get textDirection => _textPainter.textDirection!; + set textDirection(TextDirection value) { if (_textPainter.textDirection == value) { return; @@ -361,6 +367,7 @@ class RenderParagraph extends RenderBox /// effects. bool get softWrap => _softWrap; bool _softWrap; + set softWrap(bool value) { if (_softWrap == value) { return; @@ -372,6 +379,7 @@ class RenderParagraph extends RenderBox /// How visual overflow should be handled. TextOverflow get overflow => _overflow; TextOverflow _overflow; + set overflow(TextOverflow value) { if (_overflow == value) { return; @@ -394,6 +402,7 @@ class RenderParagraph extends RenderBox 'This feature was deprecated after v3.12.0-2.0.pre.', ) double get textScaleFactor => _textPainter.textScaleFactor; + @Deprecated( 'Use textScaler instead. ' 'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. ' @@ -405,6 +414,7 @@ class RenderParagraph extends RenderBox /// {@macro flutter.painting.textPainter.textScaler} TextScaler get textScaler => _textPainter.textScaler; + set textScaler(TextScaler value) { if (_textPainter.textScaler == value) { return; @@ -468,6 +478,7 @@ class RenderParagraph extends RenderBox /// {@macro flutter.painting.textPainter.textWidthBasis} TextWidthBasis get textWidthBasis => _textPainter.textWidthBasis; + set textWidthBasis(TextWidthBasis value) { if (_textPainter.textWidthBasis == value) { return; @@ -480,6 +491,7 @@ class RenderParagraph extends RenderBox /// {@macro dart.ui.textHeightBehavior} ui.TextHeightBehavior? get textHeightBehavior => _textPainter.textHeightBehavior; + set textHeightBehavior(ui.TextHeightBehavior? value) { if (_textPainter.textHeightBehavior == value) { return; @@ -494,6 +506,7 @@ class RenderParagraph extends RenderBox /// Ignored if the text is not selectable (e.g. if [registrar] is null). Color? get selectionColor => _selectionColor; Color? _selectionColor; + set selectionColor(Color? value) { if (_selectionColor == value) { return; @@ -1370,6 +1383,7 @@ class _SelectableFragment @override SelectionGeometry get value => _selectionGeometry; late SelectionGeometry _selectionGeometry; + void _updateSelectionGeometry() { final SelectionGeometry newValue = _getSelectionGeometry(); @@ -2328,6 +2342,7 @@ class _SelectableFragment PlaceholderSpan.placeholderCodeUnit, ); static final int _placeholderLength = _placeholderCharacter.length; + // This method handles updating the start edge by a text boundary that may // not be contained within this selectable fragment. It is possible // that a boundary spans multiple selectable fragments when the text contains @@ -3702,12 +3717,13 @@ class _SelectableFragment } List? _cachedBoundingBoxes; + @override List get boundingBoxes { if (_cachedBoundingBoxes == null) { final List boxes = paragraph.getBoxesForSelection( TextSelection(baseOffset: range.start, extentOffset: range.end), - boxHeightStyle: ui.BoxHeightStyle.max, + boxHeightStyle: .max, ); if (boxes.isNotEmpty) { _cachedBoundingBoxes = []; @@ -3729,10 +3745,12 @@ class _SelectableFragment } Rect? _cachedRect; + Rect get _rect { if (_cachedRect == null) { final List boxes = paragraph.getBoxesForSelection( TextSelection(baseOffset: range.start, extentOffset: range.end), + boxHeightStyle: .max, ); if (boxes.isNotEmpty) { Rect result = boxes.first.toRect(); diff --git a/lib/common/widgets/flutter/text/rich_text.dart b/lib/common/widgets/flutter/text/rich_text.dart index 3d82b9bcb5..7c1b6e66fb 100644 --- a/lib/common/widgets/flutter/text/rich_text.dart +++ b/lib/common/widgets/flutter/text/rich_text.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + import 'dart:ui' as ui show TextHeightBehavior; import 'package:PiliPlus/common/widgets/flutter/text/paragraph.dart'; diff --git a/lib/common/widgets/flutter/text/text.dart b/lib/common/widgets/flutter/text/text.dart index c58b97ff0d..846adba89d 100644 --- a/lib/common/widgets/flutter/text/text.dart +++ b/lib/common/widgets/flutter/text/text.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: uri_does_not_exist_in_doc_import +// ignore_for_file: prefer_initializing_formals, uri_does_not_exist_in_doc_import /// @docImport 'package:flutter/gestures.dart'; /// @docImport 'package:flutter/material.dart'; diff --git a/lib/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart b/lib/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart index 059145edfa..aad112abc5 100644 --- a/lib/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart +++ b/lib/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: uri_does_not_exist_in_doc_import +// ignore_for_file: prefer_initializing_formals, uri_does_not_exist_in_doc_import /// @docImport 'selectable_text.dart'; /// @docImport 'selection_area.dart'; @@ -11,7 +11,8 @@ library; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState; -import 'package:flutter/material.dart' hide EditableText, EditableTextState; +import 'package:flutter/material.dart' + hide EditableText, EditableTextState, AdaptiveTextSelectionToolbar; import 'package:flutter/rendering.dart'; /// The default context menu for text selection for the current platform. diff --git a/lib/common/widgets/flutter/text_field/controller.dart b/lib/common/widgets/flutter/text_field/controller.dart index 49cec8776f..1ae98e4ace 100644 --- a/lib/common/widgets/flutter/text_field/controller.dart +++ b/lib/common/widgets/flutter/text_field/controller.dart @@ -150,20 +150,19 @@ class RichTextItem { RichTextItem({ this.type = RichTextType.text, required this.text, - String? rawText, + this._rawText, required this.range, this.emote, this.id, - }) : _rawText = rawText; + }); RichTextItem.fromStart( this.text, { - String? rawText, + this._rawText, this.type = RichTextType.text, this.emote, this.id, - }) : range = TextRange(start: 0, end: text.length), - _rawText = rawText; + }) : range = TextRange(start: 0, end: text.length); List? onInsert( TextEditingDeltaInsertion delta, @@ -177,7 +176,6 @@ class RichTextItem { if (insertionOffset == 0 && range.start == 0) { final insertedLength = delta.textInserted.length; - controller.newSelection = TextSelection.collapsed(offset: insertedLength); if (!isRich && delta.isText) { text = delta.textInserted + text; range = TextRange(start: range.start, end: range.start + text.length); @@ -209,7 +207,6 @@ class RichTextItem { if (range.end == insertionOffset) { final end = insertionOffset + delta.textInserted.length; - controller.newSelection = TextSelection.collapsed(offset: end); if ((isText && delta.isText) || (isComposing && delta.isComposing)) { text += delta.textInserted; range = TextRange(start: range.start, end: end); @@ -233,7 +230,6 @@ class RichTextItem { final leadingText = text.substring(0, insertionOffset - range.start); final trailingString = text.substring(leadingText.length); final insertEnd = insertionOffset + delta.textInserted.length; - controller.newSelection = TextSelection.collapsed(offset: insertEnd); if (delta.isText) { text = leadingText + delta.textInserted + trailingString; range = TextRange( @@ -381,9 +377,6 @@ class RichTextItem { ); final end = range.start + text.length; range = TextRange(start: range.start, end: end); - controller.newSelection = TextSelection.collapsed( - offset: replacedRange.start + delta.replacementText.length, - ); return null; } else { final leadingText = text.substring( @@ -392,7 +385,6 @@ class RichTextItem { ); final trailString = text.substring(replacedRange.end - range.start); final insertEnd = replacedRange.start + delta.replacementText.length; - controller.newSelection = TextSelection.collapsed(offset: insertEnd); final config = delta.config; final insertedItem = RichTextItem( type: config.type, @@ -430,7 +422,6 @@ class RichTextItem { id = config.id; final end = range.start + text.length; range = TextRange(start: range.start, end: end); - controller.newSelection = TextSelection.collapsed(offset: end); return null; } @@ -444,7 +435,6 @@ class RichTextItem { id = config.id; final end = range.start + text.length; range = TextRange(start: range.start, end: end); - controller.newSelection = TextSelection.collapsed(offset: end); return (remove: false, toAdd: null); } return (remove: true, toAdd: null); @@ -460,7 +450,6 @@ class RichTextItem { ); final end = range.start + text.length; range = TextRange(start: range.start, end: end); - controller.newSelection = TextSelection.collapsed(offset: end); return null; } else { text = text.replaceRange( @@ -479,7 +468,6 @@ class RichTextItem { id: config.id, range: TextRange(start: replacedRange.start, end: end), ); - controller.newSelection = TextSelection.collapsed(offset: end); return (remove: false, toAdd: [insertedItem]); } } @@ -490,7 +478,6 @@ class RichTextItem { id = config.id; final end = range.start + text.length; range = TextRange(start: range.start, end: end); - controller.newSelection = TextSelection.collapsed(offset: end); return null; } @@ -513,7 +500,6 @@ class RichTextItem { ); final end = range.start + text.length; range = TextRange(start: range.start, end: end); - controller.newSelection = TextSelection.collapsed(offset: end); return null; } else { final end = range.start + delta.replacementText.length; @@ -526,7 +512,6 @@ class RichTextItem { id: config.id, range: TextRange(start: range.start, end: end), ); - controller.newSelection = TextSelection.collapsed(offset: end); text = text.substring(replacedRange.end - range.start); range = TextRange(start: end, end: end + text.length); return (remove: true, toAdd: [insertedItem]); @@ -539,7 +524,6 @@ class RichTextItem { id = config.id; final end = range.start + text.length; range = TextRange(start: range.start, end: end); - controller.newSelection = TextSelection.collapsed(offset: end); return null; } @@ -614,6 +598,7 @@ class RichTextEditingController extends TextEditingController { onMention?.call(); } + newSelection = delta.selection; if (items.isEmpty) { final config = delta.config; items.add( @@ -625,9 +610,6 @@ class RichTextEditingController extends TextEditingController { id: config.id, ), ); - newSelection = TextSelection.collapsed( - offset: delta.textInserted.length, - ); return; } for (int index = 0; index < items.length; index++) { @@ -653,6 +635,7 @@ class RichTextEditingController extends TextEditingController { } case TextEditingDeltaReplacement e: + newSelection = delta.selection; for (int index = 0; index < items.length; index++) { final item = items[index]; ({bool remove, List? toAdd})? res = item.onReplace( @@ -674,6 +657,9 @@ class RichTextEditingController extends TextEditingController { } case TextEditingDeltaNonTextUpdate e: + if (!_isSelectionValid(e.selection, items.lastOrNull?.range.end ?? 0)) { + return; + } newSelection = e.selection; if (newSelection.isCollapsed) { final newPos = dragOffset(newSelection.base); @@ -706,6 +692,10 @@ class RichTextEditingController extends TextEditingController { } } + static bool _isSelectionValid(TextSelection selection, int length) { + return selection.start <= length && selection.end <= length; + } + TextStyle? composingStyle; TextStyle? richStyle; diff --git a/lib/common/widgets/flutter/text_field/cupertino/adaptive_text_selection_toolbar.dart b/lib/common/widgets/flutter/text_field/cupertino/adaptive_text_selection_toolbar.dart index e8e988b51a..499dea6c61 100644 --- a/lib/common/widgets/flutter/text_field/cupertino/adaptive_text_selection_toolbar.dart +++ b/lib/common/widgets/flutter/text_field/cupertino/adaptive_text_selection_toolbar.dart @@ -2,11 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + /// @docImport 'package:flutter/material.dart'; library; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; -import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState; +import 'package:flutter/cupertino.dart' + hide EditableText, EditableTextState, CupertinoAdaptiveTextSelectionToolbar; import 'package:flutter/foundation.dart' show defaultTargetPlatform; import 'package:flutter/rendering.dart'; diff --git a/lib/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart b/lib/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart index 429b5bdb5b..1d688428ca 100644 --- a/lib/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart +++ b/lib/common/widgets/flutter/text_field/cupertino/spell_check_suggestions_toolbar.dart @@ -2,11 +2,14 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + /// @docImport 'package:flutter/material.dart'; library; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; -import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState; +import 'package:flutter/cupertino.dart' + hide EditableText, EditableTextState, CupertinoSpellCheckSuggestionsToolbar; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart' show SelectionChangedCause, SuggestionSpan; diff --git a/lib/common/widgets/flutter/text_field/cupertino/text_field.dart b/lib/common/widgets/flutter/text_field/cupertino/text_field.dart index 54d04c6d8a..c151c80949 100644 --- a/lib/common/widgets/flutter/text_field/cupertino/text_field.dart +++ b/lib/common/widgets/flutter/text_field/cupertino/text_field.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + /// @docImport 'package:flutter/material.dart'; library; @@ -294,6 +296,7 @@ class CupertinoRichTextField extends StatefulWidget { this.stylusHandwritingEnabled = EditableText.defaultStylusHandwritingEnabled, this.enableIMEPersonalizedLearning = true, + this.enableInlinePrediction, this.contextMenuBuilder = _defaultContextMenuBuilder, this.spellCheckConfiguration, this.magnifierConfiguration, @@ -439,6 +442,7 @@ class CupertinoRichTextField extends StatefulWidget { this.scribbleEnabled = true, this.stylusHandwritingEnabled = true, this.enableIMEPersonalizedLearning = true, + this.enableInlinePrediction, this.contextMenuBuilder = _defaultContextMenuBuilder, this.spellCheckConfiguration, this.magnifierConfiguration, @@ -799,6 +803,9 @@ class CupertinoRichTextField extends StatefulWidget { /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} final bool enableIMEPersonalizedLearning; + /// {@macro flutter.services.TextInputConfiguration.enableInlinePrediction} + final bool? enableInlinePrediction; + /// {@macro flutter.widgets.editableText.contentInsertionConfiguration} final ContentInsertionConfiguration? contentInsertionConfiguration; @@ -1116,6 +1123,13 @@ class CupertinoRichTextField extends StatefulWidget { defaultValue: true, ), ) + ..add( + DiagnosticsProperty( + 'enableInlinePrediction', + enableInlinePrediction, + defaultValue: null, + ), + ) ..add( DiagnosticsProperty( 'spellCheckConfiguration', @@ -1229,7 +1243,6 @@ class _CupertinoRichTextFieldState extends State @override void didUpdateWidget(CupertinoRichTextField oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.focusNode != oldWidget.focusNode) { (oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged); (widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged); @@ -1738,6 +1751,7 @@ class _CupertinoRichTextFieldState extends State scribbleEnabled: widget.scribbleEnabled, stylusHandwritingEnabled: widget.stylusHandwritingEnabled, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, + enableInlinePrediction: widget.enableInlinePrediction, contentInsertionConfiguration: widget.contentInsertionConfiguration, contextMenuBuilder: widget.contextMenuBuilder, spellCheckConfiguration: spellCheckConfiguration, diff --git a/lib/common/widgets/flutter/text_field/editable.dart b/lib/common/widgets/flutter/text_field/editable.dart index 6772e4bf62..8cce7e9994 100644 --- a/lib/common/widgets/flutter/text_field/editable.dart +++ b/lib/common/widgets/flutter/text_field/editable.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + /// @docImport 'package:flutter/cupertino.dart'; library; @@ -19,7 +21,7 @@ import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; import 'package:characters/characters.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/rendering.dart'; +import 'package:flutter/rendering.dart' hide RenderEditable; import 'package:flutter/services.dart'; const double _kCaretGap = 1.0; // pixels diff --git a/lib/common/widgets/flutter/text_field/editable_text.dart b/lib/common/widgets/flutter/text_field/editable_text.dart index 3915d9b44c..62aa509c12 100644 --- a/lib/common/widgets/flutter/text_field/editable_text.dart +++ b/lib/common/widgets/flutter/text_field/editable_text.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: uri_does_not_exist_in_doc_import +// ignore_for_file: prefer_initializing_formals, uri_does_not_exist_in_doc_import /// @docImport 'package:flutter/cupertino.dart'; /// @docImport 'package:flutter/material.dart'; @@ -25,6 +25,7 @@ import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/editable.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/spell_check.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/text_selection.dart'; +import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' @@ -394,7 +395,7 @@ class _DiscreteKeyFrameSimulation extends Simulation { /// ### Customizing User Input Accessibility Announcements /// /// To customize user input accessibility announcements triggered by text -/// changes, use [SemanticsService.announce] to make the desired +/// changes, use [SemanticsService.sendAnnouncement] to make the desired /// accessibility announcement. /// /// On iOS, the on-screen keyboard may announce the most recent input @@ -525,6 +526,7 @@ class EditableText extends StatefulWidget { this.spellCheckConfiguration, this.magnifierConfiguration = TextMagnifierConfiguration.disabled, this.hintLocales, + this.enableInlinePrediction, }) : assert(obscuringCharacter.length == 1), autocorrect = autocorrect ?? _inferAutocorrect(autofillHints: autofillHints), @@ -1683,6 +1685,9 @@ class EditableText extends StatefulWidget { /// {@macro flutter.services.TextInputConfiguration.hintLocales} final List? hintLocales; + /// {@macro flutter.services.TextInputConfiguration.enableInlinePrediction} + final bool? enableInlinePrediction; + /// The default value for [selectionHeightStyle]. /// /// On web platforms, this defaults to [ui.BoxHeightStyle.max]. @@ -2159,6 +2164,13 @@ class EditableText extends StatefulWidget { defaultValue: true, ), ) + ..add( + DiagnosticsProperty( + 'enableInlinePrediction', + enableInlinePrediction, + defaultValue: null, + ), + ) ..add( DiagnosticsProperty( 'enableInteractiveSelection', @@ -2494,7 +2506,9 @@ class EditableTextState extends State final String text = widget.controller.getSelectionText(selection) ?? selection.textInside(textEditingValue.text); - Clipboard.setData(ClipboardData(text: text)); + Clipboard.setData( + ClipboardData(text: text), + ).catchError(_reportClipboardError('while copying selection to clipboard')); if (cause == SelectionChangedCause.toolbar) { bringIntoView(textEditingValue.selection.extent); hideToolbar(false); @@ -2536,7 +2550,9 @@ class EditableTextState extends State final String text = widget.controller.getSelectionText(selection) ?? selection.textInside(textEditingValue.text); - Clipboard.setData(ClipboardData(text: text)); + Clipboard.setData( + ClipboardData(text: text), + ).catchError(_reportClipboardError('while cutting selection to clipboard')); _replaceText(ReplaceTextIntent(textEditingValue, '', selection, cause)); if (cause == SelectionChangedCause.toolbar) { // Schedule a call to bringIntoView() after renderEditable updates. @@ -2550,6 +2566,19 @@ class EditableTextState extends State clipboardStatus.update(); } + void Function(Object, StackTrace) _reportClipboardError(String context) { + return (Object exception, StackTrace stack) { + FlutterError.reportError( + FlutterErrorDetails( + exception: exception, + stack: stack, + library: 'widgets library', + context: ErrorDescription(context), + ), + ); + }; + } + bool get _allowPaste { return !widget.readOnly && textEditingValue.selection.isValid; } @@ -2581,6 +2610,10 @@ class EditableTextState extends State selection.baseOffset, selection.extentOffset, ); + final TextEditingValue collapsedTextEditingValue = textEditingValue + .copyWith(selection: .collapsed(offset: lastSelectionIndex)); + final newValue = collapsedTextEditingValue.replaced(selection, text); + // bggRGjQaUbCoE _pasteText widget.controller.syncRichText( selection.isCollapsed @@ -2588,22 +2621,18 @@ class EditableTextState extends State oldText: textEditingValue.text, textInserted: text, insertionOffset: selection.baseOffset, - selection: TextSelection.collapsed(offset: lastSelectionIndex), - composing: TextRange.empty, + selection: newValue.selection, + composing: newValue.composing, ) : TextEditingDeltaReplacement( oldText: textEditingValue.text, replacementText: text, replacedRange: selection, - selection: TextSelection.collapsed(offset: lastSelectionIndex), - composing: TextRange.empty, + selection: newValue.selection, + composing: newValue.composing, ), ); - final newValue = _value.copyWith( - text: widget.controller.plainText, - selection: widget.controller.newSelection, - composing: TextRange.empty, - ); + userUpdateTextEditingValue(newValue, cause); if (cause == SelectionChangedCause.toolbar) { // Schedule a call to bringIntoView() after renderEditable updates. @@ -2616,6 +2645,21 @@ class EditableTextState extends State } } + Future _pasteTextWithReporting(SelectionChangedCause cause) async { + try { + await pasteText(cause); + } catch (error, stack) { + FlutterError.reportError( + FlutterErrorDetails( + exception: error, + stack: stack, + library: 'widgets', + context: ErrorDescription('while pasting text to EditableText'), + ), + ); + } + } + /// Select the entire text value. @override void selectAll(SelectionChangedCause cause) { @@ -2723,7 +2767,19 @@ class EditableTextState extends State return; } if (_hasInputConnection) { - LiveText.startLiveTextInput(); + LiveText.startLiveTextInput().then( + (_) {}, + onError: (Object error, StackTrace stack) { + FlutterError.reportError( + FlutterErrorDetails( + exception: error, + stack: stack, + library: 'widgets library', + context: ErrorDescription('while starting Live Text input'), + ), + ); + }, + ); } if (cause == SelectionChangedCause.toolbar) { hideToolbar(); @@ -2846,8 +2902,8 @@ class EditableTextState extends State ), if (toolbarOptions.paste && pasteEnabled) ContextMenuButtonItem( - onPressed: () { - pasteText(SelectionChangedCause.toolbar); + onPressed: () async { + await _pasteTextWithReporting(SelectionChangedCause.toolbar); }, type: ContextMenuButtonType.paste, ), @@ -2972,7 +3028,7 @@ class EditableTextState extends State ? () => cutSelection(SelectionChangedCause.toolbar) : null, onPaste: pasteEnabled - ? () => pasteText(SelectionChangedCause.toolbar) + ? () => _pasteTextWithReporting(SelectionChangedCause.toolbar) : null, onSelectAll: selectAllEnabled ? () => selectAll(SelectionChangedCause.toolbar) @@ -3118,6 +3174,14 @@ class EditableTextState extends State _effectiveAutofillClient.textInputConfiguration, ); } + // The style may have changed due to dependency changes + // (e.g. MediaQuery.boldTextOf, MediaQuery.textScalerOf, etc.). + SchedulerBinding.instance.addPostFrameCallback((Duration _) { + if (!mounted || !_hasInputConnection) { + return; + } + _textInputConnection!.updateStyle(_getTextInputStyle(context)); + }, debugLabel: 'EditableText.updateStyle'); } if (defaultTargetPlatform != TargetPlatform.iOS && @@ -3225,8 +3289,13 @@ class EditableTextState extends State } if (_hasInputConnection) { - if (oldWidget.obscureText != widget.obscureText || - oldWidget.keyboardType != widget.keyboardType) { + final obscureTextChanged = oldWidget.obscureText != widget.obscureText; + if (obscureTextChanged || oldWidget.keyboardType != widget.keyboardType) { + if (obscureTextChanged) { + // When obscureText is toggled, we should reset its state to prevent the last character from being visible between state changes. + _obscureShowCharTicksPending = 0; + _obscureLatestCharIndex = null; + } _textInputConnection!.updateConfig( _effectiveAutofillClient.textInputConfiguration, ); @@ -3240,13 +3309,14 @@ class EditableTextState extends State ? widget.style.merge(const TextStyle(fontWeight: FontWeight.bold)) : widget.style; if (_hasInputConnection) { - _textInputConnection!.setStyle( - fontFamily: _style.fontFamily, - fontSize: _style.fontSize, - fontWeight: _style.fontWeight, - textDirection: _textDirection, - textAlign: widget.textAlign, - ); + // Schedule the style update after layout to ensure preferredLineHeight + // is computed with the new style. + SchedulerBinding.instance.addPostFrameCallback((Duration _) { + if (!mounted || !_hasInputConnection) { + return; + } + _textInputConnection!.updateStyle(_getTextInputStyle(context)); + }, debugLabel: 'EditableText.updateStyle'); } } @@ -3272,6 +3342,27 @@ class EditableTextState extends State } } + TextInputStyle _getTextInputStyle(BuildContext context) { + final double? letterSpacingOverride = + MediaQuery.maybeLetterSpacingOverrideOf(context); + final double? wordSpacingOverride = MediaQuery.maybeWordSpacingOverrideOf( + context, + ); + + return TextInputStyle( + fontFamily: _style.fontFamily, + fontSize: _style.fontSize, + fontWeight: _style.fontWeight, + textDirection: _textDirection, + textAlign: widget.textAlign, + letterSpacing: letterSpacingOverride ?? _style.letterSpacing, + wordSpacing: wordSpacingOverride ?? _style.wordSpacing, + // preferredLineHeight already includes lineHeightScaleFactor from + // _OverridingTextStyleTextSpanUtils.applyTextSpacingOverrides. + lineHeight: renderEditable.preferredLineHeight, + ); + } + @protected @override void dispose() { @@ -3466,6 +3557,8 @@ class EditableTextState extends State } } + TextRange? _deletedRange; + @override void updateEditingValueWithDeltas(List textEditingDeltas) { if (textEditingDeltas.isEmpty) { @@ -3474,14 +3567,35 @@ class EditableTextState extends State } TextEditingValue remoteValue = _value; for (final TextEditingDelta delta in textEditingDeltas) { + if (PlatformUtils.isDesktop) { + if (_deletedRange case final range?) { + final deleteDelta = TextEditingDeltaDeletion( + oldText: remoteValue.text, + deletedRange: range, + selection: remoteValue.selection, + composing: remoteValue.composing, + ); + _deletedRange = null; + widget.controller.syncRichText(deleteDelta); + } else if (delta is TextEditingDeltaInsertion && + !remoteValue.selection.isCollapsed) { + final offset = delta.textInserted.length; + _deletedRange = TextRange( + start: remoteValue.selection.start + offset, + end: remoteValue.selection.end + offset, + ); + } + } widget.controller.syncRichText(delta); remoteValue = delta.apply(remoteValue); } - final newValue = _value.copyWith( - text: widget.controller.plainText, + final plainText = widget.controller.plainText; + final composing = textEditingDeltas.last.composing; + final newValue = TextEditingValue( + text: plainText, selection: widget.controller.newSelection, - composing: textEditingDeltas.last.composing, + composing: composing.end <= plainText.length ? composing : .empty, ); updateEditingValue(newValue, remoteValue: remoteValue); @@ -3902,13 +4016,7 @@ class EditableTextState extends State _updateSizeAndTransform(); _schedulePeriodicPostFrameCallbacks(); _textInputConnection! - ..setStyle( - fontFamily: _style.fontFamily, - fontSize: _style.fontSize, - fontWeight: _style.fontWeight, - textDirection: _textDirection, - textAlign: widget.textAlign, - ) + ..updateStyle(_getTextInputStyle(context)) ..setEditingState(localValue) ..show(); if (_needsAutofill) { @@ -3974,13 +4082,7 @@ class EditableTextState extends State newConnection ..show() - ..setStyle( - fontFamily: _style.fontFamily, - fontSize: _style.fontSize, - fontWeight: _style.fontWeight, - textDirection: _textDirection, - textAlign: widget.textAlign, - ) + ..updateStyle(_getTextInputStyle(context)) ..setEditingState(_value); _lastKnownRemoteTextEditingValue = _value; } @@ -3996,6 +4098,15 @@ class EditableTextState extends State } } + @override + bool onFocusReceived() { + if (mounted && !_hasFocus && widget.focusNode.canRequestFocus) { + widget.focusNode.requestFocus(); + return true; + } + return false; + } + @override void connectionClosed() { if (_hasInputConnection) { @@ -4758,6 +4869,7 @@ class EditableTextState extends State } void _handleFocusChanged() { + _deletedRange = null; _openOrCloseInputConnectionIfNeeded(); _startOrStopCursorTimerIfNeeded(); _updateOrDisposeSelectionOverlayIfNeeded(); @@ -5248,6 +5360,7 @@ class EditableTextState extends State ? const [] : widget.contentInsertionConfiguration!.allowedMimeTypes, hintLocales: widget.hintLocales, + enableInlinePrediction: widget.enableInlinePrediction, ); } @@ -5300,9 +5413,9 @@ class EditableTextState extends State : pasteEnabled && (widget.selectionControls?.canPaste(this) ?? false)) && (clipboardStatus.value == ClipboardStatus.pasteable) - ? () { - controls?.handlePaste(this); - pasteText(SelectionChangedCause.toolbar); + ? () async { + await controls?.handlePaste(this); + await _pasteTextWithReporting(SelectionChangedCause.toolbar); } : null; } @@ -5441,33 +5554,39 @@ class EditableTextState extends State void _replaceText(ReplaceTextIntent intent) { final TextEditingValue oldValue = _value; + final TextEditingValue newValue; + // bggRGjQaUbCoE _replaceText - widget.controller.syncRichText( - intent.replacementText.isEmpty - ? TextEditingDeltaDeletion( - oldText: oldValue.text, - deletedRange: intent.replacementRange, - selection: TextSelection.collapsed( - offset: intent.replacementRange.start, - ), - composing: TextRange.empty, - ) - : TextEditingDeltaReplacement( - oldText: oldValue.text, - replacementText: intent.replacementText, - replacedRange: intent.replacementRange, - selection: TextSelection.collapsed( - offset: intent.replacementRange.start, - ), - composing: TextRange.empty, - ), - ); + if (intent.replacementText.isEmpty) { + widget.controller.syncRichText( + TextEditingDeltaDeletion( + oldText: oldValue.text, + deletedRange: intent.replacementRange, + selection: .collapsed(offset: intent.replacementRange.start), + composing: .empty, + ), + ); + newValue = TextEditingValue( + text: widget.controller.plainText, + selection: widget.controller.newSelection, + composing: .empty, + ); + } else { + newValue = intent.currentTextEditingValue.replaced( + intent.replacementRange, + intent.replacementText, + ); + widget.controller.syncRichText( + TextEditingDeltaReplacement( + oldText: oldValue.text, + replacementText: intent.replacementText, + replacedRange: intent.replacementRange, + selection: newValue.selection, + composing: newValue.composing, + ), + ); + } - final newValue = oldValue.copyWith( - text: widget.controller.plainText, - selection: widget.controller.newSelection, - composing: TextRange.empty, - ); userUpdateTextEditingValue(newValue, intent.cause); // If there's no change in text and selection (e.g. when selecting and @@ -5529,70 +5648,6 @@ class EditableTextState extends State _scrollController.jumpTo(destination); } - /// Extend the selection down by page if the `forward` parameter is true, or - /// up by page otherwise. - void _extendSelectionByPage(ExtendSelectionByPageIntent intent) { - if (widget.maxLines == 1) { - return; - } - - final TextSelection nextSelection; - final Rect extentRect = renderEditable.getLocalRectForCaret( - _value.selection.extent, - ); - final state = _scrollableKey.currentState as ScrollableState?; - final double increment = ScrollAction.getDirectionalIncrement( - state!, - ScrollIntent( - direction: intent.forward ? AxisDirection.down : AxisDirection.up, - type: ScrollIncrementType.page, - ), - ); - final ScrollPosition position = _scrollController.position; - if (intent.forward) { - if (_value.selection.extentOffset >= _value.text.length) { - return; - } - final nextExtentOffset = Offset( - extentRect.left, - extentRect.top + increment, - ); - final double height = - position.maxScrollExtent + renderEditable.size.height; - final TextPosition nextExtent = - nextExtentOffset.dy + position.pixels >= height - ? TextPosition(offset: _value.text.length) - : renderEditable.getPositionForPoint( - renderEditable.localToGlobal(nextExtentOffset), - ); - nextSelection = _value.selection.copyWith( - extentOffset: nextExtent.offset, - ); - } else { - if (_value.selection.extentOffset <= 0) { - return; - } - final nextExtentOffset = Offset( - extentRect.left, - extentRect.top + increment, - ); - final TextPosition nextExtent = nextExtentOffset.dy + position.pixels <= 0 - ? const TextPosition(offset: 0) - : renderEditable.getPositionForPoint( - renderEditable.localToGlobal(nextExtentOffset), - ); - nextSelection = _value.selection.copyWith( - extentOffset: nextExtent.offset, - ); - } - - bringIntoView(nextSelection.extent); - userUpdateTextEditingValue( - _value.copyWith(selection: nextSelection), - SelectionChangedCause.keyboard, - ); - } - void _updateSelection(UpdateSelectionIntent intent) { assert( intent.newSelection.start <= intent.currentTextEditingValue.text.length, @@ -5730,11 +5785,6 @@ class EditableTextState extends State ignoreNonCollapsedSelection: false, ), ), - ExtendSelectionByPageIntent: _makeOverridable( - CallbackAction( - onInvoke: _extendSelectionByPage, - ), - ), ExtendSelectionToNextWordBoundaryIntent: _makeOverridable( _UpdateTextSelectionAction( this, @@ -6953,7 +7003,7 @@ class _PasteSelectionAction extends ContextAction { return; } - state.pasteText(intent.cause); + state._pasteTextWithReporting(intent.cause); } } diff --git a/lib/common/widgets/flutter/text_field/spell_check.dart b/lib/common/widgets/flutter/text_field/spell_check.dart index 649a811514..b1ea2aa20a 100644 --- a/lib/common/widgets/flutter/text_field/spell_check.dart +++ b/lib/common/widgets/flutter/text_field/spell_check.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + /// @docImport 'editable_text.dart'; library; diff --git a/lib/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart b/lib/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart index 549e93149c..5692cc8773 100644 --- a/lib/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart +++ b/lib/common/widgets/flutter/text_field/spell_check_suggestions_toolbar.dart @@ -2,11 +2,17 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + import 'package:PiliPlus/common/widgets/flutter/text_field/adaptive_text_selection_toolbar.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; import 'package:flutter/cupertino.dart' hide EditableText, EditableTextState; import 'package:flutter/material.dart' - hide EditableText, EditableTextState, AdaptiveTextSelectionToolbar; + hide + EditableText, + EditableTextState, + AdaptiveTextSelectionToolbar, + SpellCheckSuggestionsToolbar; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart' show SelectionChangedCause, SuggestionSpan; diff --git a/lib/common/widgets/flutter/text_field/system_context_menu.dart b/lib/common/widgets/flutter/text_field/system_context_menu.dart index ed682f985c..ce4de4ea4a 100644 --- a/lib/common/widgets/flutter/text_field/system_context_menu.dart +++ b/lib/common/widgets/flutter/text_field/system_context_menu.dart @@ -2,12 +2,15 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + /// @docImport 'package:flutter/material.dart'; library; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter/material.dart' hide EditableText, EditableTextState; +import 'package:flutter/material.dart' + hide EditableText, EditableTextState, SystemContextMenu; import 'package:flutter/services.dart'; /// Displays the system context menu on top of the Flutter view. diff --git a/lib/common/widgets/flutter/text_field/text_field.dart b/lib/common/widgets/flutter/text_field/text_field.dart index 7cd3ed517e..b68feae483 100644 --- a/lib/common/widgets/flutter/text_field/text_field.dart +++ b/lib/common/widgets/flutter/text_field/text_field.dart @@ -2,7 +2,7 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. -// ignore_for_file: uri_does_not_exist_in_doc_import +// ignore_for_file: prefer_initializing_formals, uri_does_not_exist_in_doc_import /// @docImport 'input_border.dart'; /// @docImport 'material.dart'; @@ -38,6 +38,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart' hide + TextField, EditableText, EditableTextState, EditableTextContextMenuBuilder, @@ -48,7 +49,6 @@ import 'package:flutter/material.dart' TextSelectionGestureDetectorBuilder, TextSelectionOverlay, TextSelectionGestureDetectorBuilderDelegate; -import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; class _TextFieldSelectionGestureDetectorBuilder @@ -321,6 +321,7 @@ class RichTextField extends StatefulWidget { this.stylusHandwritingEnabled = EditableText.defaultStylusHandwritingEnabled, this.enableIMEPersonalizedLearning = true, + this.enableInlinePrediction, this.contextMenuBuilder = _defaultContextMenuBuilder, this.canRequestFocus = true, this.spellCheckConfiguration, @@ -868,6 +869,9 @@ class RichTextField extends StatefulWidget { /// {@macro flutter.services.TextInputConfiguration.enableIMEPersonalizedLearning} final bool enableIMEPersonalizedLearning; + /// {@macro flutter.services.TextInputConfiguration.enableInlinePrediction} + final bool? enableInlinePrediction; + /// {@macro flutter.widgets.editableText.contentInsertionConfiguration} final ContentInsertionConfiguration? contentInsertionConfiguration; @@ -999,9 +1003,7 @@ class RichTextField extends StatefulWidget { defaultValue: null, ), ) - ..add( - DiagnosticsProperty('enabled', enabled, defaultValue: null), - ) + ..add(DiagnosticsProperty('enabled', enabled, defaultValue: null)) ..add( DiagnosticsProperty( 'decoration', @@ -1016,9 +1018,7 @@ class RichTextField extends StatefulWidget { defaultValue: TextInputType.text, ), ) - ..add( - DiagnosticsProperty('style', style, defaultValue: null), - ) + ..add(DiagnosticsProperty('style', style, defaultValue: null)) ..add( DiagnosticsProperty('autofocus', autofocus, defaultValue: false), ) @@ -1070,9 +1070,7 @@ class RichTextField extends StatefulWidget { ) ..add(IntProperty('maxLines', maxLines, defaultValue: 1)) ..add(IntProperty('minLines', minLines, defaultValue: null)) - ..add( - DiagnosticsProperty('expands', expands, defaultValue: false), - ) + ..add(DiagnosticsProperty('expands', expands, defaultValue: false)) ..add(IntProperty('maxLength', maxLength, defaultValue: null)) ..add( EnumProperty( @@ -1116,12 +1114,8 @@ class RichTextField extends StatefulWidget { defaultValue: null, ), ) - ..add( - DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0), - ) - ..add( - DoubleProperty('cursorHeight', cursorHeight, defaultValue: null), - ) + ..add(DoubleProperty('cursorWidth', cursorWidth, defaultValue: 2.0)) + ..add(DoubleProperty('cursorHeight', cursorHeight, defaultValue: null)) ..add( DiagnosticsProperty( 'cursorRadius', @@ -1136,9 +1130,7 @@ class RichTextField extends StatefulWidget { defaultValue: null, ), ) - ..add( - ColorProperty('cursorColor', cursorColor, defaultValue: null), - ) + ..add(ColorProperty('cursorColor', cursorColor, defaultValue: null)) ..add( ColorProperty('cursorErrorColor', cursorErrorColor, defaultValue: null), ) @@ -1213,6 +1205,13 @@ class RichTextField extends StatefulWidget { defaultValue: true, ), ) + ..add( + DiagnosticsProperty( + 'enableInlinePrediction', + enableInlinePrediction, + defaultValue: null, + ), + ) ..add( DiagnosticsProperty( 'spellCheckConfiguration', @@ -1416,7 +1415,6 @@ class RichTextFieldState extends State @override void didUpdateWidget(RichTextField oldWidget) { super.didUpdateWidget(oldWidget); - if (widget.focusNode != oldWidget.focusNode) { (oldWidget.focusNode ?? _focusNode)?.removeListener(_handleFocusChanged); (widget.focusNode ?? _focusNode)?.addListener(_handleFocusChanged); @@ -1885,6 +1883,7 @@ class RichTextFieldState extends State scribbleEnabled: widget.scribbleEnabled, stylusHandwritingEnabled: widget.stylusHandwritingEnabled, enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, + enableInlinePrediction: widget.enableInlinePrediction, contentInsertionConfiguration: widget.contentInsertionConfiguration, contextMenuBuilder: widget.contextMenuBuilder, spellCheckConfiguration: spellCheckConfiguration, @@ -2028,7 +2027,7 @@ TextStyle _m2CounterErrorStyle(BuildContext context) => Theme.of( // dart format off TextStyle? _m3StateInputStyle(BuildContext context) => WidgetStateTextStyle.resolveWith((Set states) { if (states.contains(WidgetState.disabled)) { - return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color?.withValues(alpha:0.38)); + return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color?.withValues(alpha: 0.38)); } return TextStyle(color: Theme.of(context).textTheme.bodyLarge!.color); }); diff --git a/lib/common/widgets/flutter/text_field/text_selection.dart b/lib/common/widgets/flutter/text_field/text_selection.dart index f4df4cf316..c825000360 100644 --- a/lib/common/widgets/flutter/text_field/text_selection.dart +++ b/lib/common/widgets/flutter/text_field/text_selection.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + /// @docImport 'package:flutter/cupertino.dart'; /// @docImport 'package:flutter/material.dart'; library; @@ -13,7 +15,12 @@ import 'package:PiliPlus/common/widgets/flutter/text_field/editable.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/editable_text.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart' hide EditableText, EditableTextState; +import 'package:flutter/material.dart' + hide + EditableText, + EditableTextState, + TextSelectionOverlay, + TextSelectionGestureDetectorBuilder; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; @@ -575,9 +582,9 @@ class TextSelectionGestureDetectorBuilder { .getPositionForPoint( details.globalPosition, ); - final bool isAffinityTheSame = + final isAffinityTheSame = textPosition.affinity == previousSelection.affinity; - final bool wordAtCursorIndexIsMisspelled = + final wordAtCursorIndexIsMisspelled = editableText.findSuggestionSpanAtCursorIndex( textPosition.offset, ) != @@ -659,7 +666,7 @@ class TextSelectionGestureDetectorBuilder { cause: SelectionChangedCause.longPress, ); // Show the floating cursor. - final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint( + final cursorPoint = RawFloatingCursorPoint( state: FloatingCursorDragState.Start, startLocation: ( renderEditable.globalToLocal(details.globalPosition), @@ -703,7 +710,7 @@ class TextSelectionGestureDetectorBuilder { return; } // Adjust the drag start offset for possible viewport offset changes. - final Offset editableOffset = renderEditable.maxLines == 1 + final editableOffset = renderEditable.maxLines == 1 ? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0) : Offset(0.0, renderEditable.offset.pixels - _dragStartViewportOffset); final Offset scrollableOffset = switch (axisDirectionToAxis( @@ -731,7 +738,7 @@ class TextSelectionGestureDetectorBuilder { cause: SelectionChangedCause.longPress, ); // Update the floating cursor. - final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint( + final cursorPoint = RawFloatingCursorPoint( state: FloatingCursorDragState.Update, offset: details.offsetFromOrigin, ); @@ -867,7 +874,7 @@ class TextSelectionGestureDetectorBuilder { delegate.selectionEnabled && editableText.textEditingValue.selection.isCollapsed) { // Update the floating cursor. - final RawFloatingCursorPoint cursorPoint = RawFloatingCursorPoint( + final cursorPoint = RawFloatingCursorPoint( state: FloatingCursorDragState.End, ); editableText.updateFloatingCursor(cursorPoint); @@ -954,7 +961,7 @@ class TextSelectionGestureDetectorBuilder { : _moveToTextBoundary(toPosition, boundary); final bool isFromBoundaryBeforeToBoundary = fromRange.start < toRange.end; - final TextSelection newSelection = isFromBoundaryBeforeToBoundary + final newSelection = isFromBoundaryBeforeToBoundary ? TextSelection(baseOffset: fromRange.start, extentOffset: toRange.end) : TextSelection(baseOffset: fromRange.end, extentOffset: toRange.start); @@ -1117,7 +1124,7 @@ class TextSelectionGestureDetectorBuilder { if (!_isShiftPressed) { // Adjust the drag start offset for possible viewport offset changes. - final Offset editableOffset = renderEditable.maxLines == 1 + final editableOffset = renderEditable.maxLines == 1 ? Offset(renderEditable.offset.pixels - _dragStartViewportOffset, 0.0) : Offset( 0.0, @@ -2127,21 +2134,21 @@ class TextSelectionOverlay { final TextSelection lineAtOffset = renderEditable.getLineAtOffset( currentTextPosition, ); - final TextPosition positionAtEndOfLine = TextPosition( + final positionAtEndOfLine = TextPosition( offset: lineAtOffset.extentOffset, affinity: TextAffinity.upstream, ); // Default affinity is downstream. - final TextPosition positionAtBeginningOfLine = TextPosition( + final positionAtBeginningOfLine = TextPosition( offset: lineAtOffset.baseOffset, ); - final Rect localLineBoundaries = Rect.fromPoints( + final localLineBoundaries = Rect.fromPoints( renderEditable.getLocalRectForCaret(positionAtBeginningOfLine).topCenter, renderEditable.getLocalRectForCaret(positionAtEndOfLine).bottomCenter, ); - final RenderBox? overlay = + final overlay = Overlay.of(context, rootOverlay: true).context.findRenderObject() as RenderBox?; final Matrix4 transformToOverlay = renderEditable.getTransformTo(overlay); @@ -2251,7 +2258,7 @@ class TextSelectionOverlay { /// line height is used, and the return value is in local coordinates as well. double _getHandleDy(double dragDy, double handleDy) { final double distanceDragged = dragDy - handleDy; - final int dragDirection = distanceDragged < 0.0 ? -1 : 1; + final dragDirection = distanceDragged < 0.0 ? -1 : 1; final int linesDragged = dragDirection * (distanceDragged.abs() / renderObject.preferredLineHeight).floor(); @@ -2277,7 +2284,7 @@ class TextSelectionOverlay { .localToGlobal(Offset(0.0, nextEndHandleDragPositionLocal)) .dy; - final Offset handleTargetGlobal = Offset( + final handleTargetGlobal = Offset( details.globalPosition.dx, _endHandleDragPosition + _endHandleDragTarget, ); @@ -2304,9 +2311,7 @@ class TextSelectionOverlay { ), ); - final TextSelection currentSelection = TextSelection.fromPosition( - position, - ); + final currentSelection = TextSelection.fromPosition(position); _handleSelectionHandleChanged(currentSelection); return; } @@ -2334,9 +2339,7 @@ class TextSelectionOverlay { ), ); - final TextSelection currentSelection = TextSelection.fromPosition( - position, - ); + final currentSelection = TextSelection.fromPosition(position); _handleSelectionHandleChanged(currentSelection); return; } @@ -2424,7 +2427,7 @@ class TextSelectionOverlay { _startHandleDragPosition = renderObject .localToGlobal(Offset(0.0, nextStartHandleDragPositionLocal)) .dy; - final Offset handleTargetGlobal = Offset( + final handleTargetGlobal = Offset( details.globalPosition.dx, _startHandleDragPosition + _startHandleDragTarget, ); @@ -2450,9 +2453,7 @@ class TextSelectionOverlay { ), ); - final TextSelection currentSelection = TextSelection.fromPosition( - position, - ); + final currentSelection = TextSelection.fromPosition(position); _handleSelectionHandleChanged(currentSelection); return; } @@ -2480,9 +2481,7 @@ class TextSelectionOverlay { ), ); - final TextSelection currentSelection = TextSelection.fromPosition( - position, - ); + final currentSelection = TextSelection.fromPosition(position); _handleSelectionHandleChanged(currentSelection); return; } diff --git a/lib/common/widgets/flutter/vertical_slider.dart b/lib/common/widgets/flutter/vertical_slider.dart new file mode 100644 index 0000000000..f87971610d --- /dev/null +++ b/lib/common/widgets/flutter/vertical_slider.dart @@ -0,0 +1,2680 @@ +// 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. + +// ignore_for_file: prefer_initializing_formals + +import 'dart:async'; +import 'dart:math' as math; + +import 'package:flutter/cupertino.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/gestures.dart'; +import 'package:flutter/material.dart' hide Slider; +import 'package:flutter/rendering.dart'; +import 'package:flutter/scheduler.dart' show timeDilation; +import 'package:flutter/services.dart'; + +enum _SliderType { material, adaptive } + +/// A Material Design slider. +/// +/// Used to select from a range of values. +/// +/// {@youtube 560 315 https://www.youtube.com/watch?v=ufb4gIPDmEs} +/// +/// {@tool dartpad} +/// This example showcases non-discrete and discrete [VerticalSlider]s. +/// The [VerticalSlider]s will show the updated ![Material 3 Design appearance](https://m3.material.io/components/sliders/overview) +/// when setting the [VerticalSlider.year2023] flag to false. +/// +/// ** See code in examples/api/lib/material/slider/slider.0.dart ** +/// {@end-tool} +/// +/// {@tool dartpad} +/// This example shows a [VerticalSlider] widget using the [VerticalSlider.secondaryTrackValue] +/// to show a secondary track in the slider. +/// +/// ** See code in examples/api/lib/material/slider/slider.1.dart ** +/// {@end-tool} +/// +/// A slider can be used to select from either a continuous or a discrete set of +/// values. The default is to use a continuous range of values from [min] to +/// [max]. To use discrete values, use a non-null value for [divisions], which +/// indicates the number of discrete intervals. For example, if [min] is 0.0 and +/// [max] is 50.0 and [divisions] is 5, then the slider can take on the +/// discrete values 0.0, 10.0, 20.0, 30.0, 40.0, and 50.0. +/// +/// The terms for the parts of a slider are: +/// +/// * The "thumb", which is a shape that slides horizontally when the user +/// drags it. +/// * The "track", which is the line that the slider thumb slides along. +/// * The "value indicator", which is a shape that pops up when the user +/// is dragging the thumb to indicate the value being selected. +/// * The "active" side of the slider is the side between the thumb and the +/// minimum value. +/// * The "inactive" side of the slider is the side between the thumb and the +/// maximum value. +/// +/// The slider will be disabled if [onChanged] is null or if the range given by +/// [min]..[max] is empty (i.e. if [min] is equal to [max]). +/// +/// The slider widget itself does not maintain any state. Instead, when the state +/// of the slider changes, the widget calls the [onChanged] callback. Most +/// widgets that use a slider will listen for the [onChanged] callback and +/// rebuild the slider with a new [value] to update the visual appearance of the +/// slider. To know when the value starts to change, or when it is done +/// changing, set the optional callbacks [onChangeStart] and/or [onChangeEnd]. +/// +/// By default, a slider will be as wide as possible, centered vertically. When +/// given unbounded constraints, it will attempt to make the track 144 pixels +/// wide (with margins on each side) and will shrink-wrap vertically. +/// +/// Requires one of its ancestors to be a [Material] widget. +/// +/// Requires one of its ancestors to be a [MediaQuery] widget. Typically, these +/// are introduced by the [MaterialApp] or [WidgetsApp] widget at the top of +/// your application widget tree. +/// +/// To determine how it should be displayed (e.g. colors, thumb shape, etc.), +/// a slider uses the [SliderThemeData] available from either a [SliderTheme] +/// widget or the [ThemeData.sliderTheme] a [Theme] widget above it in the +/// widget tree. You can also override some of the colors with the [activeColor] +/// and [inactiveColor] properties, although more fine-grained control of the +/// look is achieved using a [SliderThemeData]. +/// +/// See also: +/// +/// * [SliderTheme] and [SliderThemeData] for information about controlling +/// the visual appearance of the slider. +/// * [Radio], for selecting among a set of explicit values. +/// * [Checkbox] and [Switch], for toggling a particular value on or off. +/// * +/// * [MediaQuery], from which the text scale factor is obtained. +class VerticalSlider extends StatefulWidget { + /// Creates a Material Design slider. + /// + /// The slider itself does not maintain any state. Instead, when the state of + /// the slider changes, the widget calls the [onChanged] callback. Most + /// widgets that use a slider will listen for the [onChanged] callback and + /// rebuild the slider with a new [value] to update the visual appearance of + /// the slider. + /// + /// * [value] determines currently selected value for this slider. + /// * [onChanged] is called while the user is selecting a new value for the + /// slider. + /// * [onChangeStart] is called when the user starts to select a new value for + /// the slider. + /// * [onChangeEnd] is called when the user is done selecting a new value for + /// the slider. + /// + /// You can override some of the colors with the [activeColor] and + /// [inactiveColor] properties, although more fine-grained control of the + /// appearance is achieved using a [SliderThemeData]. + const VerticalSlider({ + super.key, + required this.value, + this.secondaryTrackValue, + required this.onChanged, + this.onChangeStart, + this.onChangeEnd, + this.min = 0.0, + this.max = 1.0, + this.divisions, + this.label, + this.activeColor, + this.inactiveColor, + this.secondaryActiveColor, + this.thumbColor, + this.overlayColor, + this.mouseCursor, + this.semanticFormatterCallback, + this.focusNode, + this.autofocus = false, + this.allowedInteraction, + this.padding, + this.showValueIndicator, + @Deprecated( + 'Set this flag to false to opt into the 2024 slider appearance. Defaults to true. ' + 'In the future, this flag will default to false. Use SliderThemeData to customize individual properties. ' + 'This feature was deprecated after v3.27.0-0.2.pre.', + ) + this.year2023, + }) : _sliderType = _SliderType.material, + assert(min <= max), + assert( + value >= min && value <= max, + 'Value $value is not between minimum $min and maximum $max', + ), + assert( + secondaryTrackValue == null || + (secondaryTrackValue >= min && secondaryTrackValue <= max), + 'SecondaryValue $secondaryTrackValue is not between $min and $max', + ), + assert(divisions == null || divisions > 0); + + /// Creates an adaptive [VerticalSlider] based on the target platform, following + /// Material design's + /// [Cross-platform guidelines](https://material.io/design/platform-guidance/cross-platform-adaptation.html). + /// + /// Creates a [CupertinoSlider] if the target platform is iOS or macOS, creates a + /// Material Design slider otherwise. + /// + /// If a [CupertinoSlider] is created, the following parameters are ignored: + /// [secondaryTrackValue], [label], [inactiveColor], [secondaryActiveColor], + /// [semanticFormatterCallback], [showValueIndicator]. + /// + /// The target platform is based on the current [Theme]: [ThemeData.platform]. + const VerticalSlider.adaptive({ + super.key, + required this.value, + this.secondaryTrackValue, + required this.onChanged, + this.onChangeStart, + this.onChangeEnd, + this.min = 0.0, + this.max = 1.0, + this.divisions, + this.label, + this.mouseCursor, + this.activeColor, + this.inactiveColor, + this.secondaryActiveColor, + this.thumbColor, + this.overlayColor, + this.semanticFormatterCallback, + this.focusNode, + this.autofocus = false, + this.allowedInteraction, + this.showValueIndicator, + @Deprecated( + 'Set this flag to false to opt into the 2024 slider appearance. Defaults to true. ' + 'In the future, this flag will default to false. Use SliderThemeData to customize individual properties. ' + 'This feature was deprecated after v3.27.0-0.1.pre.', + ) + this.year2023, + }) : _sliderType = _SliderType.adaptive, + padding = null, + assert(min <= max), + assert( + value >= min && value <= max, + 'Value $value is not between minimum $min and maximum $max', + ), + assert( + secondaryTrackValue == null || + (secondaryTrackValue >= min && secondaryTrackValue <= max), + 'SecondaryValue $secondaryTrackValue is not between $min and $max', + ), + assert(divisions == null || divisions > 0); + + /// The currently selected value for this slider. + /// + /// The slider's thumb is drawn at a position that corresponds to this value. + final double value; + + /// The secondary track value for this slider. + /// + /// If not null, a secondary track using [VerticalSlider.secondaryActiveColor] color + /// is drawn between the thumb and this value, over the inactive track. + /// + /// If less than [VerticalSlider.value], then the secondary track is not shown. + /// + /// It can be ideal for media scenarios such as showing the buffering progress + /// while the [VerticalSlider.value] shows the play progress. + final double? secondaryTrackValue; + + /// Called during a drag when the user is selecting a new value for the slider + /// by dragging. + /// + /// The slider passes the new value to the callback but does not actually + /// change state until the parent widget rebuilds the slider with the new + /// value. + /// + /// If null, the slider will be displayed as disabled. + /// + /// The callback provided to onChanged should update the state of the parent + /// [StatefulWidget] using the [State.setState] method, so that the parent + /// gets rebuilt; for example: + /// + /// {@tool snippet} + /// + /// ```dart + /// Slider( + /// value: _duelCommandment.toDouble(), + /// min: 1.0, + /// max: 10.0, + /// divisions: 10, + /// label: '$_duelCommandment', + /// onChanged: (double newValue) { + /// setState(() { + /// _duelCommandment = newValue.round(); + /// }); + /// }, + /// ) + /// ``` + /// {@end-tool} + /// + /// See also: + /// + /// * [onChangeStart] for a callback that is called when the user starts + /// changing the value. + /// * [onChangeEnd] for a callback that is called when the user stops + /// changing the value. + final ValueChanged? onChanged; + + /// Called when the user starts selecting a new value for the slider. + /// + /// This callback shouldn't be used to update the slider [value] (use + /// [onChanged] for that), but rather to be notified when the user has started + /// selecting a new value by starting a drag or with a tap. + /// + /// The value passed will be the last [value] that the slider had before the + /// change began. + /// + /// {@tool snippet} + /// + /// ```dart + /// Slider( + /// value: _duelCommandment.toDouble(), + /// min: 1.0, + /// max: 10.0, + /// divisions: 10, + /// label: '$_duelCommandment', + /// onChanged: (double newValue) { + /// setState(() { + /// _duelCommandment = newValue.round(); + /// }); + /// }, + /// onChangeStart: (double startValue) { + /// print('Started change at $startValue'); + /// }, + /// ) + /// ``` + /// {@end-tool} + /// + /// See also: + /// + /// * [onChangeEnd] for a callback that is called when the value change is + /// complete. + final ValueChanged? onChangeStart; + + /// Called when the user is done selecting a new value for the slider. + /// + /// This callback shouldn't be used to update the slider [value] (use + /// [onChanged] for that), but rather to know when the user has completed + /// selecting a new [value] by ending a drag or a click. + /// + /// {@tool snippet} + /// + /// ```dart + /// Slider( + /// value: _duelCommandment.toDouble(), + /// min: 1.0, + /// max: 10.0, + /// divisions: 10, + /// label: '$_duelCommandment', + /// onChanged: (double newValue) { + /// setState(() { + /// _duelCommandment = newValue.round(); + /// }); + /// }, + /// onChangeEnd: (double newValue) { + /// print('Ended change on $newValue'); + /// }, + /// ) + /// ``` + /// {@end-tool} + /// + /// See also: + /// + /// * [onChangeStart] for a callback that is called when a value change + /// begins. + final ValueChanged? onChangeEnd; + + /// The minimum value the user can select. + /// + /// Defaults to 0.0. Must be less than or equal to [max]. + /// + /// If the [max] is equal to the [min], then the slider is disabled. + final double min; + + /// The maximum value the user can select. + /// + /// Defaults to 1.0. Must be greater than or equal to [min]. + /// + /// If the [max] is equal to the [min], then the slider is disabled. + final double max; + + /// The number of discrete divisions. + /// + /// Typically used with [label] to show the current discrete value. + /// + /// If null, the slider is continuous. + final int? divisions; + + /// A label to show above the slider when the slider is active and + /// [SliderThemeData.showValueIndicator] is satisfied. + /// + /// It is used to display the value of a discrete slider, and it is displayed + /// as part of the value indicator shape. + /// + /// The label is rendered using the active [ThemeData]'s [TextTheme.bodyLarge] + /// text style, with the theme data's [ColorScheme.onPrimary] color. The + /// label's text style can be overridden with + /// [SliderThemeData.valueIndicatorTextStyle]. + /// + /// If null, then the value indicator will not be displayed. + /// + /// Ignored if this slider is created with [Slider.adaptive]. + /// + /// See also: + /// + /// * [SliderComponentShape] for how to create a custom value indicator + /// shape. + final String? label; + + /// The color to use for the portion of the slider track that is active. + /// + /// The "active" side of the slider is the side between the thumb and the + /// minimum value. + /// + /// If null, [SliderThemeData.activeTrackColor] of the ambient + /// [SliderTheme] is used. If that is null, [ColorScheme.primary] of the + /// surrounding [ThemeData] is used. + /// + /// Using a [SliderTheme] gives much more fine-grained control over the + /// appearance of various components of the slider. + final Color? activeColor; + + /// The color for the inactive portion of the slider track. + /// + /// The "inactive" side of the slider is the side between the thumb and the + /// maximum value. + /// + /// If null, [SliderThemeData.inactiveTrackColor] of the ambient [SliderTheme] + /// is used. If [VerticalSlider.year2023] is false and [ThemeData.useMaterial3] is true, + /// then [ColorScheme.secondaryContainer] is used and if [ThemeData.useMaterial3] + /// is false, [ColorScheme.primary] with an opacity of 0.24 is used. Otherwise, + /// [ColorScheme.surfaceContainerHighest] is used. + /// + /// Using a [SliderTheme] gives much more fine-grained control over the + /// appearance of various components of the slider. + /// + /// Ignored if this slider is created with [Slider.adaptive]. + final Color? inactiveColor; + + /// The color to use for the portion of the slider track between the thumb and + /// the [VerticalSlider.secondaryTrackValue]. + /// + /// Defaults to the [SliderThemeData.secondaryActiveTrackColor] of the current + /// [SliderTheme]. + /// + /// If that is also null, defaults to [ColorScheme.primary] with an + /// opacity of 0.54. + /// + /// Using a [SliderTheme] gives much more fine-grained control over the + /// appearance of various components of the slider. + /// + /// Ignored if this slider is created with [Slider.adaptive]. + final Color? secondaryActiveColor; + + /// The color of the thumb. + /// + /// If this color is null, [VerticalSlider] will use [activeColor], If [activeColor] + /// is also null, [VerticalSlider] will use [SliderThemeData.thumbColor]. + /// + /// If that is also null, defaults to [ColorScheme.primary]. + /// + /// * [CupertinoSlider] will have a white thumb + /// (like the native default iOS slider). + final Color? thumbColor; + + /// The highlight color that's typically used to indicate that + /// the slider thumb is focused, hovered, or dragged. + /// + /// If this property is null, [VerticalSlider] will use [activeColor] with + /// an opacity of 0.12, If null, [SliderThemeData.overlayColor] + /// will be used. + /// + /// If that is also null, If [ThemeData.useMaterial3] is true, + /// Slider will use [ColorScheme.primary] with an opacity of 0.08 when + /// slider thumb is hovered and with an opacity of 0.1 when slider thumb + /// is focused or dragged, If [ThemeData.useMaterial3] is false, defaults + /// to [ColorScheme.primary] with an opacity of 0.12. + final WidgetStateProperty? overlayColor; + + /// {@template flutter.material.slider.mouseCursor} + /// The cursor for a mouse pointer when it enters or is hovering over the + /// widget. + /// + /// If [mouseCursor] is a [WidgetStateMouseCursor], + /// [WidgetStateProperty.resolve] is used for the following [WidgetState]s: + /// + /// * [WidgetState.dragged]. + /// * [WidgetState.hovered]. + /// * [WidgetState.focused]. + /// * [WidgetState.disabled]. + /// {@endtemplate} + /// + /// If null, then the value of [SliderThemeData.mouseCursor] is used. If that + /// is also null, then [WidgetStateMouseCursor.clickable] is used. + final MouseCursor? mouseCursor; + + /// The callback used to create a semantic value from a slider value. + /// + /// Defaults to formatting values as a percentage. + /// + /// This is used by accessibility frameworks like TalkBack on Android to + /// inform users what the currently selected value is with more context. + /// + /// {@tool snippet} + /// + /// In the example below, a slider for currency values is configured to + /// announce a value with a currency label. + /// + /// ```dart + /// Slider( + /// value: _dollars.toDouble(), + /// min: 20.0, + /// max: 330.0, + /// label: '$_dollars dollars', + /// onChanged: (double newValue) { + /// setState(() { + /// _dollars = newValue.round(); + /// }); + /// }, + /// semanticFormatterCallback: (double newValue) { + /// return '${newValue.round()} dollars'; + /// } + /// ) + /// ``` + /// {@end-tool} + /// + /// Ignored if this slider is created with [Slider.adaptive] + final SemanticFormatterCallback? semanticFormatterCallback; + + /// {@macro flutter.widgets.Focus.focusNode} + final FocusNode? focusNode; + + /// {@macro flutter.widgets.Focus.autofocus} + final bool autofocus; + + /// Allowed way for the user to interact with the [VerticalSlider]. + /// + /// For example, if this is set to [SliderInteraction.tapOnly], the user can + /// interact with the slider only by tapping anywhere on the track. Sliding + /// will have no effect. + /// + /// Defaults to [SliderInteraction.tapAndSlide]. + final SliderInteraction? allowedInteraction; + + /// Determines the padding around the [VerticalSlider]. + /// + /// If specified, this padding overrides the default vertical padding of + /// the [VerticalSlider], defaults to the height of the overlay shape, and the + /// horizontal padding, defaults to the width of the thumb shape or + /// overlay shape, whichever is larger. + final EdgeInsetsGeometry? padding; + + /// Determines the conditions under which the value indicator is shown. + /// + /// If [VerticalSlider.showValueIndicator] is null then the + /// ambient [SliderThemeData.showValueIndicator] is used. If that is also + /// null, defaults to [ShowValueIndicator.onlyForDiscrete]. + final ShowValueIndicator? showValueIndicator; + + /// When true, the [VerticalSlider] will use the 2023 Material Design 3 appearance. + /// Defaults to true. + /// + /// If this is set to false, the [VerticalSlider] will use the latest Material Design 3 + /// appearance, which was introduced in December 2023. + /// + /// If [ThemeData.useMaterial3] is false, then this property is ignored. + @Deprecated( + 'Set this flag to false to opt into the 2024 slider appearance. Defaults to true. ' + 'In the future, this flag will default to false. Use SliderThemeData to customize individual properties. ' + 'This feature was deprecated after v3.27.0-0.1.pre.', + ) + final bool? year2023; + + final _SliderType _sliderType; + + @override + State createState() => _VerticalSliderState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties + ..add(DoubleProperty('value', value)) + ..add(DoubleProperty('secondaryTrackValue', secondaryTrackValue)) + ..add( + ObjectFlagProperty>( + 'onChanged', + onChanged, + ifNull: 'disabled', + ), + ) + ..add( + ObjectFlagProperty>.has( + 'onChangeStart', + onChangeStart, + ), + ) + ..add( + ObjectFlagProperty>.has( + 'onChangeEnd', + onChangeEnd, + ), + ) + ..add(DoubleProperty('min', min)) + ..add(DoubleProperty('max', max)) + ..add(IntProperty('divisions', divisions)) + ..add(StringProperty('label', label)) + ..add(ColorProperty('activeColor', activeColor)) + ..add(ColorProperty('inactiveColor', inactiveColor)) + ..add(ColorProperty('secondaryActiveColor', secondaryActiveColor)) + ..add( + ObjectFlagProperty>.has( + 'semanticFormatterCallback', + semanticFormatterCallback, + ), + ) + ..add(ObjectFlagProperty.has('focusNode', focusNode)) + ..add( + FlagProperty('autofocus', value: autofocus, ifTrue: 'autofocus'), + ); + } +} + +class _VerticalSliderState extends State + with TickerProviderStateMixin { + static const Duration enableAnimationDuration = Duration(milliseconds: 75); + static const Duration valueIndicatorAnimationDuration = Duration( + milliseconds: 100, + ); + + // Animation controller that is run when the overlay (a.k.a radial reaction) + // is shown in response to user interaction. + late AnimationController overlayController; + // Animation controller that is run when the value indicator is being shown + // or hidden. + late AnimationController valueIndicatorController; + // Animation controller that is run when enabling/disabling the slider. + late AnimationController enableController; + // Animation controller that is run when transitioning between one value + // and the next on a discrete slider. + late AnimationController positionController; + Timer? interactionTimer; + + final GlobalKey _renderObjectKey = GlobalKey(); + + // Keyboard mapping for a focused slider. + static const Map + _traditionalNavShortcutMap = { + SingleActivator(LogicalKeyboardKey.arrowUp): _AdjustSliderIntent.up(), + SingleActivator(LogicalKeyboardKey.arrowDown): _AdjustSliderIntent.down(), + SingleActivator(LogicalKeyboardKey.arrowLeft): _AdjustSliderIntent.left(), + SingleActivator(LogicalKeyboardKey.arrowRight): _AdjustSliderIntent.right(), + }; + + // Keyboard mapping for a focused slider when using directional navigation. + // The vertical inputs are not handled to allow navigating out of the slider. + static const Map + _directionalNavShortcutMap = { + SingleActivator(LogicalKeyboardKey.arrowLeft): _AdjustSliderIntent.left(), + SingleActivator(LogicalKeyboardKey.arrowRight): _AdjustSliderIntent.right(), + }; + + // Action mapping for a focused slider. + late Map> _actionMap; + + bool get _enabled => widget.onChanged != null; + // Value Indicator Animation that appears on the Overlay. + PaintValueIndicator? paintValueIndicator; + + bool _dragging = false; + + // For discrete sliders, _handleChanged might receive the same value + // multiple times. To avoid calling widget.onChanged repeatedly, the + // value from _handleChanged is temporarily saved here. + double? _currentChangedValue; + + FocusNode? _focusNode; + FocusNode get focusNode => widget.focusNode ?? _focusNode!; + + // Always keep the ValueIndicator visible on the Overlay; otherwise, it cannot be updated during the build phase. + final OverlayPortalController _valueIndicatorOverlayPortalController = + OverlayPortalController( + debugLabel: 'Slider ValueIndicator', + )..show(); + + @override + void initState() { + super.initState(); + overlayController = AnimationController( + duration: kRadialReactionDuration, + vsync: this, + ); + valueIndicatorController = AnimationController( + duration: valueIndicatorAnimationDuration, + vsync: this, + ); + enableController = AnimationController( + duration: enableAnimationDuration, + vsync: this, + ); + positionController = AnimationController( + duration: Duration.zero, + vsync: this, + ); + enableController.value = widget.onChanged != null ? 1.0 : 0.0; + positionController.value = _convert(widget.value); + _actionMap = >{ + _AdjustSliderIntent: CallbackAction<_AdjustSliderIntent>( + onInvoke: _actionHandler, + ), + }; + if (widget.focusNode == null) { + // Only create a new node if the widget doesn't have one. + _focusNode ??= FocusNode(); + } + } + + @override + void dispose() { + interactionTimer?.cancel(); + overlayController.dispose(); + valueIndicatorController.dispose(); + enableController.dispose(); + positionController.dispose(); + _focusNode?.dispose(); + super.dispose(); + } + + void _handleChanged(double value) { + assert(widget.onChanged != null); + final double lerpValue = _lerp(value); + if (_currentChangedValue != lerpValue) { + _currentChangedValue = lerpValue; + if (_currentChangedValue != widget.value) { + widget.onChanged!(_currentChangedValue!); + } + } + } + + void _handleDragStart(double value) { + setState(() { + _dragging = true; + }); + widget.onChangeStart?.call(_lerp(value)); + } + + void _handleDragEnd(double value) { + setState(() { + _dragging = false; + }); + _currentChangedValue = null; + widget.onChangeEnd?.call(_lerp(value)); + } + + void _actionHandler(_AdjustSliderIntent intent) { + final TextDirection directionality = Directionality.of( + _renderObjectKey.currentContext!, + ); + final bool shouldIncrease = switch (intent.type) { + _SliderAdjustmentType.up => true, + _SliderAdjustmentType.down => false, + _SliderAdjustmentType.left => directionality == TextDirection.rtl, + _SliderAdjustmentType.right => directionality == TextDirection.ltr, + }; + + final slider = + _renderObjectKey.currentContext!.findRenderObject()! as _RenderSlider; + return shouldIncrease ? slider.increaseAction() : slider.decreaseAction(); + } + + bool _focused = false; + void _handleFocusHighlightChanged(bool focused) { + if (focused != _focused) { + setState(() { + _focused = focused; + }); + } + } + + bool _hovering = false; + void _handleHoverChanged(bool hovering) { + if (hovering != _hovering) { + setState(() { + _hovering = hovering; + }); + } + } + + // Returns a number between min and max, proportional to value, which must + // be between 0.0 and 1.0. + double _lerp(double value) { + assert(value >= 0.0); + assert(value <= 1.0); + return value * (widget.max - widget.min) + widget.min; + } + + double _discretize(double value) { + assert(widget.divisions != null); + assert(value >= 0.0 && value <= 1.0); + + final int divisions = widget.divisions!; + return (value * divisions).round() / divisions; + } + + double _convert(double value) { + double ret = _unlerp(value); + if (widget.divisions != null) { + ret = _discretize(ret); + } + return ret; + } + + // Returns a number between 0.0 and 1.0, given a value between min and max. + double _unlerp(double value) { + assert(value <= widget.max); + assert(value >= widget.min); + return widget.max > widget.min + ? (value - widget.min) / (widget.max - widget.min) + : 0.0; + } + + @override + Widget build(BuildContext context) { + assert(debugCheckHasMaterial(context)); + assert(debugCheckHasMediaQuery(context)); + + switch (widget._sliderType) { + case _SliderType.material: + return _buildMaterialSlider(context); + + case _SliderType.adaptive: + { + final ThemeData theme = Theme.of(context); + switch (theme.platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + return _buildMaterialSlider(context); + case TargetPlatform.iOS: + case TargetPlatform.macOS: + return _buildCupertinoSlider(context); + } + } + } + } + + Widget _buildMaterialSlider(BuildContext context) { + final ThemeData theme = Theme.of(context); + SliderThemeData sliderTheme = SliderTheme.of(context); + // ignore: deprecated_member_use + final bool year2023 = widget.year2023 ?? sliderTheme.year2023 ?? true; + final SliderThemeData defaults = switch (theme.useMaterial3) { + true => + year2023 + ? _SliderDefaultsM3Year2023(context) + : _SliderDefaultsM3(context), + false => _SliderDefaultsM2(context), + }; + + // If the widget has active or inactive colors specified, then we plug them + // in to the slider theme as best we can. If the developer wants more + // control than that, then they need to use a SliderTheme. The default + // colors come from the ThemeData.colorScheme. These colors, along with + // the default shapes and text styles are aligned to the Material + // Guidelines. + + const ShowValueIndicator defaultShowValueIndicator = + ShowValueIndicator.onlyForDiscrete; + const SliderInteraction defaultAllowedInteraction = + SliderInteraction.tapAndSlide; + + final states = { + if (!_enabled) WidgetState.disabled, + if (_hovering) WidgetState.hovered, + if (_focused) WidgetState.focused, + if (_dragging) WidgetState.dragged, + }; + + // The value indicator's color is not the same as the thumb and active track + // (which can be defined by activeColor) if the + // RectangularSliderValueIndicatorShape is used. In all other cases, the + // value indicator is assumed to be the same as the active color. + final SliderComponentShape valueIndicatorShape = + sliderTheme.valueIndicatorShape ?? defaults.valueIndicatorShape!; + final Color valueIndicatorColor; + if (valueIndicatorShape is RectangularSliderValueIndicatorShape) { + valueIndicatorColor = + sliderTheme.valueIndicatorColor ?? + Color.alphaBlend( + theme.colorScheme.onSurface.withValues(alpha: 0.60), + theme.colorScheme.surface.withValues(alpha: 0.90), + ); + } else { + valueIndicatorColor = + widget.activeColor ?? + sliderTheme.valueIndicatorColor ?? + defaults.valueIndicatorColor!; + } + + Color? effectiveOverlayColor() { + return widget.overlayColor?.resolve(states) ?? + widget.activeColor?.withValues(alpha: 0.12) ?? + WidgetStateProperty.resolveAs( + sliderTheme.overlayColor, + states, + ) ?? + WidgetStateProperty.resolveAs(defaults.overlayColor, states); + } + + TextStyle valueIndicatorTextStyle = + sliderTheme.valueIndicatorTextStyle ?? + defaults.valueIndicatorTextStyle!; + if (MediaQuery.boldTextOf(context)) { + valueIndicatorTextStyle = valueIndicatorTextStyle.merge( + const TextStyle(fontWeight: FontWeight.bold), + ); + } + + sliderTheme = sliderTheme.copyWith( + trackHeight: sliderTheme.trackHeight ?? defaults.trackHeight, + activeTrackColor: + widget.activeColor ?? + sliderTheme.activeTrackColor ?? + defaults.activeTrackColor, + inactiveTrackColor: + widget.inactiveColor ?? + sliderTheme.inactiveTrackColor ?? + defaults.inactiveTrackColor, + secondaryActiveTrackColor: + widget.secondaryActiveColor ?? + sliderTheme.secondaryActiveTrackColor ?? + defaults.secondaryActiveTrackColor, + disabledActiveTrackColor: + sliderTheme.disabledActiveTrackColor ?? + defaults.disabledActiveTrackColor, + disabledInactiveTrackColor: + sliderTheme.disabledInactiveTrackColor ?? + defaults.disabledInactiveTrackColor, + disabledSecondaryActiveTrackColor: + sliderTheme.disabledSecondaryActiveTrackColor ?? + defaults.disabledSecondaryActiveTrackColor, + activeTickMarkColor: + widget.inactiveColor ?? + sliderTheme.activeTickMarkColor ?? + defaults.activeTickMarkColor, + inactiveTickMarkColor: + widget.activeColor ?? + sliderTheme.inactiveTickMarkColor ?? + defaults.inactiveTickMarkColor, + disabledActiveTickMarkColor: + sliderTheme.disabledActiveTickMarkColor ?? + defaults.disabledActiveTickMarkColor, + disabledInactiveTickMarkColor: + sliderTheme.disabledInactiveTickMarkColor ?? + defaults.disabledInactiveTickMarkColor, + thumbColor: + widget.thumbColor ?? + widget.activeColor ?? + sliderTheme.thumbColor ?? + defaults.thumbColor, + disabledThumbColor: + sliderTheme.disabledThumbColor ?? defaults.disabledThumbColor, + overlayColor: effectiveOverlayColor(), + valueIndicatorColor: valueIndicatorColor, + trackShape: sliderTheme.trackShape ?? defaults.trackShape, + tickMarkShape: sliderTheme.tickMarkShape ?? defaults.tickMarkShape, + thumbShape: sliderTheme.thumbShape ?? defaults.thumbShape, + overlayShape: sliderTheme.overlayShape ?? defaults.overlayShape, + valueIndicatorShape: valueIndicatorShape, + showValueIndicator: + widget.showValueIndicator ?? + sliderTheme.showValueIndicator ?? + defaultShowValueIndicator, + valueIndicatorTextStyle: valueIndicatorTextStyle, + padding: widget.padding ?? sliderTheme.padding, + thumbSize: sliderTheme.thumbSize ?? defaults.thumbSize, + trackGap: sliderTheme.trackGap ?? defaults.trackGap, + ); + final MouseCursor effectiveMouseCursor = + WidgetStateProperty.resolveAs( + widget.mouseCursor, + states, + ) ?? + sliderTheme.mouseCursor?.resolve(states) ?? + WidgetStateMouseCursor.clickable.resolve(states); + final SliderInteraction effectiveAllowedInteraction = + widget.allowedInteraction ?? + sliderTheme.allowedInteraction ?? + defaultAllowedInteraction; + + // This size is used as the max bounds for the painting of the value + // indicators It must be kept in sync with the function with the same name + // in range_slider.dart. + Size screenSize() => MediaQuery.sizeOf(context); + + VoidCallback? handleDidGainAccessibilityFocus; + switch (theme.platform) { + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.iOS: + case TargetPlatform.linux: + case TargetPlatform.macOS: + break; + case TargetPlatform.windows: + handleDidGainAccessibilityFocus = () { + // Automatically activate the slider when it receives a11y focus. + if (!focusNode.hasFocus && focusNode.canRequestFocus) { + focusNode.requestFocus(); + } + }; + } + + final Map shortcutMap = + switch (MediaQuery.navigationModeOf( + context, + )) { + NavigationMode.directional => _directionalNavShortcutMap, + NavigationMode.traditional => _traditionalNavShortcutMap, + }; + + final double fontSize = + sliderTheme.valueIndicatorTextStyle?.fontSize ?? kDefaultFontSize; + final double fontSizeToScale = fontSize == 0.0 + ? kDefaultFontSize + : fontSize; + final TextScaler textScaler = theme.useMaterial3 + // TODO(tahatesser): This is an eye-balled value. + // This needs to be updated when accessibility + // guidelines are available on the material specs page + // https://m3.material.io/components/sliders/accessibility. + ? MediaQuery.textScalerOf(context).clamp(maxScaleFactor: 1.3) + : MediaQuery.textScalerOf(context); + final double effectiveTextScale = + textScaler.scale(fontSizeToScale) / fontSizeToScale; + + Widget result = CompositedTransformTarget( + link: _layerLink, + child: _SliderRenderObjectWidget( + key: _renderObjectKey, + value: _convert(widget.value), + secondaryTrackValue: (widget.secondaryTrackValue != null) + ? _convert(widget.secondaryTrackValue!) + : null, + divisions: widget.divisions, + label: widget.label, + sliderTheme: sliderTheme, + textScaleFactor: effectiveTextScale, + screenSize: screenSize(), + onChanged: (widget.onChanged != null) && (widget.max > widget.min) + ? _handleChanged + : null, + onChangeStart: _handleDragStart, + onChangeEnd: _handleDragEnd, + state: this, + semanticFormatterCallback: widget.semanticFormatterCallback, + onDidGainAccessibilityFocus: handleDidGainAccessibilityFocus, + hasFocus: _focused, + hovering: _hovering, + allowedInteraction: effectiveAllowedInteraction, + ), + ); + + final EdgeInsetsGeometry? padding = widget.padding ?? sliderTheme.padding; + if (padding != null) { + result = Padding(padding: padding, child: result); + } + result = OverlayPortal( + controller: _valueIndicatorOverlayPortalController, + overlayChildBuilder: (BuildContext context) { + return _buildValueIndicator(sliderTheme.showValueIndicator!); + }, + child: result, + ); + + return FocusableActionDetector( + actions: _actionMap, + shortcuts: shortcutMap, + focusNode: focusNode, + autofocus: widget.autofocus, + enabled: _enabled, + onShowFocusHighlight: _handleFocusHighlightChanged, + onShowHoverHighlight: _handleHoverChanged, + mouseCursor: effectiveMouseCursor, + includeFocusSemantics: false, + child: result, + ); + } + + Widget _buildCupertinoSlider(BuildContext context) { + // The render box of a slider has a fixed height but takes up the available + // width. Wrapping the [CupertinoSlider] in this manner will help maintain + // the same size. + return SizedBox( + width: double.infinity, + child: CupertinoSlider( + value: widget.value, + onChanged: widget.onChanged, + onChangeStart: widget.onChangeStart, + onChangeEnd: widget.onChangeEnd, + min: widget.min, + max: widget.max, + divisions: widget.divisions, + activeColor: widget.activeColor, + thumbColor: widget.thumbColor ?? CupertinoColors.white, + ), + ); + } + + final LayerLink _layerLink = LayerLink(); + Widget _buildValueIndicator(ShowValueIndicator showValueIndicator) { + final Widget valueIndicator = CompositedTransformFollower( + link: _layerLink, + child: _ValueIndicatorRenderObjectWidget(state: this), + ); + return switch (showValueIndicator) { + ShowValueIndicator.never => const SizedBox.shrink(), + ShowValueIndicator.onlyForDiscrete => + widget.divisions != null ? valueIndicator : const SizedBox.shrink(), + ShowValueIndicator.onlyForContinuous => + widget.divisions == null ? valueIndicator : const SizedBox.shrink(), + ShowValueIndicator.alwaysVisible || + // ignore: deprecated_member_use + ShowValueIndicator.always || + ShowValueIndicator.onDrag => valueIndicator, + }; + } +} + +class _SliderRenderObjectWidget extends LeafRenderObjectWidget { + const _SliderRenderObjectWidget({ + super.key, + required this.value, + required this.secondaryTrackValue, + required this.divisions, + required this.label, + required this.sliderTheme, + required this.textScaleFactor, + required this.screenSize, + required this.onChanged, + required this.onChangeStart, + required this.onChangeEnd, + required this.state, + required this.semanticFormatterCallback, + required this.onDidGainAccessibilityFocus, + required this.hasFocus, + required this.hovering, + required this.allowedInteraction, + }); + + final double value; + final double? secondaryTrackValue; + final int? divisions; + final String? label; + final SliderThemeData sliderTheme; + final double textScaleFactor; + final Size screenSize; + final ValueChanged? onChanged; + final ValueChanged? onChangeStart; + final ValueChanged? onChangeEnd; + final SemanticFormatterCallback? semanticFormatterCallback; + final VoidCallback? onDidGainAccessibilityFocus; + final _VerticalSliderState state; + final bool hasFocus; + final bool hovering; + final SliderInteraction allowedInteraction; + + @override + _RenderSlider createRenderObject(BuildContext context) { + return _RenderSlider( + value: value, + secondaryTrackValue: secondaryTrackValue, + divisions: divisions, + label: label, + sliderTheme: sliderTheme, + textScaleFactor: textScaleFactor, + screenSize: screenSize, + onChanged: onChanged, + onChangeStart: onChangeStart, + onChangeEnd: onChangeEnd, + state: state, + textDirection: Directionality.of(context), + semanticFormatterCallback: semanticFormatterCallback, + onDidGainAccessibilityFocus: onDidGainAccessibilityFocus, + platform: Theme.of(context).platform, + hasFocus: hasFocus, + hovering: hovering, + gestureSettings: MediaQuery.gestureSettingsOf(context), + allowedInteraction: allowedInteraction, + ); + } + + @override + void updateRenderObject(BuildContext context, _RenderSlider renderObject) { + renderObject + // We should update the `divisions` ahead of `value`, because the `value` + // setter dependent on the `divisions`. + ..divisions = divisions + ..value = value + ..secondaryTrackValue = secondaryTrackValue + ..label = label + ..sliderTheme = sliderTheme + ..textScaleFactor = textScaleFactor + ..screenSize = screenSize + ..onChanged = onChanged + ..onChangeStart = onChangeStart + ..onChangeEnd = onChangeEnd + ..textDirection = Directionality.of(context) + ..semanticFormatterCallback = semanticFormatterCallback + ..onDidGainAccessibilityFocus = onDidGainAccessibilityFocus + ..platform = Theme.of(context).platform + ..hasFocus = hasFocus + ..hovering = hovering + ..gestureSettings = MediaQuery.gestureSettingsOf(context) + ..allowedInteraction = allowedInteraction; + // Ticker provider cannot change since there's a 1:1 relationship between + // the _SliderRenderObjectWidget object and the _SliderState object. + } +} + +class _RenderSlider extends RenderBox with RelayoutWhenSystemFontsChangeMixin { + _RenderSlider({ + required double value, + required double? secondaryTrackValue, + required int? divisions, + required String? label, + required SliderThemeData sliderTheme, + required double textScaleFactor, + required Size screenSize, + required TargetPlatform platform, + required ValueChanged? onChanged, + required SemanticFormatterCallback? semanticFormatterCallback, + required this.onDidGainAccessibilityFocus, + required this.onChangeStart, + required this.onChangeEnd, + required _VerticalSliderState state, + required TextDirection textDirection, + required bool hasFocus, + required bool hovering, + required DeviceGestureSettings gestureSettings, + required SliderInteraction allowedInteraction, + }) : assert(value >= 0.0 && value <= 1.0), + assert( + secondaryTrackValue == null || + (secondaryTrackValue >= 0.0 && secondaryTrackValue <= 1.0), + ), + _platform = platform, + _semanticFormatterCallback = semanticFormatterCallback, + _label = label, + _value = value, + _secondaryTrackValue = secondaryTrackValue, + _divisions = divisions, + _sliderTheme = sliderTheme, + _textScaleFactor = textScaleFactor, + _screenSize = screenSize, + _onChanged = onChanged, + _state = state, + _textDirection = textDirection, + _hasFocus = hasFocus, + _hovering = hovering, + _allowedInteraction = allowedInteraction { + _updateLabelPainter(); + final team = GestureArenaTeam(); + _drag = VerticalDragGestureRecognizer() + ..team = team + ..onStart = _handleDragStart + ..onUpdate = _handleDragUpdate + ..onEnd = _handleDragEnd + ..onCancel = _endInteraction + ..gestureSettings = gestureSettings; + _tap = TapGestureRecognizer() + ..team = team + ..onTapDown = _handleTapDown + ..onTapUp = _handleTapUp + ..gestureSettings = gestureSettings; + _overlayAnimation = CurvedAnimation( + parent: _state.overlayController, + curve: Curves.fastOutSlowIn, + ); + _valueIndicatorAnimation = CurvedAnimation( + parent: _state.valueIndicatorController, + curve: Curves.fastOutSlowIn, + ); + _enableAnimation = CurvedAnimation( + parent: _state.enableController, + curve: Curves.easeInOut, + ); + } + static const Duration _positionAnimationDuration = Duration(milliseconds: 75); + static const Duration _minimumInteractionTime = Duration(milliseconds: 500); + + // This value is the touch target, 48, multiplied by 3. + static const double _minPreferredTrackWidth = 144.0; + + // Compute the largest width and height needed to paint the slider shapes, + // other than the track shape. It is assumed that these shapes are vertically + // centered on the track. + double get _maxSliderPartWidth => + _sliderPartSizes.map((Size size) => size.width).reduce(math.max); + double get _maxSliderPartHeight => + _sliderPartSizes.map((Size size) => size.height).reduce(math.max); + double get _thumbSizeHeight => _sliderTheme.thumbShape! + .getPreferredSize(isInteractive, isDiscrete) + .height; + double get _overlayHeight => _sliderTheme.overlayShape! + .getPreferredSize(isInteractive, isDiscrete) + .height; + List get _sliderPartSizes => [ + Size( + _sliderTheme.overlayShape! + .getPreferredSize(isInteractive, isDiscrete) + .width, + _sliderTheme.padding != null ? _thumbSizeHeight : _overlayHeight, + ), + _sliderTheme.thumbShape!.getPreferredSize(isInteractive, isDiscrete), + _sliderTheme.tickMarkShape!.getPreferredSize( + isEnabled: isInteractive, + sliderTheme: sliderTheme, + ), + ]; + double get _minPreferredTrackHeight => _sliderTheme.trackHeight!; + + final _VerticalSliderState _state; + late CurvedAnimation _overlayAnimation; + late CurvedAnimation _valueIndicatorAnimation; + late CurvedAnimation _enableAnimation; + final TextPainter _labelPainter = TextPainter(); + late VerticalDragGestureRecognizer _drag; + late TapGestureRecognizer _tap; + bool _active = false; + VoidCallback? onDidGainAccessibilityFocus; + double _currentDragValue = 0.0; + Rect? overlayRect; + + // This rect is used in gesture calculations, where the gesture coordinates + // are relative to the sliders origin. Therefore, the offset is passed as + // (0,0). + Rect get _trackRect => _sliderTheme.trackShape!.getPreferredRect( + parentBox: this, + sliderTheme: _sliderTheme, + isDiscrete: false, + ); + + bool get isInteractive => onChanged != null; + + bool get isDiscrete => false; // divisions != null && divisions! > 0; + + double get value => _value; + double _value; + set value(double newValue) { + assert(newValue >= 0.0 && newValue <= 1.0); + final double convertedValue = isDiscrete ? _discretize(newValue) : newValue; + if (convertedValue == _value) { + return; + } + _value = convertedValue; + if (isDiscrete) { + // Reset the duration to match the distance that we're traveling, so that + // whatever the distance, we still do it in _positionAnimationDuration, + // and if we get re-targeted in the middle, it still takes that long to + // get to the new location. + final double distance = (_value - _state.positionController.value).abs(); + _state.positionController.duration = distance != 0.0 + ? _positionAnimationDuration * (1.0 / distance) + : Duration.zero; + _state.positionController.animateTo( + convertedValue, + curve: Curves.easeInOut, + ); + } else { + _state.positionController.value = convertedValue; + } + markNeedsSemanticsUpdate(); + } + + double? get secondaryTrackValue => _secondaryTrackValue; + double? _secondaryTrackValue; + set secondaryTrackValue(double? newValue) { + assert(newValue == null || (newValue >= 0.0 && newValue <= 1.0)); + if (newValue == _secondaryTrackValue) { + return; + } + _secondaryTrackValue = newValue; + markNeedsPaint(); + markNeedsSemanticsUpdate(); + } + + DeviceGestureSettings? get gestureSettings => _drag.gestureSettings; + set gestureSettings(DeviceGestureSettings? gestureSettings) { + _drag.gestureSettings = gestureSettings; + _tap.gestureSettings = gestureSettings; + } + + TargetPlatform _platform; + TargetPlatform get platform => _platform; + set platform(TargetPlatform value) { + if (_platform == value) { + return; + } + _platform = value; + markNeedsSemanticsUpdate(); + } + + SemanticFormatterCallback? _semanticFormatterCallback; + SemanticFormatterCallback? get semanticFormatterCallback => + _semanticFormatterCallback; + set semanticFormatterCallback(SemanticFormatterCallback? value) { + if (_semanticFormatterCallback == value) { + return; + } + _semanticFormatterCallback = value; + markNeedsSemanticsUpdate(); + } + + int? get divisions => _divisions; + int? _divisions; + set divisions(int? value) { + if (value == _divisions) { + return; + } + _divisions = value; + markNeedsPaint(); + } + + String? get label => _label; + String? _label; + set label(String? value) { + if (value == _label) { + return; + } + _label = value; + _updateLabelPainter(); + } + + SliderThemeData get sliderTheme => _sliderTheme; + SliderThemeData _sliderTheme; + set sliderTheme(SliderThemeData value) { + if (value == _sliderTheme) { + return; + } + _sliderTheme = value; + _updateLabelPainter(); + } + + double get textScaleFactor => _textScaleFactor; + double _textScaleFactor; + set textScaleFactor(double value) { + if (value == _textScaleFactor) { + return; + } + _textScaleFactor = value; + _updateLabelPainter(); + } + + Size get screenSize => _screenSize; + Size _screenSize; + set screenSize(Size value) { + if (value == _screenSize) { + return; + } + _screenSize = value; + markNeedsPaint(); + } + + ValueChanged? get onChanged => _onChanged; + ValueChanged? _onChanged; + set onChanged(ValueChanged? value) { + if (value == _onChanged) { + return; + } + final bool wasInteractive = isInteractive; + _onChanged = value; + if (wasInteractive != isInteractive) { + if (isInteractive) { + _state.enableController.forward(); + } else { + _state.enableController.reverse(); + } + markNeedsPaint(); + markNeedsSemanticsUpdate(); + } + } + + ValueChanged? onChangeStart; + ValueChanged? onChangeEnd; + + TextDirection get textDirection => _textDirection; + TextDirection _textDirection; + set textDirection(TextDirection value) { + if (value == _textDirection) { + return; + } + _textDirection = value; + _updateLabelPainter(); + } + + /// True if this slider has the input focus. + bool get hasFocus => _hasFocus; + bool _hasFocus; + set hasFocus(bool value) { + if (value == _hasFocus) { + return; + } + _hasFocus = value; + _updateForFocus(_hasFocus); + markNeedsSemanticsUpdate(); + } + + /// True if this slider is being hovered over by a pointer. + bool get hovering => _hovering; + bool _hovering; + set hovering(bool value) { + if (value == _hovering) { + return; + } + _hovering = value; + _updateForHover(_hovering); + } + + /// True if the slider is interactive and the slider thumb is being + /// hovered over by a pointer. + bool _hoveringThumb = false; + bool get hoveringThumb => _hoveringThumb; + set hoveringThumb(bool value) { + if (value == _hoveringThumb) { + return; + } + _hoveringThumb = value; + _updateForHover(_hovering); + } + + SliderInteraction _allowedInteraction; + SliderInteraction get allowedInteraction => _allowedInteraction; + set allowedInteraction(SliderInteraction value) { + if (value == _allowedInteraction) { + return; + } + _allowedInteraction = value; + markNeedsSemanticsUpdate(); + } + + void _updateForFocus(bool focused) { + if (focused) { + _state.overlayController.forward(); + if (shouldShowValueIndicatorWhenDragged) { + _state.valueIndicatorController.forward(); + } + } else { + _state.overlayController.reverse(); + if (shouldShowValueIndicatorWhenDragged) { + _state.valueIndicatorController.reverse(); + } + } + } + + void _updateForHover(bool hovered) { + // Only show overlay when pointer is hovering the thumb. + if (hovered && hoveringThumb) { + _state.overlayController.forward(); + } else { + // Only remove overlay when Slider is inactive and unfocused. + if (!_active && !hasFocus) { + _state.overlayController.reverse(); + } + } + } + + bool get shouldAlwaysShowValueIndicator => + _sliderTheme.showValueIndicator == ShowValueIndicator.alwaysVisible; + bool get shouldShowValueIndicatorWhenDragged => + switch (_sliderTheme.showValueIndicator!) { + ShowValueIndicator.onlyForDiscrete => isDiscrete, + ShowValueIndicator.onlyForContinuous => !isDiscrete, + // ignore: deprecated_member_use + ShowValueIndicator.always || ShowValueIndicator.onDrag => true, + ShowValueIndicator.never || ShowValueIndicator.alwaysVisible => false, + }; + + double get _adjustmentUnit { + switch (_platform) { + case TargetPlatform.iOS: + case TargetPlatform.macOS: + // Matches iOS implementation of material slider. + return 0.1; + case TargetPlatform.android: + case TargetPlatform.fuchsia: + case TargetPlatform.linux: + case TargetPlatform.windows: + // Matches Android implementation of material slider. + return 0.05; + } + } + + void _updateLabelPainter() { + if (label != null) { + _labelPainter + ..text = TextSpan( + style: _sliderTheme.valueIndicatorTextStyle, + text: label, + ) + ..textDirection = textDirection + // ignore: deprecated_member_use + ..textScaleFactor = textScaleFactor + ..layout(); + } else { + _labelPainter.text = null; + } + // Changing the textDirection can result in the layout changing, because the + // bidi algorithm might line up the glyphs differently which can result in + // different ligatures, different shapes, etc. So we always markNeedsLayout. + markNeedsLayout(); + } + + @override + void systemFontsDidChange() { + super.systemFontsDidChange(); + _labelPainter.markNeedsLayout(); + _updateLabelPainter(); + } + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + _overlayAnimation.addListener(markNeedsPaint); + _valueIndicatorAnimation.addListener(markNeedsPaint); + _enableAnimation.addListener(markNeedsPaint); + _state.positionController.addListener(markNeedsPaint); + } + + @override + void detach() { + _overlayAnimation.removeListener(markNeedsPaint); + _valueIndicatorAnimation.removeListener(markNeedsPaint); + _enableAnimation.removeListener(markNeedsPaint); + _state.positionController.removeListener(markNeedsPaint); + super.detach(); + } + + @override + void dispose() { + _drag.dispose(); + _tap.dispose(); + _labelPainter.dispose(); + _enableAnimation.dispose(); + _valueIndicatorAnimation.dispose(); + _overlayAnimation.dispose(); + super.dispose(); + } + + double _getValueFromVisualPosition(double visualPosition) { + return switch (textDirection) { + TextDirection.rtl => 1.0 - visualPosition, + TextDirection.ltr => visualPosition, + }; + } + + double _getValueFromGlobalPosition(Offset globalPosition) { + final double visualPosition = + (_trackRect.bottom - globalToLocal(globalPosition).dy) / + _trackRect.height; + return _getValueFromVisualPosition(visualPosition); + } + + double _discretize(double value) { + double result = clampDouble(value, 0.0, 1.0); + if (isDiscrete) { + result = (result * divisions!).round() / divisions!; + } + return result; + } + + void _startInteraction(Offset globalPosition) { + if (!_state.mounted) { + return; + } + if (!_active && isInteractive) { + switch (allowedInteraction) { + case SliderInteraction.tapAndSlide: + case SliderInteraction.tapOnly: + _active = true; + _currentDragValue = _getValueFromGlobalPosition(globalPosition); + case SliderInteraction.slideThumb: + if (_isPointerOnOverlay(globalPosition)) { + _active = true; + _currentDragValue = value; + } + case SliderInteraction.slideOnly: + _active = true; + _currentDragValue = value; + } + + if (_active) { + // We supply the *current* value as the start location, so that if we have + // a tap, it consists of a call to onChangeStart with the previous value and + // a call to onChangeEnd with the new value. + onChangeStart?.call(_discretize(value)); + onChanged!(_discretize(_currentDragValue)); + _state.overlayController.forward(); + if (shouldShowValueIndicatorWhenDragged) { + _state.valueIndicatorController.forward(); + _state.interactionTimer?.cancel(); + _state.interactionTimer = Timer( + _minimumInteractionTime * timeDilation, + () { + _state.interactionTimer = null; + if (!_active && _state.valueIndicatorController.isCompleted) { + _state.valueIndicatorController.reverse(); + } + }, + ); + } + } + } + } + + void _endInteraction() { + if (!_state.mounted) { + return; + } + + if (_active && _state.mounted) { + onChangeEnd?.call(_discretize(_currentDragValue)); + _active = false; + _currentDragValue = 0.0; + _state.overlayController.reverse(); + if (shouldShowValueIndicatorWhenDragged && + _state.interactionTimer == null) { + _state.valueIndicatorController.reverse(); + } + } + } + + void _handleDragStart(DragStartDetails details) { + _startInteraction(details.globalPosition); + } + + void _handleDragUpdate(DragUpdateDetails details) { + if (!_state.mounted) { + return; + } + + switch (allowedInteraction) { + case SliderInteraction.tapAndSlide: + case SliderInteraction.slideOnly: + case SliderInteraction.slideThumb: + if (_active && isInteractive) { + final double valueDelta = details.primaryDelta! / _trackRect.height; + _currentDragValue -= valueDelta; + onChanged!(_discretize(_currentDragValue)); + } + case SliderInteraction.tapOnly: + // cannot slide (drag) as its tapOnly. + break; + } + } + + void _handleDragEnd(DragEndDetails details) { + _endInteraction(); + } + + void _handleTapDown(TapDownDetails details) { + _startInteraction(details.globalPosition); + } + + void _handleTapUp(TapUpDetails details) { + _endInteraction(); + } + + bool _isPointerOnOverlay(Offset globalPosition) { + return overlayRect!.contains(globalToLocal(globalPosition)); + } + + @override + bool hitTestSelf(Offset position) => true; + + @override + void handleEvent(PointerEvent event, BoxHitTestEntry entry) { + if (!_state.mounted) { + return; + } + assert(debugHandleEvent(event, entry)); + if (event is PointerDownEvent && isInteractive) { + // We need to add the drag first so that it has priority. + _drag.addPointer(event); + _tap.addPointer(event); + } + if (isInteractive && overlayRect != null) { + hoveringThumb = overlayRect!.contains(event.localPosition); + } + } + + @override + double computeMinIntrinsicWidth(double height) => + _minPreferredTrackWidth + _maxSliderPartWidth; + + @override + double computeMaxIntrinsicWidth(double height) => + _minPreferredTrackWidth + _maxSliderPartWidth; + + @override + double computeMinIntrinsicHeight(double width) => + math.max(_minPreferredTrackHeight, _maxSliderPartHeight); + + @override + double computeMaxIntrinsicHeight(double width) => + math.max(_minPreferredTrackHeight, _maxSliderPartHeight); + + @override + bool get sizedByParent => true; + + @override + Size computeDryLayout(BoxConstraints constraints) { + return Size( + constraints.hasBoundedWidth + ? constraints.maxWidth + : _minPreferredTrackWidth + _maxSliderPartWidth, + constraints.hasBoundedHeight + ? constraints.maxHeight + : math.max(_minPreferredTrackHeight, _maxSliderPartHeight), + ); + } + + @override + void paint(PaintingContext context, Offset offset) { + final double controllerValue = _state.positionController.value; + + // The visual position is the position of the thumb from 0 to 1 from left + // to right. In left to right, this is the same as the value, but it is + // reversed for right to left text. + final ( + double visualPosition, + double? secondaryVisualPosition, + ) = switch (textDirection) { + TextDirection.rtl when _secondaryTrackValue == null => ( + 1.0 - controllerValue, + null, + ), + TextDirection.rtl => (1.0 - controllerValue, 1.0 - _secondaryTrackValue!), + TextDirection.ltr => (controllerValue, _secondaryTrackValue), + }; + + final Rect trackRect = _sliderTheme.trackShape!.getPreferredRect( + parentBox: this, + offset: offset, + sliderTheme: _sliderTheme, + isDiscrete: isDiscrete, + ); + + final Offset thumbCenter = _calcThumbCenter( + trackRect: trackRect, + visualPosition: visualPosition, + ); + + if (isInteractive) { + final Size overlaySize = sliderTheme.overlayShape!.getPreferredSize( + isInteractive, + false, + ); + overlayRect = Rect.fromCircle( + center: thumbCenter, + radius: overlaySize.width / 2.0, + ); + } + final Offset? secondaryOffset = (secondaryVisualPosition != null) + ? Offset( + trackRect.left + secondaryVisualPosition * trackRect.width, + trackRect.center.dy, + ) + : null; + + // If [Slider.year2023] is false, the thumb uses handle thumb shape and gapped track shape. + // The handle width and track gap are adjusted when the thumb is pressed. + double? thumbWidth = _sliderTheme.thumbSize + ?.resolve({}) + ?.width; + final double? thumbHeight = _sliderTheme.thumbSize + ?.resolve({}) + ?.height; + double? trackGap = _sliderTheme.trackGap; + final double? pressedThumbWidth = _sliderTheme.thumbSize?.resolve( + { + WidgetState.pressed, + }, + )?.width; + final double delta; + if (_active && + thumbWidth != null && + pressedThumbWidth != null && + trackGap != null) { + delta = thumbWidth - pressedThumbWidth; + if (thumbWidth > 0.0) { + thumbWidth = pressedThumbWidth; + } + if (trackGap > 0.0) { + trackGap = trackGap - delta / 2; + } + } + + _sliderTheme.trackShape!.paint( + context, + offset, + parentBox: this, + sliderTheme: _sliderTheme.copyWith(trackGap: trackGap), + enableAnimation: _enableAnimation, + textDirection: _textDirection, + thumbCenter: thumbCenter, + secondaryOffset: secondaryOffset, + isDiscrete: isDiscrete, + isEnabled: isInteractive, + ); + + if (!_overlayAnimation.isDismissed) { + _sliderTheme.overlayShape!.paint( + context, + thumbCenter, + activationAnimation: _overlayAnimation, + enableAnimation: _enableAnimation, + isDiscrete: isDiscrete, + labelPainter: _labelPainter, + parentBox: this, + sliderTheme: _sliderTheme, + textDirection: _textDirection, + value: _value, + textScaleFactor: _textScaleFactor, + sizeWithOverflow: screenSize.isEmpty ? size : screenSize, + ); + } + + if (isDiscrete) { + final double tickMarkWidth = _sliderTheme.tickMarkShape! + .getPreferredSize(isEnabled: isInteractive, sliderTheme: _sliderTheme) + .width; + final double discreteTrackPadding = trackRect.height; + final double adjustedTrackWidth = trackRect.width - discreteTrackPadding; + // If the tick marks would be too dense, don't bother painting them. + if (adjustedTrackWidth / divisions! >= 3.0 * tickMarkWidth) { + final double dy = trackRect.center.dy; + for (var i = 0; i <= divisions!; i++) { + final double value = i / divisions!; + // The ticks are mapped to be within the track, so the tick mark width + // must be subtracted from the track width. + final double dx = + trackRect.left + + value * adjustedTrackWidth + + discreteTrackPadding / 2; + final tickMarkOffset = Offset(dx, dy); + _sliderTheme.tickMarkShape!.paint( + context, + tickMarkOffset, + parentBox: this, + sliderTheme: _sliderTheme, + enableAnimation: _enableAnimation, + textDirection: _textDirection, + thumbCenter: thumbCenter, + isEnabled: isInteractive, + ); + } + } + } + + if (isInteractive && + label != null && + ((shouldShowValueIndicatorWhenDragged && + !_valueIndicatorAnimation.isDismissed) || + shouldAlwaysShowValueIndicator)) { + _state.paintValueIndicator = (PaintingContext context, Offset offset) { + if (attached && _labelPainter.text != null) { + _sliderTheme.valueIndicatorShape?.paint( + context, + offset + thumbCenter, + activationAnimation: shouldAlwaysShowValueIndicator + ? const AlwaysStoppedAnimation(1) + : _valueIndicatorAnimation, + enableAnimation: shouldAlwaysShowValueIndicator + ? const AlwaysStoppedAnimation(1) + : _enableAnimation, + isDiscrete: isDiscrete, + labelPainter: _labelPainter, + parentBox: this, + sliderTheme: _sliderTheme, + textDirection: _textDirection, + value: _value, + textScaleFactor: textScaleFactor, + sizeWithOverflow: screenSize.isEmpty ? size : screenSize, + ); + } + }; + } else { + _state.paintValueIndicator = null; + } + + _sliderTheme.thumbShape!.paint( + context, + thumbCenter, + activationAnimation: _overlayAnimation, + enableAnimation: _enableAnimation, + isDiscrete: isDiscrete, + labelPainter: _labelPainter, + parentBox: this, + sliderTheme: thumbWidth != null && thumbHeight != null + ? _sliderTheme.copyWith( + thumbSize: WidgetStatePropertyAll( + Size(thumbWidth, thumbHeight), + ), + ) + : _sliderTheme, + textDirection: _textDirection, + value: _value, + textScaleFactor: textScaleFactor, + sizeWithOverflow: screenSize.isEmpty ? size : screenSize, + ); + } + + /// Calculates the local coordinate center of the [Slider] thumb given its + /// physical placement on the track from 0.0 (left) to 1.0 (right). + /// + /// The [visualPosition] is provided by the caller so semantics can use the + /// raw logical value while paint can use the smoothly animated value. + Offset _calcThumbCenter({ + required Rect trackRect, + required double visualPosition, + }) { + final double padding = _sliderTheme.trackShape!.isRounded + ? trackRect.width + : 0.0; + final double thumbPosition = isDiscrete + ? trackRect.left + + visualPosition * (trackRect.width - padding) + + padding / 2 + : trackRect.bottom - visualPosition * trackRect.height; + // Apply padding to trackRect.left and trackRect.right if the track height is + // greater than the thumb radius to ensure the thumb is drawn within the track. + final Size thumbPreferredSize = _sliderTheme.thumbShape!.getPreferredSize( + isInteractive, + isDiscrete, + ); + final double thumbPadding = padding > thumbPreferredSize.width / 2 + ? padding / 2 + : 0; + return Offset( + trackRect.center.dx, + clampDouble( + thumbPosition, + trackRect.top + thumbPadding, + trackRect.bottom - thumbPadding, + ), + ); + } + + Offset get _semanticThumbCenter { + final double visualPosition = switch (textDirection) { + TextDirection.rtl => 1.0 - _value, + TextDirection.ltr => _value, + }; + return _calcThumbCenter( + trackRect: _trackRect, + visualPosition: visualPosition, + ); + } + + @override + void assembleSemanticsNode( + SemanticsNode node, + SemanticsConfiguration config, + Iterable children, + ) { + node + ..rect = Rect.fromCenter( + center: _semanticThumbCenter, + width: kMinInteractiveDimension, + height: kMinInteractiveDimension, + ) + ..updateWith(config: config); + } + + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + + // The Slider widget has its own Focus widget. + // We mark the Focus widget with "includeFocusSemantics: false" + // and we want that semantics node to collect the semantics information here + // so that it's all in the same node. + config + ..isSemanticBoundary = true + ..isEnabled = isInteractive; + if (label != null) { + config.label = label!; + } + config + ..isSlider = true + ..isFocusable = isInteractive + ..isFocused = hasFocus; + + if (onDidGainAccessibilityFocus != null) { + config.onDidGainAccessibilityFocus = onDidGainAccessibilityFocus; + } + config.textDirection = textDirection; + if (isInteractive) { + config + ..onIncrease = increaseAction + ..onDecrease = decreaseAction + ..onFocus = onFocusAction; + } + + if (semanticFormatterCallback != null) { + config + ..value = semanticFormatterCallback!(_state._lerp(value)) + ..increasedValue = semanticFormatterCallback!( + _state._lerp(clampDouble(value + _semanticActionUnit, 0.0, 1.0)), + ) + ..decreasedValue = semanticFormatterCallback!( + _state._lerp(clampDouble(value - _semanticActionUnit, 0.0, 1.0)), + ); + } else { + config + ..value = '${(value * 100).round()}%' + ..increasedValue = + '${(clampDouble(value + _semanticActionUnit, 0.0, 1.0) * 100).round()}%' + ..decreasedValue = + '${(clampDouble(value - _semanticActionUnit, 0.0, 1.0) * 100).round()}%'; + } + } + + double get _semanticActionUnit => + divisions != null ? 1.0 / divisions! : _adjustmentUnit; + + void onFocusAction() { + if (isInteractive) { + if (!_state.mounted) { + return; + } + if (!hasFocus) { + _state.focusNode.requestFocus(); + } + } + } + + void increaseAction() { + if (isInteractive) { + onChangeStart!(currentValue); + final double increase = increaseValue(); + onChanged!(increase); + onChangeEnd!(increase); + if (!_state.mounted) { + return; + } + } + } + + void decreaseAction() { + if (isInteractive) { + onChangeStart!(currentValue); + final double decrease = decreaseValue(); + onChanged!(decrease); + onChangeEnd!(decrease); + if (!_state.mounted) { + return; + } + } + } + + double get currentValue { + return clampDouble(value, 0.0, 1.0); + } + + double increaseValue() { + return clampDouble(value + _semanticActionUnit, 0.0, 1.0); + } + + double decreaseValue() { + return clampDouble(value - _semanticActionUnit, 0.0, 1.0); + } +} + +class _AdjustSliderIntent extends Intent { + const _AdjustSliderIntent({required this.type}); + + const _AdjustSliderIntent.right() : type = _SliderAdjustmentType.right; + + const _AdjustSliderIntent.left() : type = _SliderAdjustmentType.left; + + const _AdjustSliderIntent.up() : type = _SliderAdjustmentType.up; + + const _AdjustSliderIntent.down() : type = _SliderAdjustmentType.down; + + final _SliderAdjustmentType type; +} + +enum _SliderAdjustmentType { right, left, up, down } + +class _ValueIndicatorRenderObjectWidget extends LeafRenderObjectWidget { + const _ValueIndicatorRenderObjectWidget({required this.state}); + + final _VerticalSliderState state; + + @override + _RenderValueIndicator createRenderObject(BuildContext context) { + return _RenderValueIndicator(state: state); + } + + @override + void updateRenderObject( + BuildContext context, + _RenderValueIndicator renderObject, + ) { + renderObject._state = state; + } +} + +class _RenderValueIndicator extends RenderBox + with RelayoutWhenSystemFontsChangeMixin { + _RenderValueIndicator({required _VerticalSliderState state}) + : _state = state { + _valueIndicatorAnimation = CurvedAnimation( + parent: _state.valueIndicatorController, + curve: Curves.fastOutSlowIn, + ); + } + late CurvedAnimation _valueIndicatorAnimation; + _VerticalSliderState _state; + + @override + bool get sizedByParent => true; + + @override + void attach(PipelineOwner owner) { + super.attach(owner); + _valueIndicatorAnimation.addListener(markNeedsPaint); + _state.positionController.addListener(markNeedsPaint); + } + + @override + void detach() { + _valueIndicatorAnimation.removeListener(markNeedsPaint); + _state.positionController.removeListener(markNeedsPaint); + super.detach(); + } + + @override + void paint(PaintingContext context, Offset offset) { + _state.paintValueIndicator?.call(context, offset); + } + + @override + Size computeDryLayout(BoxConstraints constraints) { + return constraints.smallest; + } + + @override + void dispose() { + _valueIndicatorAnimation.dispose(); + super.dispose(); + } +} + +class _SliderDefaultsM2 extends SliderThemeData { + _SliderDefaultsM2(this.context) : super(trackHeight: 4.0); + + final BuildContext context; + late final ColorScheme _colors = Theme.of(context).colorScheme; + late final SliderThemeData sliderTheme = SliderTheme.of(context); + + @override + Color? get activeTrackColor => _colors.primary; + + @override + Color? get inactiveTrackColor => _colors.primary.withValues(alpha: 0.24); + + @override + Color? get secondaryActiveTrackColor => + _colors.primary.withValues(alpha: 0.54); + + @override + Color? get disabledActiveTrackColor => + _colors.onSurface.withValues(alpha: 0.32); + + @override + Color? get disabledInactiveTrackColor => + _colors.onSurface.withValues(alpha: 0.12); + + @override + Color? get disabledSecondaryActiveTrackColor => + _colors.onSurface.withValues(alpha: 0.12); + + @override + Color? get activeTickMarkColor => _colors.onPrimary.withValues(alpha: 0.54); + + @override + Color? get inactiveTickMarkColor => _colors.primary.withValues(alpha: 0.54); + + @override + Color? get disabledActiveTickMarkColor => + _colors.onPrimary.withValues(alpha: 0.12); + + @override + Color? get disabledInactiveTickMarkColor => + _colors.onSurface.withValues(alpha: 0.12); + + @override + Color? get thumbColor => _colors.primary; + + @override + Color? get disabledThumbColor => Color.alphaBlend( + _colors.onSurface.withValues(alpha: .38), + _colors.surface, + ); + + @override + Color? get overlayColor => _colors.primary.withValues(alpha: 0.12); + + @override + TextStyle? get valueIndicatorTextStyle => + Theme.of(context).textTheme.bodyLarge!.copyWith(color: _colors.onPrimary); + + @override + Color? get valueIndicatorColor { + if (sliderTheme.valueIndicatorShape + is RoundedRectSliderValueIndicatorShape) { + return _colors.inverseSurface; + } + return _colors.primary; + } + + @override + SliderComponentShape? get valueIndicatorShape => + const RectangularSliderValueIndicatorShape(); + + @override + SliderComponentShape? get thumbShape => const RoundSliderThumbShape(); + + @override + SliderTrackShape? get trackShape => const RoundedRectSliderTrackShape(); + + @override + SliderComponentShape? get overlayShape => const RoundSliderOverlayShape(); + + @override + SliderTickMarkShape? get tickMarkShape => const RoundSliderTickMarkShape(); +} + +class _SliderDefaultsM3Year2023 extends SliderThemeData { + _SliderDefaultsM3Year2023(this.context) : super(trackHeight: 4.0); + + final BuildContext context; + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + Color? get activeTrackColor => _colors.primary; + + @override + Color? get inactiveTrackColor => _colors.surfaceContainerHighest; + + @override + Color? get secondaryActiveTrackColor => + _colors.primary.withValues(alpha: 0.54); + + @override + Color? get disabledActiveTrackColor => + _colors.onSurface.withValues(alpha: 0.38); + + @override + Color? get disabledInactiveTrackColor => + _colors.onSurface.withValues(alpha: 0.12); + + @override + Color? get disabledSecondaryActiveTrackColor => + _colors.onSurface.withValues(alpha: 0.12); + + @override + Color? get activeTickMarkColor => _colors.onPrimary.withValues(alpha: 0.38); + + @override + Color? get inactiveTickMarkColor => + _colors.onSurfaceVariant.withValues(alpha: 0.38); + + @override + Color? get disabledActiveTickMarkColor => + _colors.onSurface.withValues(alpha: 0.38); + + @override + Color? get disabledInactiveTickMarkColor => + _colors.onSurface.withValues(alpha: 0.38); + + @override + Color? get thumbColor => _colors.primary; + + @override + Color? get disabledThumbColor => Color.alphaBlend( + _colors.onSurface.withValues(alpha: 0.38), + _colors.surface, + ); + + @override + Color? get overlayColor => + WidgetStateColor.resolveWith((Set states) { + if (states.contains(WidgetState.dragged)) { + return _colors.primary.withValues(alpha: 0.1); + } + if (states.contains(WidgetState.hovered)) { + return _colors.primary.withValues(alpha: 0.08); + } + if (states.contains(WidgetState.focused)) { + return _colors.primary.withValues(alpha: 0.1); + } + + return Colors.transparent; + }); + + @override + TextStyle? get valueIndicatorTextStyle => Theme.of( + context, + ).textTheme.labelMedium!.copyWith(color: _colors.onPrimary); + + @override + Color? get valueIndicatorColor => _colors.primary; + + @override + SliderComponentShape? get valueIndicatorShape => + const DropSliderValueIndicatorShape(); + + @override + SliderComponentShape? get thumbShape => const RoundSliderThumbShape(); + + @override + SliderTrackShape? get trackShape => const RoundedRectSliderTrackShape(); + + @override + SliderComponentShape? get overlayShape => const RoundSliderOverlayShape(); + + @override + SliderTickMarkShape? get tickMarkShape => const RoundSliderTickMarkShape(); +} + +// BEGIN GENERATED TOKEN PROPERTIES - Slider + +// Do not edit by hand. The code between the "BEGIN GENERATED" and +// "END GENERATED" comments are generated from data in the Material +// Design token database by the script: +// dev/tools/gen_defaults/bin/gen_defaults.dart. + +// dart format off +class _SliderDefaultsM3 extends SliderThemeData { + _SliderDefaultsM3(this.context) + : super(trackHeight: 16.0); + + final BuildContext context; + late final ColorScheme _colors = Theme.of(context).colorScheme; + + @override + Color? get activeTrackColor => _colors.primary; + + @override + Color? get inactiveTrackColor => _colors.secondaryContainer; + + @override + Color? get secondaryActiveTrackColor => _colors.primary.withValues(alpha: 0.54); + + @override + Color? get disabledActiveTrackColor => _colors.onSurface.withValues(alpha: 0.38); + + @override + Color? get disabledInactiveTrackColor => _colors.onSurface.withValues(alpha: 0.12); + + @override + Color? get disabledSecondaryActiveTrackColor => _colors.onSurface.withValues(alpha: 0.38); + + @override + Color? get activeTickMarkColor => _colors.onPrimary.withValues(alpha: 1.0); + + @override + Color? get inactiveTickMarkColor => _colors.onSecondaryContainer.withValues(alpha: 1.0); + + @override + Color? get disabledActiveTickMarkColor => _colors.onInverseSurface; + + @override + Color? get disabledInactiveTickMarkColor => _colors.onSurface; + + @override + Color? get thumbColor => _colors.primary; + + @override + Color? get disabledThumbColor => _colors.onSurface.withValues(alpha: 0.38); + + @override + Color? get overlayColor => WidgetStateColor.resolveWith((Set states) { + if (states.contains(WidgetState.dragged)) { + return _colors.primary.withValues(alpha: 0.1); + } + if (states.contains(WidgetState.hovered)) { + return _colors.primary.withValues(alpha: 0.08); + } + if (states.contains(WidgetState.focused)) { + return _colors.primary.withValues(alpha: 0.1); + } + + return Colors.transparent; + }); + + @override + TextStyle? get valueIndicatorTextStyle => Theme.of(context).textTheme.labelLarge!.copyWith( + color: _colors.onInverseSurface, + ); + + @override + Color? get valueIndicatorColor => _colors.inverseSurface; + + @override + SliderComponentShape? get valueIndicatorShape => const RoundedRectSliderValueIndicatorShape(); + + @override + SliderComponentShape? get thumbShape => const HandleThumbShape(); + + @override + SliderTrackShape? get trackShape => const GappedSliderTrackShape(); + + @override + SliderComponentShape? get overlayShape => const RoundSliderOverlayShape(); + + @override + SliderTickMarkShape? get tickMarkShape => const RoundSliderTickMarkShape(tickMarkRadius: 4.0 / 2); + + @override + WidgetStateProperty? get thumbSize { + return WidgetStateProperty.resolveWith((Set states) { + if (states.contains(WidgetState.disabled)) { + return const Size(4.0, 44.0); + } + if (states.contains(WidgetState.hovered)) { + return const Size(4.0, 44.0); + } + if (states.contains(WidgetState.focused)) { + return const Size(2.0, 44.0); + } + if (states.contains(WidgetState.pressed)) { + return const Size(2.0, 44.0); + } + return const Size(4.0, 44.0); + }); + } + + @override + double? get trackGap => 6.0; +} +// dart format on + +// END GENERATED TOKEN PROPERTIES - Slider + +class RoundedRectSliderTrackShape extends SliderTrackShape { + /// Create a slider track that draws two rectangles with rounded outer edges. + const RoundedRectSliderTrackShape(); + + @override + Rect getPreferredRect({ + required RenderBox parentBox, + Offset offset = Offset.zero, + required SliderThemeData sliderTheme, + bool isEnabled = false, + bool isDiscrete = false, + }) { + // final double thumbHeight = sliderTheme.thumbShape! + // .getPreferredSize(isEnabled, isDiscrete) + // .height; + final double overlayHight = sliderTheme.overlayShape! + .getPreferredSize(isEnabled, isDiscrete) + .height; + double trackWidth = sliderTheme.trackHeight!; + assert(overlayHight >= 0); + assert(trackWidth >= 0); + + // If the track colors are transparent, then override only the track height + // to maintain overall Slider width. + if (sliderTheme.activeTrackColor == Colors.transparent && + sliderTheme.inactiveTrackColor == Colors.transparent) { + trackWidth = 0; + } + + final double trackLeft = + offset.dx + (parentBox.size.width - trackWidth) / 2; + final double trackTop = offset.dy + 10; + // padding + // (sliderTheme.padding == null + // ? math.max(overlayHight / 2, thumbHeight / 2) + // : 0); + final double trackRight = trackLeft + trackWidth; + final double trackBottom = trackTop + parentBox.size.height - 20; + // (sliderTheme.padding == null ? math.max(thumbHeight, overlayHight) : 0); + // If the parentBox's size less than slider's size the trackRight will be less than trackLeft, so switch them. + return Rect.fromLTRB( + trackLeft, + math.min(trackTop, trackBottom), + trackRight, + math.max(trackTop, trackBottom), + ); + } + + @override + void paint( + PaintingContext context, + Offset offset, { + required RenderBox parentBox, + required SliderThemeData sliderTheme, + required Animation enableAnimation, + required TextDirection textDirection, + required Offset thumbCenter, + Offset? secondaryOffset, + bool isDiscrete = false, + bool isEnabled = false, + double additionalActiveTrackHeight = 2, + }) { + assert(sliderTheme.disabledActiveTrackColor != null); + assert(sliderTheme.disabledInactiveTrackColor != null); + assert(sliderTheme.activeTrackColor != null); + assert(sliderTheme.inactiveTrackColor != null); + assert(sliderTheme.thumbShape != null); + // If the slider [SliderThemeData.trackHeight] is less than or equal to 0, + // then it makes no difference whether the track is painted or not, + // therefore the painting can be a no-op. + if (sliderTheme.trackHeight == null || sliderTheme.trackHeight! <= 0) { + return; + } + + // Assign the track segment paints, which are leading: active and + // trailing: inactive. + final activeTrackColorTween = ColorTween( + begin: sliderTheme.disabledActiveTrackColor, + end: sliderTheme.activeTrackColor, + ); + final inactiveTrackColorTween = ColorTween( + begin: sliderTheme.disabledInactiveTrackColor, + end: sliderTheme.inactiveTrackColor, + ); + final activePaint = Paint() + ..color = activeTrackColorTween.evaluate(enableAnimation)!; + final inactivePaint = Paint() + ..color = inactiveTrackColorTween.evaluate(enableAnimation)!; + final (Paint leftTrackPaint, Paint rightTrackPaint) = ( + activePaint, + inactivePaint, + ); + + final Rect trackRect = getPreferredRect( + parentBox: parentBox, + offset: offset, + sliderTheme: sliderTheme, + isEnabled: isEnabled, + isDiscrete: isDiscrete, + ); + final trackRadius = Radius.circular(trackRect.height / 2); + final activeTrackRadius = Radius.circular( + (trackRect.height + additionalActiveTrackHeight) / 2, + ); + + final bool drawInactiveTrack = + thumbCenter.dy > (trackRect.top - (sliderTheme.trackHeight! / 2)); + if (drawInactiveTrack) { + // Draw the inactive track segment. + context.canvas.drawRRect( + RRect.fromLTRBR( + trackRect.left, + trackRect.top, + trackRect.right, + thumbCenter.dy - (sliderTheme.trackHeight! / 2), + trackRadius, + ), + rightTrackPaint, + ); + } + final bool drawActiveTrack = + thumbCenter.dy < (trackRect.bottom - (sliderTheme.trackHeight! / 2)); + if (drawActiveTrack) { + // Draw the active track segment. + context.canvas.drawRRect( + RRect.fromLTRBR( + trackRect.left, + thumbCenter.dy + (sliderTheme.trackHeight! / 2), + trackRect.right, + trackRect.bottom, + activeTrackRadius, + ), + leftTrackPaint, + ); + } + + // final bool showSecondaryTrack = + // (secondaryOffset != null) && (secondaryOffset.dx > thumbCenter.dx); + + // if (showSecondaryTrack) { + // final secondaryTrackColorTween = ColorTween( + // begin: sliderTheme.disabledSecondaryActiveTrackColor, + // end: sliderTheme.secondaryActiveTrackColor, + // ); + // final secondaryTrackPaint = Paint() + // ..color = secondaryTrackColorTween.evaluate(enableAnimation)!; + + // context.canvas.drawRRect( + // RRect.fromLTRBAndCorners( + // thumbCenter.dx, + // trackRect.top, + // secondaryOffset.dx, + // trackRect.bottom, + // topRight: trackRadius, + // bottomRight: trackRadius, + // ), + // secondaryTrackPaint, + // ); + // } + } + + @override + bool get isRounded => true; +} diff --git a/lib/common/widgets/flutter/vertical_tabs.dart b/lib/common/widgets/flutter/vertical_tabs.dart index 70ad399456..bc0a5814ed 100644 --- a/lib/common/widgets/flutter/vertical_tabs.dart +++ b/lib/common/widgets/flutter/vertical_tabs.dart @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. +// ignore_for_file: prefer_initializing_formals + import 'dart:math' as math; import 'dart:ui' show SemanticsRole, lerpDouble; diff --git a/lib/common/widgets/fractionally_sized_box.dart b/lib/common/widgets/fractionally_sized_box.dart new file mode 100644 index 0000000000..558404fb22 --- /dev/null +++ b/lib/common/widgets/fractionally_sized_box.dart @@ -0,0 +1,115 @@ +import 'dart:math' as math; + +import 'package:flutter/rendering.dart' show RenderFractionallySizedOverflowBox; +import 'package:flutter/widgets.dart'; + +class CustomFractionallySizedBox extends FractionallySizedBox { + const CustomFractionallySizedBox({ + super.key, + super.alignment, + required double super.widthFactor, + required double super.heightFactor, + required this.maxWidth, + super.child, + }); + + final double maxWidth; + + @override + RenderFractionallySizedOverflowBox createRenderObject(BuildContext context) { + return CustomRenderFractionallySizedOverflowBox( + alignment: alignment, + widthFactor: widthFactor, + heightFactor: heightFactor, + textDirection: Directionality.maybeOf(context), + maxWidth: maxWidth, + ); + } +} + +class CustomRenderFractionallySizedOverflowBox + extends RenderFractionallySizedOverflowBox { + CustomRenderFractionallySizedOverflowBox({ + super.child, + super.widthFactor, + super.heightFactor, + super.alignment, + super.textDirection, + required this._maxWidth, + }); + + final double _maxWidth; + + BoxConstraints _getInnerConstraints(BoxConstraints constraints) { + double minWidth = constraints.minWidth; + double maxWidth = constraints.maxWidth; + if (widthFactor != null) { + double width = maxWidth * widthFactor!; + if (maxWidth > constraints.maxHeight) { + width = math.min(_maxWidth, width); + } + minWidth = width; + maxWidth = width; + } + double minHeight = constraints.minHeight; + double maxHeight = constraints.maxHeight; + if (heightFactor != null) { + final double height = maxHeight * heightFactor!; + minHeight = height; + maxHeight = height; + } + return BoxConstraints( + minWidth: minWidth, + maxWidth: maxWidth, + minHeight: minHeight, + maxHeight: maxHeight, + ); + } + + @override + @protected + Size computeDryLayout(covariant BoxConstraints constraints) { + if (child != null) { + final Size childSize = child!.getDryLayout( + _getInnerConstraints(constraints), + ); + return constraints.constrain(childSize); + } + return constraints.constrain( + _getInnerConstraints(constraints).constrain(Size.zero), + ); + } + + @override + double? computeDryBaseline( + covariant BoxConstraints constraints, + TextBaseline baseline, + ) { + final RenderBox? child = this.child; + if (child == null) { + return null; + } + final BoxConstraints childConstraints = _getInnerConstraints(constraints); + final double? result = child.getDryBaseline(childConstraints, baseline); + if (result == null) { + return null; + } + final Size childSize = child.getDryLayout(childConstraints); + final Size size = getDryLayout(constraints); + return result + + resolvedAlignment.alongOffset(size - childSize as Offset).dy; + } + + @override + void performLayout() { + if (child != null) { + child!.layout(_getInnerConstraints(constraints), parentUsesSize: true); + size = constraints.constrain(child!.size); + alignChild(); + } else { + size = constraints.constrain( + _getInnerConstraints(constraints).constrain(Size.zero), + ); + } + } +} diff --git a/lib/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart b/lib/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart index e453f8bcf9..fe7c2da033 100644 --- a/lib/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart +++ b/lib/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:flutter/foundation.dart' show PlatformDispatcher; import 'package:flutter/gestures.dart'; mixin InitialPositionMixin on GestureRecognizer { @@ -50,20 +51,30 @@ bool _computeHitSlop( Offset lastPosition, ) { switch (kind) { - case PointerDeviceKind.mouse: + case .mouse: return globalDistanceMoved > kPrecisePointerHitSlop; - case PointerDeviceKind.stylus: - case PointerDeviceKind.invertedStylus: - case PointerDeviceKind.unknown: - case PointerDeviceKind.touch: + case .stylus: + case .invertedStylus: + case .unknown: + case .touch: return globalDistanceMoved > settings.touchSlop! && - _calc(initialPosition!, lastPosition); - case PointerDeviceKind.trackpad: + _calcAngle(initialPosition!, lastPosition); + case .trackpad: return globalDistanceMoved > settings.touchSlop!; } } -bool _calc(Offset initialPosition, Offset lastPosition) { +bool _calcAngle(Offset initialPosition, Offset lastPosition) { final offset = lastPosition - initialPosition; return offset.dx.abs() > offset.dy.abs() * 3; } + +final deviceTouchSlop = _calcDeviceTouchSlop(); + +double _calcDeviceTouchSlop() { + final view = PlatformDispatcher.instance.views.first; + final physicalTouchSlop = view.gestureSettings.physicalTouchSlop; + return physicalTouchSlop == null + ? kTouchSlop + : physicalTouchSlop / view.devicePixelRatio; +} diff --git a/lib/common/widgets/gesture/immediate_tap_gesture_recognizer.dart b/lib/common/widgets/gesture/immediate_tap_gesture_recognizer.dart index 1ddf8f128b..44936dcd9e 100644 --- a/lib/common/widgets/gesture/immediate_tap_gesture_recognizer.dart +++ b/lib/common/widgets/gesture/immediate_tap_gesture_recognizer.dart @@ -150,6 +150,14 @@ class ImmediateTapGestureRecognizer extends OneSequenceGestureRecognizer { _initialPosition = null; } + @override + void resolve(GestureDisposition disposition) { + if (_wonArena && disposition == GestureDisposition.rejected) { + _cancelGesture('spontaneous'); + } + super.resolve(disposition); + } + @override String get debugDescription => 'immediate tap'; diff --git a/lib/common/widgets/gesture/mouse_interactive_viewer.dart b/lib/common/widgets/gesture/mouse_interactive_viewer.dart index a6b1eab616..a2b0e6cf9a 100644 --- a/lib/common/widgets/gesture/mouse_interactive_viewer.dart +++ b/lib/common/widgets/gesture/mouse_interactive_viewer.dart @@ -16,29 +16,30 @@ import 'package:vector_math/vector_math_64.dart' show Quad, Vector3; class MouseInteractiveViewer extends StatefulWidget { const MouseInteractiveViewer({ super.key, - this.clipBehavior = Clip.hardEdge, - this.panAxis = PanAxis.free, - this.boundaryMargin = EdgeInsets.zero, + this.clipBehavior = .hardEdge, + this.panAxis = .free, + this.boundaryMargin = .zero, this.constrained = true, this.maxScale = 2.5, this.minScale = 0.8, this.interactionEndFrictionCoefficient = _kDrag, - this.pointerSignalFallback, + required this.pointerSignalFallback, this.onPointerPanZoomUpdate, this.onPointerPanZoomEnd, - this.onPointerDown, - this.onInteractionEnd, - this.onInteractionStart, - this.onInteractionUpdate, + required this.onPointerDown, + required this.onPanEnd, + required this.onPanStart, + required this.onPanUpdate, + required this.onScaleUpdate, this.panEnabled = true, this.scaleEnabled = true, this.scaleFactor = kDefaultMouseScrollToScaleFactor, - this.transformationController, + required this.transformationController, this.alignment, this.trackpadScrollCausesScale = false, required this.childKey, required this.child, - required this.onTranslate, + required this.scaleGestureRecognizer, }) : assert(minScale > 0), assert(interactionEndFrictionCoefficient > 0), assert(maxScale > 0), @@ -57,16 +58,17 @@ class MouseInteractiveViewer extends StatefulWidget { final double maxScale; final double minScale; final double interactionEndFrictionCoefficient; - final PointerSignalEventListener? pointerSignalFallback; + final PointerSignalEventListener pointerSignalFallback; final PointerPanZoomUpdateEventListener? onPointerPanZoomUpdate; final PointerPanZoomEndEventListener? onPointerPanZoomEnd; - final PointerDownEventListener? onPointerDown; - final GestureScaleEndCallback? onInteractionEnd; - final GestureScaleStartCallback? onInteractionStart; - final GestureScaleUpdateCallback? onInteractionUpdate; - final TransformationController? transformationController; + final PointerDownEventListener onPointerDown; + final GestureScaleEndCallback onPanEnd; + final GestureScaleStartCallback onPanStart; + final GestureScaleUpdateCallback onPanUpdate; + final ValueChanged onScaleUpdate; + final TransformationController transformationController; final GlobalKey childKey; - final VoidCallback onTranslate; + final ScaleGestureRecognizer scaleGestureRecognizer; static const double _kDrag = 0.0000135; @@ -76,8 +78,7 @@ class MouseInteractiveViewer extends StatefulWidget { class _MouseInteractiveViewerState extends State with TickerProviderStateMixin { - late TransformationController _transformer = - widget.transformationController ?? TransformationController(); + late TransformationController _transformer; final GlobalKey _parentKey = GlobalKey(); Animation? _animation; @@ -234,6 +235,9 @@ class _MouseInteractiveViewerState extends State widget.minScale, widget.maxScale, ); + + widget.onScaleUpdate(clampedTotalScale); + final double clampedScale = clampedTotalScale / currentScale; return matrix.clone() ..scaleByDouble(clampedScale, clampedScale, clampedScale, 1); @@ -270,10 +274,15 @@ class _MouseInteractiveViewerState extends State } } + bool _isSinglePointer = false; + // 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 (_isSinglePointer = details.pointerCount == 1) { + widget.onPanStart(details); + return; + } if (_controller.isAnimating) { _controller @@ -300,6 +309,11 @@ class _MouseInteractiveViewerState extends State // Handle an update to an ongoing gesture. All of pan, scale, and rotate are // handled with GestureDetector's scale gesture. void _onScaleUpdate(ScaleUpdateDetails details) { + if (_isSinglePointer) { + widget.onPanUpdate(details); + return; + } + final double scale = _transformer.value.getMaxScaleOnAxis(); _scaleAnimationFocalPoint = details.localFocalPoint; final Offset focalPointScene = _transformer.toScene( @@ -316,7 +330,6 @@ class _MouseInteractiveViewerState extends State _gestureType ??= _getGestureType(details); } if (!_gestureIsSupported(_gestureType)) { - widget.onInteractionUpdate?.call(details); return; } @@ -356,7 +369,6 @@ class _MouseInteractiveViewerState extends State case _GestureType.rotate: if (details.rotation == 0.0) { - widget.onInteractionUpdate?.call(details); return; } final double desiredRotation = _rotationStart! + details.rotation; @@ -373,7 +385,6 @@ class _MouseInteractiveViewerState extends State // 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); @@ -387,13 +398,16 @@ class _MouseInteractiveViewerState extends State ); _referenceFocalPoint = _transformer.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); + if (_isSinglePointer) { + widget.onPanEnd(details); + return; + } + _scaleStart = null; _rotationStart = null; _referenceFocalPoint = null; @@ -481,10 +495,6 @@ class _MouseInteractiveViewerState extends State final double scaleChange; if (event is PointerScrollEvent) { if (event.kind == PointerDeviceKind.trackpad) { - widget.onInteractionStart?.call( - ScaleStartDetails(focalPoint: global, localFocalPoint: local), - ); - final Offset localDelta = PointerEvent.transformDeltaViaPositions( untransformedEndPosition: global + event.scrollDelta, untransformedDelta: event.scrollDelta, @@ -501,14 +511,6 @@ class _MouseInteractiveViewerState extends State newFocalPointScene - focalPointScene, ); - widget.onInteractionUpdate?.call( - ScaleUpdateDetails( - focalPoint: global - event.scrollDelta, - localFocalPoint: local - localDelta, - focalPointDelta: -localDelta, - ), - ); - widget.onInteractionEnd?.call(ScaleEndDetails()); return; } _handlePointerScrollEvent(event); @@ -518,19 +520,8 @@ class _MouseInteractiveViewerState extends State } else { return; } - widget.onInteractionStart?.call( - ScaleStartDetails(focalPoint: global, localFocalPoint: local), - ); if (!_gestureIsSupported(_GestureType.scale)) { - widget.onInteractionUpdate?.call( - ScaleUpdateDetails( - focalPoint: global, - localFocalPoint: local, - scale: scaleChange, - ), - ); - widget.onInteractionEnd?.call(ScaleEndDetails()); return; } @@ -544,22 +535,12 @@ class _MouseInteractiveViewerState extends State _transformer.value, focalPointSceneScaled - focalPointScene, ); - - widget.onInteractionUpdate?.call( - ScaleUpdateDetails( - focalPoint: global, - localFocalPoint: local, - scale: scaleChange, - ), - ); - widget.onInteractionEnd?.call(ScaleEndDetails()); } void _handlePointerScrollEvent(PointerScrollEvent event) { - final Offset local = event.localPosition; - final Offset global = event.position; - if (_gestureIsSupported(_GestureType.scale)) { + final Offset local = event.localPosition; + final Offset global = event.position; if (HardwareKeyboard.instance.isControlPressed) { _handleMouseWheelScale(event, local, global); return; @@ -569,16 +550,8 @@ class _MouseInteractiveViewerState extends State _handleMouseWheelPanAsScale(event, local, global, shift); return; } - widget.pointerSignalFallback?.call(event); + widget.pointerSignalFallback(event); } - widget.onInteractionUpdate?.call( - ScaleUpdateDetails( - focalPoint: global, - localFocalPoint: local, - scale: math.exp(-event.scrollDelta.dy / widget.scaleFactor), - ), - ); - widget.onInteractionEnd?.call(ScaleEndDetails()); } void _handleMouseWheelScale( @@ -597,15 +570,6 @@ class _MouseInteractiveViewerState extends State _transformer.value, focalPointSceneScaled - focalPointScene, ); - - widget.onInteractionUpdate?.call( - ScaleUpdateDetails( - focalPoint: global, - localFocalPoint: local, - scale: scaleChange, - ), - ); - widget.onInteractionEnd?.call(ScaleEndDetails()); } void _handleMouseWheelPanAsScale( @@ -625,8 +589,6 @@ class _MouseInteractiveViewerState extends State _transformer.value, newFocalPointScene - focalPointScene, ); - - widget.onTranslate(); } void _handleInertiaAnimation() { @@ -675,29 +637,18 @@ class _MouseInteractiveViewerState extends State setState(() {}); } - void _onPointerDown(PointerDownEvent event) { - widget.onPointerDown?.call(event); - _scaleGestureRecognizer.addPointer(event); - } - @override void initState() { super.initState(); - _scaleGestureRecognizer = - ScaleGestureRecognizer( - debugOwner: this, - dragStartBehavior: .start, - allowedButtonsFilter: (buttons) => buttons == kPrimaryButton, - trackpadScrollToScaleFactor: Offset(0, -1 / widget.scaleFactor), - trackpadScrollCausesScale: widget.trackpadScrollCausesScale, - ) - ..gestureSettings = gestureSettings - ..onStart = _onScaleStart - ..onUpdate = _onScaleUpdate - ..onEnd = _onScaleEnd; + _scaleGestureRecognizer = widget.scaleGestureRecognizer + ..gestureSettings = gestureSettings + ..onStart = _onScaleStart + ..onUpdate = _onScaleUpdate + ..onEnd = _onScaleEnd; _controller = AnimationController(vsync: this); _scaleController = AnimationController(vsync: this); + _transformer = widget.transformationController; _transformer.addListener(_handleTransformation); } @@ -705,28 +656,20 @@ class _MouseInteractiveViewerState extends State void didUpdateWidget(MouseInteractiveViewer oldWidget) { super.didUpdateWidget(oldWidget); - final TransformationController? newController = - widget.transformationController; + final newController = widget.transformationController; if (newController == oldWidget.transformationController) { return; } _transformer.removeListener(_handleTransformation); - if (oldWidget.transformationController == null) { - _transformer.dispose(); - } - _transformer = newController ?? TransformationController(); + _transformer = newController; _transformer.addListener(_handleTransformation); } @override void dispose() { - _scaleGestureRecognizer.dispose(); _controller.dispose(); _scaleController.dispose(); _transformer.removeListener(_handleTransformation); - if (widget.transformationController == null) { - _transformer.dispose(); - } super.dispose(); } @@ -738,7 +681,7 @@ class _MouseInteractiveViewerState extends State key: _parentKey, behavior: HitTestBehavior.opaque, onPointerSignal: _receivedPointerSignal, - onPointerDown: _onPointerDown, + onPointerDown: widget.onPointerDown, onPointerPanZoomStart: _scaleGestureRecognizer.addPointerPanZoom, onPointerPanZoomUpdate: widget.onPointerPanZoomUpdate, onPointerPanZoomEnd: widget.onPointerPanZoomEnd, diff --git a/lib/common/widgets/gesture/player_gesture_recognizer.dart b/lib/common/widgets/gesture/player_gesture_recognizer.dart new file mode 100644 index 0000000000..3aa14a8ec9 --- /dev/null +++ b/lib/common/widgets/gesture/player_gesture_recognizer.dart @@ -0,0 +1,28 @@ +import 'package:flutter/gestures.dart' + show ScaleGestureRecognizer, RecognizerCallback, GestureRecognizer; + +mixin PlayerGestureMixin on GestureRecognizer { + bool isPosAllowed = true; + + @override + T? invokeCallback( + String name, + RecognizerCallback callback, { + String Function()? debugReport, + }) { + if (!isPosAllowed) return null; + return super.invokeCallback(name, callback, debugReport: debugReport); + } +} + +class PlayerScaleGestureRecognizer extends ScaleGestureRecognizer + with PlayerGestureMixin { + PlayerScaleGestureRecognizer({ + super.debugOwner, + super.supportedDevices, + super.allowedButtonsFilter, + super.dragStartBehavior, + super.trackpadScrollCausesScale, + super.trackpadScrollToScaleFactor, + }); +} diff --git a/lib/common/widgets/image/cached_network_svg_image.dart b/lib/common/widgets/image/cached_network_svg_image.dart index 7e89032a87..7842d95565 100644 --- a/lib/common/widgets/image/cached_network_svg_image.dart +++ b/lib/common/widgets/image/cached_network_svg_image.dart @@ -2,50 +2,32 @@ import 'dart:developer'; +import 'package:PiliPlus/utils/cache_manager.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_svg/flutter_svg.dart'; class CachedNetworkSVGImage extends StatefulWidget { CachedNetworkSVGImage( - String url, { + this._url, { Key? key, - String? cacheKey, - Widget? placeholder, - WidgetBuilder? errorBuilder, - double? width, - double? height, - Map? headers, - BoxFit fit = BoxFit.contain, - AlignmentGeometry alignment = Alignment.center, - bool matchTextDirection = false, - bool allowDrawingOutsideViewBox = false, - String? semanticsLabel, - bool excludeFromSemantics = false, - SvgTheme theme = const SvgTheme(), - ColorFilter? colorFilter, - WidgetBuilder? placeholderBuilder, - BaseCacheManager? cacheManager, - }) : _url = url, - _cacheKey = cacheKey, - _placeholder = placeholder, - _errorBuilder = errorBuilder, - _width = width, - _height = height, - _headers = headers, - _fit = fit, - _alignment = alignment, - _matchTextDirection = matchTextDirection, - _allowDrawingOutsideViewBox = allowDrawingOutsideViewBox, - _semanticsLabel = semanticsLabel, - _excludeFromSemantics = excludeFromSemantics, - _theme = theme, - _colorFilter = colorFilter, - _placeholderBuilder = placeholderBuilder, - _cacheManager = cacheManager ?? DefaultCacheManager(), - super(key: key ?? ValueKey(cacheKey ?? url)); + this._cacheKey, + this._placeholder, + this._errorBuilder, + this._width, + this._height, + this._headers, + this._fit = BoxFit.contain, + this._alignment = Alignment.center, + this._matchTextDirection = false, + this._allowDrawingOutsideViewBox = false, + this._semanticsLabel, + this._excludeFromSemantics = false, + this._theme = const SvgTheme(), + this._colorFilter, + this._placeholderBuilder, + }) : super(key: key ?? ValueKey(_cacheKey ?? _url)); final String _url; final String? _cacheKey; @@ -63,7 +45,6 @@ class CachedNetworkSVGImage extends StatefulWidget { final SvgTheme _theme; final ColorFilter? _colorFilter; final WidgetBuilder? _placeholderBuilder; - final BaseCacheManager _cacheManager; @override State createState() => _CachedNetworkSVGImageState(); @@ -79,9 +60,7 @@ class _CachedNetworkSVGImageState extends State { double? height; late TextScaler textScaler; - static final _sizeRegExp = RegExp( - r'height="([\d\.]+)([c-x]{2})?"', - ); + static final _sizeRegExp = RegExp(r'height="([\d\.]+)([c-x]{2})?"'); @override void initState() { @@ -100,7 +79,7 @@ class _CachedNetworkSVGImageState extends State { Future _loadImage() async { try { - final file = await widget._cacheManager.getSingleFile( + final file = await CacheManager.manager.getSingleFile( widget._url, key: _cacheKey, headers: widget._headers ?? const {}, diff --git a/lib/common/widgets/image/network_img_layer.dart b/lib/common/widgets/image/network_img_layer.dart index c6f3271917..e42296744f 100644 --- a/lib/common/widgets/image/network_img_layer.dart +++ b/lib/common/widgets/image/network_img_layer.dart @@ -4,7 +4,7 @@ import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:flutter/material.dart'; class NetworkImgLayer extends StatelessWidget { diff --git a/lib/common/widgets/image_grid/image_grid_builder.dart b/lib/common/widgets/image_grid/image_grid_builder.dart index ddd65f9fe7..0136aeaacb 100644 --- a/lib/common/widgets/image_grid/image_grid_builder.dart +++ b/lib/common/widgets/image_grid/image_grid_builder.dart @@ -37,7 +37,9 @@ import 'package:flutter/rendering.dart' BoxHitTestEntry, ContainerParentDataMixin, InformationCollector, - DiagnosticsDebugCreator; + DiagnosticsDebugCreator, + RenderObjectVisitor, + SemanticsConfiguration; /// ref [LayoutBuilder] @@ -94,11 +96,10 @@ class RenderImageGrid extends RenderBox RenderBoxContainerDefaultsMixin, RenderObjectWithLayoutCallbackMixin { RenderImageGrid({ - required ValueChanged onTap, + required this._onTap, required OnShowMenu? onSecondaryTapUp, required OnShowMenu? onLongPressStart, - }) : _onTap = onTap, - _onSecondaryTapUp = onSecondaryTapUp, + }) : _onSecondaryTapUp = onSecondaryTapUp, _onLongPressStart = onLongPressStart { _tapGestureRecognizer = TapGestureRecognizer()..onTap = _handleOnTap; if (onSecondaryTapUp != null) { @@ -251,6 +252,23 @@ class RenderImageGrid extends RenderBox super.dispose(); } + @override + void visitChildrenForSemantics(RenderObjectVisitor visitor) { + RenderBox? child = firstChild; + while (child != null) { + visitor(child); + child = (child.parentData as MultiChildLayoutParentData).nextSibling; + } + } + + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config + ..explicitChildNodes = true + ..isSemanticBoundary = true; + } + @override bool get isRepaintBoundary => true; // gif repaint } @@ -497,7 +515,7 @@ class ImageGridRenderObjectElement extends RenderObjectElement { final height = img.height; final ratioWH = width / height; final ratioHW = height / width; - imageWidth = ratioWH > 1.5 + imageWidth = ratioWH > 1.45 ? maxWidth : (ratioWH >= 1 || (height > width && ratioHW < 1.5)) ? 2 * imageWidth diff --git a/lib/common/widgets/image_grid/image_grid_view.dart b/lib/common/widgets/image_grid/image_grid_view.dart index b2fbfc84d2..de226dc99c 100644 --- a/lib/common/widgets/image_grid/image_grid_view.dart +++ b/lib/common/widgets/image_grid/image_grid_view.dart @@ -22,7 +22,6 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image_grid/image_grid_builder.dart'; -import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; @@ -209,9 +208,9 @@ class ImageGridView extends StatelessWidget { width: width, height: height, decoration: BoxDecoration( - color: Theme.of( + color: ColorScheme.of( context, - ).colorScheme.onInverseSurface.withValues(alpha: 0.4), + ).onInverseSurface.withValues(alpha: 0.4), ), child: Image.asset( Assets.loading, @@ -221,6 +220,7 @@ class ImageGridView extends StatelessWidget { ), ); return List.generate(picArr.length, (index) { + void onTap() => _onTap(context, index); final item = picArr[index]; final borderRadius = _borderRadius( info.column, @@ -241,30 +241,21 @@ class ImageGridView extends StatelessWidget { getPlaceHolder: () => placeHolder, ), if (item.isLivePhoto) - const PBadge( - text: 'Live', - right: 8, - bottom: 8, - type: PBadgeType.gray, - ) + const PBadge(text: 'Live', right: 8, bottom: 8, type: .gray) else if (item.isLongPic) - const PBadge( - text: '长图', - right: 8, - bottom: 8, - ), + const PBadge(text: '长图', right: 8, bottom: 8), ], ); if (!item.isLongPic) { - child = Hero( - tag: '${item.url}$hashCode', - child: child, - ); + child = Hero(tag: '${item.url}$hashCode', child: child); } - return LayoutId( - id: index, + child = Semantics( + label: '图片,第 ${index + 1} 张,共 ${picArr.length} 张', + button: true, + onTap: onTap, child: child, ); + return LayoutId(id: index, child: child); }); }, ), diff --git a/lib/common/widgets/image_viewer/gallery_viewer.dart b/lib/common/widgets/image_viewer/gallery_viewer.dart index be8479eda3..77d77f5f68 100644 --- a/lib/common/widgets/image_viewer/gallery_viewer.dart +++ b/lib/common/widgets/image_viewer/gallery_viewer.dart @@ -18,25 +18,29 @@ import 'dart:io' show File, Platform; import 'package:PiliPlus/common/widgets/colored_box_transition.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/flutter/page/page_view.dart'; import 'package:PiliPlus/common/widgets/gesture/image_horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/image.dart'; import 'package:PiliPlus/common/widgets/image_viewer/loading_indicator.dart'; import 'package:PiliPlus/common/widgets/image_viewer/viewer.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/main.dart' show tmpPadding; import 'package:PiliPlus/models/common/image_preview_type.dart'; +import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart'; +import 'package:PiliPlus/utils/device_utils.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; +import 'package:PiliPlus/utils/max_screen_size.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/gestures.dart'; -import 'package:flutter/material.dart' hide Image, PageView, LayoutBuilder; +import 'package:flutter/material.dart' hide Image, PageView; import 'package:flutter/services.dart' show HapticFeedback; import 'package:get/get.dart'; import 'package:media_kit/media_kit.dart'; @@ -76,6 +80,7 @@ class _GalleryViewerState extends State late final int _quality; late final RxInt _currIndex; GlobalKey? _key; + EdgeInsets? _padding; late bool _hasInit = false; Player? _player; @@ -170,6 +175,44 @@ class _GalleryViewerState extends State ); } + late final bool _hideSystemBar; + + void _initHideSystemBar() { + if (Platform.isAndroid) { + if (showSystemBar_) { + final size = DeviceUtils.size; + _hideSystemBar = !MaxScreenSize.isWindowMode( + width: size.width, + height: size.height, + ); + } else { + _hideSystemBar = false; + } + } else if (Platform.isIOS) { + _hideSystemBar = showSystemBar_; + } else { + _hideSystemBar = false; + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_padding == null) { + final padding = MediaQuery.viewPaddingOf(context); + _padding = padding; + _initHideSystemBar(); + if (_hideSystemBar) { + tmpPadding = padding; + hideSystemBar()!.whenComplete( + () => WidgetsBinding.instance.addPostFrameCallback( + (_) => tmpPadding = null, + ), + ); + } + } + } + Matrix4 _onTransform(double val) { final scale = val.lerp(1.0, 0.25); @@ -259,6 +302,9 @@ class _GalleryViewerState extends State } Future.delayed(const Duration(milliseconds: 200), _currIndex.close); super.dispose(); + if (_hideSystemBar) { + showSystemBar(); + } } void _onPointerDown(PointerDownEvent event) { @@ -311,9 +357,7 @@ class _GalleryViewerState extends State right: 0, child: IgnorePointer( child: Container( - padding: - MediaQuery.viewPaddingOf(context) + - const EdgeInsets.fromLTRB(12, 8, 20, 8), + padding: _padding! + const EdgeInsets.fromLTRB(12, 8, 20, 8), decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, @@ -491,76 +535,67 @@ class _GalleryViewerState extends State HapticFeedback.mediumImpact(); showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (PlatformUtils.isMobile) - ListTile( - onTap: () { - Get.back(); - ImageUtils.onShareImg(item.url); - }, - dense: true, - title: const Text('分享', style: TextStyle(fontSize: 14)), - ), - ListTile( - onTap: () { + children: [ + if (PlatformUtils.isMobile) + DialogOption( + onPressed: () { Get.back(); - Utils.copyText(item.url); + ImageUtils.onShareImg(item.url); }, - dense: true, - title: const Text('复制链接', style: TextStyle(fontSize: 14)), + child: const Text('分享', style: TextStyle(fontSize: 14)), ), - ListTile( - onTap: () { + DialogOption( + onPressed: () { + Get.back(); + Utils.copyText(item.url); + }, + child: const Text('复制链接', style: TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () { + Get.back(); + ImageUtils.downloadImg([item.url]); + }, + child: const Text('保存图片', style: TextStyle(fontSize: 14)), + ), + if (PlatformUtils.isDesktop) + DialogOption( + onPressed: () { + Get.back(); + PageUtils.launchURL(item.url); + }, + child: const Text('网页打开', style: TextStyle(fontSize: 14)), + ) + else if (widget.sources.length > 1) + DialogOption( + onPressed: () { Get.back(); - ImageUtils.downloadImg([item.url]); + ImageUtils.downloadImg( + widget.sources.map((item) => item.url).toList(), + ); }, - dense: true, - title: const Text('保存图片', style: TextStyle(fontSize: 14)), + child: const Text('保存全部图片', style: TextStyle(fontSize: 14)), ), - if (PlatformUtils.isDesktop) - ListTile( - onTap: () { - Get.back(); - PageUtils.launchURL(item.url); - }, - dense: true, - title: const Text('网页打开', style: TextStyle(fontSize: 14)), - ) - else if (widget.sources.length > 1) - ListTile( - onTap: () { - Get.back(); - ImageUtils.downloadImg( - widget.sources.map((item) => item.url).toList(), - ); - }, - dense: true, - title: const Text('保存全部图片', style: TextStyle(fontSize: 14)), - ), - if (item.sourceType == SourceType.livePhoto) - ListTile( - onTap: () { - Get.back(); - ImageUtils.downloadLivePhoto( - url: item.url, - liveUrl: item.liveUrl!, - width: item.width!, - height: item.height!, - ); - }, - dense: true, - title: Text( - '保存${Platform.isIOS ? ' Live Photo' : '视频'}', - style: const TextStyle(fontSize: 14), - ), + if (item.sourceType == SourceType.livePhoto) + DialogOption( + onPressed: () { + Get.back(); + ImageUtils.downloadLivePhoto( + url: item.url, + liveUrl: item.liveUrl!, + width: item.width!, + height: item.height!, + ); + }, + child: Text( + '保存${Platform.isIOS ? ' Live Photo' : '视频'}', + style: const TextStyle(fontSize: 14), ), - ], - ), + ), + ], ), ); } diff --git a/lib/common/widgets/image_viewer/image.dart b/lib/common/widgets/image_viewer/image.dart index 61a5e0fc2c..c0c981bf42 100644 --- a/lib/common/widgets/image_viewer/image.dart +++ b/lib/common/widgets/image_viewer/image.dart @@ -390,9 +390,7 @@ class _ImageState extends State with WidgetsBindingObserver { void didChangeDependencies() { _resolveImage(); - _isPaused = - !TickerMode.valuesOf(context).enabled || - (MediaQuery.maybeDisableAnimationsOf(context) ?? false); + _isPaused = !TickerMode.valuesOf(context).enabled; if (_isPaused && _frameNumber != null) { _stopListeningToStream(keepStreamAlive: true); diff --git a/lib/common/widgets/image_viewer/loading_indicator.dart b/lib/common/widgets/image_viewer/loading_indicator.dart index 54efaba401..01056a5c84 100644 --- a/lib/common/widgets/image_viewer/loading_indicator.dart +++ b/lib/common/widgets/image_viewer/loading_indicator.dart @@ -55,10 +55,9 @@ class LoadingIndicator extends LeafRenderObjectWidget { class RenderLoadingIndicator extends RenderBox { RenderLoadingIndicator({ - required double preferredSize, - required double progress, - }) : _preferredSize = preferredSize, - _progress = progress; + required this._preferredSize, + required this._progress, + }); double _preferredSize; double get preferredSize => _preferredSize; diff --git a/lib/common/widgets/loading_widget/http_error.dart b/lib/common/widgets/loading_widget/http_error.dart index b527110cd3..67be7fda87 100644 --- a/lib/common/widgets/loading_widget/http_error.dart +++ b/lib/common/widgets/loading_widget/http_error.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/assets.dart'; import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; @@ -26,10 +27,7 @@ class HttpError extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ const SizedBox(height: 40), - SvgPicture.asset( - "assets/images/error.svg", - height: 200, - ), + SvgPicture.asset(Assets.error, height: 200), const SizedBox(height: 30), Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 5), diff --git a/lib/common/widgets/loading_widget/m3e_loading_indicator.dart b/lib/common/widgets/loading_widget/m3e_loading_indicator.dart index 52ebffa619..77ade6ffee 100644 --- a/lib/common/widgets/loading_widget/m3e_loading_indicator.dart +++ b/lib/common/widgets/loading_widget/m3e_loading_indicator.dart @@ -175,15 +175,12 @@ class RawM3ELoadingIndicator extends LeafRenderObjectWidget { class RenderM3ELoadingIndicator extends RenderBox { RenderM3ELoadingIndicator({ - required Morph morph, - required double progress, - required double angle, + required this._morph, + required this._progress, + required this._angle, required Color color, required Size size, - }) : _morph = morph, - _progress = progress, - _angle = angle, - _preferredSize = size, + }) : _preferredSize = size, _color = color, _paint = Paint() ..style = PaintingStyle.fill diff --git a/lib/common/widgets/marquee.dart b/lib/common/widgets/marquee.dart index 31d9b7002e..02393987dc 100644 --- a/lib/common/widgets/marquee.dart +++ b/lib/common/widgets/marquee.dart @@ -114,15 +114,13 @@ class BounceMarquee extends Marquee { abstract class MarqueeRender extends RenderBox with RenderObjectWithChildMixin { MarqueeRender({ - required Axis direction, - required double velocity, + required this._direction, + required this._velocity, required double spacing, required this.clipBehavior, required ContextSingleTicker provider, }) : _ticker = provider, _spacing = spacing, - _velocity = velocity, - _direction = direction, assert(spacing.isFinite && !spacing.isNaN); Clip clipBehavior; diff --git a/lib/common/widgets/pendant_avatar.dart b/lib/common/widgets/pendant_avatar.dart index 9589e381cf..5c88faa4f6 100644 --- a/lib/common/widgets/pendant_avatar.dart +++ b/lib/common/widgets/pendant_avatar.dart @@ -4,10 +4,10 @@ import 'package:PiliPlus/common/widgets/extra_hittest_stack.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/avatar_badge_type.dart'; import 'package:PiliPlus/models/common/image_type.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; class PendantAvatar extends StatelessWidget { const PendantAvatar( @@ -142,12 +142,11 @@ class PendantAvatar extends StatelessWidget { Widget _buildBadge(BuildContext context, ColorScheme colorScheme) { final child = switch (badgeType) { - .vip => Image.asset( + .vip => SvgPicture.asset( Assets.vipIcon, width: badgeSize, height: badgeSize, - cacheWidth: badgeSize.cacheSize(context), - semanticLabel: badgeType.desc, + semanticsLabel: badgeType.desc, ), _ => Icon( Icons.offline_bolt, diff --git a/lib/common/widgets/progress_bar/audio_video_progress_bar.dart b/lib/common/widgets/progress_bar/audio_video_progress_bar.dart index 665d8fd6ff..52a766b309 100644 --- a/lib/common/widgets/progress_bar/audio_video_progress_bar.dart +++ b/lib/common/widgets/progress_bar/audio_video_progress_bar.dart @@ -29,7 +29,7 @@ class ProgressBar extends LeafRenderObjectWidget { super.key, required this.progress, required this.total, - this.buffered = .zero, + this.buffered = 0, this.onSeek, this.onDragStart, this.onDragUpdate, @@ -48,16 +48,19 @@ class ProgressBar extends LeafRenderObjectWidget { /// The elapsed playing time of the media. /// /// This should not be greater than the [total] time. - final Duration progress; + /// seconds + final int progress; /// The total duration of the media. - final Duration total; + /// seconds + final int total; /// The currently buffered content of the media. /// /// This is useful for streamed content. If you are playing a local file /// then you can leave this out. - final Duration buffered; + /// seconds + final int buffered; /// A callback when user moves the thumb. /// @@ -70,7 +73,7 @@ class ProgressBar extends LeafRenderObjectWidget { /// If you want continuous duration updates as the user moves the thumb, /// see [onDragUpdate], where the provided [ThumbDragDetails] has a /// `timeStamp` with the seek duration on it. - final ValueChanged? onSeek; + final OnSeek? onSeek; /// A callback when the user starts to move the thumb. /// @@ -224,7 +227,7 @@ class ProgressBar extends LeafRenderObjectWidget { ..add(StringProperty('total', total.toString())) ..add(StringProperty('buffered', buffered.toString())) ..add( - ObjectFlagProperty>( + ObjectFlagProperty( 'onSeek', onSeek, ifNull: 'unimplemented', @@ -271,6 +274,8 @@ class ProgressBar extends LeafRenderObjectWidget { } } +typedef OnSeek = void Function(int milliseconds); + /// The callback signature for when the thumb begins a horizontal drag. typedef ThumbDragStartCallback = void Function(ThumbDragDetails details); @@ -281,13 +286,13 @@ typedef ThumbDragUpdateCallback = void Function(ThumbDragDetails details); /// Data to pass back on drag callback events class ThumbDragDetails { const ThumbDragDetails({ - this.timeStamp = Duration.zero, + this.seconds = 0, this.globalPosition = Offset.zero, this.localPosition = Offset.zero, }); /// The duration position of the thumb on the progress bar - final Duration timeStamp; + final int seconds; /// The global position of the drag event moving the thumb on the progress bar. final Offset globalPosition; @@ -298,7 +303,7 @@ class ThumbDragDetails { @override String toString() => '${objectRuntimeType(this, 'ThumbDragDetails')}(' - 'time: $timeStamp, ' + 'time: $seconds, ' 'global: $globalPosition, ' 'local: $localPosition)'; } @@ -320,38 +325,28 @@ class _EagerHorizontalDragGestureRecognizer class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { RenderProgressBar({ - required Duration progress, - required Duration total, - required Duration buffered, - ValueChanged? onSeek, + required this._progress, + required this._total, + required this._buffered, + this._onSeek, ThumbDragStartCallback? onDragStart, ThumbDragUpdateCallback? onDragUpdate, VoidCallback? onDragEnd, - required double barHeight, - required Color baseBarColor, - required Color progressBarColor, - required Color bufferedBarColor, + required this._barHeight, + required this._baseBarColor, + required this._progressBarColor, + required this._bufferedBarColor, double thumbRadius = 20.0, - required Color thumbColor, - required Color thumbGlowColor, + required this._thumbColor, + required this._thumbGlowColor, double thumbGlowRadius = 30.0, - bool thumbCanPaintOutsideBar = true, - }) : _total = total, - _buffered = buffered, - _onSeek = onSeek, - _onDragStartUserCallback = onDragStart, + this._thumbCanPaintOutsideBar = true, + }) : _onDragStartUserCallback = onDragStart, _onDragUpdateUserCallback = onDragUpdate, _onDragEndUserCallback = onDragEnd, - _barHeight = barHeight, - _baseBarColor = baseBarColor, - _progressBarColor = progressBarColor, - _bufferedBarColor = bufferedBarColor, _thumbRadius = thumbRadius, - _thumbColor = thumbColor, - _thumbGlowColor = thumbGlowColor, _thumbGlowRadius = thumbGlowRadius, _paintThumbGlow = thumbGlowRadius > thumbRadius, - _thumbCanPaintOutsideBar = thumbCanPaintOutsideBar, _hitTestSelf = onDragStart != null { if (onDragStart != null) { _drag = _EagerHorizontalDragGestureRecognizer() @@ -361,7 +356,6 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { ..onCancel = _finishDrag; } if (!_userIsDraggingThumb) { - _progress = progress; _thumbValue = _proportionOfTotal(_progress); } } @@ -387,14 +381,11 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { bool _userIsDraggingThumb = false; void _onDragStart(DragStartDetails details) { - if (onDragStart == null) { - return; - } _userIsDraggingThumb = true; _updateThumbPosition(details.localPosition); onDragStart?.call( ThumbDragDetails( - timeStamp: _currentThumbDuration(), + seconds: _currentThumbDuration(), globalPosition: details.globalPosition, localPosition: details.localPosition, ), @@ -402,13 +393,10 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { } void _onDragUpdate(DragUpdateDetails details) { - if (onDragUpdate == null) { - return; - } _updateThumbPosition(details.localPosition); onDragUpdate?.call( ThumbDragDetails( - timeStamp: _currentThumbDuration(), + seconds: _currentThumbDuration(), globalPosition: details.globalPosition, localPosition: details.localPosition, ), @@ -416,11 +404,8 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { } void _onDragEnd(DragEndDetails details) { - if (onSeek == null) { - return; - } onDragEnd?.call(); - onSeek?.call(_currentThumbDuration()); + onSeek?.call(_currentThumbDurationInMilliseconds()); _finishDrag(); } @@ -429,9 +414,12 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { markNeedsPaint(); } - Duration _currentThumbDuration() { - final thumbMilliseconds = _thumbValue * total.inMilliseconds; - return Duration(milliseconds: thumbMilliseconds.round()); + int _currentThumbDuration() { + return (_thumbValue * total).round(); + } + + int _currentThumbDurationInMilliseconds() { + return (_thumbValue * total * 1000).round(); } // This needs to stay in sync with the layout. This could be a potential @@ -448,7 +436,7 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { double barEnd = size.width - barCapRadius; final barWidth = barEnd - barStart; final position = (dx - barStart).clamp(0.0, barWidth); - _thumbValue = (position / barWidth); + _thumbValue = position / barWidth; _progress = _currentThumbDuration(); markNeedsPaint(); } @@ -456,9 +444,9 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { /// The play location of the media. /// /// This is used to update the thumb value and the left time label. - Duration get progress => _progress; - Duration _progress = Duration.zero; - set progress(Duration value) { + int get progress => _progress; + int _progress; + set progress(int value) { final clamp = _clampDuration(value); if (_progress == clamp) { return; @@ -471,10 +459,10 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { } /// The total time length of the media. - Duration get total => _total; - Duration _total; - set total(Duration value) { - final clamp = (value.isNegative) ? Duration.zero : value; + int get total => _total; + int _total; + set total(int value) { + final clamp = (value.isNegative) ? 0 : value; if (_total == clamp) { return; } @@ -486,9 +474,9 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { } /// The buffered length of the media when streaming. - Duration get buffered => _buffered; - Duration _buffered; - set buffered(Duration value) { + int get buffered => _buffered; + int _buffered; + set buffered(int value) { final clamp = _clampDuration(value); if (_buffered == clamp) { return; @@ -497,16 +485,16 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { markNeedsPaint(); } - Duration _clampDuration(Duration value) { - if (value.isNegative) return Duration.zero; + int _clampDuration(int value) { + if (value.isNegative) return 0; if (value.compareTo(_total) > 0) return _total; return value; } /// A callback for the audio duration position to where the thumb was moved. - ValueChanged? get onSeek => _onSeek; - ValueChanged? _onSeek; - set onSeek(ValueChanged? value) { + OnSeek? get onSeek => _onSeek; + OnSeek? _onSeek; + set onSeek(OnSeek? value) { if (value == _onSeek) { return; } @@ -726,7 +714,7 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { _drawBar( canvas: canvas, availableSize: localSize, - widthProportion: _proportionOfTotal(_progress), + widthProportion: _thumbValue, color: progressBarColor, ); } @@ -744,8 +732,9 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { final capRadius = _barHeight / 2; final adjustedWidth = availableSize.width - barHeight; final dx = widthProportion * adjustedWidth + capRadius; - final startPoint = Offset(capRadius, availableSize.height / 2); - final endPoint = Offset(dx, availableSize.height / 2); + final dy = availableSize.height / 2; + final startPoint = Offset(capRadius, dy); + final endPoint = Offset(dx, dy); canvas.drawLine(startPoint, endPoint, baseBarPaint); } @@ -765,11 +754,11 @@ class RenderProgressBar extends RenderBox implements MouseTrackerAnnotation { canvas.drawCircle(center, thumbRadius, thumbPaint); } - double _proportionOfTotal(Duration duration) { - if (total.inMilliseconds == 0) { + double _proportionOfTotal(int duration) { + if (total == 0) { return 0.0; } - return (duration.inMilliseconds / total.inMilliseconds).clamp(0.0, 1.0); + return (duration / total).clamp(0.0, 1.0); } @override diff --git a/lib/common/widgets/progress_bar/segment_progress_bar.dart b/lib/common/widgets/progress_bar/segment_progress_bar.dart index 123040714c..d83d3cc05b 100644 --- a/lib/common/widgets/progress_bar/segment_progress_bar.dart +++ b/lib/common/widgets/progress_bar/segment_progress_bar.dart @@ -207,13 +207,7 @@ class RenderViewPointProgressBar ), ), ) - ..pushStyle( - ui.TextStyle( - color: Colors.white, - fontSize: size, - height: 1, - ), - ) + ..pushStyle(.new(color: Colors.white, fontSize: size, height: 1)) ..addText(title); return builder.build() ..layout(const ui.ParagraphConstraints(width: double.infinity)); @@ -353,10 +347,9 @@ abstract class BaseSegmentProgressBar class BaseRenderProgressBar extends RenderBox { BaseRenderProgressBar({ - required double height, - required List segments, - }) : _height = height, - _segments = segments; + required this._height, + required this._segments, + }); double _height; double get height => _height; diff --git a/lib/common/widgets/progress_bar/video_progress_indicator.dart b/lib/common/widgets/progress_bar/video_progress_indicator.dart index 6bd3d49959..b64c645040 100644 --- a/lib/common/widgets/progress_bar/video_progress_indicator.dart +++ b/lib/common/widgets/progress_bar/video_progress_indicator.dart @@ -25,7 +25,7 @@ class VideoProgressIndicator extends LeafRenderObjectWidget { this.radius = 10, this.height = 4, required this.progress, - }) : assert(progress >= 0 && progress <= 1); + }); final Color color; final Color backgroundColor; @@ -60,16 +60,12 @@ class VideoProgressIndicator extends LeafRenderObjectWidget { class RenderProgressBar extends RenderBox { RenderProgressBar({ - required Color color, - required Color backgroundColor, - required double radius, - required double height, - required double progress, - }) : _color = color, - _backgroundColor = backgroundColor, - _radius = radius, - _height = height, - _progress = progress; + required this._color, + required this._backgroundColor, + required this._radius, + required this._height, + required this._progress, + }); Color _color; Color get color => _color; @@ -136,9 +132,9 @@ class RenderProgressBar extends RenderBox { bottomRight: radius, ); - if (progress == 0) { + if (progress <= 0) { canvas.drawRRect(rrect, paint..color = _backgroundColor); - } else if (progress == 1) { + } else if (progress >= 1) { canvas.drawRRect(rrect, paint..color = _color); } else { final w = size.width * progress; diff --git a/lib/common/widgets/reorder_mixin.dart b/lib/common/widgets/reorder_mixin.dart new file mode 100644 index 0000000000..6adf29a2cf --- /dev/null +++ b/lib/common/widgets/reorder_mixin.dart @@ -0,0 +1,18 @@ +import 'package:flutter/material.dart'; + +mixin ReorderMixin on State { + late ColorScheme scheme; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + scheme = ColorScheme.of(context); + } + + Widget proxyDecorator(Widget child, _, _) { + return ColoredBox( + color: scheme.onInverseSurface, + child: child, + ); + } +} diff --git a/lib/common/widgets/scale_app.dart b/lib/common/widgets/scale_app.dart index 57f7db0f1a..7e9d7cc557 100644 --- a/lib/common/widgets/scale_app.dart +++ b/lib/common/widgets/scale_app.dart @@ -11,8 +11,7 @@ import 'package:flutter/widgets.dart'; /// Adapted from [WidgetsFlutterBinding] /// class ScaledWidgetsFlutterBinding extends WidgetsFlutterBinding { - ScaledWidgetsFlutterBinding._({double scaleFactor = 1.0}) - : _scaleFactor = scaleFactor; + ScaledWidgetsFlutterBinding._({this._scaleFactor = 1.0}); /// Calculate scale factor from device size. double _scaleFactor; diff --git a/lib/common/widgets/select_mask.dart b/lib/common/widgets/select_mask.dart index 5c964bc870..b6ce4e926c 100644 --- a/lib/common/widgets/select_mask.dart +++ b/lib/common/widgets/select_mask.dart @@ -2,7 +2,7 @@ import 'package:PiliPlus/common/style.dart'; import 'package:flutter/material.dart'; Widget selectMask( - ThemeData theme, + ColorScheme colorScheme, bool checked, { BorderRadiusGeometry borderRadius = Style.mdRadius, }) { @@ -23,12 +23,12 @@ Widget selectMask( width: 34, height: 34, decoration: BoxDecoration( - color: theme.colorScheme.surface.withValues(alpha: 0.8), + color: colorScheme.surface.withValues(alpha: 0.8), shape: BoxShape.circle, ), child: Icon( Icons.done_all_outlined, - color: theme.colorScheme.primary, + color: colorScheme.primary, semanticLabel: '取消选择', ), ), diff --git a/lib/common/widgets/self_sized_horizontal_list.dart b/lib/common/widgets/self_sized_horizontal_list.dart index dea354ea8d..777267adb8 100644 --- a/lib/common/widgets/self_sized_horizontal_list.dart +++ b/lib/common/widgets/self_sized_horizontal_list.dart @@ -28,6 +28,9 @@ class _SelfSizedHorizontalListState extends State { @override Widget build(BuildContext context) { if (_height == null) { + if (widget.itemCount == 0) { + return const SizedBox.shrink(); + } return OnlyLayoutWidget( onPerformLayout: (Size size) { if (!mounted) return; diff --git a/lib/common/widgets/sliver/sliver_floating_header.dart b/lib/common/widgets/sliver/sliver_floating_header.dart index 56835cac5c..000b7ca598 100644 --- a/lib/common/widgets/sliver/sliver_floating_header.dart +++ b/lib/common/widgets/sliver/sliver_floating_header.dart @@ -20,13 +20,78 @@ import 'dart:math' as math; import 'package:flutter/foundation.dart' show clampDouble; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' - show RenderSliverSingleBoxAdapter, SliverGeometry; + show RenderSliverSingleBoxAdapter, SliverGeometry, ScrollDirection; /// ref [SliverFloatingHeader] -class SliverFloatingHeaderWidget extends SingleChildRenderObjectWidget { +class SliverFloatingHeaderWidget extends StatelessWidget { const SliverFloatingHeaderWidget({ super.key, + required this.child, + required this.backgroundColor, + }); + + final Widget child; + final Color backgroundColor; + + @override + Widget build(BuildContext context) { + return _SliverFloatingHeaderWidget( + backgroundColor: backgroundColor, + child: _SliverFloatingHeaderScroll(child: child), + ); + } +} + +class _SliverFloatingHeaderScroll extends StatefulWidget { + const _SliverFloatingHeaderScroll({required this.child}); + + final Widget child; + + @override + State<_SliverFloatingHeaderScroll> createState() => + _SliverFloatingHeaderScrollState(); +} + +class _SliverFloatingHeaderScrollState + extends State<_SliverFloatingHeaderScroll> { + ScrollPosition? _position; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + if (_position != null) { + _position!.isScrollingNotifier.removeListener(_isScrollingListener); + } + _position = Scrollable.maybeOf(context)?.position; + if (_position != null) { + _position!.isScrollingNotifier.addListener(_isScrollingListener); + } + } + + @override + void dispose() { + if (_position != null) { + _position!.isScrollingNotifier.removeListener(_isScrollingListener); + } + super.dispose(); + } + + void _isScrollingListener() { + assert(_position != null); + if (_position!.isScrollingNotifier.value) { + final RenderSliverFloatingHeader? renderer = context + .findAncestorRenderObjectOfType(); + renderer?.updateScrollStartDirection(_position!.userScrollDirection); + } + } + + @override + Widget build(BuildContext context) => widget.child; +} + +class _SliverFloatingHeaderWidget extends SingleChildRenderObjectWidget { + const _SliverFloatingHeaderWidget({ required Widget super.child, required this.backgroundColor, }); @@ -48,8 +113,8 @@ class SliverFloatingHeaderWidget extends SingleChildRenderObjectWidget { class RenderSliverFloatingHeader extends RenderSliverSingleBoxAdapter { RenderSliverFloatingHeader({ - required Color backgroundColor, - }) : _backgroundColor = backgroundColor; + required this._backgroundColor, + }); Color _backgroundColor; set backgroundColor(Color value) { @@ -70,6 +135,12 @@ class RenderSliverFloatingHeader extends RenderSliverSingleBoxAdapter { effectiveScrollOffset < child!.size.height); } + ScrollDirection? _lastStartedScrollDirection; + + void updateScrollStartDirection(ScrollDirection direction) { + _lastStartedScrollDirection = direction; + } + @override void performLayout() { if (!floatingHeaderNeedsToBeUpdated) { @@ -78,7 +149,8 @@ class RenderSliverFloatingHeader extends RenderSliverSingleBoxAdapter { double delta = lastScrollOffset! - constraints.scrollOffset; // > 0 when the header is growing - if (constraints.userScrollDirection == .forward) { + if (constraints.userScrollDirection == .forward || + _lastStartedScrollDirection == .forward) { final childExtent = child!.size.height; if (effectiveScrollOffset > childExtent) { effectiveScrollOffset = diff --git a/lib/common/widgets/sliver/sliver_pinned_dynamic_header.dart b/lib/common/widgets/sliver/sliver_pinned_dynamic_header.dart index 725e32d818..32977ed713 100644 --- a/lib/common/widgets/sliver/sliver_pinned_dynamic_header.dart +++ b/lib/common/widgets/sliver/sliver_pinned_dynamic_header.dart @@ -55,10 +55,9 @@ class SliverPinnedDynamicHeader extends SingleChildRenderObjectWidget { class RenderSliverPinnedDynamicHeader extends RenderSliverSingleBoxAdapter { RenderSliverPinnedDynamicHeader({ - required double minExtent, - required double maxExtent, - }) : _minExtent = minExtent, - _maxExtent = maxExtent; + required this._minExtent, + required this._maxExtent, + }); double _minExtent; double get minExtent => _minExtent; diff --git a/lib/common/widgets/sliver/sliver_pinned_header.dart b/lib/common/widgets/sliver/sliver_pinned_header.dart index 2a1df95a85..2d111e1921 100644 --- a/lib/common/widgets/sliver/sliver_pinned_header.dart +++ b/lib/common/widgets/sliver/sliver_pinned_header.dart @@ -47,8 +47,8 @@ class SliverPinnedHeader extends SingleChildRenderObjectWidget { class RenderSliverPinnedHeader extends RenderSliverSingleBoxAdapter { RenderSliverPinnedHeader({ - required Color? backgroundColor, - }) : _backgroundColor = backgroundColor; + required this._backgroundColor, + }); Color? _backgroundColor; set backgroundColor(Color? value) { diff --git a/lib/common/widgets/sliver/sliver_to_box_adapter.dart b/lib/common/widgets/sliver/sliver_to_box_adapter.dart new file mode 100644 index 0000000000..ee5a0a32b4 --- /dev/null +++ b/lib/common/widgets/sliver/sliver_to_box_adapter.dart @@ -0,0 +1,85 @@ +import 'package:flutter/rendering.dart' show RenderSliverToBoxAdapter; +import 'package:flutter/widgets.dart'; + +class SliverToBoxWithOffsetAdapter extends SliverToBoxAdapter { + const SliverToBoxWithOffsetAdapter({ + super.key, + required this.offset, + required this.onVisibilityChanged, + super.child, + }); + + final double offset; + final ValueChanged onVisibilityChanged; + + @override + RenderSliverToBoxWithOffsetAdapter createRenderObject(BuildContext context) => + RenderSliverToBoxWithOffsetAdapter( + offset: offset, + onVisibilityChanged: onVisibilityChanged, + ); +} + +class RenderSliverToBoxWithOffsetAdapter extends RenderSliverToBoxAdapter { + RenderSliverToBoxWithOffsetAdapter({ + required this.offset, + required this.onVisibilityChanged, + super.child, + }); + + bool? _visible; + final double offset; + final ValueChanged onVisibilityChanged; + + @override + void performLayout() { + final visible = constraints.scrollOffset > offset; + if (_visible != visible) { + _visible = visible; + WidgetsBinding.instance.addPostFrameCallback( + (_) => onVisibilityChanged(visible), + ); + } + super.performLayout(); + } +} + +class SliverToBoxWithVisibilityAdapter extends SliverToBoxAdapter { + const SliverToBoxWithVisibilityAdapter({ + super.key, + required this.onVisibilityChanged, + super.child, + }); + + final ValueChanged onVisibilityChanged; + + @override + RenderSliverToBoxWithVisibilityAdapter createRenderObject( + BuildContext context, + ) => RenderSliverToBoxWithVisibilityAdapter( + onVisibilityChanged: onVisibilityChanged, + ); +} + +class RenderSliverToBoxWithVisibilityAdapter extends RenderSliverToBoxAdapter { + RenderSliverToBoxWithVisibilityAdapter({ + required this.onVisibilityChanged, + super.child, + }); + + final ValueChanged onVisibilityChanged; + + bool? _visible; + + @override + void performLayout() { + super.performLayout(); + final visible = geometry!.visible; + if (_visible != visible) { + _visible = visible; + WidgetsBinding.instance.addPostFrameCallback( + (_) => onVisibilityChanged(visible), + ); + } + } +} diff --git a/lib/common/widgets/sliver_wrap.dart b/lib/common/widgets/sliver_wrap.dart index 492a8b8fd4..f23b79903c 100644 --- a/lib/common/widgets/sliver_wrap.dart +++ b/lib/common/widgets/sliver_wrap.dart @@ -65,11 +65,9 @@ class RenderSliverFixedWrap extends RenderSliverMultiBoxAdaptor { RenderSliverFixedWrap({ required super.childManager, required double mainAxisExtent, - double spacing = 0.0, - double runSpacing = 0.0, - }) : _mainAxisExtent = mainAxisExtent, - _spacing = spacing, - _runSpacing = runSpacing { + this._spacing = 0.0, + this._runSpacing = 0.0, + }) : _mainAxisExtent = mainAxisExtent { assert(mainAxisExtent > 0.0 && mainAxisExtent.isFinite); } diff --git a/lib/common/widgets/svg/level_icon.dart b/lib/common/widgets/svg/level_icon.dart new file mode 100644 index 0000000000..71a17746ab --- /dev/null +++ b/lib/common/widgets/svg/level_icon.dart @@ -0,0 +1,293 @@ +// dart format width=120 +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; + +class UserLevel extends LeafRenderObjectWidget { + const UserLevel( + this.level, { + super.key, + this.height = 11, + this.flash = false, + }); + + final double height; + final int level; + final bool flash; + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderLevel(height, level, flash); + } + + @override + void updateRenderObject( + BuildContext context, + RenderLevel renderObject, + ) { + renderObject + ..height = height + ..level = level + ..flash = flash; + } +} + +class RenderLevel extends RenderBox { + RenderLevel(this._height, this._level, this._flash); + + double _height; + set height(double value) { + if (_height == value) return; + _height = value; + markNeedsLayout(); + } + + int _level; + set level(int value) { + if (_level == value) return; + _level = value; + markNeedsPaint(); + markNeedsSemanticsUpdate(); + } + + bool _flash; + set flash(bool value) { + if (_flash == value) return; + _flash = value; + markNeedsLayout(); + } + + @override + Size computeDryLayout(covariant BoxConstraints constraints) { + return constraints.constrainSizeAndAttemptToPreserveAspectRatio( + Size( + (_flash ? LevelCanvas._extendR : LevelCanvas._totalR) * _height / LevelCanvas._totalB, + _height, + ), + ); + } + + @override + void performLayout() { + size = computeDryLayout(constraints); + } + + @override + void paint(PaintingContext context, Offset offset) { + final paint = Paint()..color = lookupBackgroundColor(_level); + LevelCanvas(context.canvas) + ..save() + ..translate(offset.dx, offset.dy) + ..scale(size.height / LevelCanvas._totalB) + ..drawLevelBack(paint, bolt: _flash) + ..drawLevelLv() + ..drawLEDigit(_level, paint..color = Colors.white) + ..restore(); + } + + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config.label = '${_flash ? "硬核" : ""}$_level级'; + } + + static Color lookupBackgroundColor(int level) { + return switch (level) { + 0 || 1 => const Color(0xFFC0C0C0), + 2 => const Color(0xFF8BD29B), + 3 => const Color(0xFF7BCDEF), + 4 => const Color(0xFFFEBB8B), + 5 => const Color(0xFFEE672A), + _ => const Color(0xFFF04C49), + }; + } +} + +extension type LevelCanvas(Canvas _) implements Canvas { + // ========== 布局常量 ========== + static const _r = Radius.circular(20); + + static const double _left = 629; + static const double _right = 877; + static const double _colW = 68; // 竖段宽度 + static const double _lColR = _left + _colW; // 697 + static const double _rColL = _right - _colW; // 810 + + // 三条横线的边界 + static const double _rowH = 68; + static const double _rowSp = 146; + static const double _topY = 55; + static const double _topYB = _topY + _rowH; // 123 + static const double _midY = _topY + _rowSp; // 201 + static const double _midYB = _midY + _rowH; // 269 + static const double _botY = _midY + _rowSp; // 347 + static const double _botYB = _botY + _rowH; // 415 + + // 竖段拼接用的中心线 + static const double _midMid = (_midY + _midYB) / 2; // 235 + + static final _boltIcon = + (ParagraphBuilder( + ParagraphStyle( + fontSize: 460, + fontFamily: Icons.bolt_rounded.fontFamily, + height: 1, + fontWeight: FontWeight.w900, + textDirection: TextDirection.ltr, + ), + )..addText(.fromCharCode(Icons.bolt_rounded.codePoint))).build() + ..layout(const ParagraphConstraints(width: double.infinity)); + void drawBolt() => drawParagraph(_boltIcon, const Offset(840, 5)); + + void _draw1(Paint paint) { + drawRRect(const .fromLTRBXY(673, _botY, 833, _botYB, 20, 20), paint); + drawRRect(.fromLTRBAndCorners(673, _topY, 787, _topYB, topLeft: _r, bottomLeft: _r, topRight: _r), paint); + drawRect(const .fromLTRB(719, _topYB, 787, _botY), paint); + } + + void drawLEDigit(int digit, Paint paint) { + if (digit == 1) return _draw1(paint); + final bits = switch (digit) { + 0 => 0x7E, + 2 => 0x6D, + 3 => 0x79, + 4 => 0x33, + 5 => 0x5B, + 6 => 0x5F, + 7 => 0x70, + 8 => 0x7F, + 9 => 0x7B, + // _ => throw ArgumentError('Unsupported digit: $digit'), + _ => 0x4F, // `E` + }; + + _drawSegments( + bits & 0x40 != 0, + bits & 0x20 != 0, + bits & 0x10 != 0, + bits & 0x08 != 0, + bits & 0x04 != 0, + bits & 0x02 != 0, + bits & 0x01 != 0, + paint, + ); + } + + void _drawSegments(bool a, bool b, bool c, bool d, bool e, bool f, bool g, Paint paint) { + // 横段 + if (a) { + _drawRRect(_left, _topY, _right, _topYB, _r, _r, f ? .zero : _r, b ? .zero : _r, paint); + } + if (g) { + _drawRRect(_left, _midY, _right, _midYB, f ? .zero : _r, b ? .zero : _r, e ? .zero : _r, c ? .zero : _r, paint); + } + if (d) { + _drawRRect(_left, _botY, _right, _botYB, e ? .zero : _r, c ? .zero : _r, _r, _r, paint); + } + + // 竖段 + // 左上竖段 f + if (f) { + final top = (a ? _topYB : _topY) - 1; // 有上横则齐底,否则到顶 + final bottom = (g ? _midY : (e ? _midMid : _midYB)) + 1; + final rTop = a ? Radius.zero : _r; + final rBot = g || e ? Radius.zero : _r; + _drawRRect(_left, top, _lColR, bottom, rTop, rTop, rBot, rBot, paint); + } + + // 右上竖段 b + if (b) { + final top = (a ? _topYB : _topY) - 1; + final bottom = (g ? _midY : (c ? _midMid : _midYB)) + 1; + final rTop = a ? Radius.zero : _r; + final rBot = g || c ? Radius.zero : _r; + _drawRRect(_rColL, top, _right, bottom, rTop, rTop, rBot, rBot, paint); + } + + // 左下竖段 e + if (e) { + final top = (g ? _midYB : (f ? _midMid : _midY)) - 1; + final bottom = (d ? _botY : _botYB) + 1; + final rTop = g || f ? Radius.zero : _r; + final rBot = d ? Radius.zero : _r; + _drawRRect(_left, top, _lColR, bottom, rTop, rTop, rBot, rBot, paint); + } + + // 右下竖段 c + if (c) { + final top = (g ? _midYB : (b ? _midMid : _midY)) - 1; + final bottom = (d ? _botY : _botYB) + 1; + final rTop = g || b ? Radius.zero : _r; + final rBot = d ? Radius.zero : _r; + _drawRRect(_rColL, top, _right, bottom, rTop, rTop, rBot, rBot, paint); + } + } + + /// 绘制圆角矩形,四角全零时退化为矩形 + void _drawRRect(double l, double t, double r, double b, Radius tl, Radius tr, Radius bl, Radius br, Paint paint) { + if (tl == .zero && tr == .zero && bl == .zero && br == .zero) { + drawRect(.fromLTRB(l, t, r, b), paint); + } else { + drawRRect(.fromLTRBAndCorners(l, t, r, b, topLeft: tl, topRight: tr, bottomLeft: bl, bottomRight: br), paint); + } + } + + static final _lvPicture = () { + final recorder = PictureRecorder(); + final paint = Paint()..color = Colors.white; + final canvas = Canvas(recorder); + + const double vLeft = 296; + const double lvTop = 106; + const double llr = 123; + const double vtb = 282; + + canvas + // L + ..drawRRect(.fromLTRBAndCorners(56, lvTop, llr, _botYB, topLeft: _r, topRight: _r, bottomLeft: _r), paint) + ..drawRRect(.fromLTRBAndCorners(llr - 1, _botY, 256, _botYB, topRight: _r, bottomRight: _r), paint) + // V + ..drawRRect(.fromLTRBAndCorners(vLeft, lvTop, 363, vtb + 1, topLeft: _r, topRight: _r), paint) + ..drawRRect(.fromLTRBAndCorners(476, lvTop, 543, vtb + 1, topLeft: _r, topRight: _r), paint) + ..drawPath( + Path() + ..moveTo(vLeft, vtb) + ..lineTo(vLeft, 292) + ..arcToPoint(const Offset(300, 313), radius: const .circular(50), clockwise: false) + ..lineTo(395, 408) + ..arcToPoint(const Offset(444, 408), radius: const .circular(50), clockwise: false) + ..lineTo(539, 313) + ..arcToPoint(const Offset(543, 292), radius: const .circular(50), clockwise: false) + ..lineTo(543, vtb) + ..lineTo(476, vtb) + ..lineTo(419.5, 340) + ..lineTo(363, vtb) + ..close(), + paint, + ); + return recorder.endRecording(); + }(); + + void drawLevelLv() => drawPicture(_lvPicture); + + static const double _totalR = 930; + static const double _extendR = 1250; + static const double _totalB = 466; + + void drawLevelBack(Paint paint, {bool bolt = false}) { + const radius = Radius.circular(27); + final double right = bolt ? _extendR : _totalR; + const double blockTop = 48; + drawRRect( + RRect.fromLTRBAndCorners(0, blockTop, right, _totalB, topLeft: radius, bottomLeft: radius, bottomRight: radius), + paint, + ); + drawRRect( + RRect.fromLTRBAndCorners(576, 0, right, blockTop + 1, topLeft: radius, topRight: radius), + paint, + ); + + if (bolt) drawBolt(); + } +} diff --git a/lib/common/widgets/svg/play_icon.dart b/lib/common/widgets/svg/play_icon.dart new file mode 100644 index 0000000000..2b64a09580 --- /dev/null +++ b/lib/common/widgets/svg/play_icon.dart @@ -0,0 +1,218 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/semantics.dart'; + +class PlayIcon extends LeafRenderObjectWidget { + const PlayIcon({super.key, this.size = 60}); + + final double size; + + @override + RenderObject createRenderObject(BuildContext context) { + return RenderPlay(size); + } + + @override + void updateRenderObject(BuildContext context, RenderPlay renderObject) { + renderObject.imgSize = size; + } +} + +class RenderPlay extends RenderBox { + RenderPlay(this._imgSize); + + double _imgSize; + set imgSize(double value) { + if (_imgSize == value) return; + _imgSize = value; + markNeedsLayout(); + } + + @override + Size computeDryLayout(covariant BoxConstraints constraints) { + return constraints.constrainDimensions(_imgSize, _imgSize); + } + + @override + void performLayout() { + size = computeDryLayout(constraints); + } + + @override + void paint(PaintingContext context, Offset offset) { + final canvas = context.canvas; + final size = this.size.shortestSide; + if (offset != .zero || size != 60) { + canvas.save(); + if (offset != .zero) canvas.translate(offset.dx, offset.dy); + if (size != 60) { + canvas.scale(size / 60); + } + } + canvas.drawPicture(_picture); + if (offset != .zero || size != 60) { + canvas.restore(); + } + } + + @override + void describeSemanticsConfiguration(SemanticsConfiguration config) { + super.describeSemanticsConfiguration(config); + config.label = '播放'; + } + + /// [SvgPicture] can not parse mask filter + /// fom i0.hdslb.com/bfs/static/player/img/play.svg + /// scale size from 80 to 60 + static final _picture = () { + final rec = PictureRecorder(); + final canvas = Canvas(rec); + final path = Path() + ..moveTo(41.576, 7.318) + ..cubicTo(41.244, 5.892, 39.91, 4.886, 38.41, 5.011) + ..cubicTo(38.068, 5.039, 37.813, 5.13, 37.59, 5.245) + ..cubicTo(37.37, 5.361, 37.187, 5.506, 37.034, 5.672) + ..cubicTo(36.957, 5.754, 36.891, 5.844, 36.824, 5.934) + ..lineTo(36.622, 6.203) + ..lineTo(36.222, 6.743) + ..cubicTo(35.694, 7.467, 35.178, 8.2, 34.678, 8.945) + ..cubicTo(34.179, 9.69, 33.694, 10.445, 33.231, 11.217) + ..cubicTo(33.092, 11.449, 32.954, 11.683, 32.819, 11.917) + ..cubicTo(32.258, 11.909, 31.697, 11.902, 31.137, 11.898) + ..cubicTo(29.094, 11.884, 27.051, 11.891, 25.008, 11.926) + ..cubicTo(24.871, 11.688, 24.732, 11.452, 24.591, 11.217) + ..cubicTo(24.128, 10.445, 23.643, 9.69, 23.144, 8.945) + ..cubicTo(22.645, 8.2, 22.129, 7.467, 21.6, 6.743) + ..lineTo(21.2, 6.203) + ..lineTo(20.998, 5.934) + ..cubicTo(20.931, 5.844, 20.865, 5.754, 20.788, 5.672) + ..cubicTo(20.635, 5.506, 20.452, 5.361, 20.232, 5.245) + ..cubicTo(20.009, 5.13, 19.754, 5.039, 19.412, 5.011) + ..cubicTo(17.956, 4.888, 16.59, 5.85, 16.246, 7.318) + ..cubicTo(16.168, 7.652, 16.176, 7.924, 16.217, 8.172) + ..cubicTo(16.26, 8.418, 16.34, 8.636, 16.451, 8.833) + ..cubicTo(16.506, 8.931, 16.571, 9.023, 16.635, 9.114) + ..lineTo(16.829, 9.389) + ..lineTo(17.219, 9.936) + ..cubicTo(17.743, 10.663, 18.281, 11.381, 18.834, 12.086) + ..cubicTo(18.845, 12.099, 18.855, 12.112, 18.865, 12.124) + ..cubicTo(18.025, 12.164, 17.184, 12.209, 16.344, 12.26) + ..cubicTo(15.523, 12.311, 14.701, 12.365, 13.88, 12.428) + ..lineTo(12.648, 12.525) + ..lineTo(12.032, 12.577) + ..lineTo(11.68, 12.616) + ..cubicTo(11.562, 12.63, 11.445, 12.651, 11.328, 12.668) + ..cubicTo(10.39, 12.827, 9.477, 13.141, 8.641, 13.595) + ..cubicTo(7.804, 14.049, 7.043, 14.641, 6.399, 15.34) + ..cubicTo(5.754, 16.04, 5.224, 16.845, 4.837, 17.716) + ..cubicTo(4.45, 18.586, 4.208, 19.521, 4.12, 20.467) + ..cubicTo(3.808, 23.756, 3.603, 27.055, 3.529, 30.365) + ..cubicTo(3.453, 33.676, 3.53, 36.99, 3.722, 40.289) + ..cubicTo(3.77, 41.114, 3.825, 41.939, 3.887, 42.763) + ..lineTo(3.986, 43.998) + ..lineTo(4.039, 44.616) + ..lineTo(4.046, 44.693) + ..lineTo(4.056, 44.782) + ..lineTo(4.075, 44.961) + ..cubicTo(4.087, 45.08, 4.107, 45.198, 4.126, 45.317) + ..cubicTo(4.278, 46.264, 4.586, 47.189, 5.037, 48.037) + ..cubicTo(5.486, 48.887, 6.078, 49.66, 6.777, 50.319) + ..cubicTo(7.475, 50.978, 8.283, 51.522, 9.16, 51.921) + ..cubicTo(10.035, 52.319, 10.978, 52.575, 11.935, 52.664) + ..cubicTo(11.998, 52.672, 12.047, 52.675, 12.098, 52.68) + ..lineTo(12.252, 52.693) + ..lineTo(12.56, 52.72) + ..lineTo(13.176, 52.771) + ..lineTo(14.408, 52.868) + ..cubicTo(15.23, 52.927, 16.052, 52.985, 16.874, 53.033) + ..cubicTo(23.449, 53.424, 30.03, 53.502, 36.609, 53.259) + ..cubicTo(38.254, 53.199, 39.898, 53.118, 41.542, 53.016) + ..cubicTo(42.364, 52.963, 43.186, 52.908, 44.008, 52.843) + ..lineTo(45.241, 52.743) + ..lineTo(45.857, 52.689) + ..lineTo(46.214, 52.65) + ..cubicTo(46.334, 52.635, 46.452, 52.614, 46.571, 52.596) + ..cubicTo(47.52, 52.432, 48.443, 52.112, 49.288, 51.649) + ..cubicTo(50.134, 51.188, 50.902, 50.586, 51.553, 49.878) + ..cubicTo(52.204, 49.17, 52.739, 48.353, 53.127, 47.471) + ..cubicTo(53.321, 47.03, 53.479, 46.573, 53.598, 46.107) + ..cubicTo(53.631, 45.991, 53.656, 45.873, 53.681, 45.755) + ..lineTo(53.719, 45.579) + ..lineTo(53.749, 45.401) + ..cubicTo(53.77, 45.283, 53.79, 45.164, 53.803, 45.045) + ..cubicTo(53.818, 44.927, 53.834, 44.8, 53.843, 44.704) + ..cubicTo(54.179, 41.414, 54.402, 38.111, 54.476, 34.794) + ..cubicTo(54.553, 31.475, 54.442, 28.153, 54.205, 24.853) + ..cubicTo(54.145, 24.028, 54.078, 23.204, 54.002, 22.38) + ..lineTo(53.884, 21.145) + ..lineTo(53.82, 20.528) + ..lineTo(53.804, 20.374) + ..lineTo(53.794, 20.29) + ..lineTo(53.782, 20.201) + ..cubicTo(53.766, 20.083, 53.754, 19.964, 53.731, 19.846) + ..cubicTo(53.578, 18.901, 53.266, 17.979, 52.813, 17.136) + ..cubicTo(52.362, 16.291, 51.771, 15.522, 51.073, 14.869) + ..cubicTo(50.375, 14.215, 49.57, 13.677, 48.698, 13.284) + ..cubicTo(47.827, 12.89, 46.89, 12.64, 45.94, 12.552) + ..lineTo(45.854, 12.544) + ..lineTo(45.777, 12.537) + ..lineTo(45.623, 12.524) + ..lineTo(45.315, 12.499) + ..lineTo(44.698, 12.449) + ..lineTo(43.466, 12.357) + ..cubicTo(42.644, 12.3, 41.822, 12.247, 41.0, 12.202) + ..cubicTo(40.326, 12.164, 39.651, 12.131, 38.977, 12.1) + ..cubicTo(38.98, 12.096, 38.984, 12.091, 38.988, 12.086) + ..cubicTo(39.542, 11.381, 40.079, 10.663, 40.603, 9.936) + ..lineTo(40.994, 9.389) + ..lineTo(41.187, 9.114) + ..cubicTo(41.252, 9.023, 41.316, 8.931, 41.371, 8.833) + ..cubicTo(41.482, 8.636, 41.563, 8.418, 41.605, 8.172) + ..cubicTo(41.646, 7.924, 41.654, 7.652, 41.576, 7.318) + ..close() + ..moveTo(21.283, 26.038) + ..cubicTo(21.321, 25.666, 21.427, 25.305, 21.597, 24.973) + ..cubicTo(22.351, 23.498, 24.158, 22.913, 25.634, 23.667) + ..lineTo(26.683, 24.211) + ..cubicTo(28.428, 25.126, 30.148, 26.088, 31.842, 27.097) + ..cubicTo(34.726, 28.814, 34.726, 28.814, 37.376, 30.628) + ..cubicTo(37.694, 30.846, 37.967, 31.123, 38.18, 31.444) + ..cubicTo(39.096, 32.824, 38.72, 34.686, 37.34, 35.603) + ..lineTo(36.265, 36.309) + ..cubicTo(34.823, 37.245, 33.349, 38.161, 31.842, 39.058) + ..cubicTo(28.87, 40.828, 28.87, 40.828, 25.698, 42.513) + ..cubicTo(25.352, 42.697, 24.973, 42.811, 24.583, 42.849) + ..cubicTo(22.934, 43.01, 21.466, 41.805, 21.305, 40.156) + ..lineTo(21.221, 39.247) + ..cubicTo(21.04, 37.126, 20.949, 35.005, 20.949, 32.884) + ..cubicTo(20.949, 29.361, 20.949, 29.361, 21.283, 26.038) + ..close(); + + final paint = Paint() + ..color = Colors.black.withValues(alpha: 0.3 * 0.8) + ..maskFilter = const .blur(.normal, 1.0); + + // feOffset dy="2" + canvas + ..save() + ..translate(0, 2) + ..drawPath(path, paint) + ..restore(); + + // dy=0, blur=3.5 + paint + ..color = Colors.black.withValues(alpha: 0.2 * 0.8) + ..maskFilter = const .blur(.normal, 3.5); + + canvas.drawPath(path, paint); + + paint + ..color = Colors.white.withValues(alpha: 0.8) + ..maskFilter = null; + + canvas.drawPath(path, paint); + + return rec.endRecording(); + }(); +} diff --git a/lib/common/widgets/video_card/video_card_h.dart b/lib/common/widgets/video_card/video_card_h.dart index 017f7079e3..a47c147ebb 100644 --- a/lib/common/widgets/video_card/video_card_h.dart +++ b/lib/common/widgets/video_card/video_card_h.dart @@ -1,24 +1,18 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/progress_bar/video_progress_indicator.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; import 'package:PiliPlus/common/widgets/video_popup_menu.dart'; import 'package:PiliPlus/http/search.dart'; -import 'package:PiliPlus/models/common/badge_type.dart'; -import 'package:PiliPlus/models/common/stat_type.dart'; -import 'package:PiliPlus/models/model_hot_video_item.dart'; -import 'package:PiliPlus/models/model_video.dart'; -import 'package:PiliPlus/models/search/result.dart'; +import 'package:PiliPlus/models/horizontal_video_model.dart'; import 'package:PiliPlus/models_new/video/video_detail/dimension.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:flutter/material.dart'; // 视频卡片 - 水平布局 class VideoCardH extends StatelessWidget { @@ -29,37 +23,13 @@ class VideoCardH extends StatelessWidget { this.onViewLater, this.onRemove, }); - final BaseVideoItemModel videoItem; + final HorizontalVideoModel videoItem; final VoidCallback? onTap; final ValueChanged? onViewLater; final VoidCallback? onRemove; @override Widget build(BuildContext context) { - String type = 'video'; - String? badge; - if (videoItem case final SearchVideoItemModel item) { - final typeOrNull = item.type; - if (typeOrNull != null && typeOrNull.isNotEmpty) { - type = typeOrNull; - if (type == 'ketang') { - badge = '课堂'; - } else if (type == 'live_room') { - badge = '直播'; - } - } - if (item.isUnionVideo == 1) { - badge = '合作'; - } - } else if (videoItem case final HotVideoItemModel item) { - if (item.isCharging == true) { - badge = '充电专属'; - } else if (item.isCooperation == 1) { - badge = '合作'; - } else { - badge = item.pgcLabel; - } - } void onLongPress() => imageSaveDialog( bvid: videoItem.bvid, title: videoItem.title, @@ -67,9 +37,9 @@ class VideoCardH extends StatelessWidget { ); final theme = Theme.of(context); return Material( - type: MaterialType.transparency, + type: .transparency, child: Stack( - clipBehavior: Clip.none, + clipBehavior: .none, children: [ InkWell( onLongPress: onLongPress, @@ -77,33 +47,25 @@ class VideoCardH extends StatelessWidget { onTap: onTap ?? () async { - if (type == 'ketang') { - PageUtils.viewPugv(seasonId: videoItem.aid); + if (videoItem.isPugv ?? false) { + PageUtils.viewPugv(seasonId: videoItem.seasonId); return; - } else if (type == 'live_room') { - if (videoItem case final SearchVideoItemModel item) { - int? roomId = item.id; - if (roomId != null) { - PageUtils.toLiveRoom(roomId); - } - } else { - SmartDialog.showToast( - 'err: live_room : ${videoItem.runtimeType}', - ); + } + + if (videoItem.isLive ?? false) { + if (videoItem.roomId case final roomId?) { + PageUtils.toLiveRoom(roomId); } return; } - Dimension? dimension; - if (videoItem case final HotVideoItemModel item) { - if (item.redirectUrl?.isNotEmpty == true && - PageUtils.viewPgcFromUri(item.redirectUrl!)) { - return; - } - dimension = item.dimension; + if (videoItem.redirectUrl?.isNotEmpty == true && + PageUtils.viewPgcFromUri(videoItem.redirectUrl!)) { + return; } int? cid = videoItem.cid; + Dimension? dimension = videoItem.dimension; if (cid == null) { if (await SearchHttp.ab2cWithDimension( aid: videoItem.aid, @@ -125,40 +87,38 @@ class VideoCardH extends StatelessWidget { } }, child: Padding( - padding: const EdgeInsets.symmetric( + padding: const .symmetric( horizontal: Style.safeSpace, vertical: 5, ), child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + crossAxisAlignment: .start, + children: [ AspectRatio( aspectRatio: Style.aspectRatio, child: LayoutBuilder( builder: (context, boxConstraints) { final double maxWidth = boxConstraints.maxWidth; final double maxHeight = boxConstraints.maxHeight; - num? progress; - if (videoItem case final HotVideoItemModel item) { - progress = item.progress; - } + + final progress = videoItem.progress; return Stack( - clipBehavior: Clip.none, + clipBehavior: .none, children: [ NetworkImgLayer( src: videoItem.cover, width: maxWidth, height: maxHeight, ), - if (badge != null) + if (videoItem.badge case final badge?) PBadge( text: badge, top: 6.0, right: 6.0, type: switch (badge) { - '充电专属' => PBadgeType.error, - _ => PBadgeType.primary, + '充电专属' => .error, + _ => .primary, }, ), if (progress != null && progress != 0) ...[ @@ -168,7 +128,7 @@ class VideoCardH extends StatelessWidget { : '${DurationUtils.formatDuration(progress)}/${DurationUtils.formatDuration(videoItem.duration)}', right: 6, bottom: 8, - type: PBadgeType.gray, + type: .gray, ), Positioned( left: 0, @@ -190,7 +150,7 @@ class VideoCardH extends StatelessWidget { ), right: 6.0, bottom: 6.0, - type: PBadgeType.gray, + type: .gray, ), ], ); @@ -224,45 +184,44 @@ class VideoCardH extends StatelessWidget { if (pubdate != '') pubdate += ' '; return Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: .start, children: [ - if (videoItem case final SearchVideoItemModel item) ...[ - if (item.titleList?.isNotEmpty == true) - Expanded( - child: Text.rich( - overflow: TextOverflow.ellipsis, - maxLines: 2, - TextSpan( - children: item.titleList! - .map( - (e) => TextSpan( - text: e.text, - style: TextStyle( - fontSize: theme.textTheme.bodyMedium!.fontSize, - height: 1.42, - letterSpacing: 0.3, - color: e.isEm - ? theme.colorScheme.primary - : theme.colorScheme.onSurface, - ), + if (videoItem.titleList?.isNotEmpty == true) + Expanded( + child: Text.rich( + overflow: .ellipsis, + maxLines: 2, + TextSpan( + children: videoItem.titleList! + .map( + (e) => TextSpan( + text: e.text, + style: TextStyle( + fontSize: theme.textTheme.bodyMedium!.fontSize, + height: 1.42, + letterSpacing: 0.3, + color: e.isEm + ? theme.colorScheme.primary + : theme.colorScheme.onSurface, ), - ) - .toList(), - ), + ), + ) + .toList(), ), ), - ] else + ) + else Expanded( child: Text( videoItem.title, - textAlign: TextAlign.start, + textAlign: .start, style: TextStyle( fontSize: theme.textTheme.bodyMedium!.fontSize, height: 1.42, letterSpacing: 0.3, ), maxLines: 2, - overflow: TextOverflow.ellipsis, + overflow: .ellipsis, ), ), Text( @@ -272,7 +231,7 @@ class VideoCardH extends StatelessWidget { fontSize: 12, height: 1, color: theme.colorScheme.outline, - overflow: TextOverflow.clip, + overflow: .clip, ), ), const SizedBox(height: 3), @@ -280,11 +239,11 @@ class VideoCardH extends StatelessWidget { spacing: 8, children: [ StatWidget( - type: StatType.play, + type: .play, value: videoItem.stat.view, ), StatWidget( - type: StatType.danmaku, + type: .danmaku, value: videoItem.stat.danmu, ), ], diff --git a/lib/common/widgets/video_card/video_card_v.dart b/lib/common/widgets/video_card/video_card_v.dart index 7c998f7945..1aee0e2eea 100644 --- a/lib/common/widgets/video_card/video_card_v.dart +++ b/lib/common/widgets/video_card/video_card_v.dart @@ -1,6 +1,5 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; @@ -13,11 +12,11 @@ import 'package:PiliPlus/models_new/video/video_detail/dimension.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; +import 'package:PiliPlus/utils/extension/dimension_ext.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:PiliPlus/utils/utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:intl/intl.dart'; @@ -44,7 +43,7 @@ class VideoCardV extends StatelessWidget { Dimension? dimension; if (videoItem is RcmdVideoItemAppModel) { if (videoItem.uri case final uri?) { - isVertical = Utils.getDimensionFromUri(uri); + isVertical = uri.isVerticalFromUri; } } if (cid == null) { diff --git a/lib/common/widgets/video_popup_menu.dart b/lib/common/widgets/video_popup_menu.dart index 75deb62439..1650374671 100644 --- a/lib/common/widgets/video_popup_menu.dart +++ b/lib/common/widgets/video_popup_menu.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/models/common/account_type.dart'; @@ -52,13 +53,7 @@ class VideoPopupMenu extends StatelessWidget { if (videoItem.bvid?.isNotEmpty == true) ...[ _VideoCustomAction( videoItem.bvid!, - const Stack( - clipBehavior: Clip.none, - children: [ - Icon(MdiIcons.identifier, size: 16), - Icon(MdiIcons.circleOutline, size: 16), - ], - ), + const Icon(CustomIcons.identifier_circle, size: 16), () => Utils.copyText(videoItem.bvid!), ), _VideoCustomAction( @@ -69,30 +64,7 @@ class VideoPopupMenu extends StatelessWidget { if (videoItem.cid != null && Pref.enableAi) _VideoCustomAction( 'AI总结', - const Stack( - alignment: Alignment.center, - clipBehavior: Clip.none, - children: [ - Icon(Icons.circle_outlined, size: 16), - ExcludeSemantics( - child: Text( - 'AI', - style: TextStyle( - fontSize: 10, - height: 1, - fontWeight: FontWeight.w700, - ), - strutStyle: StrutStyle( - fontSize: 10, - height: 1, - leading: 0, - fontWeight: FontWeight.w700, - ), - textScaler: TextScaler.noScaling, - ), - ), - ], - ), + const Icon(CustomIcons.ai_circle, size: 16), () async { final res = await UgcIntroController.getAiConclusion( videoItem.bvid!, @@ -175,134 +147,115 @@ class VideoPopupMenu extends StatelessWidget { showDialog( context: context, builder: (context) { - return AlertDialog( - content: SingleChildScrollView( - child: Column( - crossAxisAlignment: .start, - children: [ - if (tp.dislikeReasons != null) ...[ - const Text('我不想看'), - const SizedBox(height: 5), - Wrap( - spacing: 8.0, - runSpacing: 8.0, - children: tp.dislikeReasons!.map(( - item, - ) { - return actionButton(item, null); - }).toList(), - ), - ], - if (tp.feedbacks != null) ...[ - const SizedBox(height: 5), - const Text('反馈'), - const SizedBox(height: 5), - Wrap( - spacing: 8.0, - runSpacing: 8.0, - children: tp.feedbacks!.map((item) { - return actionButton(null, item); - }).toList(), - ), - ], - const Divider(), - Center( - child: FilledButton.tonal( - onPressed: () async { - SmartDialog.showLoading( - msg: '正在提交', - ); - final res = - await VideoHttp.feedDislikeCancel( - id: item.param!, - goto: item.goto!, - ); - SmartDialog.dismiss(); - SmartDialog.showToast( - res.isSuccess - ? "成功" - : res.toString(), + return SimpleDialog( + contentPadding: const .fromLTRB(24, 16, 24, 24), + children: [ + if (tp.dislikeReasons != null) ...[ + const Text('我不想看'), + const SizedBox(height: 5), + Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: tp.dislikeReasons! + .map((item) => actionButton(item, null)) + .toList(), + ), + ], + if (tp.feedbacks != null) ...[ + const SizedBox(height: 5), + const Text('反馈'), + const SizedBox(height: 5), + Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: tp.feedbacks! + .map((item) => actionButton(null, item)) + .toList(), + ), + ], + const Divider(), + Center( + child: FilledButton.tonal( + onPressed: () async { + SmartDialog.showLoading( + msg: '正在提交', + ); + final res = + await VideoHttp.feedDislikeCancel( + id: item.param!, + goto: item.goto!, ); - Get.back(); - }, - style: FilledButton.styleFrom( - visualDensity: VisualDensity.compact, - ), - child: const Text("撤销"), - ), + SmartDialog.dismiss(); + SmartDialog.showToast( + res.isSuccess ? "成功" : res.toString(), + ); + Get.back(); + }, + style: FilledButton.styleFrom( + visualDensity: VisualDensity.compact, ), - ], + child: const Text("撤销"), + ), ), - ), + ], ); }, ); } else { showDialog( context: context, - builder: (context) => AlertDialog( - content: SingleChildScrollView( - child: Column( + builder: (context) => SimpleDialog( + contentPadding: const .all(24), + children: [ + const Center(child: Text("web端暂不支持精细选择")), + const SizedBox(height: 5), + Wrap( + spacing: 5.0, + runSpacing: 2.0, + alignment: .center, children: [ - const SizedBox(height: 5), - const Text("web端暂不支持精细选择"), - const SizedBox(height: 5), - Wrap( - spacing: 5.0, - runSpacing: 2.0, - children: [ - FilledButton.tonal( - onPressed: () async { - Get.back(); - SmartDialog.showLoading( - msg: '正在提交', - ); - final res = - await VideoHttp.dislikeVideo( - bvid: videoItem.bvid!, - type: true, - ); - SmartDialog.dismiss(); - if (res.isSuccess) { - SmartDialog.showToast('点踩成功'); - onRemove?.call(); - } else { - res.toast(); - } - }, - style: FilledButton.styleFrom( - visualDensity: VisualDensity.compact, - ), - child: const Text("点踩"), - ), - FilledButton.tonal( - onPressed: () async { - Get.back(); - SmartDialog.showLoading( - msg: '正在提交', - ); - final res = - await VideoHttp.dislikeVideo( - bvid: videoItem.bvid!, - type: false, - ); - SmartDialog.dismiss(); - SmartDialog.showToast( - res.isSuccess - ? '取消踩' - : res.toString(), - ); - }, - style: FilledButton.styleFrom( - visualDensity: VisualDensity.compact, - ), - child: const Text("撤销"), - ), - ], + FilledButton.tonal( + onPressed: () async { + Get.back(); + SmartDialog.showLoading(msg: '正在提交'); + final res = await VideoHttp.dislikeVideo( + bvid: videoItem.bvid!, + type: true, + ); + SmartDialog.dismiss(); + if (res.isSuccess) { + SmartDialog.showToast('点踩成功'); + onRemove?.call(); + } else { + res.toast(); + } + }, + style: FilledButton.styleFrom( + visualDensity: .compact, + ), + child: const Text("点踩"), + ), + FilledButton.tonal( + onPressed: () async { + Get.back(); + SmartDialog.showLoading(msg: '正在提交'); + final res = await VideoHttp.dislikeVideo( + bvid: videoItem.bvid!, + type: false, + ); + SmartDialog.dismiss(); + SmartDialog.showToast( + res.isSuccess ? '取消踩' : res.toString(), + ); + }, + style: FilledButton.styleFrom( + visualDensity: .compact, + ), + child: const Text("撤销"), ), ], ), - ), + ], ), ); } @@ -326,9 +279,7 @@ class VideoPopupMenu extends StatelessWidget { child: Text( '点错了', style: TextStyle( - color: Theme.of( - context, - ).colorScheme.outline, + color: ColorScheme.of(context).outline, ), ), ), diff --git a/lib/grpc/audio.dart b/lib/grpc/audio.dart index 09d8eb6cf2..08b39ac670 100644 --- a/lib/grpc/audio.dart +++ b/lib/grpc/audio.dart @@ -25,8 +25,8 @@ abstract final class AudioGrpc { playerArgs: PlayerArgs( qn: Int64(qn), fnval: Int64(fnval), - forceHost: Int64(2), - voiceBalance: Int64(1), + forceHost: Int64.TWO, + voiceBalance: Int64.ONE, ), ), PlayURLResp.fromBuffer, @@ -60,8 +60,8 @@ abstract final class AudioGrpc { playerArgs: PlayerArgs( qn: Int64(qn), fnval: Int64(fnval), - forceHost: Int64(2), - voiceBalance: Int64(1), + forceHost: Int64.TWO, + voiceBalance: Int64.ONE, ), extraId: extraId, sortOpt: SortOption(order: order), diff --git a/lib/grpc/dm.dart b/lib/grpc/dm.dart index 5c5c1f4477..74ffb7bdc7 100644 --- a/lib/grpc/dm.dart +++ b/lib/grpc/dm.dart @@ -21,4 +21,12 @@ abstract final class DmGrpc { isolate: true, ); } + + static Future> dmView(int aid, int cid) { + return GrpcReq.request( + GrpcUrl.dmView, + DmViewReq(pid: Int64(aid), oid: Int64(cid), type: 1), + DmViewReply.fromBuffer, + ); + } } diff --git a/lib/grpc/space.dart b/lib/grpc/space.dart index 232b86bcc8..683c43b16c 100644 --- a/lib/grpc/space.dart +++ b/lib/grpc/space.dart @@ -17,10 +17,7 @@ abstract final class SpaceGrpc { GrpcUrl.opusSpaceFlow, OpusSpaceFlowReq( hostMid: Int64(hostMid), - pagination: Pagination( - pageSize: 20, - next: next, - ), + pagination: Pagination(pageSize: 20, next: next), filterType: filterType, ), OpusSpaceFlowResp.fromBuffer, diff --git a/lib/grpc/url.dart b/lib/grpc/url.dart index 6ace7c1cc1..78238ef021 100644 --- a/lib/grpc/url.dart +++ b/lib/grpc/url.dart @@ -14,6 +14,7 @@ abstract final class GrpcUrl { // danmaku static const dmSegMobile = '/bilibili.community.service.dm.v1.DM/DmSegMobile'; + static const dmView = '/bilibili.community.service.dm.v1.DM/DmView'; // reply static const reply = '/bilibili.main.community.reply.v1.Reply'; diff --git a/lib/grpc/view.dart b/lib/grpc/view.dart index e8de15f062..c0392d2205 100644 --- a/lib/grpc/view.dart +++ b/lib/grpc/view.dart @@ -10,9 +10,7 @@ abstract final class ViewGrpc { }) { return GrpcReq.request( GrpcUrl.view, - ViewReq( - bvid: bvid, - ), + ViewReq(bvid: bvid), ViewReply.fromBuffer, ); } diff --git a/lib/http/api.dart b/lib/http/api.dart index 7b6f64c420..27821f7aa0 100644 --- a/lib/http/api.dart +++ b/lib/http/api.dart @@ -1006,4 +1006,10 @@ abstract final class Api { '${HttpString.liveBaseUrl}/xlive/app-ucenter/v1/guard/MainGuardCardAll'; static const String bubble = '/x/tribee/v1/dyn/all'; + + static const String sortFollowTag = '/x/relation/tags/update_sort'; + + static const String replyReport = '/x/v2/reply/report'; + + static const String dynReaction = '/x/polymer/web-dynamic/v1/detail/reaction'; } diff --git a/lib/http/browser_ua.dart b/lib/http/browser_ua.dart index 9414fbd1cf..8211c2ebc4 100644 --- a/lib/http/browser_ua.dart +++ b/lib/http/browser_ua.dart @@ -7,5 +7,5 @@ abstract final class BrowserUa { 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.2 Safari/605.1.15'; static const mob = - 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36'; + 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.101 Mobile Safari/537.36 os/android build/8430300 osVer/10 sdkInt/29 network/2 BiliApp/8430300 mobi_app/android_q channel/master innerVer/8430300'; } diff --git a/lib/http/download.dart b/lib/http/download.dart index c05093cb5e..9c14f50f28 100644 --- a/lib/http/download.dart +++ b/lib/http/download.dart @@ -2,7 +2,6 @@ import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/video.dart'; import 'package:PiliPlus/models/common/account_type.dart'; import 'package:PiliPlus/models/common/video/audio_quality.dart'; -import 'package:PiliPlus/models/common/video/video_decode_type.dart'; import 'package:PiliPlus/models/common/video/video_quality.dart'; import 'package:PiliPlus/models/common/video/video_type.dart'; import 'package:PiliPlus/models/video/play/url.dart'; @@ -12,6 +11,7 @@ import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/video_utils.dart'; +import 'package:collection/collection.dart'; abstract final class DownloadHttp { static const String referer = "https://www.bilibili.com/"; @@ -39,9 +39,9 @@ abstract final class DownloadHttp { }, ); if (res case Success(:final response)) { - final Dash? dash = response.dash; + final dash = response.dash; if (dash != null) { - final List videoList = dash.video!; + final videoList = dash.video!; final curHighestVideoQa = videoList.first.quality.code; final preferVideoQa = entry.preferedVideoQuality; int targetVideoQa = curHighestVideoQa; @@ -54,19 +54,18 @@ abstract final class DownloadHttp { ); } - /// 取出符合当前画质的videoList - final List videosList = videoList - .where((e) => e.quality.code == targetVideoQa) - .toList(); - /// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式 - final List supportFormats = response.supportFormats!; + final supportFormats = response.supportFormats!; // 根据画质选编码格式 - final FormatItem targetSupportFormats = supportFormats.firstWhere( + final targetSupportFormats = supportFormats.firstWhere( (e) => e.quality == targetVideoQa, orElse: () => supportFormats.first, ); - final List supportDecodeFormats = targetSupportFormats.codecs!; + + final currentDecodeFormats = VideoUtils.selectCodec( + targetSupportFormats.codecs!, + Pref.preferCodecs, + ); entry ..typeTag = targetVideoQa.toString() @@ -76,31 +75,10 @@ abstract final class DownloadHttp { targetSupportFormats.newDesc ?? VideoQuality.fromCode(targetVideoQa).desc; - String preferDecode = Pref.defaultDecode; // def avc - String preferSecondDecode = Pref.secondDecode; // def av1 - - // 默认从设置中取AV1 - VideoDecodeFormatType currentDecodeFormats = - VideoDecodeFormatType.fromString(preferDecode); - VideoDecodeFormatType secondDecodeFormats = - VideoDecodeFormatType.fromString(preferSecondDecode); - // 当前视频没有对应格式返回第一个 - int flag = 0; - for (final e in supportDecodeFormats) { - if (currentDecodeFormats.codes.any(e.startsWith)) { - flag = 1; - break; - } else if (secondDecodeFormats.codes.any(e.startsWith)) { - flag = 2; - } - } - if (flag == 2) { - currentDecodeFormats = secondDecodeFormats; - } else if (flag == 0) { - currentDecodeFormats = VideoDecodeFormatType.fromString( - supportDecodeFormats.first, - ); - } + /// 取出符合当前画质的videoList + final videosList = videoList + .where((e) => e.quality.code == targetVideoQa) + .toList(); /// 取出符合当前解码格式的videoItem final videoDash = videosList.firstWhere( diff --git a/lib/http/dynamics.dart b/lib/http/dynamics.dart index 7ef924a8de..6553bb9b70 100644 --- a/lib/http/dynamics.dart +++ b/lib/http/dynamics.dart @@ -4,6 +4,7 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/http/api.dart'; import 'package:PiliPlus/http/constants.dart'; +import 'package:PiliPlus/http/error_msg.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/reply.dart'; @@ -18,6 +19,7 @@ import 'package:PiliPlus/models_new/article/article_view/data.dart'; import 'package:PiliPlus/models_new/bubble/data.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_mention/data.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_mention/group.dart'; +import 'package:PiliPlus/models_new/dynamic/dyn_reaction/data.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_reserve/data.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_reserve_info/data.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_feed/topic_card_list.dart'; @@ -32,19 +34,14 @@ import 'package:dio/dio.dart'; abstract final class DynamicsHttp { @pragma('vm:notify-debugger-on-exception') static Future> followDynamic({ - DynamicsTabType type = DynamicsTabType.all, + int? hostMid, String? offset, - int? mid, Set? tempBannedList, + DynamicsTabType type = .all, }) async { Map data = { - if (type == DynamicsTabType.up) - 'host_mid': mid - else ...{ - 'type': type.name, - 'timezone_offset': '-480', - }, - 'offset': offset, + if (type == .up) 'host_mid': hostMid else 'type': type.name, + 'offset': ?offset, 'features': Constants.dynFeatures, }; final res = await Request().get(Api.followDynamic, queryParameters: data); @@ -57,10 +54,10 @@ abstract final class DynamicsHttp { tempBannedList: tempBannedList, ); if (data.loadNext == true) { - return followDynamic( + return await followDynamic( type: type, offset: data.offset, - mid: mid, + hostMid: hostMid, tempBannedList: tempBannedList, ); } @@ -88,22 +85,45 @@ abstract final class DynamicsHttp { } } - static Future> dynUpList(String? offset) async { + static Future> dynUpList(String? offset) async { final res = await Request().get( Api.dynUplist, queryParameters: { - 'offset': offset, + 'offset': ?offset, 'platform': 'web', 'web_location': 333.1365, }, ); if (res.data['code'] == 0) { - return Success(DynUpList.fromJson(res.data['data'])); + return Success(FollowUpModel.fromUpList(res.data['data'])); } else { return Error(res.data['message']); } } + static Future> followings({ + int? vmid, + int? pn, + int ps = 20, + String orderType = '', // ''=>最近关注,'attention'=>最常访问 + }) async { + final res = await Request().get( + Api.followings, + queryParameters: { + 'vmid': vmid, + 'pn': pn, + 'ps': ps, + 'order': 'desc', + 'order_type': orderType, + }, + ); + if (res.data['code'] == 0) { + return Success(FollowUpModel.fromFollowList(res.data['data'])); + } else { + return Error(errorMsg[res.data['code']] ?? res.data['message']); + } + } + // 动态点赞 // static Future likeDynamic({ // required String? dynamicId, @@ -375,7 +395,10 @@ abstract final class DynamicsHttp { queryParameters: {'vote_id': voteId}, ); if (res.data['code'] == 0) { - return Success(VoteInfo.fromSeparatedJson(res.data['data'])); + final voteInfo = VoteInfo.fromSeparatedJson(res.data['data']); + return voteInfo.voteId == null + ? const Error('无效的投票id') + : Success(voteInfo); } else { return Error(res.data['message']); } @@ -804,4 +827,23 @@ abstract final class DynamicsHttp { return Error(res.data['message']); } } + + static Future> dynReaction({ + required Object id, + String? offset, + }) async { + final res = await Request().get( + Api.dynReaction, + queryParameters: { + 'id': id, + 'offset': ?offset, + 'web_location': 333.1369, + }, + ); + if (res.data['code'] == 0) { + return Success(DynReactionData.fromJson(res.data['data'])); + } else { + return Error(res.data['message']); + } + } } diff --git a/lib/http/follow.dart b/lib/http/follow.dart index d3ed1c715c..bd19e2fc49 100644 --- a/lib/http/follow.dart +++ b/lib/http/follow.dart @@ -3,6 +3,8 @@ import 'package:PiliPlus/http/error_msg.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/follow/data.dart'; +import 'package:PiliPlus/utils/accounts.dart'; +import 'package:dio/dio.dart' show Options, Headers; abstract final class FollowHttp { static Future> followings({ @@ -27,4 +29,26 @@ abstract final class FollowHttp { return Error(errorMsg[res.data['code']] ?? res.data['message']); } } + + static Future> sortFollowTag({ + required String tagids, + }) async { + final res = await Request().post( + Api.sortFollowTag, + queryParameters: { + 'x-bili-device-req-json': + '{"platform":"web","device":"pc","spmid":"333.1387"}', + }, + data: { + 'tagids': tagids, + 'csrf': Accounts.main.csrf, + }, + options: Options(contentType: Headers.formUrlEncodedContentType), + ); + if (res.data['code'] == 0) { + return const Success(null); + } else { + return Error(res.data['message']); + } + } } diff --git a/lib/http/live.dart b/lib/http/live.dart index c7b0a154fb..ffb07aa5d7 100644 --- a/lib/http/live.dart +++ b/lib/http/live.dart @@ -102,7 +102,11 @@ abstract final class LiveHttp { }), ); if (res.data['code'] == 0) { - return Success(RoomPlayInfoData.fromJson(res.data['data'])); + try { + return Success(RoomPlayInfoData.fromJson(res.data['data'])); + } catch (e) { + return Error(e.toString()); + } } else { return Error(res.data['message']); } @@ -163,7 +167,11 @@ abstract final class LiveHttp { }), ); if (res.data['code'] == 0) { - return Success(LiveDmInfoData.fromJson(res.data['data'])); + try { + return Success(LiveDmInfoData.fromJson(res.data['data'])); + } catch (e) { + return Error(e.toString()); + } } else { return Error(res.data['message']); } diff --git a/lib/http/member.dart b/lib/http/member.dart index 6cb0903339..08d40f6392 100644 --- a/lib/http/member.dart +++ b/lib/http/member.dart @@ -471,7 +471,7 @@ abstract final class MemberHttp { try { DynamicsDataModel data = DynamicsDataModel.fromJson(res.data['data']); if (data.loadNext == true) { - return memberDynamic(offset: data.offset, mid: mid); + return await memberDynamic(offset: data.offset, mid: mid); } return Success(data); } catch (e, s) { @@ -593,7 +593,7 @@ abstract final class MemberHttp { } } - static Future> createFollowTag(Object tagName) async { + static Future> createFollowTag(String tagName) async { final res = await Request().post( Api.createFollowTag, queryParameters: { @@ -607,7 +607,7 @@ abstract final class MemberHttp { options: Options(contentType: Headers.formUrlEncodedContentType), ); if (res.data['code'] == 0) { - return const Success(null); + return Success(res.data['data']['tagid']); } else { return Error(res.data['message']); } diff --git a/lib/http/reply.dart b/lib/http/reply.dart index db9bc10096..a96d81a09a 100644 --- a/lib/http/reply.dart +++ b/lib/http/reply.dart @@ -186,7 +186,7 @@ abstract final class ReplyHttp { String? reasonDesc, }) async { final res = await Request().post( - '/x/v2/reply/report', + Api.replyReport, data: { 'add_blacklist': banUid, 'csrf': Accounts.main.csrf, diff --git a/lib/http/search.dart b/lib/http/search.dart index a363671540..d2040f755e 100644 --- a/lib/http/search.dart +++ b/lib/http/search.dart @@ -11,6 +11,7 @@ import 'package:PiliPlus/models_new/pgc/pgc_info_model/result.dart'; import 'package:PiliPlus/models_new/search/search_rcmd/data.dart'; import 'package:PiliPlus/models_new/search/search_trending/data.dart'; import 'package:PiliPlus/models_new/video/video_detail/dimension.dart'; +import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/request_utils.dart'; import 'package:PiliPlus/utils/wbi_sign.dart'; import 'package:dio/dio.dart'; @@ -191,7 +192,7 @@ abstract final class SearchHttp { if (res.data['code'] == 0) { if (res.data['data'] case List list) { final target = part != null - ? (list.elementAtOrNull(part - 1) ?? list.firstOrNull) + ? (list.getOrNull(part - 1) ?? list.firstOrNull) : list.firstOrNull; if (target != null) { return ( diff --git a/lib/http/sponsor_block.dart b/lib/http/sponsor_block.dart index 553f2f051e..0dfaa69b60 100644 --- a/lib/http/sponsor_block.dart +++ b/lib/http/sponsor_block.dart @@ -79,7 +79,7 @@ abstract final class SponsorBlock { int? type, SegmentType? category, }) async { - assert((type == null) == (category == null)); + assert((type == null) != (category == null)); final res = await Request().post( _api(SponsorBlockApi.voteOnSponsorTime), queryParameters: { diff --git a/lib/http/video.dart b/lib/http/video.dart index 737806c8ec..a30f6621e5 100644 --- a/lib/http/video.dart +++ b/lib/http/video.dart @@ -208,7 +208,10 @@ abstract final class VideoHttp { required bool tryLook, required VideoType videoType, String? language, + bool voiceBalance = false, }) async { + final dmImgStr = Utils.base64EncodeRandomString(16, 64); + final dmCoverImgStr = Utils.base64EncodeRandomString(32, 128); final params = await WbiSign.makSign({ 'avid': ?avid, 'bvid': ?bvid, @@ -220,12 +223,16 @@ abstract final class VideoHttp { 'fnval': 4048, 'fourk': 1, 'fnver': 0, - 'voice_balance': 1, + 'voice_balance': voiceBalance ? 1 : 0, 'gaia_source': 'pre-load', 'isGaiaAvoided': true, 'web_location': 1315873, // 免登录查看1080p if (tryLook) 'try_look': 1, + 'dm_img_list': '[]', + 'dm_img_str': dmImgStr, + 'dm_cover_img_str': dmCoverImgStr, + 'dm_img_inter': '{"ds":[],"wh":[0,0,0],"of":[0,0,0]}', 'cur_language': ?language, }); @@ -235,25 +242,24 @@ abstract final class VideoHttp { if (res.data['code'] == 0) { late PlayUrlModel data; switch (videoType) { - case VideoType.ugc: + case .ugc: data = PlayUrlModel.fromJson(res.data['data']); - break; - case VideoType.pugv: - final result = res.data['data']; - data = PlayUrlModel.fromJson(result) - ..lastPlayTime = - result?['play_view_business_info']?['user_status']?['watch_progress']?['current_watch_progress']; - break; - case VideoType.pgc: + + case .pgc: final result = res.data['result']; data = PlayUrlModel.fromJson(result['video_info']) ..lastPlayTime = - result?['play_view_business_info']?['user_status']?['watch_progress']?['current_watch_progress']; - break; + result['play_view_business_info']?['user_status']?['watch_progress']?['current_watch_progress']; + + case .pugv: + final result = res.data['data']; + data = PlayUrlModel.fromJson(result) + ..lastPlayTime = + result['play_view_business_info']?['user_status']?['watch_progress']?['current_watch_progress']; } return Success(data); - } else if (epid != null && videoType == VideoType.ugc) { - return videoUrl( + } else if (epid != null && videoType == .ugc) { + return await videoUrl( avid: avid, bvid: bvid, cid: cid, @@ -261,7 +267,7 @@ abstract final class VideoHttp { epid: epid, seasonId: seasonId, tryLook: tryLook, - videoType: VideoType.pgc, + videoType: .pgc, ); } return Error(_parseVideoErr(res.data['code'], res.data['message'])); @@ -844,7 +850,7 @@ abstract final class VideoHttp { ..writeAll( list.map( (item) => - '${item?['sid'] ?? 0}\n${_subtitleTimecode(item['from'])} --> ${_subtitleTimecode(item['to'])}\n${item['content'].trim()}', + '${_subtitleTimecode(item['from'])} --> ${_subtitleTimecode(item['to'])}\n${item['content'].trim()}', ), '\n\n', ); diff --git a/lib/main.dart b/lib/main.dart index b9d566750b..05b6498226 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -13,13 +13,14 @@ import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart'; import 'package:PiliPlus/router/app_pages.dart'; import 'package:PiliPlus/services/account_service.dart'; import 'package:PiliPlus/services/download/download_service.dart'; +import 'package:PiliPlus/services/logger.dart'; import 'package:PiliPlus/services/service_locator.dart'; import 'package:PiliPlus/utils/cache_manager.dart'; import 'package:PiliPlus/utils/calc_window_position.dart'; import 'package:PiliPlus/utils/date_utils.dart'; -import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/json_file_handler.dart'; +import 'package:PiliPlus/utils/max_screen_size.dart'; import 'package:PiliPlus/utils/path_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/request_utils.dart'; @@ -29,7 +30,7 @@ import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:catcher_2/catcher_2.dart'; -import 'package:device_info_plus/device_info_plus.dart'; +import 'package:collection/collection.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -42,10 +43,13 @@ import 'package:get/get.dart'; import 'package:media_kit/media_kit.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; +import 'package:screen_brightness_platform_interface/screen_brightness_platform_interface.dart'; import 'package:window_manager/window_manager.dart' hide calcWindowPosition; WebViewEnvironment? webViewEnvironment; +EdgeInsets? tmpPadding; + Future _initDownPath() async { if (PlatformUtils.isDesktop) { final customDownPath = Pref.downloadPath; @@ -84,10 +88,6 @@ Future _initAppPath() async { appSupportDirPath = (await getApplicationSupportDirectory()).path; } -Future _initSdkInt() async { - Utils.sdkInt = (await DeviceInfoPlugin().androidInfo).version.sdkInt; -} - void main() async { ScaledWidgetsFlutterBinding.ensureInitialized(); MediaKit.ensureInitialized(); @@ -100,17 +100,19 @@ void main() async { exit(0); } ScaledWidgetsFlutterBinding.instance.scaleFactor = Pref.uiScale; - await Future.wait([_initDownPath(), _initTmpPath()]); + await Future.wait([ + _initDownPath(), + _initTmpPath(), + CacheManager.ensureInitialized(), + ]); Get ..lazyPut(AccountService.new) ..lazyPut(DownloadService.new); HttpOverrides.global = _CustomHttpOverrides(); - CacheManager.autoClearCache(); - if (PlatformUtils.isMobile) { + if (Platform.isAndroid) MaxScreenSize.init(); await Future.wait([ - if (Platform.isAndroid) _initSdkInt(), if (Pref.horizontalScreen) ?fullMode() else ?portraitUpMode(), setupServiceLocator(), ]); @@ -155,6 +157,8 @@ void main() async { } FlutterDisplayMode.setPreferredMode(displayMode ?? DisplayMode.auto); }); + } else { + ScreenBrightnessPlatform.instance.setAutoReset(false); } } else if (PlatformUtils.isDesktop) { await windowManager.ensureInitialized(); @@ -185,37 +189,21 @@ void main() async { if (Pref.enableLog) { // 异常捕获 logo记录 final customParameters = { - 'BuildConfig': - '\nBuild Time: ${DateFormatUtils.format(BuildConfig.buildTime, format: DateFormatUtils.longFormatDs)}\n' - 'Commit Hash: ${BuildConfig.commitHash}', + 'Build Time': DateFormatUtils.format( + BuildConfig.buildTime, + format: DateFormatUtils.longFormatDs, + ), + 'Commit Hash': BuildConfig.commitHash, + 'MPV Api Version': + '${NativePlayer.apiVersion >> 16}.${NativePlayer.apiVersion & 0xFFFF}', }; final fileHandler = await JsonFileHandler.init(); - final Catcher2Options debugConfig = Catcher2Options( - SilentReportMode(), - [ - ?fileHandler, - ConsoleHandler( - enableDeviceParameters: false, - enableApplicationParameters: false, - enableCustomParameters: true, - ), - ], - customParameters: customParameters, - ); - - final Catcher2Options releaseConfig = Catcher2Options( - SilentReportMode(), - [ - ?fileHandler, - ConsoleHandler(enableCustomParameters: true), - ], - customParameters: customParameters, - ); Catcher2( - debugConfig: debugConfig, - releaseConfig: releaseConfig, - rootWidget: const MyApp(), + [?fileHandler, const ConsoleHandler()], + const MyApp(), + logger: logger, + customParameters: customParameters, ); } else { runApp(const MyApp()); @@ -227,8 +215,6 @@ class MyApp extends StatelessWidget { static ColorScheme? _light, _dark; - static ThemeData? darkThemeData; - static void _onBack() { if (SmartDialog.checkExist()) { SmartDialog.dismiss(); @@ -254,13 +240,13 @@ class MyApp extends StatelessWidget { late final brandColor = colorThemeTypes[Pref.customColor].color; late final variant = Pref.schemeVariant; return ( - ThemeUtils.getThemeData( + ThemeUtils.lightTheme = ThemeUtils.getThemeData( colorScheme: dynamicColor ? _light! : brandColor.asColorSchemeSeed(variant, .light), isDynamic: dynamicColor, ), - ThemeUtils.getThemeData( + ThemeUtils.darkTheme = ThemeUtils.getThemeData( isDark: true, colorScheme: dynamicColor ? _dark! @@ -277,7 +263,7 @@ class MyApp extends StatelessWidget { title: Constants.appName, theme: light, darkTheme: dark, - themeMode: Pref.themeMode, + themeMode: ThemeUtils.themeMode = Pref.themeMode, localizationsDelegates: const [ GlobalCupertinoLocalizations.delegate, GlobalMaterialLocalizations.delegate, @@ -290,8 +276,11 @@ class MyApp extends StatelessWidget { getPages: Routes.getPages, defaultTransition: Pref.pageTransition, builder: FlutterSmartDialog.init( - toastBuilder: (msg) => CustomToast(msg: msg), - loadingBuilder: (msg) => LoadingWidget(msg: msg), + toastBuilder: CustomToast.new, + loadingBuilder: LoadingWidget.new, + notifyStyle: const FlutterSmartNotifyStyle( + warningBuilder: NotifyWarning.new, + ), builder: _builder, ), navigatorObservers: [ @@ -313,16 +302,20 @@ class MyApp extends StatelessWidget { data: mediaQuery.copyWith( textScaler: textScaler, size: mediaQuery.size / uiScale, - padding: mediaQuery.padding / uiScale, + padding: tmpPadding ?? mediaQuery.padding / uiScale, viewInsets: mediaQuery.viewInsets / uiScale, - viewPadding: mediaQuery.viewPadding / uiScale, + viewPadding: tmpPadding ?? mediaQuery.viewPadding / uiScale, devicePixelRatio: mediaQuery.devicePixelRatio * uiScale, ), child: child!, ); } else { child = MediaQuery( - data: mediaQuery.copyWith(textScaler: textScaler), + data: mediaQuery.copyWith( + textScaler: textScaler, + padding: tmpPadding, + viewPadding: tmpPadding, + ), child: child!, ); } diff --git a/lib/models/common/reply/reply_sort_type.dart b/lib/models/common/reply/reply_sort_type.dart index 74c4509a88..6ee4486ce7 100644 --- a/lib/models/common/reply/reply_sort_type.dart +++ b/lib/models/common/reply/reply_sort_type.dart @@ -1,10 +1,11 @@ enum ReplySortType { - time('最新评论', '最新'), - hot('最热评论', '最热'), + time('最新评论', '最新', text: '按时间'), + hot('最热评论', '最热', text: '按热度'), select('精选评论', '精选'), ; final String title; final String label; - const ReplySortType(this.title, this.label); + final String? text; + const ReplySortType(this.title, this.label, {this.text}); } diff --git a/lib/models/common/setting_type.dart b/lib/models/common/setting_type.dart index 366623f60f..64abaa2325 100644 --- a/lib/models/common/setting_type.dart +++ b/lib/models/common/setting_type.dart @@ -1,3 +1,11 @@ +import 'package:PiliPlus/pages/setting/models/extra_settings.dart'; +import 'package:PiliPlus/pages/setting/models/model.dart'; +import 'package:PiliPlus/pages/setting/models/play_settings.dart'; +import 'package:PiliPlus/pages/setting/models/privacy_settings.dart'; +import 'package:PiliPlus/pages/setting/models/recommend_settings.dart'; +import 'package:PiliPlus/pages/setting/models/style_settings.dart'; +import 'package:PiliPlus/pages/setting/models/video_settings.dart'; + enum SettingType { privacySetting('隐私设置'), recommendSetting('推荐流设置'), @@ -11,4 +19,14 @@ enum SettingType { final String title; const SettingType(this.title); + + List get settings => switch (this) { + .privacySetting => privacySettings, + .recommendSetting => recommendSettings, + .videoSetting => videoSettings, + .playSetting => playSettings, + .styleSetting => styleSettings, + .extraSetting => extraSettings, + _ => throw UnimplementedError(), + }; } diff --git a/lib/models/common/video/video_decode_type.dart b/lib/models/common/video/video_decode_type.dart index f17ec8c67b..4020de3f2c 100644 --- a/lib/models/common/video/video_decode_type.dart +++ b/lib/models/common/video/video_decode_type.dart @@ -12,9 +12,6 @@ enum VideoDecodeFormatType { const VideoDecodeFormatType(this.codes); - static VideoDecodeFormatType fromCode(String code) => - values.firstWhere((i) => i.codes.contains(code)); - static VideoDecodeFormatType fromString(String val) => values.firstWhere((i) => i.codes.any(val.startsWith)); } diff --git a/lib/models/dynamics/article_content_model.dart b/lib/models/dynamics/article_content_model.dart index 9911fe0284..6f5e6c28e6 100644 --- a/lib/models/dynamics/article_content_model.dart +++ b/lib/models/dynamics/article_content_model.dart @@ -1,6 +1,8 @@ import 'package:PiliPlus/common/style.dart' as common_style; import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/models/dynamics/vote_model.dart'; +import 'package:PiliPlus/utils/color_utils.dart'; +import 'package:PiliPlus/utils/parse_int.dart'; class ArticleContentModel { int? align; @@ -124,14 +126,14 @@ class Word { Word.fromJson(Map json) { words = json['words']; - fontSize = (json['font_size'] as num?)?.toDouble(); + if (json['font_size'] case final num rawSize when rawSize != 0) { + fontSize = rawSize.toDouble(); + } style = json['style'] == null ? null : Style.fromJson(json['style']); - color = json['color'] == null - ? null - : int.tryParse( - 'FF${(json['color'] as String).substring(1)}', - radix: 16, - ); + if (json['color'] case final String rawColor + when rawColor.startsWith('#')) { + color = ColourUtils.parse2Int(rawColor); + } fontLevel = json['font_level']; } @@ -275,7 +277,7 @@ class Music { Music.fromJson(Map json) { cover = json['cover']; - id = json['id']; + id = safeToInt(json['id']); jumpUrl = json['jump_url']; label = json['label']; title = json['title']; @@ -291,12 +293,12 @@ class Opus { int? statView; Opus.fromJson(Map json) { - authorMid = json['author']?['mid']; + authorMid = safeToInt(json['author']?['mid']); authorName = json['author']?['name']; cover = json['cover']; jumpUrl = json['jump_url']; title = json['title']; - statView = json['stat']?['view']; + statView = safeToInt(json['stat']?['view']); } } @@ -317,9 +319,9 @@ class Live { descSecond = json['desc_second']; title = json['title']; jumpUrl = json['jump_url']; - id = json['id']; - liveState = json['live_state']; - reserveType = json['reserve_type']; + id = safeToInt(json['id']); + liveState = safeToInt(json['live_state']); + reserveType = safeToInt(json['reserve_type']); badgeText = json['badge']?['text']; } } diff --git a/lib/models/dynamics/result.dart b/lib/models/dynamics/result.dart index 4aff0b96d8..18132d01db 100644 --- a/lib/models/dynamics/result.dart +++ b/lib/models/dynamics/result.dart @@ -7,9 +7,9 @@ import 'package:PiliPlus/models/model_avatar.dart'; import 'package:PiliPlus/models/model_owner.dart'; import 'package:PiliPlus/models_new/live/live_feed_index/watched_show.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; +import 'package:PiliPlus/utils/parse_int.dart'; import 'package:PiliPlus/utils/parse_string.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; -import 'package:PiliPlus/utils/utils.dart'; class DynamicsDataModel { bool? hasMore; @@ -90,7 +90,7 @@ class DynamicsDataModel { } offset = json['offset']; - total = Utils.safeToInt(json['total']); + total = safeToInt(json['total']); } } @@ -306,7 +306,7 @@ class ModuleCollection { ModuleCollection.fromJson(Map json) { count = json['count']; - id = Utils.safeToInt(json['id']); + id = safeToInt(json['id']); name = json['name']; title = json['title']; } @@ -350,7 +350,7 @@ class ModuleBlocked { ModuleBlocked.fromJson(Map json) { bgImg = json['bg_img'] == null ? null : BgImg.fromJson(json['bg_img']); - blockedType = Utils.safeToInt(json['blocked_type']); + blockedType = safeToInt(json['blocked_type']); button = json['button'] == null ? null : Button.fromJson(json['button']); title = json['title']; hintMessage = json['hint_message']; @@ -401,7 +401,7 @@ class Basic { Basic.fromJson(Map json) { commentIdStr = json['comment_id_str']; - commentType = Utils.safeToInt(json['comment_type']); + commentType = safeToInt(json['comment_type']); ridStr = json['rid_str']; } } @@ -422,7 +422,9 @@ class ModuleAuthorModel extends Avatar { } pubAction = json['pub_action']; pubTime = json['pub_time']; - pubTs = json['pub_ts'] == 0 ? null : Utils.safeToInt(json['pub_ts']); + if (safeToInt(json['pub_ts']) case final pubTs? when pubTs > 0) { + this.pubTs = pubTs; + } type = json['type']; if (PendantAvatar.showDecorate) { decorate = json['decorate'] == null @@ -432,7 +434,7 @@ class ModuleAuthorModel extends Avatar { pendant = null; } isTop = json['is_top']; - badgeText = noneNullOrEmptyString(json['icon_badge']?['text']); + badgeText = nonNullOrEmptyString(json['icon_badge']?['text']); } } @@ -683,11 +685,11 @@ class Vote { String? title; Vote.fromJson(Map json) { - joinNum = Utils.safeToInt(json['join_num']); - voteId = Utils.safeToInt(json['vote_id']); + joinNum = safeToInt(json['join_num']); + voteId = safeToInt(json['vote_id']); title = - noneNullOrEmptyString(json['title']) ?? - noneNullOrEmptyString(json['desc']); + nonNullOrEmptyString(json['title']) ?? + nonNullOrEmptyString(json['desc']); } } @@ -740,10 +742,10 @@ class Reserve { desc1 = json['desc1'] == null ? null : Desc.fromJson(json['desc1']); desc2 = json['desc2'] == null ? null : Desc.fromJson(json['desc2']); desc3 = json['desc3'] == null ? null : Desc.fromJson(json['desc3']); - reserveTotal = Utils.safeToInt(json['reserve_total']); - rid = Utils.safeToInt(json['rid']); - state = Utils.safeToInt(json['state']); - state = Utils.safeToInt(json['state']); + reserveTotal = safeToInt(json['reserve_total']); + rid = safeToInt(json['rid']); + state = safeToInt(json['state']); + state = safeToInt(json['state']); title = json['title']; } } @@ -765,11 +767,11 @@ class ReserveBtn { String? jumpUrl; ReserveBtn.fromJson(Map json) { - status = Utils.safeToInt(json['status']); - type = Utils.safeToInt(json['type']); + status = safeToInt(json['status']); + type = safeToInt(json['type']); checkText = json['check']?['text'] ?? '已预约'; uncheckText = json['uncheck']?['text'] ?? '预约'; - disable = Utils.safeToInt(json['uncheck']?['disable']); + disable = safeToInt(json['uncheck']?['disable']); jumpText = json['jump_style']?['text']; jumpUrl = json['jump_url']; } @@ -929,7 +931,7 @@ class Music { String? label; Music.fromJson(Map json) { - id = Utils.safeToInt(json['id']); + id = safeToInt(json['id']); cover = json['cover']; title = json['title']; label = json['label']; @@ -1016,8 +1018,8 @@ class LivePlayInfo { }); factory LivePlayInfo.fromJson(Map json) => LivePlayInfo( - roomId: Utils.safeToInt(json["room_id"]), - liveStatus: Utils.safeToInt(json["live_status"]), + roomId: safeToInt(json["room_id"]), + liveStatus: safeToInt(json["live_status"]), title: json["title"], cover: json["cover"], areaName: json["area_name"], @@ -1037,7 +1039,7 @@ class DynamicTopicModel { String? name; DynamicTopicModel.fromJson(Map json) { - id = Utils.safeToInt(json['id']); + id = safeToInt(json['id']); name = json['name']; } } @@ -1072,8 +1074,8 @@ class DynamicArchiveModel { int? seasonId; DynamicArchiveModel.fromJson(Map json) { - id = Utils.safeToInt(json['id']); - aid = Utils.safeToInt(json['aid']); + id = safeToInt(json['id']); + aid = safeToInt(json['aid']); badge = json['badge'] == null ? null : Badge.fromJson(json['badge']); bvid = json['bvid'] ?? json['epid'].toString() ?? ' '; cover = json['cover']; @@ -1081,9 +1083,9 @@ class DynamicArchiveModel { jumpUrl = json['jump_url']; stat = json['stat'] != null ? Stat.fromJson(json['stat']) : null; title = json['title']; - type = Utils.safeToInt(json['type']); - epid = Utils.safeToInt(json['epid']); - seasonId = Utils.safeToInt(json['season_id']); + type = safeToInt(json['type']); + epid = safeToInt(json['epid']); + seasonId = safeToInt(json['season_id']); } } @@ -1175,9 +1177,9 @@ class Emoji { Emoji.fromJson(Map json) { url = - noneNullOrEmptyString(json['webp_url']) ?? - noneNullOrEmptyString(json['gif_url']) ?? - noneNullOrEmptyString(json['icon_url']); + nonNullOrEmptyString(json['webp_url']) ?? + nonNullOrEmptyString(json['gif_url']) ?? + nonNullOrEmptyString(json['icon_url']); size = json['size'] ?? 1; } } @@ -1221,8 +1223,8 @@ class OpusPicModel extends PicModel { num? size; OpusPicModel.fromJson(Map json) { - width = Utils.safeToInt(json['width']); - height = Utils.safeToInt(json['height']); + width = safeToInt(json['width']); + height = safeToInt(json['height']); src = json['src']; url = json['url']; liveUrl = json['live_url']; @@ -1250,8 +1252,8 @@ class DynamicLiveModel { Map data = jsonDecode(json['content']); Map livePlayInfo = data['live_play_info']; - roomId = Utils.safeToInt(livePlayInfo['room_id']); - liveStatus = Utils.safeToInt(livePlayInfo['live_status']); + roomId = safeToInt(livePlayInfo['room_id']); + liveStatus = safeToInt(livePlayInfo['live_status']); cover = livePlayInfo['cover']; areaName = livePlayInfo['area_name']; title = livePlayInfo['title']; @@ -1283,8 +1285,8 @@ class DynamicLive2Model { badge = json['badge'] == null ? null : Badge.fromJson(json['badge']); cover = json['cover']; descFirst = json['desc_first']; - id = Utils.safeToInt(json['id']); - liveState = Utils.safeToInt(json['live_state']); + id = safeToInt(json['id']); + liveState = safeToInt(json['live_state']); title = json['title']; } } @@ -1297,7 +1299,7 @@ class ModuleTag { String? text; ModuleTag.fromJson(Map json) { - text = noneNullOrEmptyString(json['text']); + text = nonNullOrEmptyString(json['text']); } } @@ -1340,7 +1342,9 @@ class DynamicStat { bool? status; DynamicStat.fromJson(Map json) { - count = json['count'] == 0 ? null : Utils.safeToInt(json['count']); + if (safeToInt(json['count']) case final count? when count > 0) { + this.count = count; + } status = json['status']; } } diff --git a/lib/models/dynamics/up.dart b/lib/models/dynamics/up.dart index 2b5bfe9284..2aba3faff8 100644 --- a/lib/models/dynamics/up.dart +++ b/lib/models/dynamics/up.dart @@ -1,41 +1,43 @@ -import 'package:PiliPlus/utils/utils.dart'; +import 'package:PiliPlus/models_new/follow/list.dart'; +import 'package:PiliPlus/utils/parse_int.dart'; class FollowUpModel { - FollowUpModel({ - this.liveUsers, - required this.upList, - }); - LiveUsers? liveUsers; - late List upList; + List? upList; bool? hasMore; String? offset; - FollowUpModel.fromJson(Map json) { - liveUsers = json['live_users'] != null - ? LiveUsers.fromJson(json['live_users']) - : null; - upList = - (json['up_list']?['items'] as List?) - ?.map((e) => UpItem.fromJson(e)) - .toList() ?? - []; - hasMore = json['up_list']?['has_more']; - offset = json['up_list']?['offset']; + void addAllUpList(List newList) { + if (upList != null) { + upList!.addAll(newList); + } else { + upList = newList; + } } -} -class DynUpList { - List? upList; - bool? hasMore; - String? offset; + factory FollowUpModel.fromJson(Map json) { + final model = FollowUpModel.fromUpList(json['up_list']); + final liveUsers = json['live_users']; + if (liveUsers != null) { + model.liveUsers = LiveUsers.fromJson(liveUsers); + } + return model; + } + + FollowUpModel.fromUpList(Map? json) { + if (json != null) { + upList = (json['items'] as List?) + ?.map((e) => UpItem.fromJson(e)) + .toList(); + hasMore = json['has_more']; + offset = json['offset']; + } + } - DynUpList.fromJson(Map json) { - upList = (json['items'] as List?) - ?.map((e) => UpItem.fromJson(e)) + FollowUpModel.fromFollowList(Map json) { + upList = (json['list'] as List?) + ?.map((e) => FollowItemModel.fromJson(e)) .toList(); - hasMore = json['has_more']; - offset = json['offset']; } } @@ -51,7 +53,7 @@ class LiveUsers { List? items; LiveUsers.fromJson(Map json) { - count = Utils.safeToInt(json['count']) ?? 0; + count = safeToInt(json['count']) ?? 0; group = json['group']; items = (json['items'] as List?) ?.map((e) => LiveUserItem.fromJson(e)) @@ -68,7 +70,7 @@ class LiveUserItem extends UpItem { LiveUserItem.fromJson(Map json) : super.fromJson(json) { isReserveRecall = json['is_reserve_recall']; jumpUrl = json['jump_url']; - roomId = Utils.safeToInt(json['room_id']); + roomId = safeToInt(json['room_id']); title = json['title']; } } @@ -89,7 +91,7 @@ class UpItem { UpItem.fromJson(Map json) { face = json['face']; hasUpdate = json['has_update']; - mid = Utils.safeToInt(json['mid']) ?? 0; + mid = safeToInt(json['mid']) ?? 0; uname = json['uname']; } diff --git a/lib/models/dynamics/vote_model.dart b/lib/models/dynamics/vote_model.dart index 1c04bc8ec3..5e647d3e72 100644 --- a/lib/models/dynamics/vote_model.dart +++ b/lib/models/dynamics/vote_model.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/utils/extension/iterable_ext.dart'; +import 'package:PiliPlus/utils/parse_int.dart'; class SimpleVoteInfo { int? choiceCnt; @@ -22,14 +23,14 @@ class SimpleVoteInfo { }); SimpleVoteInfo.fromJson(Map json) { - choiceCnt = json['choice_cnt']; - defaultShare = json['default_share']; + choiceCnt = safeToInt(json['choice_cnt']); + defaultShare = safeToInt(json['default_share']); desc = json['desc']; - endTime = json['end_time']; - status = json['status']; - uid = json['uid']; - voteId = json['vote_id']; - joinNum = json['join_num'] ?? 0; + endTime = safeToInt(json['end_time']); + status = safeToInt(json['status']); + uid = safeToInt(json['uid']); + voteId = safeToInt(json['vote_id']); + joinNum = safeToInt(json['join_num']) ?? 0; } } diff --git a/lib/models/horizontal_video_model.dart b/lib/models/horizontal_video_model.dart new file mode 100644 index 0000000000..c99bceafaa --- /dev/null +++ b/lib/models/horizontal_video_model.dart @@ -0,0 +1,21 @@ +import 'package:PiliPlus/models/model_video.dart'; +import 'package:PiliPlus/models_new/video/video_detail/dimension.dart'; + +abstract class HorizontalVideoModel extends BaseVideoItemModel { + bool? isPugv; + int? seasonId; + + int? roomId; + bool? isLive; + + Dimension? dimension; + + String? badge; + + num? progress; + + String? redirectUrl; + + // search + List<({bool isEm, String text})>? titleList; +} diff --git a/lib/models/member/tags.dart b/lib/models/member/tags.dart index e6c9c4daab..692e3f99cd 100644 --- a/lib/models/member/tags.dart +++ b/lib/models/member/tags.dart @@ -17,4 +17,12 @@ class MemberTagItemModel { tagid = json['tagid']; tip = json['tip']; } + + MemberTagItemModel.fromCreate( + ({int tagid, String tagName}) res, { + this.count = 0, + }) { + tagid = res.tagid; + name = res.tagName; + } } diff --git a/lib/models/model_hot_video_item.dart b/lib/models/model_hot_video_item.dart index ddb685ae6a..8722947c64 100644 --- a/lib/models/model_hot_video_item.dart +++ b/lib/models/model_hot_video_item.dart @@ -1,25 +1,19 @@ +import 'package:PiliPlus/models/horizontal_video_model.dart'; import 'package:PiliPlus/models/model_owner.dart'; -import 'package:PiliPlus/models/model_rec_video_item.dart'; import 'package:PiliPlus/models/model_video.dart'; import 'package:PiliPlus/models_new/video/video_detail/dimension.dart'; import 'package:PiliPlus/pages/common/multi_select/base.dart'; // 稍后再看, 排行榜等网页返回也使用该类 -class HotVideoItemModel extends BaseRcmdVideoItemModel with MultiSelectData { +class HotVideoItemModel extends HorizontalVideoModel with MultiSelectData { int? videos; int? tid; String? tname; int? copyright; int? ctime; int? state; - Dimension? dimension; String? firstFrame; String? pubLocation; - String? pgcLabel; - String? redirectUrl; - num? progress; - int? isCooperation; - bool? isCharging; HotVideoItemModel.fromJson(Map json) { aid = json["aid"]; @@ -43,23 +37,16 @@ class HotVideoItemModel extends BaseRcmdVideoItemModel with MultiSelectData { : Dimension.fromJson(json['dimension']); firstFrame = json["first_frame"]; pubLocation = json["pub_location"]; - dynamic rcmd = json['rcmd_reason']; - rcmdReason = rcmd is Map ? rcmd['content'] : rcmd; // 相关视频里rcmd为String, - if (rcmdReason?.isEmpty == true) rcmdReason = null; - pgcLabel = json['pgc_label']; redirectUrl = json['redirect_url']; - // uri = json['uri']; // 仅在稍后再看存在 progress = json['progress']; - isCooperation = json['rights']?['is_cooperation']; - isCharging = json['charging_pay']?['level'] != null; + if (json['charging_pay']?['level'] != null) { + badge = '充电专属'; + } else if (json['rights']?['is_cooperation'] == 1) { + badge = '合作'; + } else { + badge = json['pgc_label']; + } } - - // @override - // get isFollowed => false; - // @override - // get goto => 'av'; - // @override - // get uri => 'bilibili://video/$aid'; } class HotStat extends Stat { diff --git a/lib/models/model_owner.dart b/lib/models/model_owner.dart index d7db6871cb..50f836e38b 100644 --- a/lib/models/model_owner.dart +++ b/lib/models/model_owner.dart @@ -1,5 +1,5 @@ import 'package:PiliPlus/models/model_video.dart'; -import 'package:PiliPlus/utils/utils.dart'; +import 'package:PiliPlus/utils/parse_int.dart'; import 'package:hive_ce/hive.dart'; part 'model_owner.g.dart'; @@ -21,7 +21,7 @@ class Owner implements BaseOwner { String? face; Owner.fromJson(Map json) { - mid = Utils.safeToInt(json["mid"]); + mid = safeToInt(json["mid"]); name = json["name"]; face = json['face']; } diff --git a/lib/models/search/result.dart b/lib/models/search/result.dart index 4aa50c4fb0..129e19b7ac 100644 --- a/lib/models/search/result.dart +++ b/lib/models/search/result.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/models/horizontal_video_model.dart'; import 'package:PiliPlus/models/model_avatar.dart'; import 'package:PiliPlus/models/model_owner.dart'; import 'package:PiliPlus/models/model_video.dart'; @@ -64,18 +65,16 @@ class SearchVideoData extends SearchNumData { } } -class SearchVideoItemModel extends BaseVideoItemModel { - String? type; +class SearchVideoItemModel extends HorizontalVideoModel { int? id; String? arcurl; String? tag; int? ctime; - int? isUnionVideo; - List<({bool isEm, String text})>? titleList; + @override + int? get seasonId => aid; SearchVideoItemModel.fromJson(Map json) { - type = json['type']; id = json['id']; arcurl = json['arcurl']; aid = json['aid']; @@ -89,7 +88,19 @@ class SearchVideoItemModel extends BaseVideoItemModel { duration = DurationUtils.parseDuration(json['duration']); owner = SearchOwner.fromJson(json); stat = SearchStat.fromJson(json); - isUnionVideo = json['is_union_video']; + switch (json['type']) { + case 'ketang': + badge = '课堂'; + isPugv = true; + case 'live_room': + badge = '直播'; + isLive = true; + roomId = json['roomid']; + default: + if (json['is_union_video'] == 1) { + badge = '合作'; + } + } } } diff --git a/lib/models/video/play/url.dart b/lib/models/video/play/url.dart index a8234de282..38b3f8aac6 100644 --- a/lib/models/video/play/url.dart +++ b/lib/models/video/play/url.dart @@ -22,7 +22,7 @@ class PlayUrlModel { this.seekType, this.dash, this.supportFormats, - this.lastPlayTime, + this._lastPlayTime = 0, this.lastPlayCid, }); @@ -42,7 +42,17 @@ class PlayUrlModel { List? durl; List? supportFormats; Volume? volume; - int? lastPlayTime; + + late int _lastPlayTime; + int get lastPlayTime => _lastPlayTime; + set lastPlayTime(int? value) { + if (value != null && value > 0) { + _lastPlayTime = value; + } else { + _lastPlayTime = 0; + } + } + int? lastPlayCid; String? curLanguage; Language? language; diff --git a/lib/models_new/download/bili_download_entry_info.dart b/lib/models_new/download/bili_download_entry_info.dart index dc4a029635..074a9ba806 100644 --- a/lib/models_new/download/bili_download_entry_info.dart +++ b/lib/models_new/download/bili_download_entry_info.dart @@ -58,7 +58,7 @@ class BiliDownloadEntryInfo with MultiSelectData { return title; } - Widget moreBtn(ThemeData theme) => SizedBox( + Widget moreBtn(ColorScheme colorScheme) => SizedBox( width: 29, height: 29, child: PopupMenuButton( @@ -66,7 +66,7 @@ class BiliDownloadEntryInfo with MultiSelectData { position: PopupMenuPosition.under, icon: Icon( Icons.more_vert_outlined, - color: theme.colorScheme.outline, + color: colorScheme.outline, size: 18, ), itemBuilder: (_) => [ diff --git a/lib/models_new/dynamic/dyn_reaction/data.dart b/lib/models_new/dynamic/dyn_reaction/data.dart new file mode 100644 index 0000000000..16269d99f5 --- /dev/null +++ b/lib/models_new/dynamic/dyn_reaction/data.dart @@ -0,0 +1,20 @@ +import 'package:PiliPlus/models_new/dynamic/dyn_reaction/item.dart'; + +class DynReactionData { + bool? hasMore; + List? items; + String? offset; + int total; + + DynReactionData({this.hasMore, this.items, this.offset, required this.total}); + + factory DynReactionData.fromJson(Map json) => + DynReactionData( + hasMore: json['has_more'] as bool?, + items: (json['items'] as List?) + ?.map((e) => DynReactionItem.fromJson(e as Map)) + .toList(), + offset: json['offset'] as String?, + total: json['total'] as int? ?? 0, + ); +} diff --git a/lib/models_new/dynamic/dyn_reaction/item.dart b/lib/models_new/dynamic/dyn_reaction/item.dart new file mode 100644 index 0000000000..2743cd7221 --- /dev/null +++ b/lib/models_new/dynamic/dyn_reaction/item.dart @@ -0,0 +1,21 @@ +class DynReactionItem { + String? action; + String? face; + String? mid; + String? name; + + DynReactionItem({ + this.action, + this.face, + this.mid, + this.name, + }); + + factory DynReactionItem.fromJson(Map json) => + DynReactionItem( + action: json['action'] as String?, + face: json['face'] as String?, + mid: json['mid'] as String?, + name: json['name'] as String?, + ); +} diff --git a/lib/models_new/follow/data.dart b/lib/models_new/follow/data.dart index 7efe4ff424..9dedeee524 100644 --- a/lib/models_new/follow/data.dart +++ b/lib/models_new/follow/data.dart @@ -1,17 +1,15 @@ import 'package:PiliPlus/models_new/follow/list.dart'; class FollowData { - late List list; + List? list; int? total; FollowData({required this.list, this.total}); factory FollowData.fromJson(Map json) => FollowData( - list: - (json['list'] as List?) - ?.map((e) => FollowItemModel.fromJson(e as Map)) - .toList() ?? - [], + list: (json['list'] as List?) + ?.map((e) => FollowItemModel.fromJson(e as Map)) + .toList(), total: json['total'] as int?, ); } diff --git a/lib/models_new/followee_votes/vote.dart b/lib/models_new/followee_votes/vote.dart index c337634288..ea42ab2d6f 100644 --- a/lib/models_new/followee_votes/vote.dart +++ b/lib/models_new/followee_votes/vote.dart @@ -12,12 +12,11 @@ class FolloweeVote extends Owner { FolloweeVote({ required super.mid, - required String name, - required String face, + required this._name, + required this._face, required this.votes, required this.ctime, - }) : _name = name, - _face = face; + }); factory FolloweeVote.fromJson(Map json) => FolloweeVote( mid: json['uid'], diff --git a/lib/models_new/live/live_dm_info/data.dart b/lib/models_new/live/live_dm_info/data.dart index ffca3bee25..519c9db21e 100644 --- a/lib/models_new/live/live_dm_info/data.dart +++ b/lib/models_new/live/live_dm_info/data.dart @@ -1,18 +1,18 @@ import 'package:PiliPlus/models_new/live/live_dm_info/host_list.dart'; class LiveDmInfoData { - String? token; - List? hostList; + String token; + List hostList; LiveDmInfoData({ - this.token, - this.hostList, + required this.token, + required this.hostList, }); factory LiveDmInfoData.fromJson(Map json) => LiveDmInfoData( - token: json['token'] as String?, - hostList: (json['host_list'] as List?) - ?.map((e) => HostList.fromJson(e as Map)) + token: json['token'] as String, + hostList: (json['host_list'] as List) + .map((e) => HostList.fromJson(e as Map)) .toList(), ); } diff --git a/lib/models_new/live/live_feed_index/card_data_list_item.dart b/lib/models_new/live/live_feed_index/card_data_list_item.dart index 16e561fd0d..41c68fc04d 100644 --- a/lib/models_new/live/live_feed_index/card_data_list_item.dart +++ b/lib/models_new/live/live_feed_index/card_data_list_item.dart @@ -27,7 +27,7 @@ class CardLiveItem { this.areaV2Id, this.areaV2ParentId, this.watchedShow, - }) : _systemCover = noneNullOrEmptyString(systemCover); + }) : _systemCover = nonNullOrEmptyString(systemCover); factory CardLiveItem.fromJson(Map json) => CardLiveItem( roomid: json['roomid'] ?? json['id'], diff --git a/lib/models_new/live/live_room_play_info/codec.dart b/lib/models_new/live/live_room_play_info/codec.dart index 1f295cde20..dc4b4e687b 100644 --- a/lib/models_new/live/live_room_play_info/codec.dart +++ b/lib/models_new/live/live_room_play_info/codec.dart @@ -2,24 +2,27 @@ import 'package:PiliPlus/models_new/live/live_room_play_info/url_info.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; class CodecItem { - int? currentQn; - List? acceptQn; - String? baseUrl; - List? urlInfo; + String? codecName; + int currentQn; + List acceptQn; + String baseUrl; + List urlInfo; CodecItem({ - this.currentQn, - this.acceptQn, - this.baseUrl, - this.urlInfo, + this.codecName, + required this.currentQn, + required this.acceptQn, + required this.baseUrl, + required this.urlInfo, }); factory CodecItem.fromJson(Map json) => CodecItem( - currentQn: json['current_qn'] as int?, - acceptQn: (json['accept_qn'] as List?)?.fromCast(), - baseUrl: json['base_url'] as String?, - urlInfo: (json['url_info'] as List?) - ?.map((e) => UrlInfo.fromJson(e as Map)) + codecName: json['codec_name'], + currentQn: json['current_qn'] as int, + acceptQn: (json['accept_qn'] as List).fromCast(), + baseUrl: json['base_url'] as String, + urlInfo: (json['url_info'] as List) + .map((e) => UrlInfo.fromJson(e as Map)) .toList(), ); } diff --git a/lib/models_new/live/live_room_play_info/format.dart b/lib/models_new/live/live_room_play_info/format.dart index afd8de5c53..35dba67a79 100644 --- a/lib/models_new/live/live_room_play_info/format.dart +++ b/lib/models_new/live/live_room_play_info/format.dart @@ -1,13 +1,18 @@ import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart'; class Format { - List? codec; + String? formatName; + List codec; - Format({this.codec}); + Format({ + this.formatName, + required this.codec, + }); factory Format.fromJson(Map json) => Format( - codec: (json['codec'] as List?) - ?.map((e) => CodecItem.fromJson(e as Map)) + formatName: json['format_name'], + codec: (json['codec'] as List) + .map((e) => CodecItem.fromJson(e as Map)) .toList(), ); } diff --git a/lib/models_new/live/live_room_play_info/playurl.dart b/lib/models_new/live/live_room_play_info/playurl.dart index 07977bb3d0..63421a37d2 100644 --- a/lib/models_new/live/live_room_play_info/playurl.dart +++ b/lib/models_new/live/live_room_play_info/playurl.dart @@ -1,15 +1,15 @@ import 'package:PiliPlus/models_new/live/live_room_play_info/stream.dart'; class Playurl { - List? stream; + List stream; Playurl({ - this.stream, + required this.stream, }); factory Playurl.fromJson(Map json) => Playurl( - stream: (json['stream'] as List?) - ?.map((e) => Stream.fromJson(e as Map)) + stream: (json['stream'] as List) + .map((e) => Stream.fromJson(e as Map)) .toList(), ); } diff --git a/lib/models_new/live/live_room_play_info/stream.dart b/lib/models_new/live/live_room_play_info/stream.dart index 18b3a6578c..7aa09ff404 100644 --- a/lib/models_new/live/live_room_play_info/stream.dart +++ b/lib/models_new/live/live_room_play_info/stream.dart @@ -1,13 +1,15 @@ import 'package:PiliPlus/models_new/live/live_room_play_info/format.dart'; class Stream { - List? format; + String? protocolName; + List format; - Stream({this.format}); + Stream({this.protocolName, required this.format}); factory Stream.fromJson(Map json) => Stream( - format: (json['format'] as List?) - ?.map((e) => Format.fromJson(e as Map)) + protocolName: json['protocol_name'], + format: (json['format'] as List) + .map((e) => Format.fromJson(e as Map)) .toList(), ); } diff --git a/lib/models_new/live/live_room_play_info/url_info.dart b/lib/models_new/live/live_room_play_info/url_info.dart index d32d111d31..458b0dab4e 100644 --- a/lib/models_new/live/live_room_play_info/url_info.dart +++ b/lib/models_new/live/live_room_play_info/url_info.dart @@ -1,11 +1,11 @@ class UrlInfo { - String? host; - String? extra; + String host; + String extra; - UrlInfo({this.host, this.extra}); + UrlInfo({required this.host, required this.extra}); factory UrlInfo.fromJson(Map json) => UrlInfo( - host: json['host'] as String?, - extra: json['extra'] as String?, + host: json['host'] as String, + extra: json['extra'] as String, ); } diff --git a/lib/models_new/live/live_superchat/item.dart b/lib/models_new/live/live_superchat/item.dart index 689a047e08..3862d85921 100644 --- a/lib/models_new/live/live_superchat/item.dart +++ b/lib/models_new/live/live_superchat/item.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/models_new/live/live_medal_wall/uinfo_medal.dart'; import 'package:PiliPlus/models_new/live/live_superchat/user_info.dart'; import 'package:PiliPlus/utils/global_data.dart'; +import 'package:PiliPlus/utils/parse_int.dart'; import 'package:PiliPlus/utils/parse_string.dart'; import 'package:PiliPlus/utils/utils.dart'; @@ -64,18 +65,18 @@ class SuperChatItem { }); factory SuperChatItem.fromJson(Map json) => SuperChatItem( - id: Utils.safeToInt(json['id']) ?? Utils.random.nextInt(2147483647), - uid: Utils.safeToInt(json['uid'])!, + id: safeToInt(json['id']) ?? Utils.random.nextInt(2147483647), + uid: safeToInt(json['uid'])!, price: json['price'], - backgroundImage: noneNullOrEmptyString(json['background_image']), + backgroundImage: nonNullOrEmptyString(json['background_image']), backgroundColor: json['background_color'] ?? '#EDF5FF', backgroundBottomColor: json['background_bottom_color'] ?? '#2A60B2', backgroundPriceColor: json['background_price_color'] ?? '#7497CD', messageFontColor: json['message_font_color'] ?? '#FFFFFF', - endTime: Utils.safeToInt(json['end_time'])!, + endTime: safeToInt(json['end_time'])!, message: json['message'], token: json['token'], - ts: Utils.safeToInt(json['ts'])!, + ts: safeToInt(json['ts'])!, userInfo: UserInfo.fromJson(json['user_info'] as Map), medalInfo: !GlobalData().showMedal || json['uinfo']?['medal'] == null ? null diff --git a/lib/models_new/member/search_archive/vlist.dart b/lib/models_new/member/search_archive/vlist.dart index 95f93d1fff..a7018313c5 100644 --- a/lib/models_new/member/search_archive/vlist.dart +++ b/lib/models_new/member/search_archive/vlist.dart @@ -1,7 +1,8 @@ +import 'package:PiliPlus/models/horizontal_video_model.dart'; import 'package:PiliPlus/models/model_video.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; -class VListItemModel extends BaseVideoItemModel { +class VListItemModel extends HorizontalVideoModel { VListItemModel.fromJson(Map json) { cover = json['pic']; desc = json['description']; @@ -14,6 +15,24 @@ class VListItemModel extends BaseVideoItemModel { bvid = json['bvid']; stat = VListStat.fromJson(json); owner = VListOwner.fromJson(json); + if (json['is_lesson_video'] == 1) { + isPugv = true; + badge = '课堂'; + } else if (json['is_charging_arc'] == true) { + badge = '充电专属'; + } else if (json['is_union_video'] == 1) { + badge = '合作'; + } + seasonId = json['season_id']; + redirectUrl = json['jump_url']; + final position = json['playback_position'] as num?; // percent + if (position != null) { + if (position == 100) { + progress = -1; + } else { + progress = ((position / 100) * duration).round(); + } + } } } diff --git a/lib/models_new/member/season_web/archive.dart b/lib/models_new/member/season_web/archive.dart index 4d11ab98ab..79e353b164 100644 --- a/lib/models_new/member/season_web/archive.dart +++ b/lib/models_new/member/season_web/archive.dart @@ -1,6 +1,7 @@ +import 'package:PiliPlus/models/horizontal_video_model.dart'; import 'package:PiliPlus/models/model_video.dart'; -class SeasonArchive extends BaseVideoItemModel { +class SeasonArchive extends HorizontalVideoModel { SeasonArchive.fromJson(Map json) { aid = json['aid']; bvid = json['bvid']; diff --git a/lib/models_new/music/bgm_detail.dart b/lib/models_new/music/bgm_detail.dart index be9fe34969..7bdb577bc8 100644 --- a/lib/models_new/music/bgm_detail.dart +++ b/lib/models_new/music/bgm_detail.dart @@ -15,7 +15,7 @@ class MusicDetail { required this.album, required this.artistsList, required this.listenPv, - required this.musicRank, + required this.achievement, required this.hotSongHeat, required this.musicComment, required this.musicRelation, @@ -26,7 +26,7 @@ class MusicDetail { final String? originArtist; final String? originArtistList; final int? mvAid; - final int? mvCid; + final int mvCid; final String? mvBvid; final String? mvCover; bool? wishListen; @@ -35,7 +35,7 @@ class MusicDetail { final String? album; final List? artistsList; final int? listenPv; - final String? musicRank; + final List achievement; final HotSongHeat? hotSongHeat; final MusicComment? musicComment; final int? musicRelation; @@ -47,7 +47,7 @@ class MusicDetail { originArtist: json["origin_artist"], originArtistList: json["origin_artist_list"], mvAid: json["mv_aid"], - mvCid: json["mv_cid"], + mvCid: json["mv_cid"] ?? 0, mvBvid: json["mv_bvid"], mvCover: json["mv_cover"], wishListen: json["wish_listen"], @@ -58,7 +58,11 @@ class MusicDetail { ?.map((x) => Artist.fromJson(x)) .toList(), listenPv: json["listen_pv"], - musicRank: json["music_rank"], + achievement: [ + ...?json["achievement"], + ?json["music_rank"], + ?json["recreation_rank"], + ], hotSongHeat: json["hot_song_heat"] == null ? null : HotSongHeat.fromJson(json["hot_song_heat"]), diff --git a/lib/models_new/space/space/reservation_card_list.dart b/lib/models_new/space/space/reservation_card_list.dart index 174b286b50..3fe8e65926 100644 --- a/lib/models_new/space/space/reservation_card_list.dart +++ b/lib/models_new/space/space/reservation_card_list.dart @@ -28,7 +28,7 @@ class ReservationCardItem { total: json['total'] ?? 0, isFollow: json['is_follow'] == 1, livePlanStartTime: json['live_plan_start_time'] as int?, - descText1: noneNullOrEmptyString(json['desc_text_1']?['text']), + descText1: nonNullOrEmptyString(json['desc_text_1']?['text']), dynamicId: json['dynamic_id'] as String?, lotteryPrizeInfo: json['lottery_prize_info'] == null ? null diff --git a/lib/models_new/space/space/top.dart b/lib/models_new/space/space/top.dart index d87410d40c..bc66cf7de0 100644 --- a/lib/models_new/space/space/top.dart +++ b/lib/models_new/space/space/top.dart @@ -28,7 +28,7 @@ class TopImage { final item = json['item']; final img = item['image']; title = json['title'] == null ? null : TopTitle.fromJson(json['title']); - _defaultImage = noneNullOrEmptyString(img?['default_image']); + _defaultImage = nonNullOrEmptyString(img?['default_image']); fullCover = json['cover']; double dy = 0; try { diff --git a/lib/models_new/video/video_detail/data.dart b/lib/models_new/video/video_detail/data.dart index b1e32ad661..aac04c2bde 100644 --- a/lib/models_new/video/video_detail/data.dart +++ b/lib/models_new/video/video_detail/data.dart @@ -103,6 +103,6 @@ class VideoDetailData { staff: (json["staff"] as List?) ?.map((item) => Staff.fromJson(item)) .toList(), - redirectUrl: noneNullOrEmptyString(json['redirect_url']), + redirectUrl: nonNullOrEmptyString(json['redirect_url']), ); } diff --git a/lib/models_new/video/video_play_info/subtitle.dart b/lib/models_new/video/video_play_info/subtitle.dart index 797adb963d..f3bf283065 100644 --- a/lib/models_new/video/video_play_info/subtitle.dart +++ b/lib/models_new/video/video_play_info/subtitle.dart @@ -1,4 +1,4 @@ -class Subtitle { +class Subtitle implements Comparable { late String lan; String? lanDoc; String? subtitleUrl; @@ -8,6 +8,8 @@ class Subtitle { Subtitle({ required this.lan, this.lanDoc, + this.subtitleUrl, + this.isAi = false, }); Subtitle.fromJson(Map json) { @@ -17,4 +19,13 @@ class Subtitle { subtitleUrl = json["subtitle_url"]; subtitleUrlV2 = json["subtitle_url_v2"]; } + + @override + int compareTo(Subtitle other) { + final thisHasZh = lan.contains('zh'); + final otherHasZh = other.lan.contains('zh'); + if (thisHasZh != otherHasZh) return thisHasZh ? -1 : 1; + if (isAi != other.isAi) return isAi ? 1 : -1; + return 0; + } } diff --git a/lib/models_new/video/video_play_info/subtitle_info.dart b/lib/models_new/video/video_play_info/subtitle_info.dart index d559c1697a..33e8861860 100644 --- a/lib/models_new/video/video_play_info/subtitle_info.dart +++ b/lib/models_new/video/video_play_info/subtitle_info.dart @@ -14,12 +14,6 @@ class SubtitleInfo { (json['subtitles'] as List?) ?.map((e) => Subtitle.fromJson(e as Map)) .toList() - ?..sort((a, b) { - final aHasZh = a.lan.contains('zh'); - final bHasZh = b.lan.contains('zh'); - if (aHasZh != bHasZh) return aHasZh ? -1 : 1; - if (a.isAi != b.isAi) return a.isAi ? 1 : -1; - return 0; - }), + ?..sort(), ); } diff --git a/lib/pages/about/view.dart b/lib/pages/about/view.dart index 29ee5b55e6..a07c4b6854 100644 --- a/lib/pages/about/view.dart +++ b/lib/pages/about/view.dart @@ -7,14 +7,16 @@ import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/dialog/export_import.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; import 'package:PiliPlus/services/logger.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts/account.dart'; +import 'package:PiliPlus/utils/android/android_helper.dart'; import 'package:PiliPlus/utils/cache_manager.dart'; import 'package:PiliPlus/utils/date_utils.dart'; -import 'package:PiliPlus/utils/extension/context_ext.dart'; +import 'package:PiliPlus/utils/device_utils.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/login_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; @@ -177,25 +179,17 @@ Commit Hash: ${BuildConfig.commitHash}''', ), if (Platform.isAndroid) ListTile( - onTap: () => Utils.channel.invokeMethod('linkVerifySettings'), + onTap: PiliAndroidHelper.openLinkVerifySettings, leading: const Icon(MdiIcons.linkBoxOutline), title: const Text('打开受支持的链接'), - trailing: Icon( - Icons.arrow_forward, - size: 16, - color: outline, - ), + trailing: Icon(Icons.arrow_forward, size: 16, color: outline), ), ListTile( onTap: () => PageUtils.launchURL('${Constants.sourceCodeUrl}/issues'), leading: const Icon(Icons.feedback_outlined), title: const Text('问题反馈'), - trailing: Icon( - Icons.arrow_forward, - size: 16, - color: outline, - ), + trailing: Icon(Icons.arrow_forward, size: 16, color: outline), ), ListTile( onTap: () => Get.toNamed('/logs'), @@ -268,7 +262,7 @@ Commit Hash: ${BuildConfig.commitHash}''', onTap: () => showImportExportDialog>( context, title: '设置', - localFileName: () => 'setting_${context.platformName}', + localFileName: () => 'setting_${DeviceUtils.platformName}', onExport: GStorage.exportAllSettings, onImport: GStorage.importAllJsonSettings, ), @@ -283,9 +277,8 @@ Commit Hash: ${BuildConfig.commitHash}''', clipBehavior: Clip.hardEdge, title: const Text('是否重置所有设置?'), children: [ - ListTile( - dense: true, - onTap: () async { + DialogOption( + onPressed: () async { Get.back(); await Future.wait([ GStorage.setting.clear(), @@ -293,16 +286,15 @@ Commit Hash: ${BuildConfig.commitHash}''', ]); SmartDialog.showToast('重置成功'); }, - title: const Text('重置可导出的设置', style: style), + child: const Text('重置可导出的设置', style: style), ), - ListTile( - dense: true, - onTap: () async { + DialogOption( + onPressed: () async { Get.back(); await GStorage.clear(); SmartDialog.showToast('重置成功'); }, - title: const Text('重置所有数据(含登录信息)', style: style), + child: const Text('重置所有数据(含登录信息)', style: style), ), ], ); diff --git a/lib/pages/article/controller.dart b/lib/pages/article/controller.dart index c2e1470d9f..aecececd93 100644 --- a/lib/pages/article/controller.dart +++ b/lib/pages/article/controller.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/video.dart'; +import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/models/dynamics/article_content_model.dart' show ArticleContentModel; import 'package:PiliPlus/models/dynamics/result.dart'; @@ -38,11 +39,17 @@ class ArticleController extends CommonDynController { final RxBool isLoaded = false.obs; DynamicItemModel? opusData; // 标题信息从summary获取, 动态没有favorite ArticleViewData? articleData; - final Rx stats = Rx(null); + final stats = Rxn(); List? get opus => opusData?.modules.moduleContent ?? articleData?.opus?.content; + List? _images; + List images() => _images ??= opus! + .where((e) => e.paraType == 2 && e.pic != null) + .map((e) => SourceModel(url: e.pic!.pics!.first.url!)) + .toList(); + @override void onInit() { super.onInit(); diff --git a/lib/pages/article/view.dart b/lib/pages/article/view.dart index 5d92d25f15..f404e1f5cd 100644 --- a/lib/pages/article/view.dart +++ b/lib/pages/article/view.dart @@ -5,6 +5,7 @@ import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_to_box_adapter.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/models/common/image_type.dart'; @@ -22,8 +23,9 @@ import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; +import 'package:PiliPlus/utils/share_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -49,46 +51,33 @@ class _ArticlePageState extends CommonDynPageState { 'id': controller.id, }; - @override - void didChangeDependencies() { - super.didChangeDependencies(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { - controller.showTitle.value = - scrollController.positions.last.pixels >= 45; - } - }); - } - @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Scaffold( + final child = Scaffold( resizeToAvoidBottomInset: false, appBar: _buildAppBar(), body: Padding( padding: EdgeInsets.only(left: padding.left, right: padding.right), - child: _buildPage(theme), + child: _buildPage(), ), floatingActionButtonLocation: floatingActionButtonLocation, floatingActionButton: SlideTransition( position: fabAnimation, - child: _buildBottom(theme), + child: _buildBottom(), ), ); + return fabAnimWrapper(child); } - Widget _buildPage(ThemeData theme) { + Widget _buildPage() { double padding = max(maxWidth / 2 - Grid.smallCardWidth, 0); if (isPortrait) { return Padding( padding: EdgeInsets.symmetric(horizontal: padding), child: CustomScrollView( - controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ _buildContent( - theme, maxWidth - this.padding.horizontal - 2 * padding - 24, ), SliverToBoxAdapter( @@ -97,8 +86,8 @@ class _ArticlePageState extends CommonDynPageState { color: theme.dividerColor.withValues(alpha: 0.05), ), ), - buildReplyHeader(theme), - Obx(() => replyList(theme, controller.loadingState.value)), + buildReplyHeader(), + Obx(() => replyList(controller.loadingState.value)), ], ), ); @@ -113,7 +102,6 @@ class _ArticlePageState extends CommonDynPageState { Expanded( flex: flex, child: CustomScrollView( - controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( @@ -122,7 +110,6 @@ class _ArticlePageState extends CommonDynPageState { bottom: this.padding.bottom + 100, ), sliver: _buildContent( - theme, (maxWidth - this.padding.horizontal) * flex / (flex + flex1) - padding - 32, @@ -145,11 +132,10 @@ class _ArticlePageState extends CommonDynPageState { body: refreshIndicator( onRefresh: controller.onRefresh, child: CustomScrollView( - controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ - buildReplyHeader(theme), - Obx(() => replyList(theme, controller.loadingState.value)), + buildReplyHeader(), + Obx(() => replyList(controller.loadingState.value)), ], ), ), @@ -160,7 +146,7 @@ class _ArticlePageState extends CommonDynPageState { ); } - Widget _buildContent(ThemeData theme, double maxWidth) => SliverPadding( + Widget _buildContent(double maxWidth) => SliverPadding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), sliver: Obx( () { @@ -170,6 +156,7 @@ class _ArticlePageState extends CommonDynPageState { // if (kDebugMode) debugPrint('json page'); content = OpusContent( opus: controller.opus!, + images: controller.images, maxWidth: maxWidth, ); } else if (controller.opusData?.modules.moduleBlocked != null) { @@ -337,7 +324,9 @@ class _ArticlePageState extends CommonDynPageState { ), ), if (controller.summary.title != null) - SliverToBoxAdapter( + SliverToBoxWithVisibilityAdapter( + onVisibilityChanged: (bool visible) => + controller.showTitle.value = !visible, child: Text( controller.summary.title!, style: const TextStyle( @@ -427,7 +416,7 @@ class _ArticlePageState extends CommonDynPageState { icon: const Icon(Icons.more_vert, size: 19), itemBuilder: (BuildContext context) => [ PopupMenuItem( - onTap: () => Utils.shareText(controller.url), + onTap: () => ShareUtils.shareText(controller.url), child: const Row( mainAxisSize: MainAxisSize.min, children: [ @@ -493,7 +482,7 @@ class _ArticlePageState extends CommonDynPageState { ], ); - Widget _buildBottom(ThemeData theme) { + Widget _buildBottom() { if (!controller.showDynActionBar) { return fabButton; } @@ -620,7 +609,7 @@ class _ArticlePageState extends CommonDynPageState { text: '分享', icon: CustomIcons.share_node, stat: null, - onPressed: () => Utils.shareText(controller.url), + onPressed: () => ShareUtils.shareText(controller.url), ), ), Expanded( diff --git a/lib/pages/article/widgets/article_ops.dart b/lib/pages/article/widgets/article_ops.dart index 368a424730..943520e785 100644 --- a/lib/pages/article/widgets/article_ops.dart +++ b/lib/pages/article/widgets/article_ops.dart @@ -6,16 +6,16 @@ import 'package:PiliPlus/pages/dynamics/widgets/vote.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; class ArticleOpus extends StatelessWidget { const ArticleOpus({ super.key, - required List? ops, + required this._ops, required this.maxWidth, - }) : _ops = ops; + }); final List? _ops; final double maxWidth; diff --git a/lib/pages/article/widgets/html_render.dart b/lib/pages/article/widgets/html_render.dart index caf10a468d..68b27fa680 100644 --- a/lib/pages/article/widgets/html_render.dart +++ b/lib/pages/article/widgets/html_render.dart @@ -4,7 +4,7 @@ import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter_html/flutter_html.dart'; diff --git a/lib/pages/article/widgets/opus_content.dart b/lib/pages/article/widgets/opus_content.dart index 2cfe06f344..e6257447bf 100644 --- a/lib/pages/article/widgets/opus_content.dart +++ b/lib/pages/article/widgets/opus_content.dart @@ -1,7 +1,6 @@ import 'dart:math' as math; import 'package:PiliPlus/common/assets.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image/cached_network_svg_image.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; @@ -16,13 +15,13 @@ import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/pages/dynamics/widgets/vote.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; -import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart' show kDebugMode; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:get/get_core/src/get_main.dart'; import 'package:get/get_navigation/src/extension_navigation.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; @@ -33,11 +32,13 @@ import 'package:re_highlight/styles/github.dart'; class OpusContent extends StatelessWidget { final List opus; + final ValueGetter> images; final double maxWidth; const OpusContent({ super.key, required this.opus, + required this.images, required this.maxWidth, }); @@ -80,9 +81,7 @@ class OpusContent extends StatelessWidget { // case 'RICH_TEXT_NODE_TYPE_TOPIC': default: if (rich.jumpUrl != null) { - PiliScheme.routePushFromUrl( - rich.jumpUrl!, - ); + PiliScheme.routePushFromUrl(rich.jumpUrl!); } } }, @@ -99,7 +98,7 @@ class OpusContent extends StatelessWidget { colorScheme.onSurfaceVariant, BlendMode.srcIn, ), - alignment: Alignment.centerLeft, + alignment: .centerLeft, placeholderBuilder: (_) => Text(latex), errorBuilder: (_) => Text(latex), ), @@ -240,9 +239,11 @@ class OpusContent extends StatelessWidget { child: child, ); } + final images = this.images(); return GestureDetector( onTap: () => PageUtils.imageView( - imgList: [SourceModel(url: pic.url!)], + imgList: images, + initialPage: images.indexWhere((e) => e.url == pic.url), quality: 60, ), child: child, @@ -260,7 +261,7 @@ class OpusContent extends StatelessWidget { .toList(), ); } - case 3 when (element.line != null): + case 3 when (element.line?.pic != null): final height = element.line!.pic!.height?.toDouble(); return CachedNetworkImage( fit: .contain, @@ -273,14 +274,14 @@ class OpusContent extends StatelessWidget { case 5 when (element.list != null): return SelectableText.rich( TextSpan( - children: element.list!.items?.indexed.map((entry) { + children: element.list!.items?.mapIndexed((i, entry) { return TextSpan( children: [ const WidgetSpan( child: Icon(MdiIcons.circleMedium), - alignment: PlaceholderAlignment.middle, + alignment: .middle, ), - ...entry.$2.nodes!.map((item) { + ...entry.nodes!.map((item) { if (item.word != null) { return _getSpan( item.word, @@ -307,7 +308,7 @@ class OpusContent extends StatelessWidget { } return const TextSpan(); }), - if (entry.$1 < element.list!.items!.length - 1) + if (i < element.list!.items!.length - 1) const TextSpan(text: '\n'), ], ); @@ -315,18 +316,280 @@ class OpusContent extends StatelessWidget { ), ); case 6: + final type = element.linkCard!.card!.type; + Widget child; + switch (type) { + case 'LINK_CARD_TYPE_UGC': + final ugc = element.linkCard!.card!.ugc!; + child = Row( + spacing: 10, + children: [ + NetworkImgLayer( + width: 104, + height: 65, + src: ugc.cover, + borderRadius: const .all(.circular(6)), + ), + Expanded( + child: Column( + crossAxisAlignment: .start, + children: [ + Text(ugc.title!), + Text( + ugc.descSecond!, + maxLines: 2, + overflow: .ellipsis, + style: TextStyle( + fontSize: 13, + color: colorScheme.outline, + ), + ), + ], + ), + ), + ], + ); + case 'LINK_CARD_TYPE_ITEM_NULL': + final itemNull = element.linkCard?.card?.itemNull; + child = Text( + ' ${itemNull?.text}: ${element.linkCard?.card?.oid}', + ); + case 'LINK_CARD_TYPE_COMMON': + final common = element.linkCard!.card!.common!; + child = Row( + spacing: 10, + children: [ + NetworkImgLayer( + width: 104, + height: 65, + src: common.cover, + borderRadius: const .all(.circular(6)), + ), + Expanded( + child: Column( + crossAxisAlignment: .start, + children: [ + Text(common.title!), + if (common.desc1 != null) + Text( + common.desc1!, + style: TextStyle( + fontSize: 13, + color: colorScheme.outline, + ), + ), + if (common.desc2 != null) + Text( + common.desc2!, + maxLines: 2, + overflow: .ellipsis, + style: TextStyle( + fontSize: 13, + color: colorScheme.outline, + ), + ), + ], + ), + ), + ], + ); + case 'LINK_CARD_TYPE_LIVE': + final live = element.linkCard!.card!.live!; + child = Row( + spacing: 10, + children: [ + NetworkImgLayer( + width: 104, + height: 65, + src: live.cover, + borderRadius: const .all(.circular(6)), + ), + Expanded( + child: Column( + crossAxisAlignment: .start, + children: [ + Text(live.title!), + if (live.descFirst != null) + Text( + live.descFirst!, + style: TextStyle( + fontSize: 13, + color: colorScheme.outline, + ), + ), + if (live.descSecond != null) + Text( + live.descSecond!, + maxLines: 2, + overflow: .ellipsis, + style: TextStyle( + fontSize: 13, + color: colorScheme.outline, + ), + ), + ], + ), + ), + ], + ); + case 'LINK_CARD_TYPE_OPUS': + final opus = element.linkCard!.card!.opus!; + child = Row( + spacing: 10, + children: [ + NetworkImgLayer( + width: 104, + height: 65, + src: opus.cover, + borderRadius: const .all(.circular(6)), + ), + Expanded( + child: Column( + crossAxisAlignment: .start, + children: [ + Text(opus.title!), + Text( + '${opus.authorName} · ${opus.statView ?? 0}阅读', + style: TextStyle( + fontSize: 13, + color: colorScheme.outline, + ), + ), + ], + ), + ), + ], + ); + case 'LINK_CARD_TYPE_VOTE': + final vote = element.linkCard!.card!.vote!; + child = Row( + spacing: 10, + children: [ + Container( + decoration: BoxDecoration( + borderRadius: const .all(.circular(6)), + color: colorScheme.secondaryContainer, + ), + width: 70, + height: 50, + alignment: .center, + child: Icon( + Icons.bar_chart_rounded, + color: colorScheme.onSurfaceVariant, + ), + ), + Expanded( + child: Column( + crossAxisAlignment: .start, + children: [ + Text(vote.desc!), + Text( + '${vote.joinNum}人参与', + style: TextStyle( + fontSize: 13, + color: colorScheme.outline, + ), + ), + ], + ), + ), + ], + ); + case 'LINK_CARD_TYPE_MUSIC': + final music = element.linkCard!.card!.music!; + child = Row( + spacing: 10, + children: [ + NetworkImgLayer( + width: 104, + height: 65, + src: music.cover, + borderRadius: const .all(.circular(6)), + ), + Expanded( + child: Column( + crossAxisAlignment: .start, + children: [ + Text(music.title!), + if (music.label != null) + Text( + music.label!, + maxLines: 2, + overflow: .ellipsis, + style: TextStyle( + fontSize: 13, + color: colorScheme.outline, + ), + ), + ], + ), + ), + ], + ); + case 'LINK_CARD_TYPE_GOODS': + final goods = element.linkCard!.card!.goods!; + child = Column( + children: goods.items!.map((e) { + return GestureDetector( + onTap: () { + if (e.jumpUrl?.isNotEmpty == true) { + PiliScheme.routePushFromUrl(e.jumpUrl!); + } + }, + child: Row( + spacing: 10, + children: [ + NetworkImgLayer( + width: 104, + height: 65, + src: e.cover, + borderRadius: const .all(.circular(6)), + ), + Expanded( + child: Column( + crossAxisAlignment: .start, + children: [ + Text(e.name!), + if (e.brief?.isNotEmpty == true) + Text( + e.brief!, + style: TextStyle( + fontSize: 13, + color: colorScheme.outline, + ), + ), + if (e.price?.isNotEmpty == true) + Text( + '${e.price!}起', + style: TextStyle( + fontSize: 13, + color: colorScheme.outline, + ), + ), + ], + ), + ), + ], + ), + ); + }).toList(), + ); + default: + throw UnimplementedError( + '\nparaType: ${element.paraType},\ncard type: $type', + ); + } return Material( shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(8)), + borderRadius: .all(.circular(8)), ), color: colorScheme.onInverseSurface, child: InkWell( - onTap: element.linkCard!.card!.type == 'LINK_CARD_TYPE_GOODS' + onTap: type == 'LINK_CARD_TYPE_GOODS' ? null : () { try { - if (element.linkCard!.card!.type == - 'LINK_CARD_TYPE_VOTE') { + if (type == 'LINK_CARD_TYPE_VOTE') { showVoteDialog( context, element.linkCard!.card!.vote?.voteId ?? @@ -334,302 +597,48 @@ class OpusContent extends StatelessWidget { ); return; } - String? url = - switch (element.linkCard!.card!.type) { - 'LINK_CARD_TYPE_UGC' => - element.linkCard!.card!.ugc!.jumpUrl, - 'LINK_CARD_TYPE_COMMON' => - element.linkCard!.card!.common!.jumpUrl, - 'LINK_CARD_TYPE_LIVE' => - element.linkCard!.card!.live!.jumpUrl, - 'LINK_CARD_TYPE_OPUS' => - element.linkCard!.card!.opus!.jumpUrl, - 'LINK_CARD_TYPE_MUSIC' => - element.linkCard!.card!.music!.jumpUrl, - _ => null, - }; + if (type == 'LINK_CARD_TYPE_ITEM_NULL') { + switch (element.linkCard?.card?.itemNull?.text) { + case '视频': + PiliScheme.videoPush( + int.parse(element.linkCard!.card!.oid!), + null, + ); + default: + PageUtils.pushDynFromId( + id: element.linkCard!.card!.oid!, + ); + } + return; + } + String? url = switch (type) { + 'LINK_CARD_TYPE_UGC' => + element.linkCard!.card!.ugc!.jumpUrl, + 'LINK_CARD_TYPE_COMMON' => + element.linkCard!.card!.common!.jumpUrl, + 'LINK_CARD_TYPE_LIVE' => + element.linkCard!.card!.live!.jumpUrl, + 'LINK_CARD_TYPE_OPUS' => + element.linkCard!.card!.opus!.jumpUrl, + 'LINK_CARD_TYPE_MUSIC' => + element.linkCard!.card!.music!.jumpUrl, + _ => null, + }; if (url != null && url.isNotEmpty) { PiliScheme.routePushFromUrl(url); } } catch (_) {} }, - borderRadius: const BorderRadius.all(Radius.circular(8)), + borderRadius: const .all(.circular(8)), child: Padding( - padding: const EdgeInsets.all(8), - child: switch (element.linkCard?.card?.type) { - 'LINK_CARD_TYPE_UGC' => Row( - spacing: 10, - children: [ - NetworkImgLayer( - width: 104, - height: 65, - src: element.linkCard!.card!.ugc!.cover, - borderRadius: const BorderRadius.all( - Radius.circular(6), - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(element.linkCard!.card!.ugc!.title!), - Text( - element.linkCard!.card!.ugc!.descSecond!, - style: TextStyle( - fontSize: 13, - color: colorScheme.outline, - ), - ), - ], - ), - ), - ], - ), - 'LINK_CARD_TYPE_ITEM_NULL' => Row( - children: [ - if (element - .linkCard - ?.card - ?.itemNull - ?.icon - ?.isNullOrEmpty == - true) - const Icon(Icons.info, size: 20), - Text(' ${element.linkCard?.card?.itemNull?.text}'), - ], - ), - 'LINK_CARD_TYPE_COMMON' => Row( - spacing: 10, - children: [ - NetworkImgLayer( - width: 104, - height: 65, - src: element.linkCard!.card!.common!.cover, - borderRadius: const BorderRadius.all( - Radius.circular(6), - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(element.linkCard!.card!.common!.title!), - if (element.linkCard!.card!.common!.desc1 != - null) - Text( - element.linkCard!.card!.common!.desc1!, - style: TextStyle( - fontSize: 13, - color: colorScheme.outline, - ), - ), - if (element.linkCard!.card!.common!.desc2 != - null) - Text( - element.linkCard!.card!.common!.desc2!, - style: TextStyle( - fontSize: 13, - color: colorScheme.outline, - ), - ), - ], - ), - ), - ], - ), - 'LINK_CARD_TYPE_LIVE' => Row( - spacing: 10, - children: [ - NetworkImgLayer( - width: 104, - height: 65, - src: element.linkCard!.card!.live!.cover, - borderRadius: const BorderRadius.all( - Radius.circular(6), - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(element.linkCard!.card!.live!.title!), - if (element.linkCard!.card!.live!.descFirst != - null) - Text( - element.linkCard!.card!.live!.descFirst!, - style: TextStyle( - fontSize: 13, - color: colorScheme.outline, - ), - ), - if (element.linkCard!.card!.live!.descSecond != - null) - Text( - element.linkCard!.card!.live!.descSecond!, - style: TextStyle( - fontSize: 13, - color: colorScheme.outline, - ), - ), - ], - ), - ), - ], - ), - 'LINK_CARD_TYPE_OPUS' => Row( - spacing: 10, - children: [ - NetworkImgLayer( - width: 104, - height: 65, - src: element.linkCard!.card!.opus!.cover, - borderRadius: const BorderRadius.all( - Radius.circular(6), - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(element.linkCard!.card!.opus!.title!), - Text( - '${element.linkCard!.card!.opus!.authorName} · ${element.linkCard!.card!.opus!.statView ?? 0}阅读', - style: TextStyle( - fontSize: 13, - color: colorScheme.outline, - ), - ), - ], - ), - ), - ], - ), - 'LINK_CARD_TYPE_VOTE' => Row( - spacing: 10, - children: [ - Container( - decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(6), - ), - color: colorScheme.secondaryContainer, - ), - width: 70, - height: 50, - alignment: Alignment.center, - child: Icon( - Icons.bar_chart_rounded, - color: colorScheme.onSurfaceVariant, - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(element.linkCard!.card!.vote!.desc!), - Text( - '${element.linkCard!.card!.vote!.joinNum}人参与', - style: TextStyle( - fontSize: 13, - color: colorScheme.outline, - ), - ), - ], - ), - ), - ], - ), - 'LINK_CARD_TYPE_MUSIC' => Row( - spacing: 10, - children: [ - NetworkImgLayer( - width: 104, - height: 65, - src: element.linkCard!.card!.music!.cover, - borderRadius: const BorderRadius.all( - Radius.circular(6), - ), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(element.linkCard!.card!.music!.title!), - if (element.linkCard!.card!.music!.label != - null) - Text( - element.linkCard!.card!.music!.label!, - style: TextStyle( - fontSize: 13, - color: colorScheme.outline, - ), - ), - ], - ), - ), - ], - ), - 'LINK_CARD_TYPE_GOODS' => Column( - children: element.linkCard!.card!.goods!.items!.map(( - e, - ) { - return GestureDetector( - onTap: () { - if (e.jumpUrl?.isNotEmpty == true) { - PiliScheme.routePushFromUrl(e.jumpUrl!); - } - }, - child: Row( - spacing: 10, - children: [ - NetworkImgLayer( - width: 104, - height: 65, - src: e.cover, - borderRadius: const BorderRadius.all( - Radius.circular(6), - ), - ), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text(e.name!), - if (e.brief?.isNotEmpty == true) - Text( - e.brief!, - style: TextStyle( - fontSize: 13, - color: colorScheme.outline, - ), - ), - if (e.price?.isNotEmpty == true) - Text( - '${e.price!}起', - style: TextStyle( - fontSize: 13, - color: colorScheme.outline, - ), - ), - ], - ), - ), - ], - ), - ); - }).toList(), - ), - _ => throw UnimplementedError( - '\nparaType: ${element.paraType},\ncard type: ${element.linkCard?.card?.type}', - ), - }, + padding: const .all(8), + child: child, ), ), ); case 7 when (element.code != null): final renderer = TextSpanRenderer( - const TextStyle(), + null, isDarkMode ? githubDarkTheme : githubTheme, ); highlight @@ -645,12 +654,12 @@ class OpusContent extends StatelessWidget { ) .render(renderer); return Container( - padding: const EdgeInsets.all(12), + padding: const .all(12), decoration: BoxDecoration( - borderRadius: const BorderRadius.all(Radius.circular(8)), + borderRadius: const .all(.circular(8)), color: colorScheme.onInverseSurface, ), - width: double.infinity, + width: .infinity, child: SelectableText.rich(renderer.span!), ); case 8 when (element.heading?.nodes?.isNotEmpty == true): @@ -688,16 +697,16 @@ class OpusContent extends StatelessWidget { return SelectableText( '不支持的类型 (${element.paraType})', style: const TextStyle( - fontWeight: FontWeight.bold, + fontWeight: .bold, color: Colors.red, ), ); } - } catch (e) { + } catch (e, s) { return SelectableText( - '错误的类型 $e', + '错误的类型 $e${kDebugMode ? '\n$s' : ''}', style: const TextStyle( - fontWeight: FontWeight.bold, + fontWeight: .bold, color: Colors.red, ), ); @@ -713,14 +722,14 @@ Widget moduleBlockedItem( ThemeData theme, ModuleBlocked moduleBlocked, ) { - late final isDarkMode = theme.brightness.isDark; + late final isDarkMode = theme.isDark; BoxDecoration? bgImg(double width) { return moduleBlocked.bgImg == null ? null : BoxDecoration( image: DecorationImage( - fit: BoxFit.fill, + fit: .fill, image: ResizeImage( CachedNetworkImageProvider( ImageUtils.thumbnailUrl( @@ -756,7 +765,7 @@ Widget moduleBlockedItem( return FilledButton.tonal( style: FilledButton.styleFrom( padding: padding, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, + tapTargetSize: .shrinkWrap, visualDensity: visualDensity, backgroundColor: theme.colorScheme.btnColor, foregroundColor: Colors.white, @@ -768,7 +777,7 @@ Widget moduleBlockedItem( } }, child: Row( - mainAxisSize: MainAxisSize.min, + mainAxisSize: .min, children: [ if (moduleBlocked.button!.icon?.isNotEmpty == true) CachedNetworkImage( @@ -795,9 +804,9 @@ Widget moduleBlockedItem( width: maxWidth, height: maxWidth, decoration: bgImg(maxWidth), - padding: const EdgeInsets.all(12), + padding: const .all(12), child: Column( - mainAxisAlignment: MainAxisAlignment.center, + mainAxisAlignment: .center, children: [ if (moduleBlocked.icon != null) icon(math.max(40, maxWidth / 7)), @@ -827,16 +836,16 @@ Widget moduleBlockedItem( builder: (context, constraints) { return Container( decoration: bgImg(constraints.maxWidth), - padding: const EdgeInsets.all(12), + padding: const .all(12), child: Row( spacing: 8, children: [ if (moduleBlocked.icon != null) icon(42), Expanded( child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, spacing: 2, + mainAxisSize: .min, + crossAxisAlignment: .start, children: [ if (moduleBlocked.title?.isNotEmpty == true) Text(moduleBlocked.title!), @@ -859,9 +868,9 @@ Widget moduleBlockedItem( horizontal: -4, ), shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(6)), + borderRadius: .all(.circular(6)), ), - padding: const EdgeInsets.symmetric(horizontal: 10), + padding: const .symmetric(horizontal: 10), ), ], ), @@ -888,15 +897,15 @@ Widget opusCollection(ThemeData theme, ModuleCollection item) { children: [ Expanded( child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: .min, + crossAxisAlignment: .start, children: [ Text(item.title!), Text.rich( TextSpan( children: [ WidgetSpan( - alignment: PlaceholderAlignment.middle, + alignment: .middle, child: Icon( size: 18, Icons.article_outlined, diff --git a/lib/pages/article_list/controller.dart b/lib/pages/article_list/controller.dart index 660d8d5153..3c3dbb6735 100644 --- a/lib/pages/article_list/controller.dart +++ b/lib/pages/article_list/controller.dart @@ -17,7 +17,7 @@ class ArticleListController queryData(); } - Rx list = Rx(null); + final list = Rxn(); Owner? author; @override diff --git a/lib/pages/article_list/widgets/item.dart b/lib/pages/article_list/widgets/item.dart index c6222f10a2..40018e4a4e 100644 --- a/lib/pages/article_list/widgets/item.dart +++ b/lib/pages/article_list/widgets/item.dart @@ -1,10 +1,9 @@ import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; import 'package:PiliPlus/models/common/stat_type.dart'; import 'package:PiliPlus/models_new/article/article_list/article.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; class ArticleListItem extends StatelessWidget { diff --git a/lib/pages/audio/controller.dart b/lib/pages/audio/controller.dart index fdb1ad1748..a03fbd5ed4 100644 --- a/lib/pages/audio/controller.dart +++ b/lib/pages/audio/controller.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:PiliPlus/common/constants.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/grpc/audio.dart'; import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart' show @@ -19,21 +20,27 @@ import 'package:PiliPlus/pages/common/common_intro_controller.dart' show FavMixin; import 'package:PiliPlus/pages/dynamics_repost/view.dart'; import 'package:PiliPlus/pages/main_reply/view.dart'; +import 'package:PiliPlus/pages/setting/models/play_settings.dart' + show kMaxVolume; import 'package:PiliPlus/pages/sponsor_block/block_mixin.dart'; import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/introduction/ugc/widgets/triple_mixin.dart'; -import 'package:PiliPlus/pages/video/pay_coins/view.dart'; +import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; import 'package:PiliPlus/services/service_locator.dart'; import 'package:PiliPlus/services/shutdown_timer_service.dart'; import 'package:PiliPlus/utils/accounts.dart'; +import 'package:PiliPlus/utils/connectivity_utils.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:PiliPlus/utils/share_utils.dart'; +import 'package:PiliPlus/utils/storage.dart'; +import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/video_utils.dart'; @@ -67,8 +74,8 @@ class AudioController extends GetxController late int cacheAudioQa; late bool isDragging = false; - final Rx position = Duration.zero.obs; - final Rx duration = Duration.zero.obs; + final RxInt position = RxInt(0); + final RxInt duration = RxInt(0); late final AnimationController animController; @@ -81,6 +88,7 @@ class AudioController extends GetxController late final Rx playMode = Pref.audioPlayMode.obs; + @override late final isLogin = Accounts.main.isLogin; Duration? _start; @@ -92,6 +100,34 @@ class AudioController extends GetxController ListOrder order = ListOrder.ORDER_NORMAL; + double? _lastVolume; + late final RxDouble desktopVolume = RxDouble(Pref.desktopVolume); + + void toggleVolume() { + if (_lastVolume == null) { + _lastVolume = desktopVolume.value; + setVolume(0, clearLastVolme: false); + } else { + setVolume(_lastVolume!); + } + } + + void setVolume(double volume, {bool clearLastVolme = true}) { + if (clearLastVolme) { + _lastVolume = null; + } + desktopVolume.value = volume; + player?.setVolume(volume * 100); + } + + void syncVolume([_]) { + final volume = desktopVolume.value; + PlPlayerController.instance + ?..volume.value = volume + ..videoPlayerController?.setVolume(volume * 100); + GStorage.setting.put(SettingBoxKey.desktopVolume, volume.toPrecision(3)); + } + @override void onInit() { super.onInit(); @@ -121,7 +157,7 @@ class AudioController extends GetxController _querySponsorBlock(); _onOpenMedia(audioUrl, ua: BrowserUa.pc, referer: HttpString.baseUrl); } - Utils.isWiFi.then((isWiFi) { + ConnectivityUtils.isWiFi.then((isWiFi) { cacheAudioQa = isWiFi ? Pref.defaultAudioQa : Pref.defaultAudioQaCellular; if (!hasAudioUrl) { _queryPlayUrl(); @@ -255,7 +291,7 @@ class AudioController extends GetxController if (audios.isEmpty) { return; } - position.value = Duration.zero; + position.value = 0; final audio = audios.findClosestTarget( (e) => e.id <= cacheAudioQa, (a, b) => a.id > b.id ? a : b, @@ -268,7 +304,7 @@ class AudioController extends GetxController return; } final durl = durls.first; - position.value = Duration.zero; + position.value = 0; _onOpenMedia(VideoUtils.getCdnUrl(durl.playUrls)); } } @@ -294,7 +330,17 @@ class AudioController extends GetxController if (_hasInit) return; _hasInit = true; assert(player == null, _subscriptions = null); - player = await Player.create(); + player = await Player.create( + configuration: PlayerConfiguration( + options: { + 'volume': PlatformUtils.isDesktop + ? (desktopVolume.value * 100).toString() + : Pref.playerVolume.toString(), + 'volume-max': kMaxVolume.toString(), + ...Pref.initBuffer(), + }, + ), + ); if (isClosed) { player!.dispose(); player = null; @@ -304,13 +350,16 @@ class AudioController extends GetxController _subscriptions = [ stream.position.listen((position) { if (isDragging) return; - if (position.inSeconds != this.position.value.inSeconds) { - this.position.value = position; + final seconds = position.inSeconds; + if (seconds != this.position.value) { + this.position.value = seconds; _videoDetailController?.playedTime = position; videoPlayerServiceHandler?.onPositionChange(position); } }), - stream.duration.listen(duration.call), + stream.duration.listen((duration) { + this.duration.value = duration.inSeconds; + }), stream.playing.listen((playing) { final PlayerStatus playerStatus; if (playing) { @@ -323,7 +372,7 @@ class AudioController extends GetxController videoPlayerServiceHandler?.onStatusChange(playerStatus, false, false); }), stream.completed.listen((completed) { - _videoDetailController?.playedTime = duration.value; + _videoDetailController?.playedTime = player!.state.duration; videoPlayerServiceHandler?.onStatusChange( PlayerStatus.completed, false, @@ -423,36 +472,11 @@ class AudioController extends GetxController } } - void actionCoinVideo() { - final audioItem = this.audioItem.value; - if (audioItem == null) { - return; - } - - if (!isLogin) { - SmartDialog.showToast('账号未登录'); - return; - } - - final int copyright = audioItem.arc.copyright; - if ((copyright != 1 && coinNum.value >= 1) || coinNum.value >= 2) { - SmartDialog.showToast('达到投币上限啦~'); - return; - } - - if (GlobalData().coins != null && GlobalData().coins! < 1) { - SmartDialog.showToast('硬币不足'); - // return; - } - - PayCoinsPage.toPayCoinsPage( - onPayCoin: _onPayCoin, - hasCoin: coinNum.value == 1, - copyright: copyright, - ); - } + @override + int get copyright => audioItem.value?.arc.copyright ?? 1; - Future _onPayCoin(int coin, bool coinWithLike) async { + @override + Future onPayCoin(int coin, bool coinWithLike) async { final res = await AudioGrpc.audioCoinAdd( oid: oid, subId: subId, @@ -513,62 +537,45 @@ class AudioController extends GetxController : '${HttpString.baseUrl}/audio/au$oid'; showDialog( context: context, - builder: (_) => AlertDialog( + builder: (_) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - title: const Text( - '复制链接', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - Utils.copyText(audioUrl); - }, - ), - ListTile( - dense: true, - title: const Text( - '其它app打开', - style: TextStyle(fontSize: 14), - ), - onTap: () { + children: [ + DialogOption( + child: const Text('复制链接', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + Utils.copyText(audioUrl); + }, + ), + DialogOption( + child: const Text('其它app打开', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + PageUtils.launchURL(audioUrl); + }, + ), + if (PlatformUtils.isMobile) + DialogOption( + child: const Text('分享视频', style: TextStyle(fontSize: 14)), + onPressed: () { Get.back(); - PageUtils.launchURL(audioUrl); + if (audioItem.value case DetailItem( + :final arc, + :final owner, + )) { + ShareUtils.shareText( + '${arc.title} ' + 'UP主: ${owner.name}' + ' - $audioUrl', + ); + } }, ), - if (PlatformUtils.isMobile) - ListTile( - dense: true, - title: const Text( - '分享视频', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - if (audioItem.value case DetailItem( - :final arc, - :final owner, - )) { - Utils.shareText( - '${arc.title} ' - 'UP主: ${owner.name}' - ' - $audioUrl', - ); - } - }, - ), - ListTile( - dense: true, - title: const Text( - '分享至动态', - style: TextStyle(fontSize: 14), - ), - onTap: () { + if (isLogin) + DialogOption( + child: const Text('分享至动态', style: TextStyle(fontSize: 14)), + onPressed: () { Get.back(); if (audioItem.value case DetailItem( :final arc, @@ -589,52 +596,41 @@ class AudioController extends GetxController } }, ), - if (isUgc) - ListTile( - dense: true, - title: const Text( - '分享至消息', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - if (audioItem.value case DetailItem( - :final arc, - :final owner, - )) { - try { - PageUtils.pmShare( - context, - content: { - "id": oid.toString(), - "title": arc.title, - "headline": arc.title, - "source": 5, - "thumb": arc.cover, - "author": owner.name, - "author_id": owner.mid.toString(), - }, - ); - } catch (e) { - SmartDialog.showToast(e.toString()); - } + if (isUgc && isLogin) + DialogOption( + child: const Text('分享至消息', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + if (audioItem.value case DetailItem( + :final arc, + :final owner, + )) { + try { + PageUtils.pmShare( + context, + content: { + "id": oid.toString(), + "title": arc.title, + "headline": arc.title, + "source": 5, + "thumb": arc.cover, + "author": owner.name, + "author_id": owner.mid.toString(), + }, + ); + } catch (e) { + SmartDialog.showToast(e.toString()); } - }, - ), - ], - ), + } + }, + ), + ], ), ); } - void playOrPause() { - if (player case final player?) { - if ((duration.value - position.value).inMilliseconds < 50) { - player.seek(Duration.zero).whenComplete(player.play); - } else { - player.playOrPause(); - } - } + Future? playOrPause() { + return player?.playOrPause(); } bool playPrev() { @@ -748,14 +744,14 @@ class AudioController extends GetxController BlockConfigMixin get blockConfig => this; @override - int get currPosInMilliseconds => position.value.inMilliseconds; + int get currPosInMilliseconds => player?.state.position.inMilliseconds ?? 0; @override - Future? seekTo(Duration duration, {required bool isSeek}) => - onSeek(duration); + int? get timeLength => player?.state.duration.inMilliseconds ?? 0; @override - int? get timeLength => duration.value.inMilliseconds; + Future? seekTo(Duration duration, {required bool isSeek}) => + onSeek(duration); @override bool get autoPlay => true; diff --git a/lib/pages/audio/view.dart b/lib/pages/audio/view.dart index de5ffc288d..87981a6e85 100644 --- a/lib/pages/audio/view.dart +++ b/lib/pages/audio/view.dart @@ -13,7 +13,12 @@ import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/pages/audio/controller.dart'; +import 'package:PiliPlus/pages/audio/volume_button.dart'; +import 'package:PiliPlus/pages/setting/models/play_settings.dart' + show showPlayerVolumeDialog; import 'package:PiliPlus/pages/video/introduction/ugc/widgets/action_item.dart'; +import 'package:PiliPlus/pages/video/widgets/header_control.dart' + show HeaderControlState; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; import 'package:PiliPlus/services/shutdown_timer_service.dart'; import 'package:PiliPlus/utils/date_utils.dart'; @@ -29,6 +34,7 @@ import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/utils.dart'; +import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart' hide DraggableScrollableSheet; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -613,28 +619,43 @@ class _AudioPageState extends State { ), ), ), - // ListTile( - // dense: true, - // title: const Text( - // '定时关闭', - // style: TextStyle(fontSize: 14), - // ), - // onTap: () { - // Get.back(); - // _controller.showTimerDialog(); - // }, - // ), ListTile( dense: true, - title: const Text( - '举报', - style: TextStyle(fontSize: 14), - ), + leading: const Icon(Icons.warning_amber_rounded, size: 20), + title: const Text('举报', style: TextStyle(fontSize: 14)), onTap: () { Get.back(); PageUtils.reportVideo(_controller.oid.toInt()); }, ), + if (_controller.player case final player?) ...[ + ListTile( + dense: true, + leading: const Icon(Icons.info_outline, size: 20), + title: const Text('播放信息', style: TextStyle(fontSize: 14)), + onTap: () { + Get.back(); + HeaderControlState.showPlayerInfo(context, player: player); + }, + ), + if (PlatformUtils.isMobile) + ListTile( + dense: true, + leading: const Icon(Icons.volume_up, size: 20), + title: Text( + '播放器音量: ${player.getProperty('volume').subLength(3)}%', + style: const TextStyle(fontSize: 14), + ), + onTap: () { + Get.back(); + showPlayerVolumeDialog( + context, + () {}, + onChanged: player.setVolume, + ); + }, + ), + ], ], ), ); @@ -734,26 +755,25 @@ class _AudioPageState extends State { } void _onDragStart(ThumbDragDetails details) { - // do nothing + _controller + ..isDragging = true + ..position.value = details.seconds; } void _onDragUpdate(ThumbDragDetails details) { - _controller - ..isDragging = true - ..position.value = details.timeStamp; + _controller.position.value = details.seconds; } - void _onSeek(Duration value) { + void _onSeek(int milliseconds) { _controller - ..player?.seek(value) - ..isDragging = false; + ..isDragging = false + ..player?.seek(Duration(milliseconds: milliseconds)); } Widget _buildProgressBar(ColorScheme colorScheme) { final primary = colorScheme.primary; final thumbGlowColor = primary.withAlpha(80); - final bufferedBarColor = primary.withValues(alpha: 0.4); - final baseBarColor = colorScheme.brightness.isDark + final baseBarColor = colorScheme.isDark ? const Color(0x33FFFFFF) : const Color(0x33999999); Widget child = Obx( @@ -762,7 +782,7 @@ class _AudioPageState extends State { total: _controller.duration.value, baseBarColor: baseBarColor, progressBarColor: primary, - bufferedBarColor: bufferedBarColor, + bufferedBarColor: Colors.transparent, thumbColor: primary, thumbGlowColor: thumbGlowColor, thumbGlowRadius: 0, @@ -795,6 +815,15 @@ class _AudioPageState extends State { ], ); } + if (kDebugMode || PlatformUtils.isDesktop) { + child = Row( + spacing: 10, + children: [ + Expanded(child: child), + VolumeButton(controller: _controller), + ], + ); + } return child; } @@ -810,7 +839,7 @@ class _AudioPageState extends State { final position = _controller.position.value; if (_controller.player != null) { return Text( - DurationUtils.formatDuration(position.inSeconds), + DurationUtils.formatDuration(position), ); } return const SizedBox.shrink(); @@ -819,7 +848,7 @@ class _AudioPageState extends State { final duration = _controller.duration.value; if (_controller.player != null) { return Text( - DurationUtils.formatDuration(duration.inSeconds), + DurationUtils.formatDuration(duration), ); } return const SizedBox.shrink(); diff --git a/lib/pages/audio/volume_button.dart b/lib/pages/audio/volume_button.dart new file mode 100644 index 0000000000..6b30bd2cf4 --- /dev/null +++ b/lib/pages/audio/volume_button.dart @@ -0,0 +1,244 @@ +import 'dart:async' show Timer; +import 'dart:math' as math; + +import 'package:PiliPlus/common/widgets/flutter/vertical_slider.dart'; +import 'package:PiliPlus/pages/audio/controller.dart'; +import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart' show RenderProxyBox, BoxHitTestResult; +import 'package:get/get.dart'; + +class VolumeButton extends StatefulWidget { + const VolumeButton({ + super.key, + required this.controller, + }); + + final AudioController controller; + + @override + State createState() => _VolumeButtonState(); +} + +class _VolumeButtonState extends State { + final _controller = OverlayPortalController(); + Timer? _timer; + late CardThemeData cardTheme; + late ColorScheme theme; + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + theme = ColorScheme.of(context); + cardTheme = CardTheme.of(context); + } + + void _stopTimer([_]) { + _timer?.cancel(); + _timer = null; + } + + void _show([_]) { + _stopTimer(); + _controller.show(); + } + + void _scheduleDismiss([_]) { + _timer ??= Timer(const Duration(milliseconds: 100), () { + _controller.hide(); + _timer = null; + }); + } + + @override + void dispose() { + _stopTimer(); + if (_controller.isShowing) { + _controller.hide(); + } + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return MouseRegion( + onEnter: _show, + onExit: _scheduleDismiss, + cursor: SystemMouseCursors.click, + child: OverlayPortal.overlayChildLayoutBuilder( + controller: _controller, + overlayChildBuilder: _overlayChildBuilder, + child: Obx(() { + final volume = widget.controller.desktopVolume.value; + return InkWell( + onTapUp: _onTapUp, + customBorder: const CircleBorder(), + child: Padding( + padding: const .all(10.0), + child: Icon( + volume == 0.0 + ? Icons.volume_off + : volume < 0.5 + ? Icons.volume_down + : Icons.volume_up, + color: theme.onSurfaceVariant, + size: 22.0, + ), + ), + ); + }), + ), + ); + } + + Widget _overlayChildBuilder( + BuildContext context, + OverlayChildLayoutInfo info, + ) { + final offset = MatrixUtils.transformPoint( + info.childPaintTransform, + info.childSize.topCenter(const Offset(0, -6)), + ); + return _volumeSlider(offset); + } + + Widget _volumeSlider(Offset offset) { + return _VolumeWidget( + offset: offset, + child: MouseRegion( + onEnter: _stopTimer, + onExit: _scheduleDismiss, + child: Container( + padding: const .fromLTRB(6, 8, 6, 2), + decoration: BoxDecoration( + color: ElevationOverlay.applySurfaceTint( + cardTheme.color ?? theme.surfaceContainerLow, + cardTheme.surfaceTintColor, + 2, + ), + borderRadius: const .all(.circular(6)), + ), + child: SliderTheme( + data: const SliderThemeData( + trackHeight: 4, + overlayColor: Colors.transparent, + thumbShape: RoundSliderThumbShape( + enabledThumbRadius: 6, + ), + ), + child: Obx( + () { + final volume = widget.controller.desktopVolume.value; + return Column( + spacing: 2, + mainAxisSize: .min, + children: [ + Text( + '${(volume * 100).round()}', + style: const TextStyle(fontSize: 13), + ), + Expanded( + child: VerticalSlider( + year2023: true, + min: 0.0, + max: Pref.maxVolume, + value: volume, + showValueIndicator: .never, + onChanged: widget.controller.setVolume, + onChangeEnd: widget.controller.syncVolume, + ), + ), + ], + ); + }, + ), + ), + ), + ), + ); + } + + void _onTapUp(TapUpDetails details) { + switch (details.kind) { + case .mouse: + widget.controller.toggleVolume(); + case _: + _showVolumeDialog(); + } + } + + void _showVolumeDialog() { + final renderBox = context.findRenderObject() as RenderBox; + final offset = renderBox.localToGlobal( + renderBox.size.topCenter(const Offset(0, -6)), + ); + Get.key.currentState!.push( + DialogRoute( + context: context, + useSafeArea: false, + barrierColor: Colors.transparent, + builder: (context) { + return _volumeSlider(offset); + }, + ), + ); + } +} + +class _VolumeWidget extends SingleChildRenderObjectWidget { + const _VolumeWidget({ + required this.offset, + required Widget super.child, + }); + + final Offset offset; + + @override + RenderObject createRenderObject(BuildContext context) { + return _RenderVolumeWidget(offset: offset); + } +} + +class _RenderVolumeWidget extends RenderProxyBox { + _RenderVolumeWidget({required this.offset}); + + final Offset offset; + late Offset _offset; + + @override + void performLayout() { + final childSize = + (child!..layout( + const BoxConstraints(maxWidth: 40, maxHeight: 170), + parentUsesSize: true, + )) + .size; + size = constraints.biggest; + _offset = Offset( + math.min(offset.dx - (childSize.width / 2), size.width - childSize.width), + math.min(offset.dy, size.height) - childSize.height, + ); + } + + @override + void paint(PaintingContext context, Offset offset) { + super.paint(context, _offset); + } + + @override + bool hitTest(BoxHitTestResult result, {required Offset position}) { + return result.addWithPaintOffset( + offset: _offset, + position: position, + hitTest: (BoxHitTestResult result, Offset transformed) { + assert(transformed == position - _offset); + return child!.hitTest(result, position: transformed); + }, + ); + } + + @override + void applyPaintTransform(covariant RenderObject child, Matrix4 transform) { + transform.translateByDouble(_offset.dx, _offset.dy, 0.0, 1.0); + } +} diff --git a/lib/pages/bubble/view.dart b/lib/pages/bubble/view.dart index 159ece5ca5..d3940c3416 100644 --- a/lib/pages/bubble/view.dart +++ b/lib/pages/bubble/view.dart @@ -91,34 +91,31 @@ class _BubblePageState extends State tooltip: '排序', onPressed: () => showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: .hardEdge, contentPadding: const .symmetric(vertical: 12), - content: Column( - mainAxisSize: .min, - children: sortInfo.sortItems!.map( - (e) { - final isSelected = item.sortType == e.sortType; - return ListTile( - dense: true, - enabled: !isSelected, - onTap: () { - Get.back(); - if (!isSelected) { - _controller.onSort(e.sortType); - } - }, - title: Text( - e.text!, - style: const TextStyle(fontSize: 14), - ), - trailing: isSelected - ? const Icon(size: 22, Icons.check) - : null, - ); - }, - ).toList(), - ), + children: sortInfo.sortItems!.map( + (e) { + final isSelected = item.sortType == e.sortType; + return ListTile( + dense: true, + enabled: !isSelected, + onTap: () { + Get.back(); + if (!isSelected) { + _controller.onSort(e.sortType); + } + }, + title: Text( + e.text!, + style: const TextStyle(fontSize: 14), + ), + trailing: isSelected + ? const Icon(size: 22, Icons.check) + : null, + ); + }, + ).toList(), ), ), icon: const Icon(Icons.sort, size: 20), diff --git a/lib/pages/common/common_intro_controller.dart b/lib/pages/common/common_intro_controller.dart index 45d5ffec56..05a1de9db8 100644 --- a/lib/pages/common/common_intro_controller.dart +++ b/lib/pages/common/common_intro_controller.dart @@ -12,13 +12,13 @@ import 'package:PiliPlus/models_new/video/video_tag/data.dart'; import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/introduction/ugc/widgets/triple_mixin.dart'; import 'package:PiliPlus/utils/accounts.dart'; -import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -43,6 +43,7 @@ abstract class CommonIntroController extends GetxController } } + @override late final isLogin = Accounts.main.isLogin; StatDetail? getStat(); @@ -59,7 +60,6 @@ abstract class CommonIntroController extends GetxController bool prevPlay(); bool nextPlay(); - void actionCoinVideo(); void actionShareVideo(BuildContext context); // 同时观看 @@ -119,7 +119,8 @@ abstract class CommonIntroController extends GetxController super.onClose(); } - Future coinVideo(int coin, [bool selectLike = false]) async { + @override + Future onPayCoin(int coin, bool coinWithLike) async { final stat = getStat(); if (stat == null) { return; @@ -127,14 +128,14 @@ abstract class CommonIntroController extends GetxController final res = await VideoHttp.coinVideo( bvid: bvid, multiply: coin, - selectLike: selectLike ? 1 : 0, + selectLike: coinWithLike ? 1 : 0, ); if (res.isSuccess) { SmartDialog.showToast('投币成功'); coinNum.value += coin; GlobalData().afterCoin(coin); stat.coin += coin; - if (selectLike && !hasLike.value) { + if (coinWithLike && !hasLike.value) { stat.like++; hasLike.value = true; } diff --git a/lib/pages/common/dyn/common_dyn_controller.dart b/lib/pages/common/dyn/common_dyn_controller.dart index 8aefd2fd27..44f74b9e83 100644 --- a/lib/pages/common/dyn/common_dyn_controller.dart +++ b/lib/pages/common/dyn/common_dyn_controller.dart @@ -20,7 +20,7 @@ abstract class CommonDynController extends ReplyController { Future> customGetData() => ReplyGrpc.mainList( type: replyType, oid: oid, - mode: mode.value, + mode: mode, cursorNext: cursorNext, offset: paginationReply?.nextOffset, ); diff --git a/lib/pages/common/dyn/common_dyn_page.dart b/lib/pages/common/dyn/common_dyn_page.dart index ca22bbcdea..cfec13a796 100644 --- a/lib/pages/common/dyn/common_dyn_page.dart +++ b/lib/pages/common/dyn/common_dyn_page.dart @@ -1,13 +1,13 @@ -import 'dart:math' show pi; - import 'package:PiliPlus/common/skeleton/video_reply.dart'; import 'package:PiliPlus/common/style.dart'; +import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/common/enum_with_label.dart'; import 'package:PiliPlus/pages/common/dyn/common_dyn_controller.dart'; import 'package:PiliPlus/pages/common/fab_mixin.dart'; import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart'; @@ -22,56 +22,70 @@ import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +enum DynType implements EnumWithLabel { + reply('评论'), + reaction('赞与转发'); + + @override + final String label; + const DynType(this.label); +} + abstract class CommonDynPageState extends State - with SingleTickerProviderStateMixin, FabMixin { - CommonDynController get controller; + with + SingleTickerProviderStateMixin, + BaseFabMixin, + FabMixin, + CommonDynPageMixin {} + +abstract class CommonDynPageMultiState + extends State + with + TickerProviderStateMixin, + BaseFabMixin, + FabMixin, + CommonDynPageMixin { + late final TabController tabController; + + @override + void initState() { + super.initState(); + tabController = TabController(length: DynType.values.length, vsync: this); + } + + @override + void dispose() { + tabController.dispose(); + super.dispose(); + } +} - late final ScrollController scrollController; +mixin CommonDynPageMixin + on State, TickerProvider, BaseFabMixin, FabMixin { + CommonDynController get controller; bool get horizontalPreview => !isPortrait && controller.horizontalPreview; dynamic get arguments; + late ThemeData theme; late EdgeInsets padding; late bool isPortrait; late double maxWidth; late double maxHeight; - @override - void initState() { - super.initState(); - scrollController = ScrollController()..addListener(listener); - } - - void listener() { - final pos = scrollController.positions; - controller.showTitle.value = pos.first.pixels > 55; - if (pos.any((e) => e.userScrollDirection == .forward)) { - showFab(); - } else if (pos.any((e) => e.userScrollDirection == .reverse)) { - hideFab(); - } - } - @override void didChangeDependencies() { super.didChangeDependencies(); final size = MediaQuery.sizeOf(context); + theme = Theme.of(context); maxWidth = size.width; maxHeight = size.height; isPortrait = size.isPortrait; padding = MediaQuery.viewPaddingOf(context); } - @override - void dispose() { - scrollController - ..removeListener(listener) - ..dispose(); - super.dispose(); - } - - Widget buildReplyHeader(ThemeData theme) { + Widget buildReplyHeader() { final secondary = theme.colorScheme.secondary; return SliverPinnedHeader( backgroundColor: theme.colorScheme.surface, @@ -105,10 +119,7 @@ abstract class CommonDynPageState extends State ); } - Widget replyList( - ThemeData theme, - LoadingState?> loadingState, - ) { + Widget replyList(LoadingState?> loadingState) { return switch (loadingState) { Loading() => SliverList.builder( itemCount: 12, @@ -138,7 +149,7 @@ abstract class CommonDynPageState extends State replyItem: response[index], replyLevel: 1, replyReply: (replyItem, id) => - replyReply(context, replyItem, id, theme), + replyReply(context, replyItem, id), onReply: controller.onReply, onDelete: (item, subIndex) => controller.onRemove(index, item, subIndex), @@ -167,12 +178,7 @@ abstract class CommonDynPageState extends State }; } - void replyReply( - BuildContext context, - ReplyInfo replyItem, - int? id, - ThemeData theme, - ) { + void replyReply(BuildContext context, ReplyInfo replyItem, int? id) { EasyThrottle.throttle('replyReply', const Duration(milliseconds: 500), () { int oid = replyItem.oid.toInt(); int rpid = replyItem.id.toInt(); @@ -268,10 +274,7 @@ abstract class CommonDynPageState extends State ), ), ), - icon: Transform.rotate( - angle: pi / 2, - child: const Icon(Icons.splitscreen, size: 19), - ), + icon: const Icon(CustomIcons.splitscreen_rotate_90, size: 19), ); FloatingActionButtonLocation get floatingActionButtonLocation => @@ -299,4 +302,22 @@ abstract class CommonDynPageState extends State tooltip: '评论', child: const Icon(Icons.reply), ); + + Widget fabAnimWrapper(Widget child) { + return NotificationListener( + onNotification: (notification) { + if (notification.metrics.axisDirection == .down) { + switch (notification.direction) { + case .forward: + showFab(); + case .reverse: + hideFab(); + default: + } + } + return false; + }, + child: child, + ); + } } diff --git a/lib/pages/common/dyn/reaction/controller.dart b/lib/pages/common/dyn/reaction/controller.dart new file mode 100644 index 0000000000..ccda5ec65d --- /dev/null +++ b/lib/pages/common/dyn/reaction/controller.dart @@ -0,0 +1,46 @@ +import 'package:PiliPlus/http/dynamics.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models_new/dynamic/dyn_reaction/data.dart'; +import 'package:PiliPlus/models_new/dynamic/dyn_reaction/item.dart'; +import 'package:PiliPlus/pages/common/common_list_controller.dart'; +import 'package:get/get.dart'; + +class DynReactController + extends CommonListController { + DynReactController(this.id, {int count = -1}) : count = RxInt(count); + final Object id; + + String? _offset; + final RxInt count; + + @override + List? getDataList(DynReactionData response) { + _offset = response.offset; + if (response.hasMore != true) { + isEnd = true; + } + return response.items; + } + + @override + bool customHandleResponse(bool isRefresh, Success response) { + if (isRefresh) { + final res = response.response; + final total = res.total; + if (!(total == 0 && res.items?.isNotEmpty == true)) { + count.value = total; + } + } + return false; + } + + @override + Future> customGetData() => + DynamicsHttp.dynReaction(id: id, offset: _offset); + + @override + Future onRefresh() { + _offset = null; + return super.onRefresh(); + } +} diff --git a/lib/pages/common/dyn/reaction/view.dart b/lib/pages/common/dyn/reaction/view.dart new file mode 100644 index 0000000000..da3c03dbd5 --- /dev/null +++ b/lib/pages/common/dyn/reaction/view.dart @@ -0,0 +1,93 @@ +import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; +import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; +import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models_new/dynamic/dyn_reaction/item.dart'; +import 'package:PiliPlus/pages/common/dyn/common_dyn_page.dart'; +import 'package:PiliPlus/pages/common/dyn/reaction/controller.dart'; +import 'package:flutter/material.dart' hide ListTile; +import 'package:get/get.dart'; + +class DynReactPage extends StatelessWidget { + const DynReactPage({ + super.key, + required this.id, + this.isPortrait = true, + required this.controller, + }); + + final Object id; + final bool isPortrait; + final DynReactController controller; + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + if (controller.loadingState.value == .loading()) { + controller.queryData(); + } + Widget buildBody( + ThemeData theme, + LoadingState?> state, + ) { + return switch (state) { + Loading() => const SliverFillRemaining(child: m3eLoading), + Success(:final response) => + response != null && response.isNotEmpty + ? SliverList.builder( + itemCount: response.length, + itemBuilder: (context, index) { + if (index == response.length - 1) { + controller.onLoadMore(); + } + + final item = response[index]; + return ListTile( + dense: true, + safeArea: false, + visualDensity: .standard, + onTap: () => Get.toNamed('/member?mid=${item.mid}'), + leading: PendantAvatar(item.face!, size: 36), + title: Text.rich( + TextSpan( + text: item.name, + style: const TextStyle(fontSize: 14), + children: [ + TextSpan( + text: ' ${item.action}', + style: TextStyle( + fontSize: 12, + color: theme.colorScheme.outline, + ), + ), + ], + ), + ), + ); + }, + ) + : HttpError(onReload: controller.onReload), + Error(:final errMsg) => HttpError( + errMsg: errMsg, + onReload: controller.onReload, + ), + }; + } + + final child = CustomScrollView( + key: const PageStorageKey(DynType.reaction), + slivers: [ + SliverPadding( + padding: .only( + bottom: MediaQuery.viewPaddingOf(context).bottom + 100, + ), + sliver: Obx(() => buildBody(theme, controller.loadingState.value)), + ), + ], + ); + if (isPortrait) return child; + return refreshIndicator(onRefresh: controller.onRefresh, child: child); + } +} diff --git a/lib/pages/common/fab_mixin.dart b/lib/pages/common/fab_mixin.dart index 50255f8c7d..365c823e06 100644 --- a/lib/pages/common/fab_mixin.dart +++ b/lib/pages/common/fab_mixin.dart @@ -1,18 +1,19 @@ import 'package:flutter/material.dart'; -mixin FabMixin on State, TickerProvider { - bool _isFabVisible = true; - late final AnimationController _fabAnimationCtr; - late final Animation fabAnimation; +mixin BaseFabMixin on State, TickerProvider { + late bool _isFabVisible = true; + AnimationController get fabAnimationCtr; + Animation get fabAnimation; - @override - void initState() { - super.initState(); - _fabAnimationCtr = AnimationController( + AnimationController _initController() { + return AnimationController( vsync: this, duration: const Duration(milliseconds: 100), ); - fabAnimation = _fabAnimationCtr.drive( + } + + Animation _initAnimation() { + return fabAnimationCtr.drive( Tween( begin: Offset.zero, end: const Offset(0.0, 1.0), @@ -23,20 +24,51 @@ mixin FabMixin on State, TickerProvider { void showFab() { if (!_isFabVisible) { _isFabVisible = true; - _fabAnimationCtr.reverse(); + fabAnimationCtr.reverse(); } } void hideFab() { if (_isFabVisible) { _isFabVisible = false; - _fabAnimationCtr.forward(); + fabAnimationCtr.forward(); } } +} + +mixin FabMixin on BaseFabMixin { + @override + late final AnimationController fabAnimationCtr; + @override + late final Animation fabAnimation; + + @override + void initState() { + super.initState(); + fabAnimationCtr = _initController(); + fabAnimation = _initAnimation(); + } + + @override + void dispose() { + fabAnimationCtr.dispose(); + super.dispose(); + } +} + +mixin LazyFabMixin on BaseFabMixin { + AnimationController? _fabAnimationCtr; + Animation? _fabAnimation; + + @override + AnimationController get fabAnimationCtr => + _fabAnimationCtr ??= _initController(); + @override + Animation get fabAnimation => _fabAnimation ??= _initAnimation(); @override void dispose() { - _fabAnimationCtr.dispose(); + _fabAnimationCtr?.dispose(); super.dispose(); } } diff --git a/lib/pages/common/publish/common_publish_page.dart b/lib/pages/common/publish/common_publish_page.dart index 54995ecd82..0bf492a91d 100644 --- a/lib/pages/common/publish/common_publish_page.dart +++ b/lib/pages/common/publish/common_publish_page.dart @@ -27,7 +27,8 @@ abstract class CommonPublishPage extends StatefulWidget { abstract class CommonPublishPageState extends State with WidgetsBindingObserver { - late final FocusNode focusNode; + late bool _paused = false; + final FocusNode focusNode = FocusNode(); late final controller = ChatBottomPanelContainerController( uiScale: Pref.uiScale, ); @@ -37,26 +38,24 @@ abstract class CommonPublishPageState late final RxBool readOnly = false.obs; late final RxBool enablePublish = false.obs; + bool isPublishing = false; + bool hasPub = false; void initPubState(); + bool get handleKeyboard => Platform.isAndroid && widget.autofocus; + @override void initState() { super.initState(); - if (Platform.isAndroid) { + if (handleKeyboard) { WidgetsBinding.instance.addObserver(this); } - focusNode = FocusNode(); - initPubState(); if (widget.autofocus) { - Future.delayed(const Duration(milliseconds: 300), () { - if (mounted) { - focusNode.requestFocus(); - } - }); + _requestFocus(duration: const Duration(milliseconds: 300)); } } @@ -67,35 +66,41 @@ abstract class CommonPublishPageState } focusNode.dispose(); editController.dispose(); - if (Platform.isAndroid) { + if (handleKeyboard) { WidgetsBinding.instance.removeObserver(this); } super.dispose(); } - void _requestFocus() { - Future.delayed(const Duration(microseconds: 200), focusNode.requestFocus); + void _safeRequestFocus() { + if (mounted) { + focusNode.requestFocus(); + } + } + + void _requestFocus({Duration duration = const Duration(microseconds: 200)}) { + Future.delayed(duration, _safeRequestFocus); } @override void didChangeAppLifecycleState(AppLifecycleState state) { - if (state == AppLifecycleState.resumed) { - if (mounted && - widget.autofocus && - (panelType.value == PanelType.keyboard || - panelType.value == PanelType.none)) { - controller.restoreChatPanel(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (focusNode.hasFocus) { - focusNode.unfocus(); - _requestFocus(); - } else { - _requestFocus(); - } - }); + if (state == .resumed) { + if (_paused) { + _paused = false; + final panelType = this.panelType.value; + if (panelType == .keyboard || panelType == .none) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (focusNode.hasFocus) { + focusNode.unfocus(); + _requestFocus(); + } else { + _requestFocus(); + } + }); + } } - } else if (state == AppLifecycleState.paused) { - controller.keepChatPanel(); + } else if (state == .paused) { + _paused = true; if (focusNode.hasFocus) { focusNode.unfocus(); } @@ -208,10 +213,16 @@ abstract class CommonPublishPageState void onSubmitted(String value) { if (enablePublish.value) { - onPublish(); + onPublishThrottle(); } } + void onPublishThrottle() { + if (isPublishing) return; + isPublishing = true; + onPublish().whenComplete(() => isPublishing = false); + } + Future onPublish(); Future onCustomPublish({List? pictures}); diff --git a/lib/pages/common/publish/common_rich_text_pub_page.dart b/lib/pages/common/publish/common_rich_text_pub_page.dart index a58ba271f7..33c9f51db1 100644 --- a/lib/pages/common/publish/common_rich_text_pub_page.dart +++ b/lib/pages/common/publish/common_rich_text_pub_page.dart @@ -14,6 +14,7 @@ import 'package:PiliPlus/models_new/emote/emote.dart' as e; import 'package:PiliPlus/models_new/live/live_emote/emoticon.dart'; import 'package:PiliPlus/pages/common/publish/common_publish_page.dart'; import 'package:PiliPlus/pages/dynamics_mention/view.dart'; +import 'package:PiliPlus/utils/cache_manager.dart'; import 'package:PiliPlus/utils/extension/file_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; @@ -22,13 +23,13 @@ import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart' + hide CacheManager; import 'package:dio/dio.dart' show CancelToken; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:image_cropper/image_cropper.dart'; @@ -111,27 +112,23 @@ abstract class CommonRichTextPubPageState clipBehavior: Clip.none, children: [ GestureDetector( - onTap: () async { - controller.keepChatPanel(); - await PageUtils.imageView( - imgList: imageList - .map( - (img) => switch (img) { - FilePicModel e => SourceModel( - url: e.path, - sourceType: .fileImage, - ), - OpusPicModel e => SourceModel( - url: e.url!, - sourceType: .networkImage, - ), - }, - ) - .toList(), - initialPage: index, - ); - controller.restoreChatPanel(); - }, + onTap: () => PageUtils.imageView( + imgList: imageList + .map( + (img) => switch (img) { + FilePicModel e => SourceModel( + url: e.path, + sourceType: .fileImage, + ), + OpusPicModel e => SourceModel( + url: e.url!, + sourceType: .networkImage, + ), + }, + ) + .toList(), + initialPage: index, + ), onLongPress: () { Feedback.forLongPress(context); onClear(); @@ -195,11 +192,10 @@ abstract class CommonRichTextPubPageState path = e.path; case OpusPicModel e: SmartDialog.showLoading(); - final file = (await DefaultCacheManager().getSingleFile( + path = (await CacheManager.manager.getSingleFile( e.url.http2https, - )); + )).path; await SmartDialog.dismiss(); - path = file.path; } if (!mounted || path.isEmpty) return; late final colorScheme = ColorScheme.of(context); @@ -229,9 +225,10 @@ abstract class CommonRichTextPubPageState const Duration(milliseconds: 500), () async { try { - List pickedFiles = await imagePicker.pickMultiImage( + final pickedFiles = await imagePicker.pickMultiImage( limit: limit, imageQuality: 100, + requestFullMetadata: false, ); if (pickedFiles.isNotEmpty) { for (int i = 0; i < pickedFiles.length; i++) { @@ -328,7 +325,6 @@ abstract class CommonRichTextPubPageState late double _mentionOffset = 0; Future? onMention([bool fromClick = false]) async { - controller.keepChatPanel(); final res = await DynMentionPanel.onDynMention( context, offset: _mentionOffset, @@ -339,13 +335,12 @@ abstract class CommonRichTextPubPageState _onInsertUser(res, fromClick); } else if (res is Set) { for (final e in res) { - e.checked = false; - _onInsertUser(e, fromClick); + _onInsertUser(e..checked = false, fromClick); + fromClick = true; } res.clear(); } } - controller.restoreChatPanel(); } void _onInsertUser(MentionItem e, bool fromClick) { @@ -550,6 +545,6 @@ abstract class CommonRichTextPubPageState return; } } - onCustomPublish(pictures: pictures); + return onCustomPublish(pictures: pictures); } } diff --git a/lib/pages/common/publish/publish_route.dart b/lib/pages/common/publish/publish_route.dart index 6d03acae42..632d1aa937 100644 --- a/lib/pages/common/publish/publish_route.dart +++ b/lib/pages/common/publish/publish_route.dart @@ -2,21 +2,16 @@ import 'package:flutter/material.dart'; class PublishRoute extends PopupRoute { PublishRoute({ - required RoutePageBuilder pageBuilder, - bool barrierDismissible = true, - String? barrierLabel, - Color barrierColor = const Color(0x80000000), - Duration transitionDuration = const Duration(milliseconds: 500), - RouteTransitionsBuilder? transitionBuilder, + required this.pageBuilder, + this._barrierDismissible = true, + this._barrierLabel, + this._barrierColor = const Color(0x80000000), + this._transitionDuration = const Duration(milliseconds: 500), + this._transitionBuilder, super.settings, - }) : widget = pageBuilder, - _barrierDismissible = barrierDismissible, - _barrierLabel = barrierLabel, - _barrierColor = barrierColor, - _transitionDuration = transitionDuration, - _transitionBuilder = transitionBuilder; + }); - final RoutePageBuilder widget; + final RoutePageBuilder pageBuilder; @override bool get barrierDismissible => _barrierDismissible; @@ -45,7 +40,7 @@ class PublishRoute extends PopupRoute { return Semantics( scopesRoute: true, explicitChildNodes: true, - child: widget(context, animation, secondaryAnimation), + child: pageBuilder(context, animation, secondaryAnimation), ); } diff --git a/lib/pages/common/reply_controller.dart b/lib/pages/common/reply_controller.dart index 7b0ad4ba58..d5e8c92418 100644 --- a/lib/pages/common/reply_controller.dart +++ b/lib/pages/common/reply_controller.dart @@ -20,7 +20,7 @@ abstract class ReplyController extends CommonListController { final RxInt count = (-1).obs; late final Rx sortType; - late final Rx mode; + late Mode mode; final savedReplies = ?>{}; @@ -45,8 +45,7 @@ abstract class ReplyController extends CommonListController { super.onInit(); final cacheSortType = Pref.replySortType; sortType = cacheSortType.obs; - mode = - (cacheSortType == .time ? Mode.MAIN_LIST_TIME : Mode.MAIN_LIST_HOT).obs; + mode = cacheSortType == .time ? Mode.MAIN_LIST_TIME : Mode.MAIN_LIST_HOT; } @override @@ -92,11 +91,11 @@ abstract class ReplyController extends CommonListController { switch (sortType.value) { case ReplySortType.time: sortType.value = ReplySortType.hot; - mode.value = Mode.MAIN_LIST_HOT; + mode = Mode.MAIN_LIST_HOT; break; case ReplySortType.hot: sortType.value = ReplySortType.time; - mode.value = Mode.MAIN_LIST_TIME; + mode = Mode.MAIN_LIST_TIME; break; case ReplySortType.select: return; diff --git a/lib/pages/common/slide/common_slide_page.dart b/lib/pages/common/slide/common_slide_page.dart index 58ba68b7c0..501bc1b7d2 100644 --- a/lib/pages/common/slide/common_slide_page.dart +++ b/lib/pages/common/slide/common_slide_page.dart @@ -1,10 +1,9 @@ import 'dart:math' show max; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/gestures.dart' show HorizontalDragGestureRecognizer; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; abstract class CommonSlidePage extends StatefulWidget { diff --git a/lib/pages/danmaku/controller.dart b/lib/pages/danmaku/controller.dart index 6206c27f09..254d36d8c0 100644 --- a/lib/pages/danmaku/controller.dart +++ b/lib/pages/danmaku/controller.dart @@ -6,7 +6,6 @@ import 'package:PiliPlus/grpc/dm.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/data_source.dart'; -import 'package:PiliPlus/plugin/pl_player/utils/danmaku_options.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/path_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; @@ -69,7 +68,6 @@ class PlDanmakuController { final uniques = HashMap(); final filters = _plPlayerController.filters; - final danmakuWeight = DanmakuOptions.danmakuWeight; final shouldFilter = filters.count != 0; for (final element in elems) { if (_isLogin) { @@ -87,8 +85,7 @@ class PlDanmakuController { } } - if (element.weight < danmakuWeight || - (shouldFilter && filters.remove(element))) { + if (shouldFilter && filters.remove(element)) { continue; } } diff --git a/lib/pages/danmaku/view.dart b/lib/pages/danmaku/view.dart index 40e57639e4..222598aec7 100644 --- a/lib/pages/danmaku/view.dart +++ b/lib/pages/danmaku/view.dart @@ -57,7 +57,7 @@ class _PlDanmakuState extends State { } else { _plDanmakuController.queryDanmaku( PlDanmakuController.calcSegment( - playerController.position.inMilliseconds, + playerController.positionInMilliseconds, ), ); } @@ -114,7 +114,9 @@ class _PlDanmakuState extends State { .getCurrentDanmaku(currentPosition); if (currentDanmakuList != null) { final blockColorful = DanmakuOptions.blockColorful; + final danmakuWeight = DanmakuOptions.danmakuWeight; for (DanmakuElem e in currentDanmakuList) { + if (e.weight < danmakuWeight) return; if (e.mode == 7) { try { _controller!.addDanmaku( diff --git a/lib/pages/download/controller.dart b/lib/pages/download/controller.dart index 3bb57c1a71..1dee8af8cd 100644 --- a/lib/pages/download/controller.dart +++ b/lib/pages/download/controller.dart @@ -5,8 +5,8 @@ import 'package:PiliPlus/models_new/download/download_info.dart'; import 'package:PiliPlus/pages/common/multi_select/base.dart' show BaseMultiSelectMixin; import 'package:PiliPlus/services/download/download_service.dart'; -import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/storage.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/widgets.dart' show Text; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; diff --git a/lib/pages/download/detail/view.dart b/lib/pages/download/detail/view.dart index be1312dda9..1ff40cf8fb 100644 --- a/lib/pages/download/detail/view.dart +++ b/lib/pages/download/detail/view.dart @@ -11,9 +11,9 @@ import 'package:PiliPlus/pages/common/multi_select/base.dart' import 'package:PiliPlus/pages/download/controller.dart'; import 'package:PiliPlus/pages/download/detail/widgets/item.dart'; import 'package:PiliPlus/services/download/download_service.dart'; -import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/storage.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart' hide SliverGridDelegateWithMaxCrossAxisExtent; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -36,7 +36,7 @@ class DownloadDetailPage extends StatefulWidget { } class _DownloadDetailPageState extends State - with BaseMultiSelectMixin { + with BaseMultiSelectMixin, GridMixin { StreamSubscription? _sub; final _downloadItems = RxList(); final _controller = Get.find(); @@ -103,16 +103,16 @@ class _DownloadDetailPageState extends State visualDensity: VisualDensity.compact, ), onPressed: () async { - final allChecked = this.allChecked.toSet(); + final futures = allChecked + .map( + (e) => _downloadService.downloadDanmaku( + entry: e, + isUpdate: true, + ), + ) + .toList(); handleSelect(); - final res = await Future.wait( - allChecked.map( - (e) => _downloadService.downloadDanmaku( - entry: e, - isUpdate: true, - ), - ), - ); + final res = await Future.wait(futures); if (res.every((e) => e)) { SmartDialog.showToast('更新成功'); } else { @@ -149,11 +149,7 @@ class _DownloadDetailPageState extends State sliver: Obx(() { if (_downloadItems.isNotEmpty) { return SliverGrid.builder( - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - mainAxisSpacing: 2, - mainAxisExtent: 100, - maxCrossAxisExtent: Grid.smallCardWidth * 2, - ), + gridDelegate: gridDelegate, itemBuilder: (context, index) { final entry = _downloadItems[index]; return DetailItem( @@ -201,22 +197,20 @@ class _DownloadDetailPageState extends State title: const Text('确定删除选中视频?'), onConfirm: () async { SmartDialog.showLoading(); - final watchProgress = GStorage.watchProgress; - final allChecked = this.allChecked.toSet(); + final allChecked = this.allChecked.toList(); final isDeleteAll = allChecked.length == _downloadItems.length; - if (isDeleteAll) { - await _closeSub(); - } - for (final entry in allChecked) { - await watchProgress.deleteAll( + await Future.wait([ + if (isDeleteAll) _closeSub(), + GStorage.watchProgress.deleteAll( allChecked.map((e) => e.cid.toString()), - ); - await _downloadService.deleteDownload( - entry: entry, - removeList: true, - refresh: false, - ); - } + ), + for (final entry in allChecked) + _downloadService.deleteDownload( + entry: entry, + removeList: true, + refresh: false, + ), + ]); _downloadService.flagNotifier.refresh(); if (isDeleteAll) { SmartDialog.dismiss(); diff --git a/lib/pages/download/detail/widgets/item.dart b/lib/pages/download/detail/widgets/item.dart index e50e453d00..685a997f0a 100644 --- a/lib/pages/download/detail/widgets/item.dart +++ b/lib/pages/download/detail/widgets/item.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/progress_bar/video_progress_indicator.dart'; import 'package:PiliPlus/common/widgets/select_mask.dart'; @@ -21,7 +21,7 @@ import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/path_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:path/path.dart' as path; @@ -62,48 +62,37 @@ class DetailItem extends StatelessWidget { void onLongPress() => canDel && !enableMultiSelect ? showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - onTap: () { - Get.back(); - showConfirmDialog( - context: context, - title: const Text('确定删除该视频?'), - onConfirm: onDelete, - ); - }, - dense: true, - title: const Text( - '删除', - style: TextStyle(fontSize: 14), - ), - ), - ListTile( - onTap: () async { - Get.back(); - final res = await downloadService.downloadDanmaku( - entry: entry, - isUpdate: true, - ); - if (res) { - SmartDialog.showToast('更新成功'); - } else { - SmartDialog.showToast('更新失败'); - } - }, - dense: true, - title: const Text( - '更新弹幕', - style: TextStyle(fontSize: 14), - ), - ), - ], - ), + children: [ + DialogOption( + onPressed: () { + Get.back(); + showConfirmDialog( + context: context, + title: const Text('确定删除该视频?'), + onConfirm: onDelete, + ); + }, + child: const Text('删除', style: TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () async { + Get.back(); + final res = await downloadService.downloadDanmaku( + entry: entry, + isUpdate: true, + ); + if (res) { + SmartDialog.showToast('更新成功'); + } else { + SmartDialog.showToast('更新失败'); + } + }, + child: const Text('更新弹幕', style: TextStyle(fontSize: 14)), + ), + ], ), ) : null; @@ -275,7 +264,10 @@ class DetailItem extends StatelessWidget { type: PBadgeType.gray, ), Positioned.fill( - child: selectMask(theme, checked ?? entry.checked), + child: selectMask( + theme.colorScheme, + checked ?? entry.checked, + ), ), ], ), @@ -343,7 +335,7 @@ class DetailItem extends StatelessWidget { Positioned( right: 0, bottom: 0, - child: entry.moreBtn(theme), + child: entry.moreBtn(theme.colorScheme), ), ] else Positioned( diff --git a/lib/pages/download/downloading/view.dart b/lib/pages/download/downloading/view.dart index 001da6ea90..4483c69ec1 100644 --- a/lib/pages/download/downloading/view.dart +++ b/lib/pages/download/downloading/view.dart @@ -22,7 +22,7 @@ class DownloadingPage extends StatefulWidget { } class _DownloadingPageState extends State - with BaseMultiSelectMixin { + with BaseMultiSelectMixin, GridMixin { final _downloadService = Get.find(); late final _waitDownloadQueue = _downloadService.waitDownloadQueue; @override @@ -69,11 +69,7 @@ class _DownloadingPageState extends State sliver: Obx(() { if (_waitDownloadQueue.isNotEmpty) { return SliverGrid.builder( - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - mainAxisSpacing: 2, - mainAxisExtent: 100, - maxCrossAxisExtent: Grid.smallCardWidth * 2, - ), + gridDelegate: gridDelegate, itemCount: _waitDownloadQueue.length, itemBuilder: (context, index) { final entry = _waitDownloadQueue[index]; diff --git a/lib/pages/download/search/view.dart b/lib/pages/download/search/view.dart index 3272d0112a..73de66f331 100644 --- a/lib/pages/download/search/view.dart +++ b/lib/pages/download/search/view.dart @@ -29,7 +29,8 @@ class _DownloadSearchPageState DownloadSearchPage, List, BiliDownloadEntryInfo - > { + > + with GridMixin { @override DownloadSearchController controller = Get.put(DownloadSearchController()); final _downloadService = Get.find(); @@ -57,16 +58,16 @@ class _DownloadSearchPageState TextButton( style: TextButton.styleFrom(visualDensity: VisualDensity.compact), onPressed: () async { - final allChecked = controller.allChecked.toSet(); + final future = controller.allChecked + .map( + (e) => _downloadService.downloadDanmaku( + entry: e, + isUpdate: true, + ), + ) + .toList(); controller.handleSelect(); - final res = await Future.wait( - allChecked.map( - (e) => _downloadService.downloadDanmaku( - entry: e, - isUpdate: true, - ), - ), - ); + final res = await Future.wait(future); if (res.every((e) => e)) { SmartDialog.showToast('更新成功'); } else { @@ -75,7 +76,7 @@ class _DownloadSearchPageState }, child: Text( '更新', - style: TextStyle(color: Get.theme.colorScheme.onSurface), + style: TextStyle(color: ColorScheme.of(context).onSurface), ), ), ]; @@ -84,11 +85,7 @@ class _DownloadSearchPageState Widget buildList(List list) { if (list.isNotEmpty) { return SliverGrid.builder( - gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent( - mainAxisSpacing: 2, - mainAxisExtent: 100, - maxCrossAxisExtent: Grid.smallCardWidth * 2, - ), + gridDelegate: gridDelegate, itemBuilder: (context, index) { final entry = list[index]; return DetailItem( diff --git a/lib/pages/download/view.dart b/lib/pages/download/view.dart index b743e20409..b1fbc27ce1 100644 --- a/lib/pages/download/view.dart +++ b/lib/pages/download/view.dart @@ -4,25 +4,25 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/appbar/appbar.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/flutter/pop_scope.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/select_mask.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; -import 'package:PiliPlus/models_new/download/bili_download_entry_info.dart'; import 'package:PiliPlus/models_new/download/download_info.dart'; import 'package:PiliPlus/pages/download/controller.dart'; import 'package:PiliPlus/pages/download/detail/view.dart'; import 'package:PiliPlus/pages/download/detail/widgets/item.dart'; import 'package:PiliPlus/pages/download/search/view.dart'; import 'package:PiliPlus/services/download/download_service.dart'; -import 'package:PiliPlus/utils/extension/iterable_ext.dart' show IterableExt; +import 'package:PiliPlus/utils/cache_manager.dart'; import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart' - hide SliverGridDelegateWithMaxCrossAxisExtent, LayoutBuilder; + hide SliverGridDelegateWithMaxCrossAxisExtent; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -33,7 +33,7 @@ class DownloadPage extends StatefulWidget { State createState() => _DownloadPageState(); } -class _DownloadPageState extends State { +class _DownloadPageState extends State with GridMixin { final _downloadService = Get.find(); final _controller = Get.put(DownloadPageController()); final _progress = ChangeNotifier(); @@ -67,20 +67,16 @@ class _DownloadPageState extends State { visualDensity: VisualDensity.compact, ), onPressed: () async { - final allChecked = _controller.allChecked.toSet(); + final future = [ + for (final page in _controller.allChecked) + for (final e in page.entries) + _downloadService.downloadDanmaku( + entry: e, + isUpdate: true, + ), + ]; _controller.handleSelect(); - final list = []; - for (final page in allChecked) { - list.addAll(page.entries); - } - final res = await Future.wait( - list.map( - (e) => _downloadService.downloadDanmaku( - entry: e, - isUpdate: true, - ), - ), - ); + final res = await Future.wait(future); if (res.every((e) => e)) { SmartDialog.showToast('更新成功'); } else { @@ -143,7 +139,7 @@ class _DownloadPageState extends State { ), SliverToBoxAdapter( child: SizedBox( - height: 100, + height: 110, child: DetailItem( entry: entry, progress: _progress, @@ -176,12 +172,7 @@ class _DownloadPageState extends State { ), ), SliverGrid.builder( - gridDelegate: - SliverGridDelegateWithMaxCrossAxisExtent( - mainAxisSpacing: 2, - mainAxisExtent: 100, - maxCrossAxisExtent: Grid.smallCardWidth * 2, - ), + gridDelegate: gridDelegate, itemBuilder: (context, index) { final item = _controller.pages[index]; if (item.entries.length == 1) { @@ -237,59 +228,48 @@ class _DownloadPageState extends State { ? null : showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - onTap: () { - Get.back(); - showConfirmDialog( - context: context, - title: const Text('确定删除?'), - onConfirm: () async { - await GStorage.watchProgress.deleteAll( - pageInfo.entries.map((e) => e.cid.toString()), - ); - _downloadService.deletePage( - pageDirPath: pageInfo.dirPath, - ); - }, - ); - }, - dense: true, - title: const Text( - '删除', - style: TextStyle(fontSize: 14), - ), - ), - ListTile( - onTap: () async { - Get.back(); - final res = await Future.wait( - pageInfo.entries.map( - (e) => _downloadService.downloadDanmaku( - entry: e, - isUpdate: true, - ), + children: [ + DialogOption( + onPressed: () { + Get.back(); + showConfirmDialog( + context: context, + title: const Text('确定删除?'), + onConfirm: () async { + await GStorage.watchProgress.deleteAll( + pageInfo.entries.map((e) => e.cid.toString()), + ); + _downloadService.deletePage( + pageDirPath: pageInfo.dirPath, + ); + }, + ); + }, + child: const Text('删除', style: TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () async { + Get.back(); + final res = await Future.wait( + pageInfo.entries.map( + (e) => _downloadService.downloadDanmaku( + entry: e, + isUpdate: true, ), - ); - if (res.every((e) => e)) { - SmartDialog.showToast('更新成功'); - } else { - SmartDialog.showToast('更新失败'); - } - }, - dense: true, - title: const Text( - '更新弹幕', - style: TextStyle(fontSize: 14), - ), - ), - ], - ), + ), + ); + if (res.every((e) => e)) { + SmartDialog.showToast('更新成功'); + } else { + SmartDialog.showToast('更新失败'); + } + }, + child: const Text('更新弹幕', style: TextStyle(fontSize: 14)), + ), + ], ), ); final first = pageInfo.entries.first; @@ -355,7 +335,7 @@ class _DownloadPageState extends State { top: 6.0, ), Positioned.fill( - child: selectMask(theme, pageInfo.checked), + child: selectMask(theme.colorScheme, pageInfo.checked), ), ], ), @@ -381,18 +361,15 @@ class _DownloadPageState extends State { crossAxisAlignment: .end, mainAxisAlignment: .spaceBetween, children: [ - if (first.ownerName case final ownerName?) - Text( - ownerName, - style: TextStyle( - fontSize: 12, - height: 1.6, - color: theme.colorScheme.outline, - ), - ) - else - const Spacer(), - pageInfo.entries.first.moreBtn(theme), + Text( + '${CacheManager.formatSize(pageInfo.entries.fold(0, (p, n) => p + n.totalBytes))} ${first.ownerName ?? ""}', + style: TextStyle( + fontSize: 12, + height: 1.6, + color: theme.colorScheme.outline, + ), + ), + pageInfo.entries.first.moreBtn(theme.colorScheme), ], ), ], diff --git a/lib/pages/dynamics/controller.dart b/lib/pages/dynamics/controller.dart index b75f8a71c9..539bd2709a 100644 --- a/lib/pages/dynamics/controller.dart +++ b/lib/pages/dynamics/controller.dart @@ -1,12 +1,10 @@ import 'dart:async'; import 'package:PiliPlus/http/dynamics.dart'; -import 'package:PiliPlus/http/follow.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/dynamic/dynamics_type.dart'; import 'package:PiliPlus/models/dynamics/up.dart'; -import 'package:PiliPlus/models_new/follow/data.dart'; -import 'package:PiliPlus/pages/common/common_controller.dart'; +import 'package:PiliPlus/pages/common/common_data_controller.dart'; import 'package:PiliPlus/pages/dynamics_tab/controller.dart'; import 'package:PiliPlus/services/account_service.dart'; import 'package:PiliPlus/utils/accounts.dart'; @@ -17,24 +15,20 @@ import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -class DynamicsController extends GetxController - with GetSingleTickerProviderStateMixin, ScrollOrRefreshMixin, AccountMixin { - @override - final ScrollController scrollController = ScrollController(); +class DynamicsController + extends CommonDataController + with GetSingleTickerProviderStateMixin, AccountMixin { late final TabController tabController; - late final RxInt mid = (-1).obs; - late int currentMid = -1; - - Set tempBannedList = {}; + final Set tempBannedList = {}; - final Rx> upState = - LoadingState.loading().obs; - late int _upPage = 1; - late bool _upEnd = false; + String? _offset; + late int _page = 1; + late bool _isEnd = false; Set? _cacheUpList; - late final _showAllUp = Pref.dynamicsShowAllFollowedUp; + late int hostMid = -1, currentMid = -1; late bool showLiveUp = Pref.expandDynLivePanel; + late final _showAllUp = Pref.dynamicsShowAllFollowedUp; final upPanelPosition = Pref.upPanelPosition; @@ -55,154 +49,56 @@ class DynamicsController extends GetxController void onInit() { super.onInit(); tabController = TabController( - length: DynamicsTabType.values.length, vsync: this, + length: DynamicsTabType.values.length, initialIndex: Pref.defaultDynamicTypeIndex, ); - queryFollowUp(); + queryData(); } - void onLoadMoreUp() { - if (_showAllUp) { - queryAllUp(); - } else { - queryUpList(); - } + void _jumpToTab(int mid) { + tabController.index = mid == -1 ? 0 : 4; } - Future queryUpList() async { - if (isQuerying || _upEnd) return; - isQuerying = true; - - final res = await DynamicsHttp.dynUpList(upState.value.data.offset); - - if (res case Success(:final response)) { - if (response.hasMore == false || response.offset.isNullOrEmpty) { - _upEnd = true; - } - final upData = upState.value.data - ..hasMore = response.hasMore - ..offset = response.offset; - final list = response.upList; - if (list != null && list.isNotEmpty) { - upData.upList.addAll(list); - upState.refresh(); - } - } - - isQuerying = false; - } - - Future queryAllUp() async { - if (isQuerying || _upEnd) return; - isQuerying = true; - - final res = await FollowHttp.followings( - vmid: Accounts.main.mid, - pn: _upPage, - orderType: 'attention', - ps: 50, - ); - - if (res case Success(:final response)) { - _upPage++; - final list = response.list; - if (list.isEmpty) { - _upEnd = true; + void onSelectUp(int mid) { + if (currentMid == mid) { + _jumpToTab(mid); + if (mid == -1) { + singleRefresh(); } - upState - ..value.data.upList.addAll( - list..removeWhere((e) => _cacheUpList?.contains(e) == true), - ) - ..refresh(); - } - - isQuerying = false; - } - - late bool isQuerying = false; - Future queryFollowUp() async { - if (isQuerying) return; - isQuerying = true; - - if (!accountService.isLogin.value) { - upState.value = const Error(null); - isQuerying = false; + controller?.onReload(); return; } - // reset - _upEnd = false; - if (_showAllUp) _upPage = 1; - - final res = await Future.wait([ - DynamicsHttp.followUp(), - if (_showAllUp) - FollowHttp.followings( - vmid: Accounts.main.mid, - pn: _upPage, - orderType: 'attention', - ps: 50, - ), - ]); - - final first = res.first; - if (first case final Success i) { - final data = i.response; - final second = res.elementAtOrNull(1); - if (second case final Success j) { - final data1 = j.response; - final list1 = data1.list; - - _upPage++; - if (list1.isEmpty || list1.length >= (data1.total ?? 0)) { - _upEnd = true; - } - - final list = data.upList; - list.addAll(list1..removeWhere((_cacheUpList = list.toSet()).contains)); - } - if (!_showAllUp) { - if (data.hasMore == false || data.offset.isNullOrEmpty) { - _upEnd = true; - } - } - upState.value = Success(data); - } else { - upState.value = const Error(null); + if (mid != -1) { + hostMid = mid; + try { + Get.find( + tag: DynamicsTabType.up.name, + ).onReload(); + } catch (_) {} } - isQuerying = false; + currentMid = mid; + _jumpToTab(mid); } - void onSelectUp(int mid) { - if (this.mid.value == mid) { - tabController.index = (mid == -1 ? 0 : 4); - if (mid == -1) { - queryFollowUp(); - } - controller?.onReload(); - return; + Future singleRefresh() { + if (_showAllUp) { + _page = 1; + _cacheUpList = null; } - - this.mid.value = mid; - tabController.index = (mid == -1 ? 0 : 4); + _offset = null; + _isEnd = false; + return super.onRefresh(); } @override Future onRefresh() { - _refreshFollowUp(); + singleRefresh(); return controller!.onRefresh(); } - void _refreshFollowUp() { - if (_showAllUp) { - _upPage = 1; - _cacheUpList = null; - } - queryFollowUp(); - } - @override void animateToTop() { controller?.animateToTop(); @@ -234,10 +130,71 @@ class DynamicsController extends GetxController @override void onClose() { tabController.dispose(); - scrollController.dispose(); super.onClose(); } @override - void onChangeAccount(bool isLogin) => _refreshFollowUp(); + void onChangeAccount(bool isLogin) => onReload(); + + @override + Future> customGetData() { + if (_offset == null) { + return DynamicsHttp.followUp(); + } + if (_showAllUp) { + return DynamicsHttp.followings( + vmid: Accounts.main.mid, + pn: _page, + orderType: 'attention', + ps: 50, + ); + } else { + return DynamicsHttp.dynUpList(_offset); + } + } + + @override + Future queryData([bool isRefresh = true]) { + if (!isRefresh && _isEnd) return Future.value(); + return super.queryData(isRefresh); + } + + @override + bool customHandleResponse(bool isRefresh, Success response) { + final res = response.response; + + if (_showAllUp) { + if (res.upList?.isNotEmpty != true) { + _isEnd = true; + } + } else { + _offset = res.offset; + if (res.hasMore != true || _offset.isNullOrEmpty) { + _isEnd = true; + } + } + + if (isRefresh) { + if (_showAllUp) { + _offset = ''; + _cacheUpList = res.upList?.toSet(); + } + loadingState.value = response; + } else { + if (_showAllUp) { + _page++; + } + + if (res.upList case final upList? when upList.isNotEmpty) { + if (_showAllUp && _cacheUpList != null) { + upList.removeWhere(_cacheUpList!.contains); + } + loadingState + ..value.data.addAllUpList(upList) + ..refresh(); + } + } + + return true; + } } diff --git a/lib/pages/dynamics/view.dart b/lib/pages/dynamics/view.dart index 9d3d1b446c..1b06398e1f 100644 --- a/lib/pages/dynamics/view.dart +++ b/lib/pages/dynamics/view.dart @@ -65,11 +65,13 @@ class _DynamicsPageState extends CommonPageState onNotification: (notification) { final metrics = notification.metrics; if (metrics.pixels >= metrics.maxScrollExtent - 300) { - _dynamicsController.onLoadMoreUp(); + _dynamicsController.onLoadMore(); } return false; }, - child: Obx(() => _buildUpPanel(_dynamicsController.upState.value)), + child: Obx( + () => _buildUpPanel(_dynamicsController.loadingState.value), + ), ), ), ); @@ -78,15 +80,14 @@ class _DynamicsPageState extends CommonPageState Widget _buildUpPanel(LoadingState upState) { return switch (upState) { Loading() => const SizedBox.shrink(), - Success() => UpPanel( + Success(:final response) => UpPanel( + upData: response, dynamicsController: _dynamicsController, ), Error() => Center( child: IconButton( icon: const Icon(Icons.refresh), - onPressed: () => _dynamicsController - ..upState.value = LoadingState.loading() - ..queryFollowUp(), + onPressed: _dynamicsController.onReload, ), ), }; diff --git a/lib/pages/dynamics/widgets/action_panel.dart b/lib/pages/dynamics/widgets/action_panel.dart index c503563502..12332a0e33 100644 --- a/lib/pages/dynamics/widgets/action_panel.dart +++ b/lib/pages/dynamics/widgets/action_panel.dart @@ -67,7 +67,11 @@ class ActionPanel extends StatelessWidget { ), Expanded( child: TextButton.icon( - onPressed: () => PageUtils.pushDynDetail(item, isPush: true), + onPressed: () => PageUtils.pushDynDetail( + item, + isPush: true, + viewComment: true, + ), icon: Icon( FontAwesomeIcons.comment, size: 16, diff --git a/lib/pages/dynamics/widgets/author_panel.dart b/lib/pages/dynamics/widgets/author_panel.dart index 46379727c7..008cb5bb83 100644 --- a/lib/pages/dynamics/widgets/author_panel.dart +++ b/lib/pages/dynamics/widgets/author_panel.dart @@ -2,6 +2,7 @@ import 'dart:math'; import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/style.dart'; +import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/dialog/report.dart'; import 'package:PiliPlus/common/widgets/extra_hit_test_widget.dart'; import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; @@ -14,6 +15,7 @@ import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/pages/dynamics/controller.dart'; import 'package:PiliPlus/pages/save_panel/view.dart'; import 'package:PiliPlus/utils/accounts.dart'; +import 'package:PiliPlus/utils/color_utils.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; @@ -22,8 +24,8 @@ import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/request_utils.dart'; -import 'package:PiliPlus/utils/utils.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:PiliPlus/utils/share_utils.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -205,7 +207,7 @@ class AuthorPanel extends StatelessWidget { height: 1, fontSize: 11, fontFamily: Assets.digitalNum, - color: Utils.parseColor( + color: ColourUtils.parseColor( moduleAuthor.decorate!.fan!.color!, ), ), @@ -318,7 +320,7 @@ class AuthorPanel extends StatelessWidget { leading: const Icon(Icons.share_outlined, size: 19), onTap: () { Get.back(); - Utils.shareText( + ShareUtils.shareText( '${HttpString.dynamicShareBaseUrl}/${item.idStr}', ); }, @@ -394,14 +396,7 @@ class AuthorPanel extends StatelessWidget { ); }, minLeadingWidth: 0, - leading: const Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - Icon(Icons.shield_outlined, size: 19), - Icon(Icons.published_with_changes_sharp, size: 12), - ], - ), + leading: const Icon(CustomIcons.shield_published, size: 19), title: Text('检查动态', style: theme.textTheme.titleSmall!), ), if (onSetTop != null) @@ -436,41 +431,37 @@ class AuthorPanel extends StatelessWidget { final reply = response.upReply; final enableReply = reply.status == 1; - return AlertDialog( + return SimpleDialog( clipBehavior: .hardEdge, contentPadding: const .symmetric(vertical: 12), - content: Column( - mainAxisSize: .min, - crossAxisAlignment: .start, - children: [ - ListTile( - dense: true, - enabled: selection.canModify, - title: Text( - '${enableSelection ? '停止' : '开启'}评论精选', - style: const TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - onSetReplySubject!( - enableSelection ? 2 : 1, - ); - }, + children: [ + ListTile( + dense: true, + enabled: selection.canModify, + title: Text( + '${enableSelection ? '停止' : '开启'}评论精选', + style: const TextStyle(fontSize: 14), ), - ListTile( - dense: true, - enabled: reply.canModify, - title: Text( - '${enableReply ? '关闭' : '恢复'}评论', - style: const TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - onSetReplySubject!(enableReply ? 3 : 4); - }, + onTap: () { + Get.back(); + onSetReplySubject!( + enableSelection ? 2 : 1, + ); + }, + ), + ListTile( + dense: true, + enabled: reply.canModify, + title: Text( + '${enableReply ? '关闭' : '恢复'}评论', + style: const TextStyle(fontSize: 14), ), - ], - ), + onTap: () { + Get.back(); + onSetReplySubject!(enableReply ? 3 : 4); + }, + ), + ], ); }, ); @@ -509,32 +500,29 @@ class AuthorPanel extends StatelessWidget { showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const .symmetric(vertical: 12), - content: Column( - mainAxisSize: .min, - children: [ - ListTile( - dense: true, - enabled: isPrivate, - title: const Text( - '所有用户可见', - style: TextStyle(fontSize: 14), - ), - onTap: onTap, + children: [ + ListTile( + dense: true, + enabled: isPrivate, + title: const Text( + '所有用户可见', + style: TextStyle(fontSize: 14), ), - ListTile( - dense: true, - enabled: !isPrivate, - title: const Text( - '仅自己可见', - style: TextStyle(fontSize: 14), - ), - onTap: onTap, + onTap: onTap, + ), + ListTile( + dense: true, + enabled: !isPrivate, + title: const Text( + '仅自己可见', + style: TextStyle(fontSize: 14), ), - ], - ), + onTap: onTap, + ), + ], ), ); }, diff --git a/lib/pages/dynamics/widgets/dynamic_panel.dart b/lib/pages/dynamics/widgets/dynamic_panel.dart index 36c7078362..74ffe9bc68 100644 --- a/lib/pages/dynamics/widgets/dynamic_panel.dart +++ b/lib/pages/dynamics/widgets/dynamic_panel.dart @@ -260,7 +260,7 @@ class DynamicPanel extends StatelessWidget { padding: const .symmetric(horizontal: 8, vertical: 6), decoration: BoxDecoration( color: theme.colorScheme.secondaryContainer.withValues( - alpha: theme.brightness.isLight ? 0.5 : 0.7, + alpha: theme.isLight ? 0.5 : 0.7, ), borderRadius: const BorderRadius.all(Radius.circular(6)), ), diff --git a/lib/pages/dynamics/widgets/live_panel_sub.dart b/lib/pages/dynamics/widgets/live_panel_sub.dart index 26f44f0b2e..2ac63e4933 100644 --- a/lib/pages/dynamics/widgets/live_panel_sub.dart +++ b/lib/pages/dynamics/widgets/live_panel_sub.dart @@ -1,12 +1,11 @@ import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; Widget livePanelSub( BuildContext context, { diff --git a/lib/pages/dynamics/widgets/live_rcmd_panel.dart b/lib/pages/dynamics/widgets/live_rcmd_panel.dart index 9c2fd05453..d8f8ef07f1 100644 --- a/lib/pages/dynamics/widgets/live_rcmd_panel.dart +++ b/lib/pages/dynamics/widgets/live_rcmd_panel.dart @@ -1,12 +1,11 @@ import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; Widget liveRcmdPanel( BuildContext context, { diff --git a/lib/pages/dynamics/widgets/module_panel.dart b/lib/pages/dynamics/widgets/module_panel.dart index 507be62f7d..a9d3c1a0df 100644 --- a/lib/pages/dynamics/widgets/module_panel.dart +++ b/lib/pages/dynamics/widgets/module_panel.dart @@ -13,7 +13,7 @@ import 'package:PiliPlus/pages/dynamics/widgets/video_panel.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; diff --git a/lib/pages/dynamics/widgets/up_panel.dart b/lib/pages/dynamics/widgets/up_panel.dart index 8a53c6ca61..a798324b64 100644 --- a/lib/pages/dynamics/widgets/up_panel.dart +++ b/lib/pages/dynamics/widgets/up_panel.dart @@ -1,7 +1,6 @@ import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/dynamic/up_panel_position.dart'; -import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models/dynamics/up.dart'; import 'package:PiliPlus/pages/dynamics/controller.dart'; import 'package:PiliPlus/pages/live_follow/view.dart'; @@ -15,10 +14,12 @@ import 'package:get/get.dart'; class UpPanel extends StatefulWidget { const UpPanel({ - required this.dynamicsController, super.key, + required this.upData, + required this.dynamicsController, }); + final FollowUpModel upData; final DynamicsController dynamicsController; @override @@ -33,16 +34,12 @@ class _UpPanelState extends State { @override Widget build(BuildContext context) { - final accountService = controller.accountService; - if (!accountService.isLogin.value) { - return const SizedBox.shrink(); - } final theme = Theme.of(context); - final upData = controller.upState.value.data; - final List upList = upData.upList; - final List? liveList = upData.liveUsers?.items; + final upData = widget.upData; + final upList = upData.upList; + final liveList = upData.liveUsers?.items; return CustomScrollView( - scrollDirection: isTop ? Axis.horizontal : Axis.vertical, + scrollDirection: isTop ? .horizontal : .vertical, physics: const AlwaysScrollableScrollPhysics(), controller: controller.scrollController, slivers: [ @@ -54,11 +51,11 @@ class _UpPanelState extends State { onLongPress: toFollowPage, onSecondaryTap: PlatformUtils.isMobile ? null : toFollowPage, child: Container( - alignment: Alignment.center, + alignment: .center, height: isTop ? 76 : 60, - padding: isTop ? const EdgeInsets.only(left: 12, right: 6) : null, + padding: isTop ? const .only(left: 12, right: 6) : null, child: Text.rich( - textAlign: TextAlign.center, + textAlign: .center, style: TextStyle( fontSize: 13, color: theme.colorScheme.primary, @@ -71,7 +68,7 @@ class _UpPanelState extends State { if (!isTop) ...[ const TextSpan(text: '\n'), WidgetSpan( - alignment: PlaceholderAlignment.middle, + alignment: .middle, child: Icon( controller.showLiveUp ? Icons.expand_less @@ -82,7 +79,7 @@ class _UpPanelState extends State { ), ] else WidgetSpan( - alignment: PlaceholderAlignment.middle, + alignment: .middle, child: Icon( controller.showLiveUp ? Icons.keyboard_arrow_right @@ -113,13 +110,13 @@ class _UpPanelState extends State { theme, UpItem( uname: '我', - face: accountService.face.value, + face: controller.accountService.face.value, mid: Accounts.main.mid, ), ), ), ), - if (upList.isNotEmpty) + if (upList != null && upList.isNotEmpty) SliverList.builder( itemCount: upList.length, itemBuilder: (context, index) { @@ -131,49 +128,43 @@ class _UpPanelState extends State { ); } - void _onSelect(UpItem data) { - controller - ..currentMid = data.mid - ..onSelectUp(data.mid); - - data.hasUpdate = false; - + void _onSelect(UpItem item) { + item.hasUpdate = false; + controller.onSelectUp(item.mid); setState(() {}); } - Widget upItemBuild(ThemeData theme, UpItem data) { + Widget upItemBuild(ThemeData theme, UpItem item) { final currentMid = controller.currentMid; - final isLive = data is LiveUserItem; - final isCurrent = isLive || currentMid == data.mid || currentMid == -1; + final isLive = item is LiveUserItem; + final isCurrent = isLive || currentMid == item.mid || currentMid == -1; - final isAll = data.mid == -1; - void toMemberPage() => Get.toNamed('/member?mid=${data.mid}'); + final isAll = item.mid == -1; + void toMemberPage() => Get.toNamed('/member?mid=${item.mid}'); Widget avatar; if (isAll) { avatar = DecoratedBox( - decoration: BoxDecoration( + decoration: const BoxDecoration( shape: .circle, - border: Border.all( - width: 5, - color: const Color(0xFF5CB67B), - ), + color: Color(0xFF5CB67B), ), child: Image.asset( width: 38, height: 38, cacheWidth: 38.cacheSize(context), - Assets.logo, + Assets.logo2, + color: Colors.white, ), ); } else { avatar = Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), + padding: const .symmetric(horizontal: 4), child: NetworkImgLayer( width: 38, height: 38, - src: data.face, - type: ImageType.avatar, + src: item.face, + type: .avatar, ), ); if (isLive) { @@ -193,7 +184,7 @@ class _UpPanelState extends State { ), ], ); - } else if (data.hasUpdate ?? false) { + } else if (item.hasUpdate ?? false) { avatar = Stack( clipBehavior: .none, children: [ @@ -218,9 +209,9 @@ class _UpPanelState extends State { onTap: () { feedBack(); if (isLive) { - PageUtils.toLiveRoom(data.roomId); + PageUtils.toLiveRoom(item.roomId); } else { - _onSelect(data); + _onSelect(item); } }, // onDoubleTap: isLive ? () => _onSelect(data) : null, @@ -230,18 +221,18 @@ class _UpPanelState extends State { opacity: isCurrent ? 1 : 0.6, child: Column( spacing: 4, - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: .min, + mainAxisAlignment: .center, children: [ avatar, Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), + padding: const .symmetric(horizontal: 4), child: Text( - isTop ? '${data.uname}\n' : data.uname!, + isTop ? '${item.uname}\n' : item.uname!, maxLines: 2, - textAlign: TextAlign.center, + textAlign: .center, style: TextStyle( - color: currentMid == data.mid + color: currentMid == item.mid ? theme.colorScheme.primary : theme.colorScheme.outline, height: 1.1, diff --git a/lib/pages/dynamics/widgets/video_panel.dart b/lib/pages/dynamics/widgets/video_panel.dart index 001e8998f6..18c4157d2d 100644 --- a/lib/pages/dynamics/widgets/video_panel.dart +++ b/lib/pages/dynamics/widgets/video_panel.dart @@ -1,14 +1,12 @@ // 视频or合集 -import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; +import 'package:PiliPlus/common/widgets/svg/play_icon.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/num_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; Widget videoSeasonWidget( BuildContext context, { @@ -88,9 +86,7 @@ Widget videoSeasonWidget( Colors.black54, ], ), - borderRadius: BorderRadius.vertical( - bottom: Style.imgRadius, - ), + borderRadius: .vertical(bottom: Style.imgRadius), ), child: DefaultTextStyle.merge( style: TextStyle( @@ -104,30 +100,19 @@ Widget videoSeasonWidget( DecoratedBox( decoration: const BoxDecoration( color: Colors.black45, - borderRadius: BorderRadius.all( - Radius.circular(4), - ), + borderRadius: .all(.circular(4)), ), child: Text(' $durationText '), ), const SizedBox(width: 6), ], if (video.stat case final stat?) ...[ - Text( - '${NumUtils.numFormat(stat.play)}播放', - ), + Text('${NumUtils.numFormat(stat.play)}播放'), const SizedBox(width: 6), - Text( - '${NumUtils.numFormat(stat.danmu)}弹幕', - ), + Text('${NumUtils.numFormat(stat.danmu)}弹幕'), ], const Spacer(), - Image.asset( - Assets.play, - width: 50, - height: 50, - cacheHeight: 50.cacheSize(context), - ), + const PlayIcon(size: 50), ], ), ), diff --git a/lib/pages/dynamics/widgets/vote.dart b/lib/pages/dynamics/widgets/vote.dart index 17bbb126e0..be95742127 100644 --- a/lib/pages/dynamics/widgets/vote.dart +++ b/lib/pages/dynamics/widgets/vote.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:PiliPlus/common/widgets/avatars.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/dialog/report.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -13,12 +12,12 @@ import 'package:PiliPlus/models/dynamics/vote_model.dart'; import 'package:PiliPlus/models_new/followee_votes/vote.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/date_utils.dart'; -import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:collection/collection.dart'; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; class VotePanel extends StatefulWidget { @@ -48,7 +47,7 @@ class _VotePanelState extends State { late bool _showPercentage = !_enabled; late final _maxCnt = _voteInfo.choiceCnt ?? _voteInfo.options.length; final isLogin = Accounts.main.isLogin; - late final Rxn> followeeVote = Rxn>(); + late final followeeVote = Rxn>(); @override void initState() { @@ -160,57 +159,53 @@ class _VotePanelState extends State { context: context, builder: (context) { final colorScheme = ColorScheme.of(context); - return AlertDialog( + return SimpleDialog( clipBehavior: .hardEdge, title: const Text('关注的人的投票'), - contentPadding: const .only(top: 10, bottom: 12), - content: SingleChildScrollView( - child: Column( - mainAxisSize: .min, - children: list - .map( - (e) => ListTile( - dense: true, - onTap: () => - Get.toNamed('/member?mid=${e.mid}'), - leading: NetworkImgLayer( - src: e.face, - width: 40, - height: 40, - type: .avatar, - ), - title: Text.rich( - style: const TextStyle(fontSize: 13), + contentPadding: const .only(bottom: 12), + titlePadding: const .fromLTRB(20, 20, 20, 10), + children: list + .map( + (e) => ListTile( + dense: true, + onTap: () => + Get.toNamed('/member?mid=${e.mid}'), + leading: NetworkImgLayer( + src: e.face, + width: 40, + height: 40, + type: .avatar, + ), + title: Text.rich( + style: const TextStyle(fontSize: 13), + TextSpan( + children: [ + TextSpan(text: e.name), TextSpan( - children: [ - TextSpan(text: e.name), - TextSpan( - text: ' 投给了', - style: TextStyle( - fontSize: 12, - color: colorScheme.outline, - ), - ), - ], + text: ' 投给了', + style: TextStyle( + fontSize: 12, + color: colorScheme.outline, + ), ), - ), - subtitle: Text( - style: const TextStyle(fontSize: 13), - e.votes - .map( - (vote) => _voteInfo.options - .firstWhereOrNull( - (e) => e.optIdx == vote, - ) - ?.optDesc, - ) - .join('、'), - ), + ], ), - ) - .toList(), - ), - ), + ), + subtitle: Text( + style: const TextStyle(fontSize: 13), + e.votes + .map( + (vote) => _voteInfo.options + .firstWhereOrNull( + (e) => e.optIdx == vote, + ) + ?.optDesc, + ) + .join('、'), + ), + ), + ) + .toList(), ); }, ); @@ -551,18 +546,15 @@ Future showVoteDialog( ]) async { final voteInfo = await DynamicsHttp.voteInfo(voteId); if (context.mounted) { - if (voteInfo.isSuccess) { + if (voteInfo case Success(:final response)) { showDialog( context: context, builder: (context) => Dialog( - constraints: const BoxConstraints( - minWidth: 280, - maxWidth: 625, - ), + constraints: const BoxConstraints(minWidth: 280, maxWidth: 625), child: Padding( - padding: const EdgeInsets.all(24), + padding: const .all(24), child: VotePanel( - voteInfo: voteInfo.data, + voteInfo: response, onVote: (votes, anonymous) => DynamicsHttp.doVote( voteId: voteId, votes: votes.toList(), diff --git a/lib/pages/dynamics_create/view.dart b/lib/pages/dynamics_create/view.dart index 8dfe8da83f..9eeb8614ff 100644 --- a/lib/pages/dynamics_create/view.dart +++ b/lib/pages/dynamics_create/view.dart @@ -28,9 +28,9 @@ import 'package:PiliPlus/pages/emote/view.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; -import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/request_utils.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart' hide showTimePicker; import 'package:flutter/services.dart' show LengthLimitingTextInputFormatter; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -104,8 +104,8 @@ class _CreateDynPanelState extends CommonRichTextPubPageState { late final Rx?> _topic; late final Rx _replyOption; late final TextEditingController _titleEditCtr; - late final Rx _publishTime = Rx(null); - final Rx _reserveCard = Rx(null); + late final _publishTime = Rxn(); + final _reserveCard = Rxn(); @override void initState() { @@ -361,7 +361,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState { alignment: Alignment.centerRight, child: Obx( () => FilledButton.tonal( - onPressed: enablePublish.value ? onPublish : null, + onPressed: enablePublish.value ? onPublishThrottle : null, style: FilledButton.styleFrom( tapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.symmetric( @@ -502,7 +502,6 @@ class _CreateDynPanelState extends CommonRichTextPubPageState { onPressed: _isEdit || _isPrivate.value ? null : () async { - controller.keepChatPanel(); DateTime nowDate = DateTime.now(); final selectedDate = await showDatePicker( context: context, @@ -548,7 +547,6 @@ class _CreateDynPanelState extends CommonRichTextPubPageState { ); } } - controller.restoreChatPanel(); }, child: const Text('定时发布'), ) @@ -653,11 +651,10 @@ class _CreateDynPanelState extends CommonRichTextPubPageState { Widget get voteBtn => ToolbarIconButton( onPressed: () async { - controller.keepChatPanel(); - RichTextItem? voteItem = editController.items.firstWhereOrNull( + final voteItem = editController.items.firstWhereOrNull( (e) => e.type == RichTextType.vote, ); - final VoteInfo? voteInfo = await Navigator.of(context).push( + final voteInfo = await Navigator.of(context).push( GetPageRoute( page: () => CreateVotePage( voteId: voteItem?.id == null ? null : int.parse(voteItem!.id!), @@ -698,7 +695,6 @@ class _CreateDynPanelState extends CommonRichTextPubPageState { ); } } - controller.restoreChatPanel(); }, icon: const Icon(Icons.bar_chart_rounded, size: 24), tooltip: '投票', @@ -801,7 +797,7 @@ class _CreateDynPanelState extends CommonRichTextPubPageState { SmartDialog.showToast('发布成功'); final id = response?['dyn_id']; RequestUtils.insertCreatedDyn(id); - if (!_isPrivate.value) { + if (!_isPrivate.value && _publishTime.value == null) { RequestUtils.checkCreatedDyn( id: id, dynText: editController.rawText, @@ -814,7 +810,6 @@ class _CreateDynPanelState extends CommonRichTextPubPageState { double _topicOffset = 0; Future _onSelectTopic() async { - controller.keepChatPanel(); TopicItem? res = await SelectTopicPanel.onSelectTopic( context, offset: _topicOffset, @@ -823,7 +818,6 @@ class _CreateDynPanelState extends CommonRichTextPubPageState { if (res != null) { _topic.value = Pair(first: res.id, second: res.name); } - controller.restoreChatPanel(); } @override @@ -881,7 +875,6 @@ class _CreateDynPanelState extends CommonRichTextPubPageState { } Future _onReserve() async { - controller.keepChatPanel(); final ReserveInfoData? reserveInfo = await Navigator.of(context).push( GetPageRoute( page: () => CreateReservePage(sid: _reserveCard.value?.id), @@ -890,6 +883,5 @@ class _CreateDynPanelState extends CommonRichTextPubPageState { if (reserveInfo != null) { _reserveCard.value = reserveInfo; } - controller.restoreChatPanel(); } } diff --git a/lib/pages/dynamics_create_vote/view.dart b/lib/pages/dynamics_create_vote/view.dart index f7610a54d5..f235242601 100644 --- a/lib/pages/dynamics_create_vote/view.dart +++ b/lib/pages/dynamics_create_vote/view.dart @@ -428,9 +428,10 @@ class _CreateVotePageState extends State { const Duration(milliseconds: 500), () async { try { - XFile? pickedFile = await imagePicker.pickImage( + final pickedFile = await imagePicker.pickImage( imageQuality: 100, source: ImageSource.gallery, + requestFullMetadata: false, ); if (pickedFile != null) { final path = pickedFile.path; diff --git a/lib/pages/dynamics_detail/controller.dart b/lib/pages/dynamics_detail/controller.dart index 261ea8155e..4a02ce59f9 100644 --- a/lib/pages/dynamics_detail/controller.dart +++ b/lib/pages/dynamics_detail/controller.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/scroll_physics.dart' show ReloadMixin; import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/reply.dart'; @@ -7,7 +8,7 @@ import 'package:PiliPlus/utils/id_utils.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -class DynamicDetailController extends CommonDynController { +class DynamicDetailController extends CommonDynController with ReloadMixin { @override late int oid; @override @@ -73,4 +74,10 @@ class DynamicDetailController extends CommonDynController { }); } } + + @override + Future onReload() { + reload = true; + return super.onReload(); + } } diff --git a/lib/pages/dynamics_detail/view.dart b/lib/pages/dynamics_detail/view.dart index f6080dbeaf..3beba2edaf 100644 --- a/lib/pages/dynamics_detail/view.dart +++ b/lib/pages/dynamics_detail/view.dart @@ -1,15 +1,21 @@ import 'dart:math'; +import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; +import 'package:PiliPlus/common/widgets/scroll_physics.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_to_box_adapter.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/dynamics.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/reply/reply_option_type.dart'; import 'package:PiliPlus/models/dynamics/result.dart'; import 'package:PiliPlus/pages/common/dyn/common_dyn_page.dart'; +import 'package:PiliPlus/pages/common/dyn/reaction/controller.dart'; +import 'package:PiliPlus/pages/common/dyn/reaction/view.dart'; import 'package:PiliPlus/pages/dynamics/widgets/author_panel.dart'; import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart'; import 'package:PiliPlus/pages/dynamics_create/view.dart'; @@ -18,13 +24,20 @@ import 'package:PiliPlus/pages/dynamics_repost/view.dart'; import 'package:PiliPlus/utils/extension/get_ext.dart'; import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/num_utils.dart'; +import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/request_utils.dart'; -import 'package:PiliPlus/utils/utils.dart'; +import 'package:PiliPlus/utils/share_utils.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; +const Set _kDesktopPlatforms = { + TargetPlatform.macOS, + TargetPlatform.windows, + TargetPlatform.linux, +}; + class DynamicDetailPage extends StatefulWidget { const DynamicDetailPage({super.key}); @@ -32,32 +45,72 @@ class DynamicDetailPage extends StatefulWidget { State createState() => _DynamicDetailPageState(); } -class _DynamicDetailPageState extends CommonDynPageState { +class _DynamicDetailPageState + extends CommonDynPageMultiState { @override - final DynamicDetailController controller = Get.putOrFind( - DynamicDetailController.new, - tag: (Get.arguments['item'] as DynamicItemModel).idStr.toString(), - ); + late final DynamicDetailController controller; + late final DynReactController _reactController; + + late final RxBool _isRefreshing = false.obs; + + void _startRefresh() { + _isRefreshing.value = true; + _refreshController.repeat(); + } + + void _stopRefresh() { + if (!mounted) return; + _isRefreshing.value = false; + _refreshController.stop(); + } + + void _onRefresh(Future future) { + _startRefresh(); + future.whenComplete(_stopRefresh); + // Future.delayed( + // const Duration(milliseconds: 800), + // ).whenComplete(_stopRefresh); + } + + AnimationController? refreshController; + AnimationController get _refreshController => + refreshController ??= AnimationController( + vsync: this, + duration: CircularProgressIndicator.defaultAnimationDuration, + ); @override - dynamic get arguments => { - 'item': controller.dynItem, - }; + dynamic get arguments => {'item': controller.dynItem}; @override - void didChangeDependencies() { - super.didChangeDependencies(); - WidgetsBinding.instance.addPostFrameCallback((_) { - if (scrollController.hasClients) { - controller.showTitle.value = - scrollController.positions.first.pixels > 55; - } - }); + void initState() { + super.initState(); + final args = Get.arguments; + final item = args['item'] as DynamicItemModel; + final id = item.idStr.toString(); + if (args['viewComment'] ?? false) { + WidgetsBinding.instance.addPostFrameCallback(_jumpToComment); + } + controller = Get.putOrFind(DynamicDetailController.new, tag: id); + final stat = item.modules.moduleStat; + controller.count.value = stat?.comment?.count ?? -1; + _reactController = Get.put( + DynReactController( + id, + count: (stat?.like?.count ?? -1) + (stat?.forward?.count ?? -1), + ), + tag: id, + ); + } + + @override + void dispose() { + refreshController?.dispose(); + super.dispose(); } @override Widget build(BuildContext context) { - final theme = Theme.of(context); return Scaffold( resizeToAvoidBottomInset: false, appBar: _buildAppBar(), @@ -66,14 +119,14 @@ class _DynamicDetailPageState extends CommonDynPageState { child: isPortrait ? refreshIndicator( onRefresh: controller.onRefresh, - child: _buildBody(theme), + child: _buildBody(), ) - : _buildBody(theme), + : _buildBody(), ), floatingActionButtonLocation: floatingActionButtonLocation, floatingActionButton: SlideTransition( position: fabAnimation, - child: _buildBottom(theme), + child: _buildBottom(), ), ); } @@ -242,17 +295,129 @@ class _DynamicDetailPageState extends CommonDynPageState { : [ratioWidget(maxWidth), const SizedBox(width: 16)], ); - Widget _buildBody(ThemeData theme) { - double padding = max(maxWidth / 2 - Grid.smallCardWidth, 0); - Widget child; + Widget _buildTabBar() { + return SizedBox( + height: 40, + child: TabBar( + padding: .zero, + isScrollable: true, + indicatorSize: .tab, + tabAlignment: .start, + controller: tabController, + labelPadding: const .symmetric(horizontal: 12), + dividerColor: theme.colorScheme.outline.withValues(alpha: 0.1), + onTap: (value) { + if (!tabController.indexIsChanging) { + final positions = PrimaryScrollController.of(context).positions; + if (positions.length == 1) { + final postion = positions.single; + if (postion.pixels >= postion.maxScrollExtent) { + postion.jumpTo(postion.pixels); + } + switch (value) { + case 0: + _onRefresh(controller.onRefresh()); + case 1: + _onRefresh(_reactController.onRefresh()); + } + } else if (positions.length > 1) { + positions.elementAt(1).jumpTo(0); + } + } + }, + tabs: [ + Tab( + child: Obx(() { + final count = controller.count.value; + return Text( + '${DynType.reply.label}${count < 0 ? '' : ' ${NumUtils.numFormat(count)}'}', + ); + }), + ), + Tab( + child: Obx(() { + final count = _reactController.count.value; + return Text( + '${DynType.reaction.label}${count < 0 ? '' : ' ${NumUtils.numFormat(count)}'}', + ); + }), + ), + ], + ), + ); + } + + Widget _buildTabBody([bool isPortrait = true]) { + final reply = CustomScrollView( + key: const PageStorageKey(DynType.reply), + physics: ReloadScrollPhysics(controller: controller), + slivers: [ + buildReplyHeader(isPortrait), + Obx(() => replyList(controller.loadingState.value)), + ], + ); + final child = tabBarView( + controller: tabController, + children: [ + isPortrait + ? reply + : refreshIndicator(onRefresh: controller.onRefresh, child: reply), + DynReactPage( + isPortrait: isPortrait, + id: controller.dynItem.idStr, + controller: _reactController, + ), + ], + ); if (isPortrait) { - child = Padding( - padding: EdgeInsets.symmetric(horizontal: padding), - child: CustomScrollView( - controller: scrollController, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverToBoxAdapter( + return Stack( + clipBehavior: .none, + children: [ + child, + Positioned( + left: 0, + right: 0, + top: displacement, + child: Obx(() { + final isRefreshing = _isRefreshing.value; + return AnimatedScale( + scale: isRefreshing ? 1 : 0, + duration: const Duration(milliseconds: 200), + child: Center( + child: SizedBox.fromSize( + size: const .square(40), + child: Material( + type: .circle, + color: theme.colorScheme.onSecondary, + elevation: 2.0, + child: Padding( + padding: const .all(6), + child: CircularProgressIndicator( + strokeWidth: 2.5, + controller: _refreshController, + ), + ), + ), + ), + ), + ); + }), + ), + ], + ); + } + return child; + } + + Widget _buildPortrait(double padding) { + return Padding( + padding: EdgeInsets.symmetric(horizontal: padding), + child: NestedScrollView( + headerSliverBuilder: (context, innerBoxIsScrolled) { + return [ + SliverToBoxWithOffsetAdapter( + offset: 55, + onVisibilityChanged: controller.showTitle.call, child: DynamicPanel( item: controller.dynItem, isDetail: true, @@ -262,72 +427,89 @@ class _DynamicDetailPageState extends CommonDynPageState { onSetReplySubject: controller.onSetReplySubject, ), ), - buildReplyHeader(theme), - Obx(() => replyList(theme, controller.loadingState.value)), + ]; + }, + body: Column( + children: [ + _buildTabBar(), + Expanded(child: _buildTabBody()), ], ), - ); - } else { - padding = padding / 4; - final flex = controller.ratio[0].toInt(); - final flex1 = controller.ratio[1].toInt(); - child = Row( - children: [ - Expanded( - flex: flex, - child: CustomScrollView( - controller: scrollController, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverPadding( - padding: EdgeInsets.only( - left: padding, - bottom: this.padding.bottom + 100, - ), - sliver: SliverToBoxAdapter( - child: DynamicPanel( - item: controller.dynItem, - isDetail: true, - isDetailPortraitW: isPortrait, - onSetPubSetting: controller.onSetPubSetting, - onEdit: _onEdit, - onSetReplySubject: controller.onSetReplySubject, - ), - ), + ), + ); + } + + Widget _buildHorizontal(double padding) { + padding = padding / 4; + final flex = controller.ratio[0].toInt(); + final flex1 = controller.ratio[1].toInt(); + final child = Row( + children: [ + Expanded( + flex: flex, + child: CustomScrollView( + slivers: [ + SliverPadding( + padding: .only( + left: padding, + bottom: this.padding.bottom + 100, ), - ], - ), - ), - Expanded( - flex: flex1, - child: Padding( - padding: EdgeInsets.only(right: padding), - child: Scaffold( - backgroundColor: Colors.transparent, - resizeToAvoidBottomInset: false, - body: refreshIndicator( - onRefresh: controller.onRefresh, - child: CustomScrollView( - controller: scrollController, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - buildReplyHeader(theme), - Obx( - () => replyList(theme, controller.loadingState.value), - ), - ], + sliver: SliverToBoxWithOffsetAdapter( + offset: 55, + onVisibilityChanged: controller.showTitle.call, + child: DynamicPanel( + item: controller.dynItem, + isDetail: true, + isDetailPortraitW: isPortrait, + onSetPubSetting: controller.onSetPubSetting, + onEdit: _onEdit, + onSetReplySubject: controller.onSetReplySubject, ), ), ), + ], + ), + ), + Expanded( + flex: flex1, + child: Padding( + padding: EdgeInsets.only(right: padding), + child: Scaffold( + backgroundColor: Colors.transparent, + resizeToAvoidBottomInset: false, + body: Column( + children: [ + _buildTabBar(), + Expanded(child: _buildTabBody(false)), + ], + ), ), ), - ], + ), + ], + ); + if (PlatformUtils.isDesktop) { + return PrimaryScrollController( + controller: PrimaryScrollController.of(context), + automaticallyInheritForPlatforms: _kDesktopPlatforms, + child: child, ); } return child; } - Widget _buildBottom(ThemeData theme) { + Widget _buildBody() { + double padding = max(maxWidth / 2 - Grid.smallCardWidth, 0); + Widget child; + if (isPortrait) { + child = _buildPortrait(padding); + } else { + child = _buildHorizontal(padding); + } + return fabAnimWrapper(child); + } + + Widget _buildBottom() { if (!controller.showDynActionBar) { return fabButton; } @@ -427,11 +609,19 @@ class _DynamicDetailPageState extends CommonDynPageState { icon: CustomIcons.share_node, text: '分享', stat: null, - onPressed: (_) => Utils.shareText( + onPressed: (_) => ShareUtils.shareText( '${HttpString.dynamicShareBaseUrl}/${controller.dynItem.idStr}', ), ), ), + Expanded( + child: textIconButton( + icon: FontAwesomeIcons.comment, + text: '评论', + stat: moduleStat?.comment, + onPressed: _jumpToComment, + ), + ), Expanded( child: Builder( builder: (context) { @@ -460,4 +650,44 @@ class _DynamicDetailPageState extends CommonDynPageState { ), ); } + + @override + Widget buildReplyHeader([bool isPortrait = true]) { + final secondary = theme.colorScheme.secondary; + final child = Padding( + padding: const .fromLTRB(12, 2.5, 6, 2.5), + child: Obx( + () { + final sortType = controller.sortType.value; + return Row( + mainAxisAlignment: .spaceBetween, + children: [ + Text(sortType.title), + TextButton.icon( + style: Style.buttonStyle, + onPressed: controller.queryBySort, + icon: Icon(Icons.sort, size: 16, color: secondary), + label: Text( + sortType.label, + style: TextStyle(fontSize: 13, color: secondary), + ), + ), + ], + ); + }, + ), + ); + return SliverFloatingHeaderWidget( + backgroundColor: theme.colorScheme.surface, + child: child, + ); + } + + void _jumpToComment([_]) { + if (!isPortrait) return; + try { + final position = PrimaryScrollController.of(context).position; + position.jumpTo(position.maxScrollExtent); + } catch (_) {} + } } diff --git a/lib/pages/dynamics_mention/view.dart b/lib/pages/dynamics_mention/view.dart index be7d52c36f..da9e8f4a05 100644 --- a/lib/pages/dynamics_mention/view.dart +++ b/lib/pages/dynamics_mention/view.dart @@ -123,10 +123,7 @@ class _DynMentionPanelState padding: EdgeInsets.only(left: 12, right: 4), child: Icon(Icons.search, size: 20), ), - prefixIconConstraints: const BoxConstraints( - minHeight: 0, - minWidth: 0, - ), + prefixIconConstraints: const .new(minHeight: 0, minWidth: 0), contentPadding: const EdgeInsets.symmetric( horizontal: 16, vertical: 6, diff --git a/lib/pages/dynamics_repost/view.dart b/lib/pages/dynamics_repost/view.dart index 0d1bc46acc..123b9344ff 100644 --- a/lib/pages/dynamics_repost/view.dart +++ b/lib/pages/dynamics_repost/view.dart @@ -267,7 +267,7 @@ class _RepostPanelState extends CommonRichTextPubPageState { ), const Spacer(), TextButton( - onPressed: onPublish, + onPressed: onPublishThrottle, style: TextButton.styleFrom( padding: const EdgeInsets.symmetric( horizontal: 20, @@ -320,7 +320,7 @@ class _RepostPanelState extends CommonRichTextPubPageState { Align( alignment: Alignment.centerRight, child: FilledButton.tonal( - onPressed: onPublish, + onPressed: onPublishThrottle, style: FilledButton.styleFrom( tapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.symmetric( diff --git a/lib/pages/dynamics_tab/controller.dart b/lib/pages/dynamics_tab/controller.dart index e3f6d6b0a9..14e21587a8 100644 --- a/lib/pages/dynamics_tab/controller.dart +++ b/lib/pages/dynamics_tab/controller.dart @@ -16,9 +16,10 @@ class DynamicsTabController with AccountMixin { DynamicsTabController({required this.dynamicsType}); final DynamicsTabType dynamicsType; - String offset = ''; - int? mid; - late final MainController mainController = Get.find(); + + String? offset; + + late final mainController = Get.find(); final dynamicsController = Get.find(); @override @@ -29,25 +30,25 @@ class DynamicsTabController @override Future onRefresh() { - if (dynamicsType == DynamicsTabType.all) { + if (dynamicsType == .all) { mainController.setDynCount(); } - offset = ''; + offset = null; return super.onRefresh(); } @override List? getDataList(DynamicsDataModel response) { - offset = response.offset ?? ''; + offset = response.offset; return response.items; } @override Future> customGetData() => DynamicsHttp.followDynamic( - type: dynamicsType, offset: offset, - mid: mid, + type: dynamicsType, + hostMid: dynamicsController.hostMid, tempBannedList: dynamicsController.tempBannedList, ); @@ -70,7 +71,7 @@ class DynamicsTabController } void onBlock(int index) { - if (dynamicsType != DynamicsTabType.up) { + if (dynamicsType != .up) { loadingState ..value.data!.removeAt(index) ..refresh(); diff --git a/lib/pages/dynamics_tab/view.dart b/lib/pages/dynamics_tab/view.dart index c14c2e08bf..370407755f 100644 --- a/lib/pages/dynamics_tab/view.dart +++ b/lib/pages/dynamics_tab/view.dart @@ -27,9 +27,7 @@ class DynamicsTabPage extends StatefulWidget { class _DynamicsTabPageState extends State with AutomaticKeepAliveClientMixin, DynMixin { - StreamSubscription? _listener; - - DynamicsController dynamicsController = Get.putOrFind(DynamicsController.new); + final dynamicsController = Get.putOrFind(DynamicsController.new); late final DynamicsTabController controller; @override @@ -38,38 +36,22 @@ class _DynamicsTabPageState extends State @override void initState() { controller = Get.putOrFind( - () => - DynamicsTabController(dynamicsType: widget.dynamicsType) - ..mid = dynamicsController.mid.value, + () => DynamicsTabController(dynamicsType: widget.dynamicsType), tag: widget.dynamicsType.name, ); super.initState(); - if (widget.dynamicsType == DynamicsTabType.up) { - _listener = dynamicsController.mid.listen((mid) { - if (mid != -1) { - controller - ..mid = mid - ..onReload(); - } - }); - } } - @override - void dispose() { - _listener?.cancel(); - dynamicsController.mid.close(); - super.dispose(); + Future onRefresh() { + dynamicsController.singleRefresh(); + return controller.onRefresh(); } @override Widget build(BuildContext context) { super.build(context); return refreshIndicator( - onRefresh: () { - dynamicsController.queryFollowUp(); - return controller.onRefresh(); - }, + onRefresh: onRefresh, child: CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), controller: controller.scrollController, diff --git a/lib/pages/dynamics_topic/controller.dart b/lib/pages/dynamics_topic/controller.dart index 1c7623dc15..17e8b96418 100644 --- a/lib/pages/dynamics_topic/controller.dart +++ b/lib/pages/dynamics_topic/controller.dart @@ -18,13 +18,13 @@ class DynTopicController int sortBy = 0; String offset = ''; - Rx topicSortByConf = Rx(null); + final topicSortByConf = Rxn(); double? appbarOffset; // top - Rx isFav = Rx(null); - Rx isLike = Rx(null); + final isFav = false.obs; + final isLike = false.obs; Rx> topState = LoadingState.loading().obs; @@ -42,8 +42,8 @@ class DynTopicController if (topState.value case Success(:final response)) { final topicItem = response!.topicItem!; topicName = topicItem.name; - isFav.value = topicItem.isFav; - isLike.value = topicItem.isLike; + isFav.value = topicItem.isFav ?? false; + isLike.value = topicItem.isLike ?? false; } } @@ -96,7 +96,7 @@ class DynTopicController SmartDialog.showToast('账号未登录'); return; } - bool isFav = this.isFav.value ?? false; + final isFav = this.isFav.value; final res = isFav ? await FavHttp.delFavTopic(topicId) : await FavHttp.addFavTopic(topicId); @@ -117,7 +117,7 @@ class DynTopicController SmartDialog.showToast('账号未登录'); return; } - bool isLike = this.isLike.value ?? false; + final isLike = this.isLike.value; final res = await FavHttp.likeTopic(topicId, isLike); if (res.isSuccess) { if (isLike) { diff --git a/lib/pages/dynamics_topic/view.dart b/lib/pages/dynamics_topic/view.dart index 544046c856..f292e74ae6 100644 --- a/lib/pages/dynamics_topic/view.dart +++ b/lib/pages/dynamics_topic/view.dart @@ -6,10 +6,12 @@ import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; +import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_feed/item.dart'; import 'package:PiliPlus/models_new/dynamic/dyn_topic_top/top_details.dart'; +import 'package:PiliPlus/pages/common/fab_mixin.dart'; import 'package:PiliPlus/pages/dynamics/widgets/dynamic_panel.dart'; import 'package:PiliPlus/pages/dynamics_create/view.dart'; import 'package:PiliPlus/pages/dynamics_topic/controller.dart'; @@ -18,6 +20,8 @@ import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; +import 'package:PiliPlus/utils/share_utils.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/waterfall.dart'; import 'package:flutter/material.dart'; @@ -35,7 +39,8 @@ class DynTopicPage extends StatefulWidget { State createState() => _DynTopicPageState(); } -class _DynTopicPageState extends State with DynMixin { +class _DynTopicPageState extends State + with DynMixin, SingleTickerProviderStateMixin, BaseFabMixin, FabMixin { final DynTopicController _controller = Get.put( DynTopicController(), tag: Utils.generateRandomString(8), @@ -43,110 +48,138 @@ class _DynTopicPageState extends State with DynMixin { @override Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); + final colorScheme = ColorScheme.of(context); final padding = MediaQuery.viewPaddingOf(context); - return Scaffold( - resizeToAvoidBottomInset: false, - floatingActionButton: FloatingActionButton.extended( - onPressed: () { - if (_controller.isLogin) { - CreateDynPanel.onCreateDyn( - context, - topic: Pair( - first: int.parse(_controller.topicId), - second: _controller.topicName, - ), - ); - } else { - SmartDialog.showToast('账号未登录'); - } - }, - icon: const Icon(CustomIcons.topic_tag, size: 20), - label: const Text('参与话题'), - ), - body: refreshIndicator( - onRefresh: _controller.onRefresh, - child: CustomScrollView( - controller: _controller.scrollController, - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - Obx( - () => _buildAppBar( - theme, - padding, - _controller.topState.value, - ), - ), - Obx(() { - final allSortBy = _controller.topicSortByConf.value?.allSortBy; - if (allSortBy != null && allSortBy.isNotEmpty) { - return SliverPinnedHeader( - backgroundColor: theme.colorScheme.surface, - child: Padding( - padding: EdgeInsets.only( - left: 12 + padding.left, - top: 6, - bottom: 6, + return Material( + child: Stack( + clipBehavior: .none, + children: [ + refreshIndicator( + onRefresh: _controller.onRefresh, + child: NotificationListener( + onNotification: (notification) { + final direction = notification.direction; + if (direction == .forward) { + showFab(); + } else if (direction == .reverse) { + hideFab(); + } + return false; + }, + child: CustomScrollView( + controller: _controller.scrollController, + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + Obx( + () => _buildAppBar( + colorScheme, + padding, + _controller.topState.value, ), - child: Builder( - builder: (context) { - return ToggleButtons( - fillColor: theme.colorScheme.secondaryContainer, - selectedColor: theme.colorScheme.onSecondaryContainer, - constraints: const BoxConstraints( - minWidth: 54, - minHeight: 24, + ), + Obx(() { + final allSortBy = + _controller.topicSortByConf.value?.allSortBy; + if (allSortBy != null && allSortBy.isNotEmpty) { + return SliverPinnedHeader( + backgroundColor: colorScheme.surface, + child: Padding( + padding: EdgeInsets.only( + left: 12 + padding.left, + top: 6, + bottom: 6, ), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - borderRadius: const .all(.circular(25)), - onPressed: (index) { - _controller.onSort(allSortBy[index].sortBy!); - (context as Element).markNeedsBuild(); - }, - isSelected: allSortBy - .map((e) => e.sortBy == _controller.sortBy) - .toList(), - children: allSortBy.map((e) { - return Text( - e.sortName!, - style: const TextStyle( - fontSize: 13, - height: 1, - ), - strutStyle: const StrutStyle( - height: 1, - leading: 0, - fontSize: 13, - ), - textScaler: TextScaler.noScaling, - ); - }).toList(), - ); - }, + child: Builder( + builder: (context) { + return ToggleButtons( + fillColor: colorScheme.secondaryContainer, + selectedColor: colorScheme.onSecondaryContainer, + constraints: const BoxConstraints( + minWidth: 54, + minHeight: 24, + ), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + borderRadius: const .all(.circular(25)), + onPressed: (index) { + _controller.onSort(allSortBy[index].sortBy!); + (context as Element).markNeedsBuild(); + }, + isSelected: allSortBy + .map((e) => e.sortBy == _controller.sortBy) + .toList(), + children: allSortBy.map((e) { + return Text( + e.sortName!, + style: const TextStyle( + fontSize: 13, + height: 1, + ), + strutStyle: const StrutStyle( + height: 1, + leading: 0, + fontSize: 13, + ), + textScaler: TextScaler.noScaling, + ); + }).toList(), + ); + }, + ), + ), + ); + } + return const SliverToBoxAdapter(); + }), + SliverPadding( + padding: EdgeInsets.only( + left: padding.left, + right: padding.right, + bottom: padding.bottom + 100, + ), + sliver: buildPage( + Obx(() => _buildBody(_controller.loadingState.value)), ), ), - ); - } - return const SliverToBoxAdapter(); - }), - SliverPadding( - padding: EdgeInsets.only( - left: padding.left, - right: padding.right, - bottom: padding.bottom + 100, + ], ), - sliver: buildPage( - Obx(() => _buildBody(_controller.loadingState.value)), + ), + ), + Positioned( + right: padding.right + kFloatingActionButtonMargin, + bottom: 0, + child: SlideTransition( + position: fabAnimation, + child: Padding( + padding: .only( + bottom: padding.bottom + kFloatingActionButtonMargin, + ), + child: FloatingActionButton.extended( + onPressed: () { + if (_controller.isLogin) { + CreateDynPanel.onCreateDyn( + context, + topic: Pair( + first: int.parse(_controller.topicId), + second: _controller.topicName, + ), + ); + } else { + SmartDialog.showToast('账号未登录'); + } + }, + icon: const Icon(CustomIcons.topic_tag, size: 20), + label: const Text('参与话题'), + ), ), ), - ], - ), + ), + ], ), ); } Widget _buildAppBar( - ThemeData theme, + ColorScheme colorScheme, EdgeInsets padding, LoadingState topState, ) { @@ -203,7 +236,7 @@ class _DynTopicPageState extends State with DynMixin { ), Text( ' 发起', - style: TextStyle(color: theme.colorScheme.outline), + style: TextStyle(color: colorScheme.outline), ), ], ), @@ -219,7 +252,7 @@ class _DynTopicPageState extends State with DynMixin { const SizedBox(height: 6), SelectableText( response.topicItem!.description!, - style: TextStyle(color: theme.colorScheme.onSurfaceVariant), + style: TextStyle(color: colorScheme.onSurfaceVariant), ), const SizedBox(height: 10), Row( @@ -228,7 +261,7 @@ class _DynTopicPageState extends State with DynMixin { '${NumUtils.numFormat(response.topicItem!.view)}浏览 · ${NumUtils.numFormat(response.topicItem!.discuss)}讨论', style: TextStyle( fontSize: 13, - color: theme.colorScheme.outline, + color: colorScheme.outline, ), ), const Spacer(), @@ -236,13 +269,11 @@ class _DynTopicPageState extends State with DynMixin { style: OutlinedButton.styleFrom( side: BorderSide( width: 1, - color: theme.colorScheme.outline.withValues( - alpha: 0.2, - ), + color: colorScheme.outline.withValues(alpha: 0.2), ), - foregroundColor: _controller.isLike.value == true + foregroundColor: _controller.isLike.value ? null - : theme.colorScheme.onSurfaceVariant, + : colorScheme.onSurfaceVariant, padding: const EdgeInsets.symmetric(horizontal: 10), visualDensity: const VisualDensity( horizontal: -4, @@ -251,7 +282,7 @@ class _DynTopicPageState extends State with DynMixin { tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), onPressed: _controller.onLike, - icon: _controller.isLike.value == true + icon: _controller.isLike.value ? const Icon(FontAwesomeIcons.solidThumbsUp, size: 13) : const Icon(FontAwesomeIcons.thumbsUp, size: 13), label: Text( @@ -265,22 +296,17 @@ class _DynTopicPageState extends State with DynMixin { style: OutlinedButton.styleFrom( side: BorderSide( width: 1, - color: theme.colorScheme.outline.withValues( - alpha: 0.2, - ), + color: colorScheme.outline.withValues(alpha: 0.2), ), - foregroundColor: _controller.isFav.value == true + foregroundColor: _controller.isFav.value ? null - : theme.colorScheme.onSurfaceVariant, + : colorScheme.onSurfaceVariant, padding: const EdgeInsets.symmetric(horizontal: 10), - visualDensity: const VisualDensity( - horizontal: -4, - vertical: -4, - ), + visualDensity: const .new(horizontal: -4, vertical: -4), tapTargetSize: MaterialTapTargetSize.shrinkWrap, ), onPressed: _controller.onFav, - icon: _controller.isFav.value == true + icon: _controller.isFav.value ? const Icon(FontAwesomeIcons.solidStar, size: 13) : const Icon(FontAwesomeIcons.star, size: 13), label: Text( @@ -297,7 +323,7 @@ class _DynTopicPageState extends State with DynMixin { ), actions: [ IconButton( - onPressed: () => Utils.shareText( + onPressed: () => ShareUtils.shareText( '${_controller.topicName} https://m.bilibili.com/topic-detail?topic_id=${_controller.topicId}', ), // https://www.bilibili.com/v/topic/detail?topic_id=${_controller.topicId} @@ -309,7 +335,7 @@ class _DynTopicPageState extends State with DynMixin { PopupMenuItem( onTap: _controller.onFav, child: Text( - '${_controller.isFav.value == true ? '取消' : ''}收藏', + '${_controller.isFav.value ? '取消' : ''}收藏', ), ), PopupMenuItem( @@ -320,7 +346,7 @@ class _DynTopicPageState extends State with DynMixin { return; } PageUtils.inAppWebview( - 'https://www.bilibili.com/h5/topic-active/topic-report?topic_id=${_controller.topicId}&topic_name=${_controller.topicName}&${Utils.themeUrl(theme.colorScheme.isDark)}', + '${HttpString.baseUrl}/h5/topic-active/topic-report?topic_id=${_controller.topicId}&topic_name=${_controller.topicName}&${ThemeUtils.themeUrl(colorScheme.isDark)}', ); }, ), diff --git a/lib/pages/emote/view.dart b/lib/pages/emote/view.dart index 8e8a35f941..5cbc89cefb 100644 --- a/lib/pages/emote/view.dart +++ b/lib/pages/emote/view.dart @@ -9,7 +9,7 @@ import 'package:PiliPlus/models_new/emote/emote.dart'; import 'package:PiliPlus/models_new/emote/package.dart'; import 'package:PiliPlus/pages/emote/controller.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; -import 'package:PiliPlus/utils/utils.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -182,7 +182,7 @@ class _EmotePanelState extends State '/webview', parameters: { 'url': - 'https://www.bilibili.com/h5/mall/emoji-package/home?navhide=1&${Utils.themeUrl(theme.colorScheme.isDark)}', + 'https://www.bilibili.com/h5/mall/emoji-package/home?navhide=1&${ThemeUtils.themeUrl(theme.isDark)}', }, ), icon: const Icon(Icons.settings), diff --git a/lib/pages/episode_panel/view.dart b/lib/pages/episode_panel/view.dart index ee082b0a3c..362f6ea75c 100644 --- a/lib/pages/episode_panel/view.dart +++ b/lib/pages/episode_panel/view.dart @@ -262,15 +262,15 @@ class _EpisodePanelState extends State } return offset + 7; } else { - return index * 100 + 7; + return index * 112 + 7; } } double _calcItemHeight(ugc.BaseEpisodeItem episode) { if (episode is ugc.EpisodeItem && episode.pages!.length > 1) { - return 145; // 98 + 2 + 10 + 35 + return 157; // 110 + 2 + 10 + 35 } - return 100; + return 112; } Widget _buildBody( @@ -311,7 +311,7 @@ class _EpisodePanelState extends State mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - episodeItem, // 98 + episodeItem, // 110 Padding( padding: const EdgeInsets.symmetric( horizontal: 12, @@ -352,7 +352,7 @@ class _EpisodePanelState extends State isCurrentIndex: isCurrItem, ); }, - itemExtent: 100, + itemExtent: 112, ), ), ], @@ -428,9 +428,9 @@ class _EpisodePanelState extends State return Padding( padding: const EdgeInsets.only(bottom: 2), child: SizedBox( - height: 98, + height: 110, child: Material( - type: MaterialType.transparency, + type: .transparency, child: InkWell( onTap: () { if (episode.badge == "会员" && @@ -462,7 +462,7 @@ class _EpisodePanelState extends State onLongPress: onLongPress, onSecondaryTap: PlatformUtils.isMobile ? null : onLongPress, child: Padding( - padding: const EdgeInsets.symmetric( + padding: const .symmetric( horizontal: Style.safeSpace, vertical: 5, ), @@ -475,8 +475,8 @@ class _EpisodePanelState extends State children: [ NetworkImgLayer( src: cover, - width: 140.8, - height: 88, + width: 160, + height: 100, cacheWidth: cacheWidth, ), if (duration != null && duration > 0) diff --git a/lib/pages/fan/view.dart b/lib/pages/fan/view.dart index d10ec5d19c..e3ff2cb4dd 100644 --- a/lib/pages/fan/view.dart +++ b/lib/pages/fan/view.dart @@ -4,6 +4,7 @@ import 'package:PiliPlus/pages/fan/controller.dart'; import 'package:PiliPlus/pages/follow_type/view.dart'; import 'package:PiliPlus/pages/follow_type/widgets/item.dart'; import 'package:PiliPlus/pages/share/view.dart' show UserModel; +import 'package:PiliPlus/utils/parse_int.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; @@ -29,7 +30,7 @@ class FansPage extends StatefulWidget { Get.toNamed( '/fan', arguments: { - 'mid': Utils.safeToInt(mid), + 'mid': safeToInt(mid), 'name': name, }, ); diff --git a/lib/pages/fav/article/widget/item.dart b/lib/pages/fav/article/widget/item.dart index abf6ba187a..3adf99110a 100644 --- a/lib/pages/fav/article/widget/item.dart +++ b/lib/pages/fav/article/widget/item.dart @@ -1,11 +1,10 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; import 'package:PiliPlus/models/common/stat_type.dart'; import 'package:PiliPlus/models_new/fav/fav_article/item.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; class FavArticleItem extends StatelessWidget { diff --git a/lib/pages/fav/note/widget/item.dart b/lib/pages/fav/note/widget/item.dart index 61396624e7..f2b711f370 100644 --- a/lib/pages/fav/note/widget/item.dart +++ b/lib/pages/fav/note/widget/item.dart @@ -1,12 +1,11 @@ import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/select_mask.dart'; import 'package:PiliPlus/models_new/fav/fav_note/list.dart'; import 'package:PiliPlus/pages/fav/note/controller.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; class FavNoteItem extends StatelessWidget { const FavNoteItem({ @@ -29,7 +28,7 @@ class FavNoteItem extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); + final colorScheme = ColorScheme.of(context); return Material( type: MaterialType.transparency, child: InkWell( @@ -71,7 +70,7 @@ class FavNoteItem extends StatelessWidget { ), Positioned.fill( child: selectMask( - theme, + colorScheme, item.checked, ), ), @@ -100,7 +99,7 @@ class FavNoteItem extends StatelessWidget { style: TextStyle( fontSize: 14, height: 1, - color: theme.colorScheme.outline, + color: colorScheme.outline, ), ), const Spacer(), @@ -110,7 +109,7 @@ class FavNoteItem extends StatelessWidget { style: TextStyle( fontSize: 13, height: 1, - color: theme.colorScheme.outline, + color: colorScheme.outline, ), ), ], diff --git a/lib/pages/fav/pgc/widget/item.dart b/lib/pages/fav/pgc/widget/item.dart index 3e4bd32478..4876a1262e 100644 --- a/lib/pages/fav/pgc/widget/item.dart +++ b/lib/pages/fav/pgc/widget/item.dart @@ -1,7 +1,6 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/select_mask.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; @@ -9,7 +8,7 @@ import 'package:PiliPlus/models_new/fav/fav_pgc/list.dart'; import 'package:PiliPlus/pages/common/multi_select/base.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; class FavPgcItem extends StatelessWidget { const FavPgcItem({ @@ -34,7 +33,7 @@ class FavPgcItem extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); + final colorScheme = ColorScheme.of(context); return Material( type: MaterialType.transparency, child: Stack( @@ -86,7 +85,7 @@ class FavPgcItem extends StatelessWidget { ), Positioned.fill( child: selectMask( - theme, + colorScheme, item.checked, borderRadius: const BorderRadius.all( Radius.circular(4), @@ -111,7 +110,7 @@ class FavPgcItem extends StatelessWidget { '${item.newEp!.indexShow}${item.isFinish == 0 && item.renewalTime?.isNotEmpty == true ? ',${item.renewalTime}' : ''}', style: TextStyle( fontSize: 13, - color: theme.colorScheme.onSurfaceVariant, + color: colorScheme.onSurfaceVariant, ), ), ], @@ -123,7 +122,7 @@ class FavPgcItem extends StatelessWidget { item.progress!, style: TextStyle( fontSize: 13, - color: theme.colorScheme.onSurfaceVariant, + color: colorScheme.onSurfaceVariant, ), ), ], @@ -141,7 +140,7 @@ class FavPgcItem extends StatelessWidget { iconSize: 18, onPressed: onUpdateStatus, icon: const Icon(Icons.more_vert), - iconColor: theme.colorScheme.outline, + iconColor: colorScheme.outline, ), ), ], diff --git a/lib/pages/fav/video/widgets/item.dart b/lib/pages/fav/video/widgets/item.dart index 6174490ea4..4f1a5bbe00 100644 --- a/lib/pages/fav/video/widgets/item.dart +++ b/lib/pages/fav/video/widgets/item.dart @@ -1,10 +1,9 @@ import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models_new/fav/fav_folder/list.dart'; -import 'package:PiliPlus/utils/fav_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:PiliPlus/utils/bili_utils.dart'; +import 'package:flutter/material.dart'; class FavVideoItem extends StatelessWidget { final String heroTag; @@ -97,7 +96,7 @@ class FavVideoItem extends StatelessWidget { ), const Spacer(), Text( - FavUtils.isPublicFavText(item.attr), + BiliUtils.isPublicFavText(item.attr), style: TextStyle( fontSize: fontSize, color: color, diff --git a/lib/pages/fav_create/view.dart b/lib/pages/fav_create/view.dart index efea2da1b5..3ee8946820 100644 --- a/lib/pages/fav_create/view.dart +++ b/lib/pages/fav_create/view.dart @@ -1,13 +1,14 @@ import 'dart:io' show File; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/msg.dart'; +import 'package:PiliPlus/utils/bili_utils.dart'; import 'package:PiliPlus/utils/extension/file_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; -import 'package:PiliPlus/utils/fav_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:flutter/material.dart'; @@ -51,7 +52,7 @@ class _CreateFavPageState extends State { if (res case Success(:final response)) { _titleController.text = response.title; _introController.text = response.intro ?? ''; - _isPublic = FavUtils.isPublicFav(response.attr); + _isPublic = BiliUtils.isPublicFav(response.attr); _cover = response.cover; _attr = response.attr; } else { @@ -90,8 +91,10 @@ class _CreateFavPageState extends State { intro: _introController.text, ).then((res) { if (res case Success(:final response)) { - Get.back(result: response); SmartDialog.showToast('${_mediaId != null ? '编辑' : '创建'}成功'); + if (mounted) { + Get.back(result: response); + } } else { res.toast(); } @@ -114,9 +117,10 @@ class _CreateFavPageState extends State { Future _pickImg(BuildContext context, ThemeData theme) async { try { - XFile? pickedFile = await _imagePicker.pickImage( + final pickedFile = await _imagePicker.pickImage( source: ImageSource.gallery, imageQuality: 100, + requestFullMetadata: false, ); if (pickedFile != null && mounted) { String imgPath = pickedFile.path; @@ -128,7 +132,7 @@ class _CreateFavPageState extends State { toolbarTitle: '裁剪', toolbarColor: theme.colorScheme.secondaryContainer, toolbarWidgetColor: theme.colorScheme.onSecondaryContainer, - statusBarLight: theme.colorScheme.isLight, + statusBarLight: theme.isLight, aspectRatioPresets: [CropAspectRatioPreset.ratio16x9], lockAspectRatio: true, hideBottomControls: true, @@ -178,7 +182,7 @@ class _CreateFavPageState extends State { child: Column( spacing: 12, children: [ - if (_attr == null || !FavUtils.isDefaultFav(_attr!)) + if (_attr == null || !BiliUtils.isDefaultFav(_attr!)) Builder( builder: (context) { return ListTile( @@ -192,37 +196,32 @@ class _CreateFavPageState extends State { if (_cover?.isNotEmpty == true) { showDialog( context: context, - builder: (_) => AlertDialog( + builder: (_) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const .symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - onTap: () { - Get.back(); - _pickImg(context, theme); - }, - title: const Text( - '替换封面', - style: TextStyle(fontSize: 14), - ), + children: [ + DialogOption( + onPressed: () { + Get.back(); + _pickImg(context, theme); + }, + child: const Text( + '替换封面', + style: TextStyle(fontSize: 14), ), - ListTile( - dense: true, - onTap: () { - Get.back(); - _cover = null; - (context as Element).markNeedsBuild(); - }, - title: const Text( - '移除封面', - style: TextStyle(fontSize: 14), - ), + ), + DialogOption( + onPressed: () { + Get.back(); + _cover = null; + (context as Element).markNeedsBuild(); + }, + child: const Text( + '移除封面', + style: TextStyle(fontSize: 14), ), - ], - ), + ), + ], ), ); } else { @@ -287,11 +286,11 @@ class _CreateFavPageState extends State { Expanded( child: TextField( autofocus: true, - readOnly: _attr != null && FavUtils.isDefaultFav(_attr!), + readOnly: _attr != null && BiliUtils.isDefaultFav(_attr!), controller: _titleController, style: TextStyle( fontSize: 14, - color: _attr != null && FavUtils.isDefaultFav(_attr!) + color: _attr != null && BiliUtils.isDefaultFav(_attr!) ? theme.colorScheme.outline : null, ), @@ -316,7 +315,7 @@ class _CreateFavPageState extends State { ], ), ), - if (_attr == null || !FavUtils.isDefaultFav(_attr!)) + if (_attr == null || !BiliUtils.isDefaultFav(_attr!)) ListTile( tileColor: theme.colorScheme.onInverseSurface, title: Row( diff --git a/lib/pages/fav_detail/controller.dart b/lib/pages/fav_detail/controller.dart index c22f217566..97f2c3b469 100644 --- a/lib/pages/fav_detail/controller.dart +++ b/lib/pages/fav_detail/controller.dart @@ -80,11 +80,11 @@ class FavDetailController late int mediaId; late String heroTag; final Rx folderInfo = FavFolderInfo().obs; - final Rx _isOwner = Rx(null); + final RxBool _isOwner = false.obs; final Rx order = FavOrderType.mtime.obs; @override - bool get isOwner => _isOwner.value ?? false; + bool get isOwner => _isOwner.value; late final account = Accounts.main; @@ -184,8 +184,10 @@ class FavDetailController folderInfo ..value.favState = isFav ? 0 : 1 ..refresh(); + SmartDialog.showToast('${isFav ? '取消' : ''}收藏成功'); + } else { + res.toast(); } - res.toast(); } Future cleanFav() async { diff --git a/lib/pages/fav_detail/view.dart b/lib/pages/fav_detail/view.dart index a083e49cc8..c541a4bf6b 100644 --- a/lib/pages/fav_detail/view.dart +++ b/lib/pages/fav_detail/view.dart @@ -14,9 +14,10 @@ import 'package:PiliPlus/pages/common/fab_mixin.dart' import 'package:PiliPlus/pages/dynamics_repost/view.dart'; import 'package:PiliPlus/pages/fav_detail/controller.dart'; import 'package:PiliPlus/pages/fav_detail/widget/fav_video_card.dart'; -import 'package:PiliPlus/utils/fav_utils.dart'; +import 'package:PiliPlus/utils/bili_utils.dart'; import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/request_utils.dart'; +import 'package:PiliPlus/utils/share_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -194,12 +195,12 @@ class _FavDetailPageState extends State with GridMixin { ), Obx(() { final attr = _favDetailController.folderInfo.value.attr; - return attr == -1 || !FavUtils.isPublicFav(attr) + return attr == -1 || !BiliUtils.isPublicFav(attr) ? const SizedBox.shrink() : IconButton( iconSize: 22, tooltip: '分享', - onPressed: () => Utils.shareText( + onPressed: () => ShareUtils.shareText( 'https://www.bilibili.com/medialist/detail/ml${_favDetailController.mediaId}', ), icon: const Icon(Icons.share), @@ -225,84 +226,85 @@ class _FavDetailPageState extends State with GridMixin { ); }, ), - PopupMenuButton( - icon: const Icon(Icons.more_vert), - itemBuilder: (context) { - final isOwner = _favDetailController.isOwner; - final folderInfo = _favDetailController.folderInfo.value; - return [ - if (isOwner) ...[ - PopupMenuItem( - onTap: _favDetailController.onSort, - child: const Text('排序'), - ), - PopupMenuItem( - onTap: () => - Get.toNamed( - '/createFav', - parameters: {'mediaId': mediaId}, - )?.then((res) { - if (res is FavFolderInfo) { - _favDetailController.folderInfo.value = res; - } - }), - child: const Text('编辑信息'), - ), - ] else - PopupMenuItem( - onTap: () => - _favDetailController.onFav(folderInfo.favState == 1), - child: Text('${folderInfo.favState == 1 ? '取消' : ''}收藏'), - ), - if (FavUtils.isPublicFav(folderInfo.attr)) - PopupMenuItem( - onTap: () => showModalBottomSheet( - context: context, - isScrollControlled: true, - useSafeArea: true, - builder: (context) => RepostPanel( - rid: _favDetailController.mediaId, - dynType: 4300, - pic: folderInfo.cover, - title: folderInfo.title, - uname: folderInfo.upper?.name, - ), + if (_favDetailController.account.isLogin) + PopupMenuButton( + icon: const Icon(Icons.more_vert), + itemBuilder: (context) { + final isOwner = _favDetailController.isOwner; + final folderInfo = _favDetailController.folderInfo.value; + return [ + if (isOwner) ...[ + PopupMenuItem( + onTap: _favDetailController.onSort, + child: const Text('排序'), ), - child: const Text('分享至动态'), - ), - if (isOwner) ...[ - PopupMenuItem( - onTap: _favDetailController.cleanFav, - child: const Text('清除失效内容'), - ), - if (!FavUtils.isDefaultFav(folderInfo.attr)) ...[ - const PopupMenuDivider(height: 12), PopupMenuItem( - onTap: () => showConfirmDialog( + onTap: () => + Get.toNamed( + '/createFav', + parameters: {'mediaId': mediaId}, + )?.then((res) { + if (res is FavFolderInfo) { + _favDetailController.folderInfo.value = res; + } + }), + child: const Text('编辑信息'), + ), + ] else + PopupMenuItem( + onTap: () => + _favDetailController.onFav(folderInfo.favState == 1), + child: Text('${folderInfo.favState == 1 ? '取消' : ''}收藏'), + ), + if (BiliUtils.isPublicFav(folderInfo.attr)) + PopupMenuItem( + onTap: () => showModalBottomSheet( context: context, - title: const Text('确定删除该收藏夹?'), - onConfirm: () => - FavHttp.deleteFolder(mediaIds: mediaId).then((res) { - if (res.isSuccess) { - SmartDialog.showToast('删除成功'); - Get.back(result: true); - } else { - res.toast(); - } - }), - ), - child: Text( - '删除', - style: TextStyle( - color: theme.colorScheme.error, + isScrollControlled: true, + useSafeArea: true, + builder: (context) => RepostPanel( + rid: _favDetailController.mediaId, + dynType: 4300, + pic: folderInfo.cover, + title: folderInfo.title, + uname: folderInfo.upper?.name, ), ), + child: const Text('分享至动态'), + ), + if (isOwner) ...[ + PopupMenuItem( + onTap: _favDetailController.cleanFav, + child: const Text('清除失效内容'), ), + if (!BiliUtils.isDefaultFav(folderInfo.attr)) ...[ + const PopupMenuDivider(height: 12), + PopupMenuItem( + onTap: () => showConfirmDialog( + context: context, + title: const Text('确定删除该收藏夹?'), + onConfirm: () => + FavHttp.deleteFolder(mediaIds: mediaId).then((res) { + if (res.isSuccess) { + SmartDialog.showToast('删除成功'); + Get.back(result: true); + } else { + res.toast(); + } + }), + ), + child: Text( + '删除', + style: TextStyle( + color: theme.colorScheme.error, + ), + ), + ), + ], ], - ], - ]; - }, - ), + ]; + }, + ), const SizedBox(width: 10), ]; } @@ -452,7 +454,7 @@ class _FavDetailPageState extends State with GridMixin { ], Text( '共${folderInfo.mediaCount}条视频 · ' - '${FavUtils.isPublicFavText(folderInfo.attr)}', + '${BiliUtils.isPublicFavText(folderInfo.attr)}', style: style, ), ], diff --git a/lib/pages/fav_detail/widget/fav_video_card.dart b/lib/pages/fav_detail/widget/fav_video_card.dart index 1cbe344446..bbe84b018e 100644 --- a/lib/pages/fav_detail/widget/fav_video_card.dart +++ b/lib/pages/fav_detail/widget/fav_video_card.dart @@ -1,7 +1,6 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/select_mask.dart'; @@ -17,7 +16,7 @@ import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; // 收藏视频卡片 - 水平布局 @@ -39,7 +38,7 @@ class FavVideoCardH extends StatelessWidget { Widget build(BuildContext context) { final isOwner = !isSort && ctr!.isOwner; late final enableMultiSelect = ctr?.enableMultiSelect.value ?? false; - final theme = Theme.of(context); + final colorScheme = ColorScheme.of(context); final onLongPress = isSort || enableMultiSelect ? null @@ -135,7 +134,7 @@ class FavVideoCardH extends StatelessWidget { if (!isSort) Positioned.fill( child: selectMask( - theme, + colorScheme, item.checked, ), ), @@ -145,7 +144,7 @@ class FavVideoCardH extends StatelessWidget { ), ), const SizedBox(width: 10), - content(context, theme, isOwner), + content(context, colorScheme, isOwner), ], ), ), @@ -153,7 +152,7 @@ class FavVideoCardH extends StatelessWidget { ); } - Widget content(BuildContext context, ThemeData theme, isOwner) { + Widget content(BuildContext context, ColorScheme colorScheme, bool isOwner) { return Expanded( child: Stack( clipBehavior: Clip.none, @@ -178,7 +177,7 @@ class FavVideoCardH extends StatelessWidget { overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 13, - color: theme.colorScheme.outline, + color: colorScheme.outline, ), ), const Spacer(), @@ -189,7 +188,7 @@ class FavVideoCardH extends StatelessWidget { style: TextStyle( height: 1, fontSize: 12, - color: theme.colorScheme.outline, + color: colorScheme.outline, ), ), if (item.type != 24) @@ -215,7 +214,7 @@ class FavVideoCardH extends StatelessWidget { child: iconButton( icon: const Icon(Icons.clear), tooltip: '取消收藏', - iconColor: theme.colorScheme.outline, + iconColor: colorScheme.outline, onPressed: () => showDialog( context: context, builder: (context) => AlertDialog( @@ -226,7 +225,7 @@ class FavVideoCardH extends StatelessWidget { onPressed: Get.back, child: Text( '取消', - style: TextStyle(color: theme.colorScheme.outline), + style: TextStyle(color: colorScheme.outline), ), ), TextButton( diff --git a/lib/pages/fav_folder_sort/view.dart b/lib/pages/fav_folder_sort/view.dart index 093d4f44c5..c06bdaa6a1 100644 --- a/lib/pages/fav_folder_sort/view.dart +++ b/lib/pages/fav_folder_sort/view.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/reorder_mixin.dart'; import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/fav/fav_folder/list.dart'; @@ -16,10 +17,10 @@ class FavFolderSortPage extends StatefulWidget { State createState() => _FavFolderSortPageState(); } -class _FavFolderSortPageState extends State { +class _FavFolderSortPageState extends State + with ReorderMixin { FavController get _favController => widget.favController; - final GlobalKey _key = GlobalKey(); late List sortList = List.from( _favController.loadingState.value.data!, ); @@ -39,7 +40,9 @@ class _FavFolderSortPageState extends State { if (res.isSuccess) { SmartDialog.showToast('排序完成'); _favController.loadingState.value = Success(sortList); - Get.back(); + if (mounted) { + Get.back(); + } } else { res.toast(); } @@ -53,26 +56,21 @@ class _FavFolderSortPageState extends State { ); } - void onReorder(int oldIndex, int newIndex) { + void onReorderItem(int oldIndex, int newIndex) { if (oldIndex == 0 || newIndex == 0) { SmartDialog.showToast('默认收藏夹不支持排序'); return; } - if (newIndex > oldIndex) { - newIndex -= 1; - } - - final tabsItem = sortList.removeAt(oldIndex); - sortList.insert(newIndex, tabsItem); + sortList.insert(newIndex, sortList.removeAt(oldIndex)); setState(() {}); } Widget get _buildBody { return ReorderableListView.builder( - key: _key, - onReorder: onReorder, + onReorderItem: onReorderItem, + proxyDecorator: proxyDecorator, physics: const AlwaysScrollableScrollPhysics(), itemCount: sortList.length, padding: @@ -83,7 +81,7 @@ class _FavFolderSortPageState extends State { final key = item.id.toString(); return SizedBox( key: Key(key), - height: 98, + height: 110, child: FavVideoItem( heroTag: key, item: item, diff --git a/lib/pages/fav_panel/view.dart b/lib/pages/fav_panel/view.dart index bb2fc822f2..dd9fed660a 100644 --- a/lib/pages/fav_panel/view.dart +++ b/lib/pages/fav_panel/view.dart @@ -2,7 +2,7 @@ import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/fav/fav_folder/list.dart'; import 'package:PiliPlus/pages/common/common_intro_controller.dart'; -import 'package:PiliPlus/utils/fav_utils.dart'; +import 'package:PiliPlus/utils/bili_utils.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -27,10 +27,10 @@ class _FavPanelState extends State { @override void initState() { super.initState(); - _query(); + _queryVideoInFolder(); } - Future _query() async { + Future _queryVideoInFolder() async { final res = await widget.ctr.queryVideoInFolder(); if (mounted) { loadingState = res; @@ -39,58 +39,60 @@ class _FavPanelState extends State { } Widget get _buildBody { - late final list = widget.ctr.favFolderData.value.list!; - return switch (loadingState) { - Loading() => m3eLoading, - Success() => ListView.builder( - controller: widget.scrollController, - itemCount: list.length, - itemBuilder: (context, index) { - FavFolderInfo item = list[index]; - return Material( - type: MaterialType.transparency, - child: Builder( - builder: (context) { - void onTap() { - bool isChecked = item.favState == 1; - item - ..favState = isChecked ? 0 : 1 - ..mediaCount = isChecked - ? item.mediaCount - 1 - : item.mediaCount + 1; - (context as Element).markNeedsBuild(); - } + switch (loadingState) { + case Loading(): + return m3eLoading; + case Success(): + final list = widget.ctr.favFolderData.value.list!; + return ListView.builder( + controller: widget.scrollController, + itemCount: list.length, + itemBuilder: (context, index) { + FavFolderInfo item = list[index]; + return Material( + type: .transparency, + child: Builder( + builder: (context) { + final isChecked = item.favState == 1; + + void onTap() { + item + ..favState = isChecked ? 0 : 1 + ..mediaCount += isChecked ? -1 : 1; + (context as Element).markNeedsBuild(); + } - return ListTile( - onTap: onTap, - dense: true, - leading: FavUtils.isPublicFav(item.attr) - ? const Icon(Icons.folder_outlined) - : const Icon(Icons.lock_outline), - minLeadingWidth: 0, - title: Text(item.title), - subtitle: Text( - '${item.mediaCount}个内容 . ${FavUtils.isPublicFavText(item.attr)}', - ), - trailing: Transform.scale( - scale: 0.9, - child: Checkbox( - value: item.favState == 1, - onChanged: (bool? checkValue) => onTap(), + return ListTile( + onTap: onTap, + dense: true, + leading: BiliUtils.isPublicFav(item.attr) + ? const Icon(Icons.folder_outlined) + : const Icon(Icons.lock_outline), + minLeadingWidth: 0, + title: Text(item.title), + subtitle: Text( + '${item.mediaCount}个内容 . ${BiliUtils.isPublicFavText(item.attr)}', ), - ), - ); - }, - ), - ); - }, - ), - Error(:final errMsg) => scrollErrorWidget( - controller: widget.scrollController, - errMsg: errMsg, - onReload: _query, - ), - }; + trailing: Transform.scale( + scale: 0.9, + child: Checkbox( + value: isChecked, + onChanged: (bool? checkValue) => onTap(), + ), + ), + ); + }, + ), + ); + }, + ); + case Error(:final errMsg): + return scrollErrorWidget( + errMsg: errMsg, + controller: widget.scrollController, + onReload: _queryVideoInFolder, + ); + } } @override @@ -109,23 +111,23 @@ class _FavPanelState extends State { actions: [ TextButton.icon( onPressed: () => Get.toNamed('/createFav')?.then((data) { - if (data != null) { - widget.ctr.favFolderData - ..value.list?.insert(1, data) - ..refresh(); + if (data is FavFolderInfo && mounted) { + widget.ctr.favFolderData.value.list?.insert( + 1, + data + ..favState = 1 + ..mediaCount = 1, + ); + setState(() {}); } }), - icon: Icon( - Icons.add, - color: theme.primary, - ), + icon: Icon(Icons.add, color: theme.primary), label: const Text('新建收藏夹'), - style: TextButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: 18, - vertical: 14, + style: const ButtonStyle( + visualDensity: .compact, + padding: WidgetStatePropertyAll( + .symmetric(horizontal: 18, vertical: 14), ), - visualDensity: VisualDensity.compact, ), ), const SizedBox(width: 16), @@ -137,7 +139,7 @@ class _FavPanelState extends State { color: theme.outline.withValues(alpha: 0.1), ), Padding( - padding: EdgeInsets.only( + padding: .only( left: 20, right: 20, top: 12, @@ -145,12 +147,12 @@ class _FavPanelState extends State { ), child: Row( spacing: 25, - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: .end, children: [ FilledButton.tonal( onPressed: Get.back, style: FilledButton.styleFrom( - visualDensity: VisualDensity.compact, + visualDensity: .compact, foregroundColor: theme.outline, backgroundColor: theme.onInverseSurface, ), @@ -161,9 +163,7 @@ class _FavPanelState extends State { feedBack(); widget.ctr.actionFavVideo(); }, - style: FilledButton.styleFrom( - visualDensity: VisualDensity.compact, - ), + style: const ButtonStyle(visualDensity: .compact), child: const Text('完成'), ), ], diff --git a/lib/pages/fav_search/controller.dart b/lib/pages/fav_search/controller.dart index d84371f405..b57c601daf 100644 --- a/lib/pages/fav_search/controller.dart +++ b/lib/pages/fav_search/controller.dart @@ -16,13 +16,24 @@ class FavSearchController CommonMultiSelectMixin, DeleteItemMixin, BaseFavController { - int type = Get.arguments['type']; + late int type; @override - int mediaId = Get.arguments['mediaId']; + late int mediaId; @override - bool isOwner = Get.arguments['isOwner']; - dynamic count = Get.arguments['count']; - dynamic title = Get.arguments['title']; + late bool isOwner; + late dynamic count; + late dynamic title; + + @override + void onInit() { + final args = Get.arguments; + type = args['type']; + mediaId = args['mediaId']; + isOwner = args['isOwner']; + count = args['count']; + title = args['title']; + super.onInit(); + } final Rx order = FavOrderType.mtime.obs; diff --git a/lib/pages/fav_search/view.dart b/lib/pages/fav_search/view.dart index db843841af..aca90dcf12 100644 --- a/lib/pages/fav_search/view.dart +++ b/lib/pages/fav_search/view.dart @@ -20,11 +20,8 @@ class FavSearchPage extends StatefulWidget { class _FavSearchPageState extends - CommonSearchPageState< - FavSearchPage, - FavDetailData, - FavDetailItemModel - > { + CommonSearchPageState + with GridMixin { @override final FavSearchController controller = Get.put( FavSearchController(), @@ -88,8 +85,6 @@ class _FavSearchPageState ), ]; - late final gridDelegate = Grid.videoCardHDelegate(context, minHeight: 110); - @override Widget buildList(List list) { return SliverGrid.builder( diff --git a/lib/pages/fav_sort/view.dart b/lib/pages/fav_sort/view.dart index cc61cddf7a..67674afb15 100644 --- a/lib/pages/fav_sort/view.dart +++ b/lib/pages/fav_sort/view.dart @@ -1,8 +1,10 @@ +import 'package:PiliPlus/common/widgets/reorder_mixin.dart'; import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/fav/fav_detail/media.dart'; import 'package:PiliPlus/pages/fav_detail/controller.dart'; import 'package:PiliPlus/pages/fav_detail/widget/fav_video_card.dart'; +import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -16,10 +18,9 @@ class FavSortPage extends StatefulWidget { State createState() => _FavSortPageState(); } -class _FavSortPageState extends State { +class _FavSortPageState extends State with ReorderMixin { FavDetailController get _favDetailController => widget.favDetailController; - final GlobalKey _key = GlobalKey(); late List sortList = List.from( _favDetailController.loadingState.value.data!, ); @@ -51,22 +52,25 @@ class _FavSortPageState extends State { title: Text('排序: ${_favDetailController.folderInfo.value.title}'), actions: [ TextButton( - onPressed: () async { + onPressed: () { if (sort.isEmpty) { Get.back(); return; } - final res = await FavHttp.sortFav( + FavHttp.sortFav( mediaId: _favDetailController.mediaId, sort: sort.join(','), - ); - if (res.isSuccess) { - SmartDialog.showToast('排序完成'); - _favDetailController.loadingState.value = Success(sortList); - Get.back(); - } else { - res.toast(); - } + ).then((res) { + if (res.isSuccess) { + SmartDialog.showToast('排序完成'); + _favDetailController.loadingState.value = Success(sortList); + if (mounted) { + Get.back(); + } + } else { + res.toast(); + } + }); }, child: const Text('完成'), ), @@ -77,29 +81,24 @@ class _FavSortPageState extends State { ); } - void onReorder(int oldIndex, int newIndex) { - if (newIndex > oldIndex) { - newIndex -= 1; - } - + void onReorderItem(int oldIndex, int newIndex) { final oldItem = sortList[oldIndex]; - final newItem = sortList.elementAtOrNull( - oldIndex > newIndex ? newIndex - 1 : newIndex, + final newItem = sortList.getOrNull( + oldIndex > newIndex ? newIndex - 1 : newIndex, // might be Negative ); sort.add( '${newItem == null ? '0:0' : '${newItem.id}:${newItem.type}'}:${oldItem.id}:${oldItem.type}', ); - final tabsItem = sortList.removeAt(oldIndex); - sortList.insert(newIndex, tabsItem); + sortList.insert(newIndex, sortList.removeAt(oldIndex)); setState(() {}); } Widget get _buildBody { final child = ReorderableListView.builder( - key: _key, - onReorder: onReorder, + onReorderItem: onReorderItem, + proxyDecorator: proxyDecorator, physics: const AlwaysScrollableScrollPhysics(), padding: MediaQuery.viewPaddingOf(context).copyWith(top: 0) + @@ -108,8 +107,8 @@ class _FavSortPageState extends State { itemBuilder: (context, index) { final item = sortList[index]; return SizedBox( - key: Key(item.id.toString()), - height: 98, + key: ValueKey(item.id), + height: 110, child: FavVideoCardH(item: item), ); }, diff --git a/lib/pages/follow/child/child_view.dart b/lib/pages/follow/child/child_view.dart index 649bb6d8d0..adbeda4ab9 100644 --- a/lib/pages/follow/child/child_view.dart +++ b/lib/pages/follow/child/child_view.dart @@ -7,6 +7,7 @@ import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/follow_order_type.dart'; import 'package:PiliPlus/models_new/follow/list.dart'; +import 'package:PiliPlus/pages/common/fab_mixin.dart'; import 'package:PiliPlus/pages/follow/child/child_controller.dart'; import 'package:PiliPlus/pages/follow/controller.dart'; import 'package:PiliPlus/pages/follow/widgets/follow_item.dart'; @@ -37,18 +38,45 @@ class FollowChildPage extends StatefulWidget { } class _FollowChildPageState extends State - with AutomaticKeepAliveClientMixin { - late final FollowChildController _followController; + with + AutomaticKeepAliveClientMixin, + SingleTickerProviderStateMixin, + BaseFabMixin, + LazyFabMixin { + late String _tag; + late FollowChildController _followController; + + String get _newTag => + '${widget.tag ?? Utils.generateRandomString(8)}${widget.tagid}'; @override void initState() { super.initState(); + _initController(); + } + + void _initController() { + _tag = _newTag; _followController = Get.put( FollowChildController(widget.controller, widget.mid, widget.tagid), - tag: '${widget.tag ?? Utils.generateRandomString(8)}${widget.tagid}', + tag: _tag, ); } + @override + void didUpdateWidget(FollowChildPage oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.tagid != widget.tagid) { + final newTag = _newTag; + if (Get.isRegistered(tag: newTag)) { + _followController = Get.find(tag: newTag); + } else { + Get.delete(tag: _tag); + _initController(); + } + } + } + @override Widget build(BuildContext context) { super.build(context); @@ -84,20 +112,41 @@ class _FollowChildPageState extends State return Stack( clipBehavior: Clip.none, children: [ - child, + NotificationListener( + onNotification: (notification) { + final direction = notification.direction; + if (direction == .forward) { + showFab(); + } else if (direction == .reverse) { + hideFab(); + } + return false; + }, + child: child, + ), Positioned( right: kFloatingActionButtonMargin + padding.right, - bottom: kFloatingActionButtonMargin + padding.bottom, - child: FloatingActionButton.extended( - onPressed: () => _followController - ..setOrderType( - _followController.orderType.value == FollowOrderType.def - ? FollowOrderType.attention - : FollowOrderType.def, - ) - ..onReload(), - icon: const Icon(Icons.format_list_bulleted, size: 20), - label: Obx(() => Text(_followController.orderType.value.title)), + bottom: 0, + child: SlideTransition( + position: fabAnimation, + child: Padding( + padding: .only( + bottom: kFloatingActionButtonMargin + padding.bottom, + ), + child: FloatingActionButton.extended( + onPressed: () => _followController + ..setOrderType( + _followController.orderType.value == FollowOrderType.def + ? FollowOrderType.attention + : FollowOrderType.def, + ) + ..onReload(), + icon: const Icon(Icons.format_list_bulleted, size: 20), + label: Obx( + () => Text(_followController.orderType.value.title), + ), + ), + ), ), ), ], diff --git a/lib/pages/follow/controller.dart b/lib/pages/follow/controller.dart index b803ed8a1f..095d05155c 100644 --- a/lib/pages/follow/controller.dart +++ b/lib/pages/follow/controller.dart @@ -45,36 +45,35 @@ class FollowController extends GetxController with GetTickerProviderStateMixin { tabs ..assign(MemberTagItemModel(name: '全部关注')) ..addAll(response); - int initialIndex = 0; - if (tabController != null) { - initialIndex = tabController!.index.clamp(0, tabs.length - 1); - tabController!.dispose(); - } - tabController = TabController( - initialIndex: initialIndex, - length: tabs.length, - vsync: this, - ); + onInitTab(); followState.value = Success(tabs.hashCode); } else { followState.value = res; } } - @override - void onClose() { - tabController?.dispose(); - super.onClose(); + void onInitTab() { + int initialIndex = 0; + if (tabController != null) { + initialIndex = tabController!.index.clamp(0, tabs.length - 1); + tabController!.dispose(); + } + tabController = TabController( + initialIndex: initialIndex, + length: tabs.length, + vsync: this, + ); } - Future onCreateTag(String tagName) async { - final res = await MemberHttp.createFollowTag(tagName); - if (res.isSuccess) { + void onCreateFavTag(({int tagid, String tagName}) res) { + if (isClosed) return; + if (followState.value.isSuccess) { + tabs.add(MemberTagItemModel.fromCreate(res)); + onInitTab(); + followState.refresh(); + } else { followState.value = LoadingState.loading(); queryFollowUpTags(); - SmartDialog.showToast('创建成功'); - } else { - res.toast(); } } @@ -89,14 +88,22 @@ class FollowController extends GetxController with GetTickerProviderStateMixin { } } - Future onDelTag(int tagid) async { + Future onDelTag(int index, int tagid) async { final res = await MemberHttp.delFollowTag(tagid); if (res.isSuccess) { - followState.value = LoadingState.loading(); - queryFollowUpTags(); + tabs.removeAt(index); + onInitTab(); + followState.refresh(); SmartDialog.showToast('删除成功'); } else { res.toast(); } } + + @override + void onClose() { + tabController?.dispose(); + tabController = null; + super.onClose(); + } } diff --git a/lib/pages/follow/view.dart b/lib/pages/follow/view.dart index 3401b387da..2b0fefa8fc 100644 --- a/lib/pages/follow/view.dart +++ b/lib/pages/follow/view.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; @@ -7,7 +8,11 @@ import 'package:PiliPlus/models/member/tags.dart'; import 'package:PiliPlus/pages/follow/child/child_controller.dart'; import 'package:PiliPlus/pages/follow/child/child_view.dart'; import 'package:PiliPlus/pages/follow/controller.dart'; +import 'package:PiliPlus/pages/follow_tag_sort/view.dart'; +import 'package:PiliPlus/utils/bili_utils.dart'; +import 'package:PiliPlus/utils/parse_int.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:PiliPlus/utils/request_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show LengthLimitingTextInputFormatter; @@ -24,7 +29,7 @@ class FollowPage extends StatefulWidget { Get.toNamed( '/follow', arguments: { - 'mid': Utils.safeToInt(mid), + 'mid': safeToInt(mid), 'name': name, }, ); @@ -45,57 +50,72 @@ class _FollowPageState extends State { Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, - appBar: AppBar( - title: _followController.isOwner - ? const Text('我的关注') - : Obx(() { - final name = _followController.name.value; - if (name != null) return Text('$name的关注'); - return const SizedBox.shrink(); - }), - actions: _followController.isOwner - ? [ - IconButton( - onPressed: _onCreateTag, - icon: const Icon(Icons.add), - tooltip: '新建分组', - ), - IconButton( - onPressed: () => Get.toNamed( - '/followSearch', - arguments: { - 'mid': _followController.mid, - }, - ), - icon: const Icon(Icons.search_outlined), - tooltip: '搜索', - ), - PopupMenuButton( - icon: const Icon(Icons.more_vert), - itemBuilder: (context) => [ - PopupMenuItem( - onTap: () => Get.toNamed('/blackListPage'), - child: const Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.block, size: 19), - SizedBox(width: 10), - Text('黑名单管理'), - ], - ), - ), - ], - ), - const SizedBox(width: 6), - ] - : null, - ), + appBar: _buildAppBar, body: _followController.isOwner ? Obx(() => _buildBody(_followController.followState.value)) : _childPage(), ); } + PreferredSizeWidget get _buildAppBar => AppBar( + title: _followController.isOwner + ? const Text('我的关注') + : Obx(() { + final name = _followController.name.value; + if (name != null) return Text('$name的关注'); + return const SizedBox.shrink(); + }), + actions: _followController.isOwner + ? [ + IconButton( + onPressed: () => RequestUtils.createFavTag( + context, + _followController.onCreateFavTag, + ), + icon: const Icon(Icons.add), + tooltip: '新建分组', + ), + IconButton( + onPressed: () { + if (_followController.followState.value is! Success) { + return; + } + Get.to(FollowTagSortPage(controller: _followController)); + }, + icon: const Icon(Icons.sort), + tooltip: '分组排序', + ), + IconButton( + onPressed: () => Get.toNamed( + '/followSearch', + arguments: { + 'mid': _followController.mid, + }, + ), + icon: const Icon(Icons.search_outlined), + tooltip: '搜索', + ), + PopupMenuButton( + icon: const Icon(Icons.more_vert), + itemBuilder: (context) => [ + PopupMenuItem( + onTap: () => Get.toNamed('/blackListPage'), + child: const Row( + spacing: 10, + mainAxisSize: .min, + children: [ + Icon(Icons.block, size: 19), + Text('黑名单管理'), + ], + ), + ), + ], + ), + const SizedBox(width: 6), + ] + : null, + ); + Widget _childPage([MemberTagItemModel? item]) => FollowChildPage( tag: _tag, controller: _followController, @@ -103,10 +123,6 @@ class _FollowPageState extends State { tagid: item?.tagid, ); - bool _isCustomTag(int? tagid) { - return tagid != null && tagid != 0 && tagid != -10 && tagid != -2; - } - Widget _buildBody(LoadingState loadingState) { return switch (loadingState) { Loading() => m3eLoading, @@ -122,7 +138,7 @@ class _FollowPageState extends State { return Obx(() { final item = _followController.tabs[index]; int? count = item.count; - if (_isCustomTag(item.tagid)) { + if (BiliUtils.isCustomFollowTag(item.tagid)) { return GestureDetector( behavior: HitTestBehavior.translucent, onLongPress: () { @@ -179,80 +195,49 @@ class _FollowPageState extends State { void _onHandleTag(int index, MemberTagItemModel item) { showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - onTap: () { - Get.back(); - String tagName = item.name!; - showConfirmDialog( - context: context, - title: const Text('编辑分组名称'), - content: TextFormField( - autofocus: true, - initialValue: tagName, - onChanged: (value) => tagName = value, - inputFormatters: [ - LengthLimitingTextInputFormatter(16), - ], - decoration: const InputDecoration( - border: OutlineInputBorder(), - ), + children: [ + DialogOption( + onPressed: () { + Get.back(); + String tagName = item.name!; + showConfirmDialog( + context: context, + title: const Text('编辑分组名称'), + content: TextFormField( + autofocus: true, + initialValue: tagName, + onChanged: (value) => tagName = value, + inputFormatters: [LengthLimitingTextInputFormatter(16)], + decoration: const InputDecoration( + border: OutlineInputBorder(), ), - onConfirm: () { - if (tagName.isNotEmpty) { - _followController.onUpdateTag(item, tagName); - } - }, - ); - }, - dense: true, - title: const Text( - '修改名称', - style: TextStyle(fontSize: 14), - ), - ), - ListTile( - onTap: () { - Get.back(); - showConfirmDialog( - context: context, - title: const Text('删除分组'), - content: const Text('删除后,该分组下的用户依旧保留?'), - onConfirm: () => _followController.onDelTag(item.tagid!), - ); - }, - dense: true, - title: const Text( - '删除分组', - style: TextStyle(fontSize: 14), - ), - ), - ], - ), - ), - ); - } - - void _onCreateTag() { - String tagName = ''; - showConfirmDialog( - context: context, - title: const Text('新建分组'), - content: TextFormField( - autofocus: true, - initialValue: tagName, - onChanged: (value) => tagName = value, - inputFormatters: [ - LengthLimitingTextInputFormatter(16), + ), + onConfirm: () { + if (tagName.isNotEmpty) { + _followController.onUpdateTag(item, tagName); + } + }, + ); + }, + child: const Text('修改名称', style: TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () { + Get.back(); + showConfirmDialog( + context: context, + title: const Text('删除分组'), + content: const Text('删除后,该分组下的用户依旧保留?'), + onConfirm: () => _followController.onDelTag(index, item.tagid!), + ); + }, + child: const Text('删除分组', style: TextStyle(fontSize: 14)), + ), ], - decoration: const InputDecoration(border: OutlineInputBorder()), ), - onConfirm: () => _followController.onCreateTag(tagName), ); } } diff --git a/lib/pages/follow/widgets/follow_item.dart b/lib/pages/follow/widgets/follow_item.dart index 4aecd35740..469cf98122 100644 --- a/lib/pages/follow/widgets/follow_item.dart +++ b/lib/pages/follow/widgets/follow_item.dart @@ -8,7 +8,7 @@ import 'package:get/get.dart'; class FollowItem extends StatelessWidget { final FollowItemModel item; - final bool? isOwner; + final bool isOwner; final ValueChanged? afterMod; final ValueChanged? onSelect; @@ -16,15 +16,38 @@ class FollowItem extends StatelessWidget { super.key, required this.item, this.afterMod, - this.isOwner, + bool? isOwner, this.onSelect, - }); + }) : isOwner = isOwner ?? false; @override Widget build(BuildContext context) { final colorScheme = ColorScheme.of(context); + Widget? followBtn; + if (isOwner) { + final isFollow = item.attribute != -1; + followBtn = FilledButton.tonal( + onPressed: () => RequestUtils.actionRelationMod( + context: context, + mid: item.mid, + isFollow: isFollow, + afterMod: afterMod, + ), + style: FilledButton.styleFrom( + visualDensity: .compact, + tapTargetSize: .shrinkWrap, + padding: const .symmetric(horizontal: 15), + foregroundColor: isFollow ? colorScheme.outline : null, + backgroundColor: isFollow ? colorScheme.onInverseSurface : null, + ), + child: Text( + '${isFollow ? '已' : ''}关注', + style: const TextStyle(fontSize: 12), + ), + ); + } return Material( - type: MaterialType.transparency, + type: .transparency, child: InkWell( onTap: () { if (onSelect != null) { @@ -42,10 +65,7 @@ class FollowItem extends StatelessWidget { } }, child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 10, - ), + padding: const .symmetric(horizontal: 12, vertical: 10), child: Row( children: [ PendantAvatar( @@ -58,19 +78,19 @@ class FollowItem extends StatelessWidget { Expanded( child: Column( spacing: 3, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: .start, children: [ Text( item.uname!, maxLines: 1, - overflow: TextOverflow.ellipsis, + overflow: .ellipsis, style: const TextStyle(fontSize: 14), ), if (item.sign != null) Text( item.sign!, maxLines: 1, - overflow: TextOverflow.ellipsis, + overflow: .ellipsis, style: TextStyle( fontSize: 13, color: colorScheme.outline, @@ -79,30 +99,7 @@ class FollowItem extends StatelessWidget { ], ), ), - if (isOwner ?? false) - FilledButton.tonal( - onPressed: () => RequestUtils.actionRelationMod( - context: context, - mid: item.mid, - isFollow: item.attribute != -1, - afterMod: afterMod, - ), - style: FilledButton.styleFrom( - visualDensity: .compact, - tapTargetSize: .shrinkWrap, - padding: const .symmetric(horizontal: 15), - foregroundColor: item.attribute == -1 - ? null - : colorScheme.outline, - backgroundColor: item.attribute == -1 - ? null - : colorScheme.onInverseSurface, - ), - child: Text( - '${item.attribute == -1 ? '' : '已'}关注', - style: const TextStyle(fontSize: 12), - ), - ), + ?followBtn, ], ), ), diff --git a/lib/pages/follow_tag_sort/view.dart b/lib/pages/follow_tag_sort/view.dart new file mode 100644 index 0000000000..481e073f51 --- /dev/null +++ b/lib/pages/follow_tag_sort/view.dart @@ -0,0 +1,116 @@ +import 'package:PiliPlus/common/widgets/reorder_mixin.dart'; +import 'package:PiliPlus/http/follow.dart'; +import 'package:PiliPlus/http/loading_state.dart'; +import 'package:PiliPlus/models/member/tags.dart'; +import 'package:PiliPlus/pages/follow/controller.dart'; +import 'package:PiliPlus/utils/bili_utils.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:get/get.dart'; + +class FollowTagSortPage extends StatefulWidget { + const FollowTagSortPage({super.key, required this.controller}); + + final FollowController controller; + + @override + State createState() => _FollowTagSortPageState(); +} + +class _FollowTagSortPageState extends State + with ReorderMixin { + final List _defTags = []; + final List _customTags = []; + + @override + void initState() { + super.initState(); + for (final e in widget.controller.tabs) { + if (BiliUtils.isCustomFollowTag(e.tagid)) { + _customTags.add(e); + } else { + _defTags.add(e); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + title: const Text('关注分组排序'), + actions: _customTags.isNotEmpty + ? [ + TextButton( + onPressed: () async { + final res = await FollowHttp.sortFollowTag( + tagids: _customTags.map((e) => e.tagid).join(','), + ); + if (res.isSuccess) { + SmartDialog.showToast('排序完成'); + final tabs = _defTags + _customTags; + widget.controller + ..tabs.value = tabs + ..onInitTab() + ..followState.value = Success(tabs.hashCode); + if (mounted) { + Get.back(); + } + } else { + res.toast(); + } + }, + child: const Text('完成'), + ), + const SizedBox(width: 16), + ] + : null, + ), + body: _buildBody, + ); + } + + void onReorderItem(int oldIndex, int newIndex) { + _customTags.insert(newIndex, _customTags.removeAt(oldIndex)); + setState(() {}); + } + + Widget get _buildBody { + return ReorderableListView.builder( + onReorderItem: onReorderItem, + proxyDecorator: proxyDecorator, + physics: const AlwaysScrollableScrollPhysics(), + padding: EdgeInsets.only( + bottom: MediaQuery.viewPaddingOf(context).bottom + 100, + ), + header: Column( + children: _defTags.map((e) => _buildItem(e, enabled: false)).toList(), + ), + itemCount: _customTags.length, + itemBuilder: (context, index) { + return _buildItem(_customTags[index]); + }, + ); + } + + Widget _buildItem( + MemberTagItemModel item, { + bool enabled = true, + }) { + return ListTile( + textColor: enabled ? null : scheme.outline, + key: ValueKey(item.tagid), + leading: enabled + ? const Icon(Icons.group_outlined) + : Icon( + size: 23, + Icons.lock_outline, + color: scheme.outline, + ), + minLeadingWidth: 0, + title: Text('${item.name} (${item.count})'), + subtitle: item.tip?.isNotEmpty == true ? Text(item.tip!) : null, + ); + } +} diff --git a/lib/pages/follow_type/follow_same/view.dart b/lib/pages/follow_type/follow_same/view.dart index 0b7e4d283c..98a7910880 100644 --- a/lib/pages/follow_type/follow_same/view.dart +++ b/lib/pages/follow_type/follow_same/view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/pages/follow_type/follow_same/controller.dart'; import 'package:PiliPlus/pages/follow_type/view.dart'; import 'package:PiliPlus/utils/extension/get_ext.dart'; +import 'package:PiliPlus/utils/parse_int.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -16,7 +17,7 @@ class FollowSamePage extends StatefulWidget { Get.toNamed( '/sameFollowing', arguments: { - 'mid': Utils.safeToInt(mid), + 'mid': safeToInt(mid), 'name': name, }, ); diff --git a/lib/pages/follow_type/followed/view.dart b/lib/pages/follow_type/followed/view.dart index 89f5319eec..6b544af3ad 100644 --- a/lib/pages/follow_type/followed/view.dart +++ b/lib/pages/follow_type/followed/view.dart @@ -1,6 +1,7 @@ import 'package:PiliPlus/pages/follow_type/followed/controller.dart'; import 'package:PiliPlus/pages/follow_type/view.dart'; import 'package:PiliPlus/utils/extension/get_ext.dart'; +import 'package:PiliPlus/utils/parse_int.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -16,7 +17,7 @@ class FollowedPage extends StatefulWidget { Get.toNamed( '/followed', arguments: { - 'mid': Utils.safeToInt(mid), + 'mid': safeToInt(mid), 'name': name, }, ); diff --git a/lib/pages/group_panel/view.dart b/lib/pages/group_panel/view.dart index 921e41e25e..8ad1d22582 100644 --- a/lib/pages/group_panel/view.dart +++ b/lib/pages/group_panel/view.dart @@ -5,6 +5,7 @@ import 'package:PiliPlus/models/member/tags.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/feed_back.dart'; +import 'package:PiliPlus/utils/request_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -26,7 +27,7 @@ class GroupPanel extends StatefulWidget { class _GroupPanelState extends State { LoadingState> loadingState = LoadingState.loading(); - RxBool showDefaultBtn = true.obs; + final RxBool showDefaultBtn = true.obs; late final Set tags = widget.tags == null ? {} : Set.from(widget.tags!); @@ -34,10 +35,10 @@ class _GroupPanelState extends State { @override void initState() { super.initState(); - _query(); + _queryFollowUpTags(); } - void _query() { + void _queryFollowUpTags() { MemberHttp.followUpTags().then((res) { if (mounted) { loadingState = res..dataOrNull?.removeFirstWhere((e) => e.tagid == 0); @@ -116,7 +117,7 @@ class _GroupPanelState extends State { Error(:final errMsg) => scrollErrorWidget( controller: widget.scrollController, errMsg: errMsg, - onReload: _query, + onReload: _queryFollowUpTags, ), }; } @@ -125,8 +126,8 @@ class _GroupPanelState extends State { Widget build(BuildContext context) { final theme = Theme.of(context); return Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ + crossAxisAlignment: .end, + children: [ AppBar( backgroundColor: Colors.transparent, leading: IconButton( @@ -135,6 +136,21 @@ class _GroupPanelState extends State { icon: const Icon(Icons.close_outlined), ), title: const Text('设置关注分组'), + actions: [ + TextButton.icon( + onPressed: () => + RequestUtils.createFavTag(context, _onCreateFavTag), + icon: Icon(Icons.add, color: theme.colorScheme.primary), + label: const Text('新建分组'), + style: const ButtonStyle( + visualDensity: .compact, + padding: WidgetStatePropertyAll( + .symmetric(horizontal: 18, vertical: 14), + ), + ), + ), + const SizedBox(width: 16), + ], ), Expanded(child: _buildBody), Divider( @@ -142,20 +158,30 @@ class _GroupPanelState extends State { color: theme.disabledColor.withValues(alpha: 0.08), ), Padding( - padding: EdgeInsets.only( + padding: .only( right: 20, top: 12, bottom: MediaQuery.viewPaddingOf(context).bottom + 12, ), child: FilledButton.tonal( onPressed: onSave, - style: FilledButton.styleFrom( - visualDensity: VisualDensity.compact, - ), + style: const ButtonStyle(visualDensity: .compact), child: Obx(() => Text(showDefaultBtn.value ? '保存至默认分组' : '保存')), ), ), ], ); } + + void _onCreateFavTag(({int tagid, String tagName}) res) { + if (!mounted) return; + if (loadingState case Success(:final response)) { + response.add(MemberTagItemModel.fromCreate(res, count: 1)); + tags.add(res.tagid); + showDefaultBtn.value = false; + setState(() {}); + } else { + _queryFollowUpTags(); + } + } } diff --git a/lib/pages/history/widgets/item.dart b/lib/pages/history/widgets/item.dart index 6b13166024..2c06d7f394 100644 --- a/lib/pages/history/widgets/item.dart +++ b/lib/pages/history/widgets/item.dart @@ -1,6 +1,5 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/progress_bar/video_progress_indicator.dart'; import 'package:PiliPlus/common/widgets/select_mask.dart'; @@ -15,7 +14,7 @@ import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; @@ -177,7 +176,10 @@ class HistoryItem extends StatelessWidget { ), ), Positioned.fill( - child: selectMask(theme, item.checked), + child: selectMask( + theme.colorScheme, + item.checked, + ), ), ], ); diff --git a/lib/pages/history_search/view.dart b/lib/pages/history_search/view.dart index 1a50284e8f..b5a9ba0f03 100644 --- a/lib/pages/history_search/view.dart +++ b/lib/pages/history_search/view.dart @@ -17,19 +17,14 @@ class HistorySearchPage extends StatefulWidget { class _HistorySearchPageState extends - CommonSearchPageState< - HistorySearchPage, - HistoryData, - HistoryItemModel - > { + CommonSearchPageState + with GridMixin { @override final HistorySearchController controller = Get.put( HistorySearchController(), tag: Utils.generateRandomString(8), ); - late final gridDelegate = Grid.videoCardHDelegate(context, minHeight: 110); - @override Widget buildList(List list) { return SliverGrid.builder( diff --git a/lib/pages/later/widgets/video_card_h_later.dart b/lib/pages/later/widgets/video_card_h_later.dart index 3edc472290..3ceb19cc7d 100644 --- a/lib/pages/later/widgets/video_card_h_later.dart +++ b/lib/pages/later/widgets/video_card_h_later.dart @@ -1,7 +1,6 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/progress_bar/video_progress_indicator.dart'; import 'package:PiliPlus/common/widgets/select_mask.dart'; @@ -14,7 +13,7 @@ import 'package:PiliPlus/pages/later/controller.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; // 视频卡片 - 水平布局 @@ -158,7 +157,7 @@ class VideoCardHLater extends StatelessWidget { ), Positioned.fill( child: selectMask( - theme, + theme.colorScheme, videoItem.checked, ), ), diff --git a/lib/pages/later_search/controller.dart b/lib/pages/later_search/controller.dart index 7774e9e7a6..d3420f4046 100644 --- a/lib/pages/later_search/controller.dart +++ b/lib/pages/later_search/controller.dart @@ -13,8 +13,16 @@ class LaterSearchController CommonMultiSelectMixin, DeleteItemMixin, BaseLaterController { - dynamic mid = Get.arguments['mid']; - dynamic count = Get.arguments['count']; + dynamic mid; + dynamic count; + + @override + void onInit() { + final args = Get.arguments; + mid = args['mid']; + count = args['count']; + super.onInit(); + } @override Future> customGetData() => UserHttp.seeYouLater( diff --git a/lib/pages/later_search/view.dart b/lib/pages/later_search/view.dart index 8f2f3737b4..20646a51c8 100644 --- a/lib/pages/later_search/view.dart +++ b/lib/pages/later_search/view.dart @@ -19,7 +19,8 @@ class LaterSearchPage extends StatefulWidget { } class _LaterSearchPageState - extends CommonSearchPageState { + extends CommonSearchPageState + with GridMixin { @override final LaterSearchController controller = Get.put( LaterSearchController(), @@ -58,8 +59,6 @@ class _LaterSearchPageState ]; } - late final gridDelegate = Grid.videoCardHDelegate(context, minHeight: 110); - @override Widget buildList(List list) { return SliverGrid.builder( diff --git a/lib/pages/live/view.dart b/lib/pages/live/view.dart index 3fe46b972e..d72e725cf3 100644 --- a/lib/pages/live/view.dart +++ b/lib/pages/live/view.dart @@ -18,7 +18,7 @@ import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:PiliPlus/utils/utils.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; @@ -154,7 +154,7 @@ class _LivePageState extends State parameters: { 'uaType': 'mob', 'url': - 'https://www.bilibili.com/h5/match/data/home?navhide=1&${Utils.themeUrl(theme.brightness.isDark)}', + 'https://www.bilibili.com/h5/match/data/home?navhide=1&${ThemeUtils.themeUrl(theme.isDark)}', }, ), ), diff --git a/lib/pages/live/widgets/live_item_app.dart b/lib/pages/live/widgets/live_item_app.dart index 794b562aba..9c1225af01 100644 --- a/lib/pages/live/widgets/live_item_app.dart +++ b/lib/pages/live/widgets/live_item_app.dart @@ -1,11 +1,10 @@ import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models_new/live/live_feed_index/card_data_list_item.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; // 视频卡片 - 垂直布局 class LiveCardVApp extends StatelessWidget { diff --git a/lib/pages/live_dm_block/view.dart b/lib/pages/live_dm_block/view.dart index e79b27cff4..d8a4d519fa 100644 --- a/lib/pages/live_dm_block/view.dart +++ b/lib/pages/live_dm_block/view.dart @@ -1,5 +1,4 @@ import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; @@ -10,8 +9,9 @@ import 'package:PiliPlus/pages/live_dm_block/controller.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart'; import 'package:PiliPlus/utils/utils.dart'; +import 'package:collection/collection.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show FilteringTextInputFormatter; import 'package:get/get.dart'; @@ -169,15 +169,14 @@ class _LiveDmBlockPageState extends State { child: Wrap( spacing: 12, runSpacing: 12, - children: list.indexed.map( - (e) { - final item = e.$2; + children: list.mapIndexed( + (i, e) { return SearchText( - text: item is ShieldUserList ? item.uname! : item as String, + text: e is ShieldUserList ? e.uname! : e as String, onTap: (value) => showConfirmDialog( context: context, title: const Text('确定删除该规则?'), - onConfirm: () => _controller.onRemove(e.$1, item), + onConfirm: () => _controller.onRemove(i, e), ), ); }, diff --git a/lib/pages/live_follow/controller.dart b/lib/pages/live_follow/controller.dart index 7fe52762c4..98a663613d 100644 --- a/lib/pages/live_follow/controller.dart +++ b/lib/pages/live_follow/controller.dart @@ -13,7 +13,7 @@ class LiveFollowController queryData(); } - Rx count = Rx(null); + final count = RxnInt(); @override void checkIsEnd(int length) { diff --git a/lib/pages/live_follow/widgets/live_item_follow.dart b/lib/pages/live_follow/widgets/live_item_follow.dart index 6b20ab64f9..90463c8cf4 100644 --- a/lib/pages/live_follow/widgets/live_item_follow.dart +++ b/lib/pages/live_follow/widgets/live_item_follow.dart @@ -1,11 +1,10 @@ import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models_new/live/live_follow/item.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; // 视频卡片 - 垂直布局 class LiveCardVFollow extends StatelessWidget { diff --git a/lib/pages/live_room/contribution_rank/view.dart b/lib/pages/live_room/contribution_rank/view.dart index b29e6d1443..8a64d5c55e 100644 --- a/lib/pages/live_room/contribution_rank/view.dart +++ b/lib/pages/live_room/contribution_rank/view.dart @@ -9,6 +9,7 @@ import 'package:PiliPlus/models/common/live/live_contribution_rank_type.dart'; import 'package:PiliPlus/models_new/live/live_contribution_rank/item.dart'; import 'package:PiliPlus/pages/live_room/contribution_rank/controller.dart'; import 'package:PiliPlus/pages/member/widget/medal_widget.dart'; +import 'package:PiliPlus/utils/color_utils.dart'; import 'package:PiliPlus/utils/extension/scroll_controller_ext.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/foundation.dart' show kDebugMode; @@ -228,7 +229,7 @@ class _Item extends StatelessWidget { textScaler: .noScaling, style: TextStyle( fontWeight: FontWeight.bold, - color: Utils.index2Color(index, colorScheme.outline), + color: ColourUtils.index2Color(index, colorScheme.outline), fontSize: 16, fontStyle: FontStyle.italic, ), diff --git a/lib/pages/live_room/controller.dart b/lib/pages/live_room/controller.dart index 604ec8b8ec..2cd59efe63 100644 --- a/lib/pages/live_room/controller.dart +++ b/lib/pages/live_room/controller.dart @@ -1,5 +1,6 @@ import 'dart:async'; import 'dart:convert'; +import 'dart:math' as math; import 'package:PiliPlus/common/widgets/dialog/report.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart'; @@ -15,6 +16,7 @@ import 'package:PiliPlus/models_new/live/live_dm_info/data.dart'; import 'package:PiliPlus/models_new/live/live_medal_wall/uinfo_medal.dart'; import 'package:PiliPlus/models_new/live/live_room_info_h5/data.dart'; import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart'; +import 'package:PiliPlus/models_new/live/live_room_play_info/stream.dart'; import 'package:PiliPlus/models_new/live/live_superchat/item.dart'; import 'package:PiliPlus/pages/common/publish/publish_route.dart'; import 'package:PiliPlus/pages/danmaku/danmaku_model.dart'; @@ -26,6 +28,7 @@ import 'package:PiliPlus/plugin/pl_player/utils/danmaku_options.dart'; import 'package:PiliPlus/services/service_locator.dart'; import 'package:PiliPlus/tcp/live.dart'; import 'package:PiliPlus/utils/accounts.dart'; +import 'package:PiliPlus/utils/connectivity_utils.dart'; import 'package:PiliPlus/utils/danmaku_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; @@ -33,11 +36,12 @@ import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/video_utils.dart'; import 'package:canvas_danmaku/canvas_danmaku.dart'; import 'package:easy_debounce/easy_throttle.dart'; -import 'package:flutter/foundation.dart' show kDebugMode; +import 'package:flutter/foundation.dart' show kDebugMode, kReleaseMode; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -49,14 +53,14 @@ class LiveRoomController extends GetxController { int roomId = Get.arguments; int? ruid; DanmakuController? danmakuController; - PlPlayerController plPlayerController = PlPlayerController.getInstance( + final plPlayerController = PlPlayerController.getInstance( isLive: true, ); - RxBool isLoaded = false.obs; - Rx roomInfoH5 = Rx(null); + final isLoaded = false.obs; + final roomInfoH5 = Rxn(); - Rx liveTime = Rx(null); + final liveTime = Rxn(); Timer? liveTimeTimer; void startLiveTimer() { @@ -98,10 +102,12 @@ class LiveRoomController extends GetxController { // dm LiveDmInfoData? dmInfo; List? savedDanmaku; - RxList messages = [].obs; - late final Rx fsSC = Rx(null); + int builtLength = 0; + final messages = [].obs; + bool get shouldRefresh => builtLength != messages.length; + late final fsSC = Rxn(); late final RxList superChatMsg = [].obs; - RxBool disableAutoScroll = false.obs; + final disableAutoScroll = false.obs; bool autoScroll = true; LiveMessageStream? _msgStream; late final ScrollController scrollController; @@ -109,7 +115,7 @@ class LiveRoomController extends GetxController { PageController? pageController; int? currentQn = PlatformUtils.isMobile ? null : Pref.liveQuality; - RxString currentQnDesc = ''.obs; + final currentQnDesc = ''.obs; final RxBool isPortrait = false.obs; late List<({int code, String desc})> acceptQnList = []; @@ -177,7 +183,7 @@ class LiveRoomController extends GetxController { } Future queryLiveUrl({bool autoFullScreenFlag = false}) async { - currentQn ??= await Utils.isWiFi + currentQn ??= await ConnectivityUtils.isWiFi ? Pref.liveQuality : Pref.liveQualityCellular; final res = await LiveHttp.liveRoomInfo( @@ -190,38 +196,96 @@ class LiveRoomController extends GetxController { _showDialog('当前直播间未开播'); return; } - if (response.playurlInfo?.playurl == null) { + final playurl = response.playurlInfo?.playurl; + if (playurl == null) { _showDialog('无法获取播放地址'); return; } ruid = response.uid; - if (response.roomId != null) { - roomId = response.roomId!; + if (response.roomId case final roomId?) { + this.roomId = roomId; } liveTime.value = response.liveTime; startLiveTimer(); isPortrait.value = response.isPortrait ?? false; - List codec = - response.playurlInfo!.playurl!.stream!.first.format!.first.codec!; - CodecItem item = codec.first; - // 以服务端返回的码率为准 - currentQn = item.currentQn!; - acceptQnList = item.acceptQn!.map((e) { - return ( - code: e, - desc: LiveQuality.fromCode(e)?.desc ?? e.toString(), - ); - }).toList(); - currentQnDesc.value = - LiveQuality.fromCode(currentQn)?.desc ?? currentQn.toString(); - videoUrl = VideoUtils.getLiveCdnUrl(item); - await playerInit(autoFullScreenFlag: autoFullScreenFlag); + stream = playurl.stream; + _initStreamIndex(); + await initLiveUrl( + streamIndex: streamIndex, + formatIndex: formatIndex, + codecIndex: codecIndex, + liveUrlIndex: liveUrlIndex, + ); isLoaded.value = true; } else { _showDialog(res.toString()); } } + late List stream; + int streamIndex = 0; + int formatIndex = 0; + int codecIndex = 0; + int liveUrlIndex = 0; + + void _initStreamIndex() { + final pref = Pref.liveStream; + if (pref != null) { + try { + final String protocolName = pref[0]; + final String formatName = pref[1]; + final String codecName = pref[2]; + for (var (i, s) in stream.indexed) { + if (s.protocolName == protocolName) { + streamIndex = i; + for (var (j, f) in s.format.indexed) { + if (f.formatName == formatName) { + formatIndex = j; + for (var (k, c) in f.codec.indexed) { + if (c.codecName == codecName) { + codecIndex = k; + return; + } + } + } + } + } + } + } catch (_) {} + } + } + + Future? initLiveUrl({ + int streamIndex = 0, + int formatIndex = 0, + int codecIndex = 0, + int liveUrlIndex = 0, + }) { + this.streamIndex = streamIndex; + this.formatIndex = formatIndex; + this.codecIndex = codecIndex; + this.liveUrlIndex = liveUrlIndex; + + final CodecItem item = stream + .getOrFirst(streamIndex) + .format + .getOrFirst(formatIndex) + .codec + .getOrFirst(codecIndex); + // 以服务端返回的码率为准 + currentQn = item.currentQn; + acceptQnList = item.acceptQn.map((e) { + return ( + code: e, + desc: LiveQuality.fromCode(e)?.desc ?? e.toString(), + ); + }).toList(); + currentQnDesc.value = + LiveQuality.fromCode(currentQn)?.desc ?? currentQn.toString(); + videoUrl = VideoUtils.getLiveCdnUrl(item, index: liveUrlIndex); + return playerInit(); + } + Future queryLiveInfoH5() async { final res = await LiveHttp.liveRoomInfoH5(roomId: roomId); if (res case Success(:final response)) { @@ -244,7 +308,7 @@ class LiveRoomController extends GetxController { onPressed: Get.back, child: Text( '关闭', - style: TextStyle(color: Get.theme.colorScheme.outline), + style: TextStyle(color: ThemeUtils.theme.colorScheme.outline), ), ), TextButton( @@ -283,7 +347,17 @@ class LiveRoomController extends GetxController { } } - void jumpToBottom() { + void handleJumpToBottom() { + disableAutoScroll.value = false; + if (shouldRefresh) { + messages.refresh(); + WidgetsBinding.instance.addPostFrameCallback(_jumpToBottom); + } else { + _jumpToBottom(); + } + } + + void _jumpToBottom([_]) { if (scrollController.hasClients) { scrollController.jumpTo(scrollController.position.maxScrollExtent); } @@ -347,12 +421,21 @@ class LiveRoomController extends GetxController { disableAutoScroll.value = true; } else if (userScrollDirection == .reverse) { final pos = scrollController.position; - if (pos.maxScrollExtent - pos.pixels <= 100) { + if (pos.maxScrollExtent - pos.pixels <= 100 && disableAutoScroll.value) { disableAutoScroll.value = false; + refreshMsgIfNeeded(); } } } + void refreshMsgIfNeeded() { + if (shouldRefresh) { + WidgetsBinding.instance.addPostFrameCallback((_) { + messages.refresh(); + }); + } + } + @override void onClose() { closeLiveMsg(); @@ -390,10 +473,10 @@ class LiveRoomController extends GetxController { } _msgStream = LiveMessageStream( - streamToken: info.token!, + streamToken: info.token, roomId: roomId, uid: Accounts.heartbeat.mid, - servers: info.hostList! + servers: info.hostList .map((host) => 'wss://${host.host}:${host.wssPort}/sub') .toList(), ) @@ -480,9 +563,13 @@ class LiveRoomController extends GetxController { case 'SUPER_CHAT_MESSAGE' when showSuperChat: final item = SuperChatItem.fromJson(obj['data']); superChatMsg.insert(0, item); - if (isFullScreen || plPlayerController.isDesktopPip) { + if (plPlayerController.showDanmaku && + (isFullScreen || plPlayerController.isDesktopPip)) { fsSC.value = item.copyWith( - endTime: DateTime.now().millisecondsSinceEpoch ~/ 1000 + 10, + endTime: math.min( + item.endTime, + DateTime.now().millisecondsSinceEpoch ~/ 1000 + 10, + ), ); } addDm(item); @@ -565,26 +652,30 @@ class LiveRoomController extends GetxController { } void onSendDanmaku([bool fromEmote = false]) { - if (!isLogin) { + if (kReleaseMode && !isLogin) { SmartDialog.showToast('账号未登录'); return; } Get.key.currentState!.push( PublishRoute( + barrierColor: Colors.transparent, pageBuilder: (context, animation, secondaryAnimation) { - return LiveSendDmPanel( - fromEmote: fromEmote, - liveRoomController: this, - items: savedDanmaku, - autofocus: !fromEmote, - onSave: (msg) { - if (msg.isEmpty) { - savedDanmaku?.clear(); - savedDanmaku = null; - } else { - savedDanmaku = msg.toList(); - } - }, + return Theme( + data: ThemeUtils.darkTheme, + child: LiveSendDmPanel( + fromEmote: fromEmote, + liveRoomController: this, + items: savedDanmaku, + autofocus: !fromEmote, + onSave: (msg) { + if (msg.isEmpty) { + savedDanmaku?.clear(); + savedDanmaku = null; + } else { + savedDanmaku = msg.toList(); + } + }, + ), ); }, transitionDuration: fromEmote diff --git a/lib/pages/live_room/send_danmaku/view.dart b/lib/pages/live_room/send_danmaku/view.dart index fa91485628..958c48b998 100644 --- a/lib/pages/live_room/send_danmaku/view.dart +++ b/lib/pages/live_room/send_danmaku/view.dart @@ -135,7 +135,7 @@ class _ReplyPageState extends CommonRichTextPubPageState { emojiBtn, Obx( () => FilledButton.tonal( - onPressed: enablePublish.value ? onPublish : null, + onPressed: enablePublish.value ? onPublishThrottle : null, style: FilledButton.styleFrom( visualDensity: .compact, padding: const .symmetric(horizontal: 20, vertical: 10), diff --git a/lib/pages/live_room/superchat/superchat_card.dart b/lib/pages/live_room/superchat/superchat_card.dart index 5a542d2835..b2e89d9ba0 100644 --- a/lib/pages/live_room/superchat/superchat_card.dart +++ b/lib/pages/live_room/superchat/superchat_card.dart @@ -4,11 +4,12 @@ import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models_new/live/live_superchat/item.dart'; import 'package:PiliPlus/pages/member/widget/medal_widget.dart'; +import 'package:PiliPlus/utils/color_utils.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -19,13 +20,13 @@ class SuperChatCard extends StatefulWidget { required this.item, this.onRemove, this.persistentSC = false, - required this.onReport, + this.onReport, }); final SuperChatItem item; final VoidCallback? onRemove; final bool persistentSC; - final VoidCallback onReport; + final VoidCallback? onReport; @override State createState() => _SuperChatCardState(); @@ -135,7 +136,7 @@ class _SuperChatCardState extends State { @override Widget build(BuildContext context) { final item = widget.item; - final bottomColor = Utils.parseColor(item.backgroundBottomColor); + final bottomColor = ColourUtils.parseColor(item.backgroundBottomColor); final border = BorderSide(color: bottomColor); void showMenu(TapUpDetails e) => _showMenu(e.globalPosition, item); @@ -144,7 +145,7 @@ class _SuperChatCardState extends State { maxLines: 1, overflow: .ellipsis, style: TextStyle( - color: Utils.parseColor(item.userInfo.nameColor), + color: ColourUtils.parseColor(item.userInfo.nameColor), ), ); if (item.medalInfo case final medal?) { @@ -167,6 +168,7 @@ class _SuperChatCardState extends State { } return Column( + mainAxisSize: .min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ GestureDetector( @@ -175,7 +177,7 @@ class _SuperChatCardState extends State { child: Container( decoration: BoxDecoration( borderRadius: const .vertical(top: .circular(8)), - color: Utils.parseColor(item.backgroundColor), + color: ColourUtils.parseColor(item.backgroundColor), border: Border(top: border, left: border, right: border), image: item.backgroundImage == null ? null @@ -205,7 +207,9 @@ class _SuperChatCardState extends State { Text( "¥${item.price}", style: TextStyle( - color: Utils.parseColor(item.backgroundPriceColor), + color: ColourUtils.parseColor( + item.backgroundPriceColor, + ), ), ), ], @@ -235,7 +239,7 @@ class _SuperChatCardState extends State { child: Text( item.message, style: TextStyle( - color: Utils.parseColor(item.messageFontColor), + color: ColourUtils.parseColor(item.messageFontColor), decoration: widget.persistentSC && item.deleted ? .lineThrough : null, diff --git a/lib/pages/live_room/view.dart b/lib/pages/live_room/view.dart index 5fafa36467..d931cbf713 100644 --- a/lib/pages/live_room/view.dart +++ b/lib/pages/live_room/view.dart @@ -35,25 +35,31 @@ import 'package:PiliPlus/plugin/pl_player/utils/danmaku_options.dart'; import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart'; import 'package:PiliPlus/plugin/pl_player/view/view.dart'; import 'package:PiliPlus/services/service_locator.dart'; +import 'package:PiliPlus/utils/android/bindings.g.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; +import 'package:PiliPlus/utils/max_screen_size.dart'; import 'package:PiliPlus/utils/mobile_observer.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:PiliPlus/utils/share_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; +import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:canvas_danmaku/canvas_danmaku.dart'; -import 'package:floating/floating.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; +import 'package:canvas_danmaku/danmaku_screen.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart' hide PageView; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:screen_brightness_platform_interface/screen_brightness_platform_interface.dart'; +const baseWhite = Color(0xFFEEEEEE); + class LiveRoomPage extends StatefulWidget { const LiveRoomPage({super.key}); @@ -63,6 +69,7 @@ class LiveRoomPage extends StatefulWidget { class _LiveRoomPageState extends State with WidgetsBindingObserver, RouteAware, RouteAwareMixin { + late final fullScreenSCWidth = Pref.fullScreenSCWidth; final String heroTag = Utils.generateRandomString(6); late final LiveRoomController _liveRoomController; late final PlPlayerController plPlayerController; @@ -85,7 +92,7 @@ class _LiveRoomPageState extends State ..addStatusLister(playerListener); PlPlayerController.setPlayCallBack(plPlayerController.play); if (plPlayerController.removeSafeArea) { - hideStatusBar(); + hideSystemBar(); } } @@ -100,6 +107,10 @@ class _LiveRoomPageState extends State final size = MediaQuery.sizeOf(context); maxWidth = size.width; maxHeight = size.height; + isWindowMode = MaxScreenSize.isWindowMode( + width: maxWidth * plPlayerController.uiScale, + height: maxHeight * plPlayerController.uiScale, + ); isPortrait = size.isPortrait; plPlayerController.screenRatio = maxHeight / maxWidth; } @@ -107,9 +118,12 @@ class _LiveRoomPageState extends State @override Future didPopNext() async { addObserverMobile(this); - plPlayerController - ..isLive = true - ..danmakuController = _liveRoomController.danmakuController; + if (!plPlayerController.isLive) { + plPlayerController.isLive = true; + _liveRoomController.isLoaded.refresh(); + } + plPlayerController.danmakuController = + _liveRoomController.danmakuController; PlPlayerController.setPlayCallBack(plPlayerController.play); _liveRoomController.startLiveTimer(); if (plPlayerController.playerStatus.isPlaying && @@ -181,7 +195,9 @@ class _LiveRoomPageState extends State void didChangeAppLifecycleState(AppLifecycleState state) { if (plPlayerController.visible = state == .resumed) { if (!plPlayerController.showDanmaku) { - _liveRoomController.startLiveTimer(); + _liveRoomController + ..refreshMsgIfNeeded() + ..startLiveTimer(); plPlayerController.showDanmaku = true; } } else if (state == .paused) { @@ -194,13 +210,14 @@ class _LiveRoomPageState extends State late double maxWidth; late double maxHeight; + bool isWindowMode = false; late EdgeInsets padding; late bool isPortrait; @override Widget build(BuildContext context) { Widget child; - if (Platform.isAndroid && Floating().isPipMode) { + if (Platform.isAndroid && AndroidHelper.isPipMode) { child = videoPlayerPanel( isFullScreen, width: maxWidth, @@ -219,7 +236,10 @@ class _LiveRoomPageState extends State child: child, ); } - return child; + return Theme( + data: ThemeUtils.darkTheme, + child: child, + ); } Widget videoPlayerPanel( @@ -231,9 +251,6 @@ class _LiveRoomPageState extends State Alignment alignment = Alignment.center, bool needDm = true, }) { - if (!plPlayerController.isLive) { - return const SizedBox.shrink(); - } if (!isFullScreen && !plPlayerController.isDesktopPip) { _liveRoomController.fsSC.value = null; } @@ -241,7 +258,7 @@ class _LiveRoomPageState extends State Widget player = Obx( key: playerKey, () { - if (_liveRoomController.isLoaded.value) { + if (_liveRoomController.isLoaded.value && plPlayerController.isLive) { final roomInfoH5 = _liveRoomController.roomInfoH5.value; return PLVideoPlayer( maxWidth: width, @@ -313,7 +330,7 @@ class _LiveRoomPageState extends State Positioned( left: padding.left + 25, bottom: 25, - width: 255, + width: fullScreenSCWidth, child: Obx(() { final item = _liveRoomController.fsSC.value; if (item == null) { @@ -402,7 +419,7 @@ class _LiveRoomPageState extends State primary: !plPlayerController.removeSafeArea, resizeToAvoidBottomInset: false, backgroundColor: Colors.transparent, - appBar: isFullScreen && !isPortrait + appBar: isWindowMode && isFullScreen && !isPortrait ? null : _buildAppBar(isFullScreen), body: isPortrait @@ -424,7 +441,7 @@ class _LiveRoomPageState extends State Widget _buildPH(bool isFullScreen) { final height = maxWidth / Style.aspectRatio16x9; final videoHeight = isFullScreen - ? maxHeight - (isPortrait ? padding.top : 0) + ? maxHeight - (isWindowMode && !isPortrait ? 0 : padding.top) : height; final bottomHeight = maxHeight - padding.top - height - kToolbarHeight; return Column( @@ -453,7 +470,7 @@ class _LiveRoomPageState extends State Widget _buildPP(bool isFullScreen) { final bottomHeight = 70 + padding.bottom; final videoHeight = isFullScreen - ? maxHeight - (isPortrait ? padding.top : 0) + ? maxHeight - (isWindowMode && !isPortrait ? 0 : padding.top) : maxHeight - bottomHeight; return Stack( clipBehavior: Clip.none, @@ -530,7 +547,6 @@ class _LiveRoomPageState extends State } PreferredSizeWidget _buildAppBar(bool isFullScreen) { - final color = Theme.of(context).colorScheme.onSurfaceVariant; return AppBar( primary: !plPlayerController.removeSafeArea, toolbarHeight: isFullScreen ? 0 : null, @@ -552,7 +568,7 @@ class _LiveRoomPageState extends State Get.toNamed('/member?mid=${roomInfoH5.roomInfo?.uid}'), child: Row( spacing: 10, - mainAxisSize: MainAxisSize.min, + mainAxisSize: .min, children: [ NetworkImgLayer( width: 34, @@ -612,47 +628,35 @@ class _LiveRoomPageState extends State return [ PopupMenuItem( onTap: () => Utils.copyText(liveUrl), - child: Row( + child: const Row( spacing: 10, - mainAxisSize: MainAxisSize.min, + mainAxisSize: .min, children: [ - Icon( - Icons.copy, - size: 19, - color: color, - ), - const Text('复制链接'), + Icon(Icons.copy, size: 19), + Text('复制链接'), ], ), ), if (PlatformUtils.isMobile) PopupMenuItem( - onTap: () => Utils.shareText(liveUrl), - child: Row( + onTap: () => ShareUtils.shareText(liveUrl), + child: const Row( spacing: 10, - mainAxisSize: MainAxisSize.min, + mainAxisSize: .min, children: [ - Icon( - Icons.share, - size: 19, - color: color, - ), - const Text('分享直播间'), + Icon(Icons.share, size: 19), + Text('分享直播间'), ], ), ), PopupMenuItem( onTap: () => PageUtils.inAppWebview(liveUrl, off: true), - child: Row( + child: const Row( spacing: 10, - mainAxisSize: MainAxisSize.min, + mainAxisSize: .min, children: [ - Icon( - Icons.open_in_browser, - size: 19, - color: color, - ), - const Text('浏览器打开'), + Icon(Icons.open_in_browser, size: 19), + Text('浏览器打开'), ], ), ), @@ -679,16 +683,12 @@ class _LiveRoomPageState extends State SmartDialog.showToast(e.toString()); } }, - child: Row( + child: const Row( spacing: 10, - mainAxisSize: MainAxisSize.min, + mainAxisSize: .min, children: [ - Icon( - Icons.forward_to_inbox, - size: 19, - color: color, - ), - const Text('分享至消息'), + Icon(Icons.forward_to_inbox, size: 19), + Text('分享至消息'), ], ), ), @@ -707,7 +707,7 @@ class _LiveRoomPageState extends State final videoHeight = maxHeight - padding.top - kToolbarHeight; final width = isFullScreen ? maxWidth : videoWidth; final height = isFullScreen - ? maxHeight - (isPortrait ? padding.top : 0) + ? maxHeight - (isWindowMode && !isPortrait ? 0 : padding.top) : videoHeight; return Padding( padding: isFullScreen @@ -789,25 +789,20 @@ class _LiveRoomPageState extends State Widget get _buildInputWidget { final child = Container( - padding: EdgeInsets.only( - top: 5, - left: 10, - right: 10, - bottom: padding.bottom, - ), + padding: .only(top: 5, left: 10, right: 10, bottom: padding.bottom), height: 70 + padding.bottom, decoration: const BoxDecoration( - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + borderRadius: .vertical(top: .circular(20)), border: Border(top: BorderSide(color: Color(0x1AFFFFFF))), color: Color(0x1AFFFFFF), ), child: GestureDetector( onTap: _liveRoomController.onSendDanmaku, - behavior: HitTestBehavior.opaque, + behavior: .opaque, child: Padding( - padding: const EdgeInsets.only(top: 5, bottom: 10), + padding: const .only(top: 5, bottom: 10), child: Align( - alignment: Alignment.topCenter, + alignment: .topCenter, child: Row( spacing: 6, children: [ @@ -819,9 +814,7 @@ class _LiveRoomPageState extends State width: 34, height: 34, child: IconButton( - style: IconButton.styleFrom( - padding: EdgeInsets.zero, - ), + style: IconButton.styleFrom(padding: .zero), onPressed: () { final newVal = !enableShowLiveDanmaku; plPlayerController.enableShowDanmaku.value = newVal; @@ -836,12 +829,12 @@ class _LiveRoomPageState extends State ? const Icon( size: 22, CustomIcons.dm_on, - color: Color(0xFFEEEEEE), + color: baseWhite, ) : const Icon( size: 22, CustomIcons.dm_off, - color: Color(0xFFEEEEEE), + color: baseWhite, ), ), ); @@ -850,7 +843,7 @@ class _LiveRoomPageState extends State const Expanded( child: Text( '发送弹幕', - style: TextStyle(color: Color(0xFFEEEEEE)), + style: TextStyle(color: baseWhite), ), ), Builder( @@ -871,7 +864,7 @@ class _LiveRoomPageState extends State dimension: 34, child: Icon( size: 22, - color: Color(0xFFEEEEEE), + color: baseWhite, Icons.thumb_up_off_alt, ), ), @@ -919,7 +912,7 @@ class _LiveRoomPageState extends State onPressed: () => _liveRoomController.onSendDanmaku(true), icon: const Icon( size: 22, - color: Color(0xFFEEEEEE), + color: baseWhite, Icons.emoji_emotions_outlined, ), ), @@ -1007,10 +1000,9 @@ class _BorderIndicator extends LeafRenderObjectWidget { class _RenderBorderIndicator extends RenderBox { _RenderBorderIndicator({ - required Radius radius, - required bool isLeft, - }) : _radius = radius, - _isLeft = isLeft; + required this._radius, + required this._isLeft, + }); Radius _radius; Radius get radius => _radius; diff --git a/lib/pages/live_room/widgets/bottom_control.dart b/lib/pages/live_room/widgets/bottom_control.dart index 345792da74..2e90bea823 100644 --- a/lib/pages/live_room/widgets/bottom_control.dart +++ b/lib/pages/live_room/widgets/bottom_control.dart @@ -7,6 +7,7 @@ import 'package:PiliPlus/plugin/pl_player/widgets/common_btn.dart'; import 'package:PiliPlus/plugin/pl_player/widgets/play_pause_btn.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -36,6 +37,8 @@ class _BottomControlState extends State with HeaderMixin { late final LiveRoomController liveRoomCtr = widget.liveRoomCtr; @override late final PlPlayerController plPlayerController = widget.plPlayerController; + @override + ThemeData get theme => ThemeUtils.darkTheme; @override Widget build(BuildContext context) { diff --git a/lib/pages/live_room/widgets/chat_panel.dart b/lib/pages/live_room/widgets/chat_panel.dart index cd37b90436..6fcbd75316 100644 --- a/lib/pages/live_room/widgets/chat_panel.dart +++ b/lib/pages/live_room/widgets/chat_panel.dart @@ -52,7 +52,8 @@ class LiveRoomChatPanel extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: 12), controller: liveRoomController.scrollController, separatorBuilder: (_, _) => const SizedBox(height: 8), - itemCount: liveRoomController.messages.length, + itemCount: liveRoomController.builtLength = + liveRoomController.messages.length, physics: const ClampingScrollPhysics(), itemBuilder: (_, index) { final item = liveRoomController.messages[index]; @@ -218,17 +219,10 @@ class LiveRoomChatPanel extends StatelessWidget { right: 12, bottom: 0, child: ElevatedButton.icon( - style: ElevatedButton.styleFrom( - visualDensity: VisualDensity.comfortable, - ), - icon: const Icon( - Icons.arrow_downward_rounded, - size: 20, - ), + style: const ButtonStyle(visualDensity: .comfortable), + icon: const Icon(Icons.arrow_downward_rounded, size: 20), label: const Text('回到底部'), - onPressed: () => liveRoomController - ..disableAutoScroll.value = false - ..jumpToBottom(), + onPressed: liveRoomController.handleJumpToBottom, ), ) : const SizedBox.shrink(), diff --git a/lib/pages/live_room/widgets/header_control.dart b/lib/pages/live_room/widgets/header_control.dart index 16b8d27fca..911758fc14 100644 --- a/lib/pages/live_room/widgets/header_control.dart +++ b/lib/pages/live_room/widgets/header_control.dart @@ -1,14 +1,26 @@ -import 'dart:io'; +import 'dart:io' show Platform; +import 'dart:math' as math; +import 'package:PiliPlus/common/style.dart'; +import 'package:PiliPlus/common/widgets/flutter/draggable_scrollable_sheet.dart'; import 'package:PiliPlus/common/widgets/marquee.dart'; +import 'package:PiliPlus/models/common/video/live_quality.dart'; import 'package:PiliPlus/pages/live_room/controller.dart'; +import 'package:PiliPlus/pages/setting/models/play_settings.dart' + show showPlayerVolumeDialog; import 'package:PiliPlus/pages/video/widgets/header_control.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/widgets/common_btn.dart'; import 'package:PiliPlus/services/shutdown_timer_service.dart' show shutdownTimerService; +import 'package:PiliPlus/utils/android/bindings.g.dart'; +import 'package:PiliPlus/utils/extension/context_ext.dart'; +import 'package:PiliPlus/utils/extension/size_ext.dart'; +import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:floating/floating.dart'; +import 'package:PiliPlus/utils/storage.dart'; +import 'package:PiliPlus/utils/storage_key.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -159,12 +171,12 @@ class _LiveHeaderControlState extends State ComBtn( height: 30, tooltip: '画中画', - onTap: () async { + onTap: () { if (PlatformUtils.isDesktop) { plPlayerController.toggleDesktopPip(); return; } - if (await Floating().isPipAvailable) { + if (AndroidHelper.isPipAvailable) { plPlayerController.enterPip(); } }, @@ -233,21 +245,233 @@ class _LiveHeaderControlState extends State color: Colors.white, ), ), - ComBtn( - height: 30, - tooltip: '播放信息', - onTap: () => HeaderControlState.showPlayerInfo( - context, - plPlayerController: plPlayerController, - ), - icon: const Icon( - size: 18, - Icons.info_outline, - color: Colors.white, + if (plPlayerController.videoPlayerController case final player?) + SizedBox.square( + dimension: 30, + child: PopupMenuButton( + iconSize: 18, + padding: .zero, + iconColor: Colors.white, + itemBuilder: (context) => [ + PopupMenuItem( + height: 35, + onTap: _showLiveStreamDialog, + child: const Row( + spacing: 8, + children: [ + Icon(Icons.alt_route, size: 17), + Text('切换路线', style: TextStyle(fontSize: 14)), + ], + ), + ), + PopupMenuItem( + height: 35, + child: const Row( + spacing: 8, + children: [ + Icon(Icons.info_outline, size: 17), + Text('播放信息', style: TextStyle(fontSize: 14)), + ], + ), + onTap: () => HeaderControlState.showPlayerInfo( + context, + player: player, + ), + ), + if (PlatformUtils.isMobile) + PopupMenuItem( + height: 35, + child: Row( + spacing: 8, + children: [ + const Icon(Icons.volume_up, size: 17), + Text( + '播放器音量: ${player.getProperty('volume').subLength(3)}%', + style: const TextStyle(fontSize: 14), + ), + ], + ), + onTap: () => showPlayerVolumeDialog( + context, + () {}, + onChanged: player.setVolume, + ), + ), + ], + ), ), - ), ], ), ); } + + void _showLiveStreamDialog() { + final controller = widget.liveController; + showModalBottomSheet( + context: context, + useSafeArea: true, + isScrollControlled: true, + constraints: BoxConstraints( + maxWidth: math.min(640, context.mediaQueryShortestSide), + ), + builder: (context) { + final maxChildSize = + PlatformUtils.isMobile && !context.mediaQuerySize.isPortrait + ? 1.0 + : 0.7; + return DynDraggableScrollableSheet( + minChildSize: 0, + maxChildSize: maxChildSize, + snap: true, + expand: false, + snapSizes: [maxChildSize], + initialChildSize: maxChildSize, + builder: (context, scrollController) { + final theme = Theme.of(context); + final secondary = theme.colorScheme.secondary; + final onSurfaceVariant = theme.colorScheme.onSurfaceVariant; + final currStyle = TextStyle(fontSize: 14, color: secondary); + return Theme( + data: theme.copyWith(dividerColor: Colors.transparent), + child: Column( + children: [ + InkWell( + onTap: Get.back, + borderRadius: Style.bottomSheetRadius, + child: SizedBox( + height: 35, + child: Center( + child: Container( + width: 32, + height: 3, + decoration: BoxDecoration( + color: theme.colorScheme.outline, + borderRadius: const .all(.circular(1.5)), + ), + ), + ), + ), + ), + Expanded( + child: ListView( + controller: scrollController, + padding: .only( + bottom: MediaQuery.viewPaddingOf(context).bottom + 100, + ), + children: controller.stream.mapIndexed((si, stream) { + final isCurrStream = si == controller.streamIndex; + final streamColor = isCurrStream + ? secondary + : onSurfaceVariant; + return _ExpansionTile( + initiallyExpanded: isCurrStream, + iconColor: streamColor, + collapsedIconColor: streamColor, + title: Text( + stream.protocolName ?? si.toString(), + style: isCurrStream + ? currStyle + : const TextStyle(fontSize: 14), + ), + children: stream.format.mapIndexed((fi, format) { + final isCurrFormat = + isCurrStream && fi == controller.formatIndex; + final formatColor = isCurrFormat + ? secondary + : onSurfaceVariant; + return _ExpansionTile( + initiallyExpanded: isCurrFormat, + iconColor: formatColor, + collapsedIconColor: formatColor, + title: Text( + format.formatName ?? fi.toString(), + style: isCurrFormat + ? currStyle + : const TextStyle(fontSize: 14), + ), + children: format.codec.mapIndexed((ci, codec) { + final isCurrCodec = + isCurrFormat && ci == controller.codecIndex; + final codecColor = isCurrCodec + ? secondary + : onSurfaceVariant; + return _ExpansionTile( + initiallyExpanded: isCurrCodec, + iconColor: codecColor, + collapsedIconColor: codecColor, + title: Text( + '${codec.codecName ?? ci.toString()} (${LiveQuality.fromCode(codec.currentQn)?.desc ?? codec.currentQn})', + style: isCurrCodec + ? currStyle + : const TextStyle(fontSize: 14), + ), + children: codec.urlInfo.mapIndexed((ui, url) { + final isCurrUrl = + isCurrCodec && + ui == controller.liveUrlIndex; + return ListTile( + dense: true, + title: Text( + '${url.host}...', + style: isCurrUrl + ? const TextStyle(fontSize: 14) + : TextStyle( + fontSize: 14, + color: onSurfaceVariant, + ), + ), + selected: isCurrUrl, + onTap: isCurrUrl + ? null + : () { + Get.back(); + controller.initLiveUrl( + streamIndex: si, + formatIndex: fi, + codecIndex: ci, + liveUrlIndex: ui, + ); + GStorage.setting.put( + SettingBoxKey.liveStream, + [ + stream.protocolName!, + format.formatName!, + codec.codecName!, + ], + ); + }, + ); + }).toList(), + ); + }).toList(), + ); + }).toList(), + ); + }).toList(), + ), + ), + ], + ), + ); + }, + ); + }, + ); + } +} + +class _ExpansionTile extends ExpansionTile { + const _ExpansionTile({ + required super.title, + // ignore: unused_element_parameter + super.dense = true, + // ignore: unused_element_parameter + super.controlAffinity = .leading, + // ignore: unused_element_parameter + super.childrenPadding = const .only(left: 20), + super.initiallyExpanded, + super.iconColor, + super.collapsedIconColor, + super.children, + }); } diff --git a/lib/pages/live_search/widgets/live_search_room.dart b/lib/pages/live_search/widgets/live_search_room.dart index 49993d439d..314bb2953c 100644 --- a/lib/pages/live_search/widgets/live_search_room.dart +++ b/lib/pages/live_search/widgets/live_search_room.dart @@ -1,11 +1,10 @@ import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models_new/live/live_search/room_item.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; // 视频卡片 - 垂直布局 class LiveCardVSearch extends StatelessWidget { diff --git a/lib/pages/login/controller.dart b/lib/pages/login/controller.dart index d9bf12348b..7c2db501f2 100644 --- a/lib/pages/login/controller.dart +++ b/lib/pages/login/controller.dart @@ -13,12 +13,12 @@ import 'package:PiliPlus/pages/login/geetest/geetest_webview_dialog.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts/account.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart'; class LoginPageController extends GetxController with GetSingleTickerProviderStateMixin { @@ -33,8 +33,6 @@ class LoginPageController extends GetxController late final TabController tabController; - late final Gt3FlutterPlugin captcha = Gt3FlutterPlugin(); - late final CaptchaDataModel captchaData = CaptchaDataModel(); late final RxInt qrCodeLeftTime = 180.obs; late final RxString statusQRCode = ''.obs; @@ -124,115 +122,33 @@ class LoginPageController extends GetxController String geeChallenge, VoidCallback onSuccess, ) { - void updateCaptchaData(Map json) { - captchaData - ..validate = json['geetest_validate'] - ..seccode = json['geetest_seccode'] - ..geetest = GeetestData( - challenge: json['geetest_challenge'], - gt: geeGt, - ); - SmartDialog.showToast('验证成功'); - onSuccess(); - } - - if (PlatformUtils.isDesktop) { - showDialog>( - context: Get.context!, - builder: (context) => GeetestWebviewDialog(geeGt, geeChallenge), - ).then((res) { - if (res != null) { - updateCaptchaData(res); - } - }); - } else { - final registerData = Gt3RegisterData( - challenge: geeChallenge, - gt: geeGt, - success: true, - ); - - captcha - ..addEventHandler( - onShow: (Map message) {}, - onClose: (Map message) { - SmartDialog.showToast('关闭验证'); - }, - onResult: (Map message) { - if (kDebugMode) debugPrint("Captcha result: $message"); - final String code = message["code"]; - if (code == "1") { - // 发送 message["result"] 中的数据向 B 端的业务服务接口进行查询 - updateCaptchaData(message['result']); - } else { - // 终端用户完成验证失败,自动重试 If the verification fails, it will be automatically retried. - if (kDebugMode) debugPrint("Captcha result code : $code"); - } - }, - onError: (Map message) { - SmartDialog.showToast("Captcha onError: $message"); - String code = message["code"]; - // 处理验证中返回的错误 Handling errors returned in verification - if (Platform.isAndroid) { - // Android 平台 - if (code == "-2") { - // Dart 调用异常 Call exception - } else if (code == "-1") { - // Gt3RegisterData 参数不合法 Parameter is invalid - } else if (code == "201") { - // 网络无法访问 Network inaccessible - } else if (code == "202") { - // Json 解析错误 Analysis error - } else if (code == "204") { - // WebView 加载超时,请检查是否混淆极验 SDK Load timed out - } else if (code == "204_1") { - // WebView 加载前端页面错误,请查看日志 Error loading front-end page, please check the log - } else if (code == "204_2") { - // WebView 加载 SSLError - } else if (code == "206") { - // gettype 接口错误或返回为 null API error or return null - } else if (code == "207") { - // getphp 接口错误或返回为 null API error or return null - } else if (code == "208") { - // ajax 接口错误或返回为 null API error or return null - } else { - // 更多错误码参考开发文档 More error codes refer to the development document - // https://docs.geetest.com/sensebot/apirefer/errorcode/android - } - } + GeetestWebviewDialog.geetest(geeGt, geeChallenge).then((res) { + if (res is Map) { + captchaData + ..validate = res['geetest_validate'] + ..seccode = res['geetest_seccode'] + ..geetest = GeetestData( + challenge: res['geetest_challenge'], + gt: geeGt, + ); + SmartDialog.showToast('验证成功'); + onSuccess(); + } + }); + } - if (Platform.isIOS) { - // iOS 平台 - if (code == "-1009") { - // 网络无法访问 Network inaccessible - } else if (code == "-1004") { - // 无法查找到 HOST Unable to find HOST - } else if (code == "-1002") { - // 非法的 URL Illegal URL - } else if (code == "-1001") { - // 网络超时 Network timeout - } else if (code == "-999") { - // 请求被意外中断, 一般由用户进行取消操作导致 The interrupted request was usually caused by the user cancelling the operation - } else if (code == "-21") { - // 使用了重复的 challenge Duplicate challenges are used - // 检查获取 challenge 是否进行了缓存 Check if the fetch challenge is cached - } else if (code == "-20") { - // 尝试过多, 重新引导用户触发验证即可 Try too many times, lead the user to request verification again - } else if (code == "-10") { - // 预判断时被封禁, 不会再进行图形验证 Banned during pre-judgment, and no more image captcha verification - } else if (code == "-2") { - // Dart 调用异常 Call exception - } else if (code == "-1") { - // Gt3RegisterData 参数不合法 Parameter is invalid - } else { - // 更多错误码参考开发文档 More error codes refer to the development document - // https://docs.geetest.com/sensebot/apirefer/errorcode/ios - } - } - }, - ) - ..startCaptcha(registerData); - } + static String validateCookie(String cookie) { + return cookie + .split(';') + .where((e) { + try { + Cookie.fromSetCookieValue(e.trim()); + } catch (_) { + return false; + } + return true; + }) + .join(';'); } // cookie登录 @@ -246,7 +162,7 @@ class LoginPageController extends GetxController "/x/member/web/account", options: Options( headers: { - "cookie": cookieTextController.text, + "cookie": validateCookie(cookieTextController.text), }, extra: {'account': AnonymousAccount()}, ), @@ -439,7 +355,7 @@ class LoginPageController extends GetxController onPressed: Get.back, child: Text( "取消", - style: TextStyle(color: Get.theme.colorScheme.outline), + style: TextStyle(color: ThemeUtils.theme.colorScheme.outline), ), ), TextButton( @@ -727,6 +643,7 @@ class LoginPageController extends GetxController SmartDialog.showToast('请先登录'); return Get.toNamed('/loginPage'); } + final colorScheme = ColorScheme.of(context); final selectAccount = List.of(Accounts.accountMode); final options = { AnonymousAccount(): '0', @@ -748,11 +665,8 @@ class LoginPageController extends GetxController children: [ const TextSpan(text: '账号切换'), TextSpan( - text: '\nmid 为0时使用匿名', - style: TextStyle( - fontSize: 14, - color: ColorScheme.of(context).outline, - ), + text: '\nmid为0时使用匿名', + style: TextStyle(fontSize: 14, color: colorScheme.outline), ), ], ), @@ -766,7 +680,7 @@ class LoginPageController extends GetxController quickSelect = !quickSelect; (context as Element).markNeedsBuild(); }, - child: const Text('切换'), + child: Text(quickSelect ? '详细' : '快速'), ), ], ), @@ -828,10 +742,7 @@ class LoginPageController extends GetxController actions: [ TextButton( onPressed: Get.back, - child: Text( - '取消', - style: TextStyle(color: ColorScheme.of(context).outline), - ), + child: Text('取消', style: TextStyle(color: colorScheme.outline)), ), TextButton( onPressed: () { diff --git a/lib/pages/login/geetest/geetest_webview_dialog.dart b/lib/pages/login/geetest/geetest_webview_dialog.dart index b78ad963e1..dad4afa075 100644 --- a/lib/pages/login/geetest/geetest_webview_dialog.dart +++ b/lib/pages/login/geetest/geetest_webview_dialog.dart @@ -1,5 +1,5 @@ -import 'dart:convert' show jsonDecode, jsonEncode; -import 'dart:io' show Platform, Directory, File; +import 'dart:convert' show base64, jsonDecode, jsonEncode, utf8; +import 'dart:io' show Platform; import 'package:PiliPlus/http/browser_ua.dart'; import 'package:PiliPlus/http/init.dart'; @@ -20,6 +20,13 @@ class GeetestWebviewDialog extends StatefulWidget { @override State createState() => _GeetestWebviewDialogState(); + + static Future geetest(String gt, String challenge) { + return showDialog( + context: Get.context!, + builder: (context) => GeetestWebviewDialog(gt, challenge), + ); + } } class _GeetestWebviewDialogState extends State { @@ -30,6 +37,9 @@ class _GeetestWebviewDialogState extends State { Webview? _linuxWebview; late bool _linuxWebviewLoading = true; + static String _showJs(String response) => + 't=Geetest($response).onSuccess(()=>R("success",t.getValidate())).onError(o=>R("error",o)).onClose(o=>R("close",o));t.onReady(()=>t.verify())'; + @override void initState() { super.initState(); @@ -122,6 +132,8 @@ class _GeetestWebviewDialogState extends State { } } else if (msgStr.startsWith("error:")) { debugPrint('geetest error: $msgStr'); + } else if (msgStr.startsWith('close:')) { + Get.back(); } }); @@ -136,27 +148,15 @@ class _GeetestWebviewDialogState extends State { '''; - final tempDir = Directory.systemTemp; - final file = File( - '${tempDir.path}/geetest_${DateTime.now().millisecondsSinceEpoch}.html', + _linuxWebview!.launch( + 'data:text/html;base64,${base64.encode(utf8.encode(html))}', ); - await file.writeAsString(html); - - if (!mounted) { - _closeLinuxWebview(); - return; - } - - _linuxWebview!.launch('file://${file.path}'); if (mounted) { setState(() { @@ -202,12 +202,9 @@ class _GeetestWebviewDialogState extends State { ); } - return AlertDialog( - title: const Text('验证码'), - content: SizedBox( - width: 300, - height: 400, - child: InAppWebView( + return Stack( + children: [ + InAppWebView( webViewEnvironment: webViewEnvironment, initialSettings: InAppWebViewSettings( clearCache: true, @@ -217,11 +214,33 @@ class _GeetestWebviewDialogState extends State { algorithmicDarkeningAllowed: true, useShouldOverrideUrlLoading: true, userAgent: BrowserUa.mob, - mixedContentMode: MixedContentMode.MIXED_CONTENT_ALWAYS_ALLOW, + mixedContentMode: .MIXED_CONTENT_ALWAYS_ALLOW, + + incognito: true, + allowFileAccess: false, + allowsLinkPreview: false, + allowContentAccess: false, + useOnDownloadStart: false, + geolocationEnabled: false, + thirdPartyCookiesEnabled: false, + enterpriseAuthenticationAppLinkPolicyEnabled: false, + saveFormData: false, + safeBrowsingEnabled: false, + isFraudulentWebsiteWarningEnabled: false, + domStorageEnabled: false, + databaseEnabled: false, + cacheEnabled: false, + cacheMode: .LOAD_NO_CACHE, + + horizontalScrollBarEnabled: false, + verticalScrollBarEnabled: false, + overScrollMode: .NEVER, + + pageZoom: Platform.isIOS ? 3 : 1, ), initialData: InAppWebViewInitialData( data: - '', + '', ), onWebViewCreated: (ctr) { ctr @@ -242,29 +261,30 @@ class _GeetestWebviewDialogState extends State { callback: (args) { debugPrint('geetest error: $args'); }, + ) + ..addJavaScriptHandler( + handlerName: 'close', + callback: (args) => Get.back(), ); }, onLoadStop: (ctr, _) async { final config = await _future; if (!mounted) return; if (config case Success(:final response)) { - ctr.evaluateJavascript( - source: - 'let t=Geetest($response).onSuccess(()=>R("success",t.getValidate())).onError((o)=>R("error",o));t.onReady(()=>t.verify());', - ); + ctr.evaluateJavascript(source: _showJs(response)); } else { config.toast(); Get.back(); } }, ), - ), - actions: [ - TextButton( - onPressed: Get.back, - child: Text( - '取消', - style: TextStyle(color: ColorScheme.of(context).outline), + Positioned( + left: 8, + top: 8, + child: IconButton( + icon: const Icon(Icons.close), + onPressed: Get.back, + tooltip: '关闭', ), ), ], diff --git a/lib/pages/login/view.dart b/lib/pages/login/view.dart index c8ed426b07..b4cc05df44 100644 --- a/lib/pages/login/view.dart +++ b/lib/pages/login/view.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/dial_prefix.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; @@ -68,17 +66,16 @@ class _LoginPageState extends State { TextButton.icon( onPressed: () async { SmartDialog.showLoading(msg: '正在生成截图'); - RenderRepaintBoundary boundary = - globalKey.currentContext!.findRenderObject()! + final boundary = + globalKey.currentContext!.findRenderObject() as RenderRepaintBoundary; final image = await boundary.toImage(pixelRatio: 3); - ByteData? byteData = await image.toByteData( - format: ImageByteFormat.png, - ); - Uint8List pngBytes = byteData!.buffer.asUint8List(); + final byteData = await image.toByteData(format: .png); + final pngBytes = byteData!.buffer.asUint8List(); + image.dispose(); SmartDialog.dismiss(); - String picName = - "${Constants.appName}_loginQRCode_${ImageUtils.time}"; + final picName = + "${Constants.appName}_loginQRCode_${_loginPageCtr.codeInfo.value.data.authCode.hashCode.toUnsigned(32).toRadixString(16)}"; ImageUtils.saveByteImg(bytes: pngBytes, fileName: picName); }, icon: const Icon(Icons.save), diff --git a/lib/pages/main/controller.dart b/lib/pages/main/controller.dart index 203a2c17af..95bd293b99 100644 --- a/lib/pages/main/controller.dart +++ b/lib/pages/main/controller.dart @@ -232,7 +232,8 @@ class MainController extends GetxController .toList(); } this.navigationBars = navigationBars; - selectedIndex.value = Pref.defaultHomePageIndex; + final defPage = Pref.defaultHomePage; + selectedIndex.value = navigationBars.indexOf(defPage); } void checkDefaultSearch([bool shouldCheck = false]) { diff --git a/lib/pages/main/view.dart b/lib/pages/main/view.dart index 8dc1948779..71e073fe23 100644 --- a/lib/pages/main/view.dart +++ b/lib/pages/main/view.dart @@ -13,6 +13,7 @@ import 'package:PiliPlus/pages/home/view.dart'; import 'package:PiliPlus/pages/main/controller.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; +import 'package:PiliPlus/utils/android/android_helper.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart'; @@ -21,11 +22,11 @@ import 'package:PiliPlus/utils/mobile_observer.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; -import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:get/get.dart'; import 'package:tray_manager/tray_manager.dart'; +import 'package:win32/win32.dart' as kernel32; import 'package:window_manager/window_manager.dart'; class MainApp extends StatefulWidget { @@ -45,6 +46,8 @@ class _MainAppState extends PopScopeState final _mainController = Get.put(MainController()); late final _setting = GStorage.setting; late EdgeInsets _padding; + late ThemeData theme; + Brightness? _brightness; @override bool get initCanPop => false; @@ -71,11 +74,15 @@ class _MainAppState extends PopScopeState void didChangeDependencies() { super.didChangeDependencies(); _padding = MediaQuery.viewPaddingOf(context); - final brightness = Theme.brightnessOf(context); + theme = Theme.of(context); + final brightness = theme.brightness; NetworkImgLayer.reduce = NetworkImgLayer.reduceLuxColor != null && brightness.isDark; if (PlatformUtils.isDesktop) { - windowManager.setBrightness(brightness); + if (_brightness != brightness) { + _brightness = brightness; + windowManager.setBrightness(brightness); + } } if (!_mainController.useSideBar) { _mainController.useBottomNav = MediaQuery.sizeOf(context).isPortrait; @@ -166,7 +173,11 @@ class _MainAppState extends PopScopeState await GStorage.close(); await trayManager.destroy(); if (Platform.isWindows) { - const MethodChannel('window_control').invokeMethod('closeWindow'); + // flutter_inappwebview + // 6.2.0-beta.2+ https://github.com/pichillilorenzo/flutter_inappwebview/issues/2482 + // 6.1.5 https://github.com/pichillilorenzo/flutter_inappwebview/issues/2512#issuecomment-3031039587 + final hProcess = kernel32.GetCurrentProcess(); + kernel32.TerminateProcess(hProcess, 0); } else { exit(0); } @@ -247,9 +258,10 @@ class _MainAppState extends PopScopeState await trayManager.setContextMenu(trayMenu); } + @pragma('vm:prefer-inline') static void _onBack() { if (Platform.isAndroid) { - Utils.channel.invokeMethod('back'); + PiliAndroidHelper.back(); } } @@ -425,7 +437,6 @@ class _MainAppState extends PopScopeState @override Widget build(BuildContext context) { - final theme = Theme.of(context); Widget child; if (_mainController.mainTabBarView) { child = CustomTabBarView( diff --git a/lib/pages/main_reply/controller.dart b/lib/pages/main_reply/controller.dart index b12ac75ce9..679482b269 100644 --- a/lib/pages/main_reply/controller.dart +++ b/lib/pages/main_reply/controller.dart @@ -26,7 +26,7 @@ class MainReplyController extends ReplyController { Future> customGetData() => ReplyGrpc.mainList( type: replyType, oid: oid, - mode: mode.value, + mode: mode, cursorNext: cursorNext, offset: paginationReply?.nextOffset, ); diff --git a/lib/pages/main_reply/view.dart b/lib/pages/main_reply/view.dart index 40b305c104..1242169787 100644 --- a/lib/pages/main_reply/view.dart +++ b/lib/pages/main_reply/view.dart @@ -40,7 +40,7 @@ class MainReplyPage extends StatefulWidget { } class _MainReplyPageState extends State - with SingleTickerProviderStateMixin, FabMixin { + with SingleTickerProviderStateMixin, BaseFabMixin, FabMixin { final _controller = Get.put( MainReplyController(), tag: Utils.generateRandomString(8), diff --git a/lib/pages/match_info/view.dart b/lib/pages/match_info/view.dart index c903a44560..882d786933 100644 --- a/lib/pages/match_info/view.dart +++ b/lib/pages/match_info/view.dart @@ -40,20 +40,18 @@ class _MatchInfoPageState extends CommonDynPageState { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Scaffold( + final child = Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar(title: const Text('比赛详情')), body: ViewSafeArea( child: refreshIndicator( onRefresh: controller.onRefresh, child: CustomScrollView( - controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ - Obx(() => _buildInfo(theme, controller.infoState.value)), - buildReplyHeader(theme), - Obx(() => replyList(theme, controller.loadingState.value)), + Obx(() => _buildInfo(controller.infoState.value)), + buildReplyHeader(), + Obx(() => replyList(controller.loadingState.value)), ], ), ), @@ -64,9 +62,10 @@ class _MatchInfoPageState extends CommonDynPageState { child: fabButton, ), ); + return fabAnimWrapper(child); } - Widget _buildInfo(ThemeData theme, LoadingState infoState) { + Widget _buildInfo(LoadingState infoState) { if (infoState case Success(:final response?)) { try { Widget teamInfo(MatchTeam team) { @@ -188,12 +187,7 @@ class _MatchInfoPageState extends CommonDynPageState { } @override - void replyReply( - BuildContext context, - ReplyInfo replyItem, - int? id, - ThemeData theme, - ) { + void replyReply(BuildContext context, ReplyInfo replyItem, int? id) { EasyThrottle.throttle('replyReply', const Duration(milliseconds: 500), () { int oid = replyItem.oid.toInt(); int rpid = replyItem.id.toInt(); diff --git a/lib/pages/member/controller.dart b/lib/pages/member/controller.dart index 1822bcebd9..fd46538781 100644 --- a/lib/pages/member/controller.dart +++ b/lib/pages/member/controller.dart @@ -14,9 +14,10 @@ import 'package:PiliPlus/models_new/space/space/setting.dart'; import 'package:PiliPlus/models_new/space/space/tab2.dart'; import 'package:PiliPlus/pages/common/common_data_controller.dart'; import 'package:PiliPlus/utils/accounts.dart'; +import 'package:PiliPlus/utils/extension/nested_scroll_ext.dart'; import 'package:PiliPlus/utils/request_utils.dart'; +import 'package:PiliPlus/utils/share_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; -import 'package:PiliPlus/utils/utils.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart' show ExtendedNestedScrollViewState; import 'package:flutter/material.dart'; @@ -59,7 +60,7 @@ class MemberController extends CommonDataController final fromViewAid = Get.parameters['from_view_aid']; - final key = GlobalKey(); + final scrollKey = GlobalKey(); @override void onInit() { @@ -201,7 +202,7 @@ class MemberController extends CommonDataController } void shareUser() { - Utils.shareText('https://space.bilibili.com/$mid'); + ShareUtils.shareText('https://space.bilibili.com/$mid'); } Future _onBlock() async { @@ -255,13 +256,8 @@ class MemberController extends CommonDataController } void onTapTab(int value) { - if (tabController?.indexIsChanging == false && - key.currentState?.outerController.hasClients == true) { - key.currentState!.outerController.animateTo( - key.currentState!.outerController.offset, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ); + if (tabController?.indexIsChanging == false) { + scrollKey.currentState?.animToTop(); } } diff --git a/lib/pages/member/view.dart b/lib/pages/member/view.dart index 63ae223803..e0b52d4dcb 100644 --- a/lib/pages/member/view.dart +++ b/lib/pages/member/view.dart @@ -32,6 +32,8 @@ import 'package:PiliPlus/pages/member_pgc/view.dart'; import 'package:PiliPlus/pages/member_shop/view.dart'; import 'package:PiliPlus/pages/member_video_web/archive/view.dart'; import 'package:PiliPlus/pages/member_video_web/season_series/view.dart'; +import 'package:PiliPlus/utils/android/android_helper.dart'; +import 'package:PiliPlus/utils/cache_manager.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; @@ -41,7 +43,6 @@ import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -91,7 +92,7 @@ class _MemberPageState extends State { () => switch (_userController.loadingState.value) { Loading() => m3eLoading, Success(:final response) => ExtendedNestedScrollView( - key: _userController.key, + key: _userController.scrollKey, onlyOneScrollInBody: true, pinnedHeaderSliverHeightBuilder: () => kToolbarHeight + MediaQuery.viewPaddingOf(context).top, @@ -702,18 +703,15 @@ class _MemberPageState extends State { Future _createShortcutAndroid() async { try { SmartDialog.showLoading(); - final file = (await DefaultCacheManager().getSingleFile( + final file = (await CacheManager.manager.getSingleFile( '${_userController.userAvatar!}@200w_200h.webp'.http2https, )); SmartDialog.dismiss(); - await Utils.channel.invokeMethod( - 'createShortcut', - { - 'id': _userController.mid.toString(), - 'uri': 'bilibili://space/${_userController.mid}', - 'label': _userController.username!, - 'icon': file.path, - }, + PiliAndroidHelper.createShortcut( + _userController.mid.toString(), + 'bilibili://space/${_userController.mid}', + _userController.username!, + file.path, ); } catch (e) { SmartDialog.showToast(e.toString()); diff --git a/lib/pages/member/widget/medal_widget.dart b/lib/pages/member/widget/medal_widget.dart index 5321792623..45b2ef9e4f 100644 --- a/lib/pages/member/widget/medal_widget.dart +++ b/lib/pages/member/widget/medal_widget.dart @@ -1,6 +1,6 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/models_new/live/live_medal_wall/uinfo_medal.dart'; -import 'package:PiliPlus/utils/utils.dart'; +import 'package:PiliPlus/utils/color_utils.dart'; import 'package:flutter/material.dart'; const _kFontSize = 10.0; @@ -27,8 +27,8 @@ class MedalWidget extends StatelessWidget { this.padding = _kPadding, }) : medalName = medal.name!, level = medal.level!, - backgroundColor = Utils.parseMedalColor(medal.v2MedalColorStart!), - nameColor = Utils.parseColor(medal.v2MedalColorText!); + backgroundColor = ColourUtils.parseMedalColor(medal.v2MedalColorStart!), + nameColor = ColourUtils.parseColor(medal.v2MedalColorText!); final String medalName; final int level; diff --git a/lib/pages/member/widget/reserve_button.dart b/lib/pages/member/widget/reserve_button.dart index c19649f875..7d06ca62ce 100644 --- a/lib/pages/member/widget/reserve_button.dart +++ b/lib/pages/member/widget/reserve_button.dart @@ -45,9 +45,8 @@ class ReserveButton extends SingleChildRenderObjectWidget { class RenderReserveBtn extends RenderProxyBox { RenderReserveBtn({ required int count, - required Color color, - }) : _count = count, - _color = color { + required this._color, + }) : _count = count { _textPainter = TextPainter( textDirection: .ltr, text: _getTextSpan(count), diff --git a/lib/pages/member/widget/user_info_card.dart b/lib/pages/member/widget/user_info_card.dart index 6736e0c959..9a887b204d 100644 --- a/lib/pages/member/widget/user_info_card.dart +++ b/lib/pages/member/widget/user_info_card.dart @@ -24,6 +24,8 @@ import 'package:PiliPlus/pages/member_guard/view.dart'; import 'package:PiliPlus/pages/member_upower_rank/view.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; +import 'package:PiliPlus/utils/bili_utils.dart'; +import 'package:PiliPlus/utils/color_utils.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; @@ -33,7 +35,7 @@ import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -165,8 +167,8 @@ class UserInfoCard extends StatelessWidget { Color? nameColor; Color? backgroundColor; try { - nameColor = Utils.parseColor(detailV2.medalColorName!); - backgroundColor = Utils.parseColor(detailV2.medalColor!); + nameColor = ColourUtils.parseColor(detailV2.medalColorName!); + backgroundColor = ColourUtils.parseColor(detailV2.medalColor!); } catch (e, s) { if (kDebugMode) { Utils.reportError(e, s); @@ -215,14 +217,10 @@ class UserInfoCard extends StatelessWidget { ), ), ), - Image.asset( - Utils.levelName( - card.levelInfo!.currentLevel!, - isSeniorMember: card.levelInfo?.identity == 2, - ), + BiliUtils.levelPicture( + card.levelInfo!.currentLevel!, + isSeniorMember: card.levelInfo?.identity == 2, height: 11, - cacheHeight: 11.cacheSize(context), - semanticLabel: '等级${card.levelInfo?.currentLevel}', ), if (card.vip?.status == 1) Container( @@ -575,7 +573,8 @@ class UserInfoCard extends StatelessWidget { ) { if (imgUrls.length == 1) { final img = imgUrls.first; - return _buildHeader( + final title = img.title; + Widget child = _buildHeader( context, isLight, width, @@ -584,6 +583,20 @@ class UserInfoCard extends StatelessWidget { fullCover: img.fullCover, alignment: Alignment(0.0, img.dy), ); + if (title != null) { + return Stack( + clipBehavior: .none, + children: [ + child, + Positioned( + right: 0, + bottom: 0, + child: _headerWrapper(_headerTitle(title)), + ), + ], + ); + } + return child; } final controller = headerControllerBuilder(); final memCacheWidth = width.cacheSize(context); @@ -595,6 +608,7 @@ class UserInfoCard extends StatelessWidget { onPageChanged: controller.jumpToPage, ), child: Stack( + clipBehavior: .none, children: [ SizedBox( width: .infinity, @@ -626,30 +640,10 @@ class UserInfoCard extends StatelessWidget { Positioned( right: 0, bottom: 3.5, - child: IgnorePointer( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 125), - child: DecoratedBox( - decoration: const BoxDecoration( - gradient: LinearGradient( - begin: .centerLeft, - end: .centerRight, - colors: [ - Colors.transparent, - Colors.black12, - Colors.black38, - Colors.black45, - ], - ), - ), - child: Padding( - padding: const .only(left: 15, right: 5, bottom: 2), - child: HeaderTitle( - images: imgUrls, - pageController: controller, - ), - ), - ), + child: _headerWrapper( + HeaderTitle( + images: imgUrls, + pageController: controller, ), ), ), @@ -714,7 +708,7 @@ class UserInfoCard extends StatelessWidget { bool isLight, SpacePrInfo prInfo, ) { - final textColor = Utils.parseColor( + final textColor = ColourUtils.parseColor( isLight ? prInfo.textColor : prInfo.textColorNight, ); String? icon = !isLight && prInfo.iconNight?.isNotEmpty == true @@ -726,7 +720,9 @@ class UserInfoCard extends StatelessWidget { Widget child = Container( margin: const .only(top: 8), padding: const .symmetric(horizontal: 16, vertical: 10), - color: Utils.parseColor(isLight ? prInfo.bgColor : prInfo.bgColorNight), + color: ColourUtils.parseColor( + isLight ? prInfo.bgColor : prInfo.bgColorNight, + ), child: Row( children: [ if (icon != null) ...[ @@ -1038,36 +1034,66 @@ class _HeaderTitleState extends State { Widget build(BuildContext context) { final title = widget.images[_index].title; if (title == null) return const SizedBox.shrink(); - try { - return Column( - crossAxisAlignment: .end, - children: [ + return _headerTitle(title); + } +} + +Widget _headerTitle(TopTitle title) { + try { + return Column( + crossAxisAlignment: .end, + children: [ + Text( + title.title!, + maxLines: 1, + overflow: .ellipsis, + style: const TextStyle(fontSize: 12, color: Colors.white), + ), + if (title.subTitle?.isNotEmpty ?? false) Text( - title.title!, - maxLines: 1, - overflow: .ellipsis, - style: const TextStyle(fontSize: 12, color: Colors.white), - ), - if (title.subTitle?.isNotEmpty ?? false) - Text( - title.subTitle!, - style: TextStyle( - fontSize: 12, - fontFamily: Assets.digitalNum, - color: title.subTitleColorFormat?.colors?.isNotEmpty == true - ? Utils.parseMedalColor( - title.subTitleColorFormat!.colors!.last, - ) - : Colors.white, - ), + title.subTitle!, + style: TextStyle( + fontSize: 12, + fontFamily: Assets.digitalNum, + color: title.subTitleColorFormat?.colors?.isNotEmpty == true + ? ColourUtils.parseMedalColor( + title.subTitleColorFormat!.colors!.last, + ) + : Colors.white, ), - ], - ); - } catch (e, s) { - if (kDebugMode) { - Utils.reportError(e, s); - } - return const SizedBox.shrink(); + ), + ], + ); + } catch (e, s) { + if (kDebugMode) { + Utils.reportError(e, s); } + return const SizedBox.shrink(); } } + +Widget _headerWrapper(Widget child) { + return IgnorePointer( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 125), + child: DecoratedBox( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: .centerLeft, + end: .centerRight, + colors: [ + Colors.transparent, + Colors.black12, + Colors.black38, + Colors.black45, + ], + ), + ), + child: Padding( + padding: const .only(left: 15, right: 5, bottom: 2), + child: child, + ), + ), + ), + ); +} diff --git a/lib/pages/member_article/widget/item.dart b/lib/pages/member_article/widget/item.dart index 19bda1ef5b..6f8d4f481d 100644 --- a/lib/pages/member_article/widget/item.dart +++ b/lib/pages/member_article/widget/item.dart @@ -1,5 +1,4 @@ import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; @@ -7,7 +6,7 @@ import 'package:PiliPlus/models/common/stat_type.dart'; import 'package:PiliPlus/models_new/space/space_article/item.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; class MemberArticleItem extends StatelessWidget { const MemberArticleItem({super.key, required this.item}); diff --git a/lib/pages/member_audio/view.dart b/lib/pages/member_audio/view.dart index 78ce6f1924..8f8c7d1d2c 100644 --- a/lib/pages/member_audio/view.dart +++ b/lib/pages/member_audio/view.dart @@ -26,7 +26,7 @@ class MemberAudio extends StatefulWidget { } class _MemberAudioState extends State - with AutomaticKeepAliveClientMixin { + with AutomaticKeepAliveClientMixin, GridMixin { late final MemberAudioController _controller; @override @@ -63,13 +63,6 @@ class _MemberAudioState extends State @override bool get wantKeepAlive => true; - late final gridDelegate = SliverGridDelegateWithExtentAndRatio( - mainAxisSpacing: 2, - maxCrossAxisExtent: Grid.smallCardWidth * 2, - childAspectRatio: Style.aspectRatio * 2.6, - minHeight: MediaQuery.textScalerOf(context).scale(90), - ); - Widget _buildBody( ColorScheme colorScheme, LoadingState?> loadingState, diff --git a/lib/pages/member_audio/widgets/item.dart b/lib/pages/member_audio/widgets/item.dart index bc694086d5..68c23ab1c8 100644 --- a/lib/pages/member_audio/widgets/item.dart +++ b/lib/pages/member_audio/widgets/item.dart @@ -1,5 +1,4 @@ import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; @@ -10,7 +9,7 @@ import 'package:PiliPlus/models_new/space/space_audio/item.dart'; import 'package:PiliPlus/pages/audio/view.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; class MemberAudioItem extends StatelessWidget { const MemberAudioItem({super.key, required this.item}); diff --git a/lib/pages/member_cheese/widgets/item.dart b/lib/pages/member_cheese/widgets/item.dart index ff1a720866..14a5222b7f 100644 --- a/lib/pages/member_cheese/widgets/item.dart +++ b/lib/pages/member_cheese/widgets/item.dart @@ -1,14 +1,13 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models_new/space/space_cheese/item.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; class MemberCheeseItem extends StatelessWidget { const MemberCheeseItem({ diff --git a/lib/pages/member_coin_arc/widgets/item.dart b/lib/pages/member_coin_arc/widgets/item.dart index 5b0625d3fa..3ade8508cb 100644 --- a/lib/pages/member_coin_arc/widgets/item.dart +++ b/lib/pages/member_coin_arc/widgets/item.dart @@ -1,6 +1,5 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; @@ -13,7 +12,7 @@ import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; class MemberCoinLikeItem extends StatelessWidget { final CoinLikeArcItem item; diff --git a/lib/pages/member_comic/widgets/item.dart b/lib/pages/member_comic/widgets/item.dart index c1e456440d..95fe55801c 100644 --- a/lib/pages/member_comic/widgets/item.dart +++ b/lib/pages/member_comic/widgets/item.dart @@ -1,10 +1,9 @@ import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models_new/space/space_archive/item.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; class MemberComicItem extends StatelessWidget { diff --git a/lib/pages/member_favorite/view.dart b/lib/pages/member_favorite/view.dart index 599f33eaed..1ad4b187af 100644 --- a/lib/pages/member_favorite/view.dart +++ b/lib/pages/member_favorite/view.dart @@ -70,7 +70,7 @@ class _MemberFavoriteState extends State Loading() => SliverPadding( padding: const EdgeInsets.only(top: 7), sliver: SliverGrid.builder( - gridDelegate: Grid.videoCardHDelegate(context), + gridDelegate: gridDelegate, itemBuilder: (context, index) => const VideoCardHSkeleton(), itemCount: 10, ), @@ -169,7 +169,7 @@ class _MemberFavoriteState extends State itemBuilder: (context, index) { final item = list[index]; return SizedBox( - height: 98, + height: 110, child: MemberFavItem( item: item, onDelete: (isDeleted) { diff --git a/lib/pages/member_favorite/widget/item.dart b/lib/pages/member_favorite/widget/item.dart index 33f94627e8..1793b5ff40 100644 --- a/lib/pages/member_favorite/widget/item.dart +++ b/lib/pages/member_favorite/widget/item.dart @@ -1,15 +1,14 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models_new/space/space_fav/list.dart'; import 'package:PiliPlus/pages/subscription_detail/view.dart'; -import 'package:PiliPlus/utils/fav_utils.dart'; +import 'package:PiliPlus/utils/bili_utils.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; class MemberFavItem extends StatelessWidget { @@ -101,7 +100,7 @@ class MemberFavItem extends StatelessWidget { const Spacer(), Text( item.type == 0 - ? '${item.mediaCount}个内容 · ${FavUtils.isPublicFavText(item.attr)}' + ? '${item.mediaCount}个内容 · ${BiliUtils.isPublicFavText(item.attr)}' : item.type == 11 ? '${item.mediaCount}个内容 · ${item.upper?.name}' : item.type == 21 diff --git a/lib/pages/member_home/view.dart b/lib/pages/member_home/view.dart index d8c6e3acc3..113f76a1b5 100644 --- a/lib/pages/member_home/view.dart +++ b/lib/pages/member_home/view.dart @@ -52,12 +52,7 @@ class _MemberHomeState extends State mainAxisExtent: MediaQuery.textScalerOf(context).scale(55), ); - late final gridDelegateAudio = SliverGridDelegateWithExtentAndRatio( - mainAxisSpacing: 2, - maxCrossAxisExtent: Grid.smallCardWidth * 2, - childAspectRatio: Style.aspectRatio * 2.6, - minHeight: MediaQuery.textScalerOf(context).scale(90), - ); + late final gridDelegateAudio = Grid.videoCardHDelegate(); late final gridDelegatePgc = SliverGridDelegateWithExtentAndRatio( mainAxisSpacing: Style.cardSpace, @@ -114,7 +109,7 @@ class _MemberHomeState extends State ), SliverToBoxAdapter( child: SizedBox( - height: 98, + height: 110, child: MemberFavItem( item: res.favourite2!.item!.first, ), @@ -183,7 +178,7 @@ class _MemberHomeState extends State ), SliverToBoxAdapter( child: SizedBox( - height: 98, + height: 110, child: MemberArticleItem( item: res.article!.item!.first, ), diff --git a/lib/pages/member_home/widgets/fav_item.dart b/lib/pages/member_home/widgets/fav_item.dart index 138ba9540a..1bdcebb3be 100644 --- a/lib/pages/member_home/widgets/fav_item.dart +++ b/lib/pages/member_home/widgets/fav_item.dart @@ -42,8 +42,8 @@ class MemberFavItem extends StatelessWidget { children: [ NetworkImgLayer( src: item.cover, - width: 140.8, - height: 88, + width: 160, + height: 100, ), const SizedBox(width: 10), Expanded( diff --git a/lib/pages/member_home/widgets/video_card_v_member_home.dart b/lib/pages/member_home/widgets/video_card_v_member_home.dart index 7b3edc6a20..1fbf026cd9 100644 --- a/lib/pages/member_home/widgets/video_card_v_member_home.dart +++ b/lib/pages/member_home/widgets/video_card_v_member_home.dart @@ -1,6 +1,5 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/http/search.dart'; @@ -12,7 +11,7 @@ import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; // 视频卡片 - 垂直布局 class VideoCardVMemberHome extends StatelessWidget { diff --git a/lib/pages/member_opus/controller.dart b/lib/pages/member_opus/controller.dart index 46d42f117f..ad043d7c53 100644 --- a/lib/pages/member_opus/controller.dart +++ b/lib/pages/member_opus/controller.dart @@ -5,7 +5,7 @@ import 'package:PiliPlus/models_new/space/space_opus/data.dart'; import 'package:PiliPlus/models_new/space/space_opus/item.dart'; import 'package:PiliPlus/pages/common/common_list_controller.dart'; import 'package:PiliPlus/pages/member/controller.dart'; -import 'package:PiliPlus/utils/extension/iterable_ext.dart'; +import 'package:collection/collection.dart'; import 'package:get/get.dart'; class MemberOpusController diff --git a/lib/pages/member_opus/view.dart b/lib/pages/member_opus/view.dart index bbc67d92bb..b4e0b1c77a 100644 --- a/lib/pages/member_opus/view.dart +++ b/lib/pages/member_opus/view.dart @@ -4,6 +4,7 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/space/space_opus/item.dart'; +import 'package:PiliPlus/pages/common/fab_mixin.dart'; import 'package:PiliPlus/pages/member_opus/controller.dart'; import 'package:PiliPlus/pages/member_opus/widgets/space_opus_item.dart'; import 'package:PiliPlus/utils/grid.dart'; @@ -30,7 +31,11 @@ class MemberOpus extends StatefulWidget { } class _MemberOpusState extends State - with AutomaticKeepAliveClientMixin { + with + AutomaticKeepAliveClientMixin, + SingleTickerProviderStateMixin, + BaseFabMixin, + LazyFabMixin { late final MemberOpusController _controller; @override @@ -50,71 +55,86 @@ class _MemberOpusState extends State super.build(context); final bottom = MediaQuery.viewPaddingOf(context).bottom; return Stack( + clipBehavior: .none, children: [ refreshIndicator( onRefresh: _controller.onRefresh, - child: CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - slivers: [ - SliverPadding( - padding: EdgeInsets.only( - top: widget.isSingle ? 12 : 0, - left: Style.safeSpace, - right: Style.safeSpace, - bottom: bottom + 100, + child: NotificationListener( + onNotification: (notification) { + final direction = notification.direction; + if (direction == .forward) { + showFab(); + } else if (direction == .reverse) { + hideFab(); + } + return false; + }, + child: CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + slivers: [ + SliverPadding( + padding: EdgeInsets.only( + top: widget.isSingle ? 12 : 0, + left: Style.safeSpace, + right: Style.safeSpace, + bottom: bottom + 100, + ), + sliver: Obx(() => _buildBody(_controller.loadingState.value)), ), - sliver: Obx(() => _buildBody(_controller.loadingState.value)), - ), - ], + ], + ), ), ), if (_controller.filter?.isNotEmpty == true) Positioned( right: kFloatingActionButtonMargin, - bottom: bottom + kFloatingActionButtonMargin, - child: FloatingActionButton.extended( - onPressed: () => showDialog( - context: context, - builder: (context) => AlertDialog( - clipBehavior: Clip.hardEdge, - contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: _controller.filter! - .map( - (e) => ListTile( - onTap: () { - if (e == _controller.type.value) { - return; - } - Get.back(); - _controller - ..type.value = e - ..onReload(); - }, - tileColor: e == _controller.type.value - ? Theme.of( - context, - ).colorScheme.onInverseSurface - : null, - dense: true, - title: Text( - e.text ?? e.tabName!, - style: const TextStyle(fontSize: 14), + bottom: 0, + child: SlideTransition( + position: fabAnimation, + child: Padding( + padding: .only( + bottom: bottom + kFloatingActionButtonMargin, + ), + child: FloatingActionButton.extended( + onPressed: () => showDialog( + context: context, + builder: (context) => SimpleDialog( + clipBehavior: Clip.hardEdge, + contentPadding: const EdgeInsets.symmetric(vertical: 12), + children: _controller.filter! + .map( + (e) => ListTile( + dense: true, + onTap: () { + if (e == _controller.type.value) { + return; + } + Get.back(); + _controller + ..type.value = e + ..onReload(); + }, + tileColor: e == _controller.type.value + ? ColorScheme.of(context).onInverseSurface + : null, + title: Text( + e.text ?? e.tabName!, + style: const TextStyle(fontSize: 14), + ), ), - ), - ) - .toList(), + ) + .toList(), + ), + ), + icon: const Icon(size: 20, Icons.sort), + label: Obx( + () { + final type = _controller.type.value; + return Text(type.text ?? type.tabName!); + }, ), ), ), - icon: const Icon(size: 20, Icons.sort), - label: Obx( - () { - final type = _controller.type.value; - return Text(type.text ?? type.tabName!); - }, - ), ), ), ], diff --git a/lib/pages/member_opus/widgets/space_opus_item.dart b/lib/pages/member_opus/widgets/space_opus_item.dart index 471014a42c..84f7e4fde1 100644 --- a/lib/pages/member_opus/widgets/space_opus_item.dart +++ b/lib/pages/member_opus/widgets/space_opus_item.dart @@ -1,11 +1,10 @@ -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models/common/stat_type.dart'; import 'package:PiliPlus/models_new/space/space_opus/item.dart'; import 'package:PiliPlus/utils/page_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; class SpaceOpusItem extends StatelessWidget { const SpaceOpusItem({ diff --git a/lib/pages/member_pgc/widgets/pgc_card_v_member_pgc.dart b/lib/pages/member_pgc/widgets/pgc_card_v_member_pgc.dart index cd6c382491..4ff94c0564 100644 --- a/lib/pages/member_pgc/widgets/pgc_card_v_member_pgc.dart +++ b/lib/pages/member_pgc/widgets/pgc_card_v_member_pgc.dart @@ -1,11 +1,10 @@ import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models_new/space/space_archive/item.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; // 视频卡片 - 垂直布局 class PgcCardVMemberPgc extends StatelessWidget { diff --git a/lib/pages/member_profile/view.dart b/lib/pages/member_profile/view.dart index fa4082b48b..debf999a1a 100644 --- a/lib/pages/member_profile/view.dart +++ b/lib/pages/member_profile/view.dart @@ -247,17 +247,14 @@ class _EditProfilePageState extends State { } Widget _sexDialog(int current) { - return AlertDialog( + return SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - _sexDialogItem(1, current, '男'), - _sexDialogItem(0, current, '保密'), - _sexDialogItem(2, current, '女'), - ], - ), + children: [ + _sexDialogItem(1, current, '男'), + _sexDialogItem(0, current, '保密'), + _sexDialogItem(2, current, '女'), + ], ); } @@ -475,19 +472,20 @@ class _EditProfilePageState extends State { Future _pickImg(ThemeData theme) async { try { - XFile? pickedFile = await _imagePicker.pickImage( + final pickedFile = await _imagePicker.pickImage( source: ImageSource.gallery, imageQuality: 100, + requestFullMetadata: false, ); if (pickedFile != null && mounted) { - String? mimeType = lookupMimeType( - pickedFile.path, - )?.split('/').elementAtOrNull(1); + String? imagePath = pickedFile.path; + String? mimeType = (pickedFile.mimeType ?? lookupMimeType(imagePath)) + ?.split('/') + .elementAtOrNull(1); if (mimeType == 'gif') { SmartDialog.showToast('不能选GIF'); return; } - String? imagePath = pickedFile.path; if (PlatformUtils.isMobile) { final croppedFile = await ImageCropper.platform.cropImage( sourcePath: imagePath, @@ -496,7 +494,7 @@ class _EditProfilePageState extends State { toolbarTitle: '裁剪', toolbarColor: theme.colorScheme.secondaryContainer, toolbarWidgetColor: theme.colorScheme.onSecondaryContainer, - statusBarLight: theme.colorScheme.isLight, + statusBarLight: theme.isLight, aspectRatioPresets: const [CropAspectRatioPresetCustom()], lockAspectRatio: true, hideBottomControls: true, diff --git a/lib/pages/member_search/child/widgets/search_archive_grpc.dart b/lib/pages/member_search/child/widgets/search_archive_grpc.dart index ff4098e52a..32fadefcb7 100644 --- a/lib/pages/member_search/child/widgets/search_archive_grpc.dart +++ b/lib/pages/member_search/child/widgets/search_archive_grpc.dart @@ -1,6 +1,6 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; +import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; @@ -17,7 +17,7 @@ import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:fixnum/fixnum.dart' show Int64; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; class SearchArchiveGrpc extends StatelessWidget { @@ -132,13 +132,7 @@ class SearchArchiveGrpc extends StatelessWidget { child: Row( spacing: 6, children: [ - const Stack( - clipBehavior: Clip.none, - children: [ - Icon(MdiIcons.identifier, size: 16), - Icon(MdiIcons.circleOutline, size: 16), - ], - ), + const Icon(CustomIcons.identifier_circle, size: 16), Text(bvid, style: const TextStyle(fontSize: 13)), ], ), diff --git a/lib/pages/member_season_series/widget/season_series_card.dart b/lib/pages/member_season_series/widget/season_series_card.dart index a6400026ef..ea7ecef45c 100644 --- a/lib/pages/member_season_series/widget/season_series_card.dart +++ b/lib/pages/member_season_series/widget/season_series_card.dart @@ -1,12 +1,11 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models_new/space/space_season_series/season.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; class SeasonSeriesCard extends StatelessWidget { const SeasonSeriesCard({ diff --git a/lib/pages/member_shop/widgets/item.dart b/lib/pages/member_shop/widgets/item.dart index b386653e26..5c1beda8ce 100644 --- a/lib/pages/member_shop/widgets/item.dart +++ b/lib/pages/member_shop/widgets/item.dart @@ -1,10 +1,9 @@ import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models_new/space/space_shop/item.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; class MemberShopItem extends StatelessWidget { diff --git a/lib/pages/member_video/controller.dart b/lib/pages/member_video/controller.dart index 9ed2bb8345..e7d95fc068 100644 --- a/lib/pages/member_video/controller.dart +++ b/lib/pages/member_video/controller.dart @@ -10,10 +10,10 @@ import 'package:PiliPlus/models_new/space/space_archive/data.dart'; import 'package:PiliPlus/models_new/space/space_archive/episodic_button.dart'; import 'package:PiliPlus/models_new/space/space_archive/item.dart'; import 'package:PiliPlus/pages/common/common_list_controller.dart'; +import 'package:PiliPlus/utils/extension/dimension_ext.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; -import 'package:PiliPlus/utils/utils.dart'; import 'package:get/get.dart'; class MemberVideoCtr @@ -195,7 +195,7 @@ class MemberVideoCtr : desc; bool isVertical = false; if (element.uri case final uri?) { - isVertical = Utils.getDimensionFromUri(uri); + isVertical = uri.isVerticalFromUri; } PageUtils.toVideoPage( bvid: element.bvid, diff --git a/lib/pages/member_video/view.dart b/lib/pages/member_video/view.dart index 1ec37c9d5e..3f608f5833 100644 --- a/lib/pages/member_video/view.dart +++ b/lib/pages/member_video/view.dart @@ -6,11 +6,11 @@ import 'package:PiliPlus/common/widgets/sliver/sliver_floating_header.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/member/contribute_type.dart'; import 'package:PiliPlus/models_new/space/space_archive/item.dart'; +import 'package:PiliPlus/pages/common/fab_mixin.dart'; import 'package:PiliPlus/pages/member/controller.dart'; import 'package:PiliPlus/pages/member_video/controller.dart'; import 'package:PiliPlus/pages/member_video/widgets/video_card_h_member_video.dart'; import 'package:PiliPlus/utils/grid.dart'; -import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -41,25 +41,29 @@ class MemberVideo extends StatefulWidget { } class _MemberVideoState extends State - with AutomaticKeepAliveClientMixin, GridMixin { + with + AutomaticKeepAliveClientMixin, + GridMixin, + SingleTickerProviderStateMixin, + BaseFabMixin, + LazyFabMixin { @override bool get wantKeepAlive => true; late final MemberVideoCtr _controller; - int? _index; - late ExtendedNestedScrollController _scrollController; - void _jumpToIndex(int index) { final scrollOffset = gridDelegate.layoutCache! .getGeometryForChildIndex(index) .scrollOffset; try { - _scrollController.nestedPositions - .elementAt(_index!) - .localJumpTo(scrollOffset); + final state = Get.find( + tag: widget.heroTag, + ).scrollKey.currentState; + if (state != null && state.mounted) { + state.innerNestedPositions.first.localJumpTo(scrollOffset); + } } catch (e) { - _scrollController.jumpTo(scrollOffset); if (kDebugMode) debugPrint('jump error: $e'); } } @@ -112,41 +116,56 @@ class _MemberVideoState extends State ), ); if (_controller.isVideo && _controller.fromViewAid?.isNotEmpty == true) { - if (_index == null) { - _scrollController = - PrimaryScrollController.of(this.context) - as ExtendedNestedScrollController; - _index = _scrollController.nestedPositions.length; - } return Stack( clipBehavior: Clip.none, children: [ - child, + NotificationListener( + onNotification: (notification) { + final direction = notification.direction; + if (direction == .forward) { + showFab(); + } else if (direction == .reverse) { + hideFab(); + } + return false; + }, + child: child, + ), Obx( () => !_controller.isLocating.value ? Positioned( right: kFloatingActionButtonMargin, - bottom: kFloatingActionButtonMargin + padding.bottom, - child: FloatingActionButton.extended( - onPressed: () { - final fromViewAid = _controller.fromViewAid; - _controller.isLocating.value = true; - final locatedIndex = - _controller.loadingState.value.dataOrNull - ?.indexWhere((i) => i.param == fromViewAid) ?? - -1; - if (locatedIndex == -1) { - _controller - ..lastAid = fromViewAid - ..reload = true - ..page = 0 - ..loadingState.value = LoadingState.loading() - ..queryData(); - } else { - _jumpToIndex(locatedIndex); - } - }, - label: const Text('定位至上次观看'), + bottom: 0, + child: SlideTransition( + position: fabAnimation, + child: Padding( + padding: .only( + bottom: padding.bottom + kFloatingActionButtonMargin, + ), + child: FloatingActionButton.extended( + onPressed: () { + final fromViewAid = _controller.fromViewAid; + _controller.isLocating.value = true; + final locatedIndex = + _controller.loadingState.value.dataOrNull + ?.indexWhere( + (i) => i.param == fromViewAid, + ) ?? + -1; + if (locatedIndex == -1) { + _controller + ..lastAid = fromViewAid + ..reload = true + ..page = 0 + ..loadingState.value = LoadingState.loading() + ..queryData(); + } else { + _jumpToIndex(locatedIndex); + } + }, + label: const Text('定位至上次观看'), + ), + ), ), ) : const SizedBox.shrink(), diff --git a/lib/pages/member_video/widgets/video_card_h_member_video.dart b/lib/pages/member_video/widgets/video_card_h_member_video.dart index fe48040b6e..e1159bc4a8 100644 --- a/lib/pages/member_video/widgets/video_card_h_member_video.dart +++ b/lib/pages/member_video/widgets/video_card_h_member_video.dart @@ -1,6 +1,5 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/progress_bar/video_progress_indicator.dart'; @@ -11,10 +10,10 @@ import 'package:PiliPlus/models/common/stat_type.dart'; import 'package:PiliPlus/models_new/space/space_archive/item.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; +import 'package:PiliPlus/utils/extension/dimension_ext.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:PiliPlus/utils/utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; // 视频卡片 - 水平布局 class VideoCardHMemberVideo extends StatelessWidget { @@ -64,7 +63,7 @@ class VideoCardHMemberVideo extends StatelessWidget { } bool isVertical = false; if (videoItem.uri case final uri?) { - isVertical = Utils.getDimensionFromUri(uri); + isVertical = uri.isVerticalFromUri; } PageUtils.toVideoPage( bvid: videoItem.bvid, diff --git a/lib/pages/member_video_web/base/view.dart b/lib/pages/member_video_web/base/view.dart index fc9aebbaca..56abe1cc42 100644 --- a/lib/pages/member_video_web/base/view.dart +++ b/lib/pages/member_video_web/base/view.dart @@ -7,7 +7,7 @@ import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; import 'package:PiliPlus/common/widgets/video_card/video_card_h.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models/common/enum_with_label.dart'; -import 'package:PiliPlus/models/model_video.dart'; +import 'package:PiliPlus/models/horizontal_video_model.dart'; import 'package:PiliPlus/pages/member_video_web/base/controller.dart'; import 'package:PiliPlus/pages/search/widgets/search_text.dart'; import 'package:PiliPlus/utils/grid.dart'; @@ -19,7 +19,7 @@ import 'package:get/get.dart'; abstract class BaseVideoWebState< S extends StatefulWidget, R, - T extends BaseVideoItemModel, + T extends HorizontalVideoModel, V extends EnumWithLabel > extends State @@ -121,6 +121,7 @@ abstract class BaseVideoWebState< child: Padding( padding: const .fromLTRB(14, 0, 8, 4), child: Stack( + clipBehavior: .none, alignment: .centerLeft, children: [ ?buildCount(), diff --git a/lib/pages/mine/controller.dart b/lib/pages/mine/controller.dart index 03ea079f5f..3b6d0a8a4e 100644 --- a/lib/pages/mine/controller.dart +++ b/lib/pages/mine/controller.dart @@ -11,10 +11,10 @@ import 'package:PiliPlus/services/account_service.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts/account.dart'; import 'package:PiliPlus/utils/extension/scroll_controller_ext.dart'; -import 'package:PiliPlus/utils/login_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -32,7 +32,7 @@ class MineController extends CommonDataController // 用户状态 动态、关注、粉丝 final Rx userStat = const UserStat().obs; - Rx themeType = Pref.themeType.obs; + final Rx themeType = Pref.themeType.obs; ThemeType get nextThemeType => ThemeType.values[(themeType.value.index + 1) % ThemeType.values.length]; @@ -69,7 +69,7 @@ class MineController extends CommonDataController }, ), ( - size: 22, + size: 21, icon: Icons.watch_later_outlined, title: '稍后再看', onTap: () { @@ -111,20 +111,22 @@ class MineController extends CommonDataController ..face.value = response.face! ..isLogin.value = true; } else { - LoginUtils.onLogoutMain(); + _onLogoutMain(); return; } } else { final errMsg = res.toString(); SmartDialog.showToast(errMsg); if (errMsg == '账号未登录') { - LoginUtils.onLogoutMain(); + _onLogoutMain(); return; } } queryUserStatOwner(); } + void _onLogoutMain() => Accounts.deleteAll({Accounts.main}); + Future queryUserStatOwner() async { final res = await UserHttp.userStatOwner(); if (res case Success(:final response)) { @@ -189,9 +191,10 @@ class MineController extends CommonDataController ), const SizedBox(height: 10), Text( - '搜索、观看视频/直播不携带身份信息(包含大会员)\n' + '搜索不携带身份信息\n' '不产生查询或播放记录\n' '点赞等其它操作不受影响\n' + '播放进度信息跟随视频取流\n' '(前往隐私设置了解详情)', style: theme.textTheme.bodySmall, ), @@ -266,7 +269,7 @@ class MineController extends CommonDataController final newVal = nextThemeType; themeType.value = newVal; GStorage.setting.put(SettingBoxKey.themeMode, newVal.index); - Get.changeThemeMode(newVal.toThemeMode); + Get.changeThemeMode(ThemeUtils.themeMode = newVal.toThemeMode); } void push(String name) { diff --git a/lib/pages/mine/view.dart b/lib/pages/mine/view.dart index e133eb72ac..8f5c92b940 100644 --- a/lib/pages/mine/view.dart +++ b/lib/pages/mine/view.dart @@ -14,6 +14,7 @@ import 'package:PiliPlus/pages/login/controller.dart'; import 'package:PiliPlus/pages/main/controller.dart'; import 'package:PiliPlus/pages/mine/controller.dart'; import 'package:PiliPlus/pages/mine/widgets/item.dart'; +import 'package:PiliPlus/utils/bili_utils.dart'; import 'package:PiliPlus/utils/extension/get_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; @@ -21,6 +22,7 @@ import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart' hide ListTile; +import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; @@ -190,7 +192,7 @@ class _MediaPageState extends CommonPageState iconSize: iconSize, padding: padding, style: style, - tooltip: '设置账号模式', + tooltip: '切换账号', onPressed: () => LoginPageController.switchAccountDialog(context), icon: const Icon(Icons.switch_account_outlined), ), @@ -273,11 +275,10 @@ class _MediaPageState extends CommonPageState Positioned( right: -1, bottom: -2, - child: Image.asset( + child: SvgPicture.asset( Assets.vipIcon, height: 19, - cacheHeight: 19.cacheSize(context), - semanticLabel: "大会员", + semanticsLabel: "大会员", ), ), ], @@ -314,13 +315,10 @@ class _MediaPageState extends CommonPageState overflow: .ellipsis, ), ), - Image.asset( - Utils.levelName( - levelInfo?.currentLevel ?? 0, - isSeniorMember: userInfo.isSeniorMember == 1, - ), + BiliUtils.levelPicture( + levelInfo?.currentLevel ?? 0, + isSeniorMember: userInfo.isSeniorMember == 1, height: 10, - cacheHeight: 10.cacheSize(context), ), ], ), diff --git a/lib/pages/mine/widgets/item.dart b/lib/pages/mine/widgets/item.dart index cc21dc10ba..a06a4913cd 100644 --- a/lib/pages/mine/widgets/item.dart +++ b/lib/pages/mine/widgets/item.dart @@ -1,6 +1,6 @@ import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models_new/fav/fav_folder/list.dart'; -import 'package:PiliPlus/utils/fav_utils.dart'; +import 'package:PiliPlus/utils/bili_utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -64,7 +64,7 @@ class FavFolderItem extends StatelessWidget { maxLines: 1, ), Text( - ' 共${item.mediaCount}条视频 · ${FavUtils.isPublicFavText(item.attr)}', + ' 共${item.mediaCount}条视频 · ${BiliUtils.isPublicFavText(item.attr)}', style: theme.textTheme.labelSmall!.copyWith( color: theme.colorScheme.outline, ), diff --git a/lib/pages/msg_feed_top/like_me/view.dart b/lib/pages/msg_feed_top/like_me/view.dart index c5164002c9..92ec062f47 100644 --- a/lib/pages/msg_feed_top/like_me/view.dart +++ b/lib/pages/msg_feed_top/like_me/view.dart @@ -1,5 +1,6 @@ import 'package:PiliPlus/common/skeleton/msg_feed_top.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; @@ -97,13 +98,9 @@ class _LikeMePageState extends State { if (total.isEmpty && index == latest.length - 1) { _likeMeController.onLoadMore(); } - return _buildItem( - theme, - latest[index], - (id) { - _likeMeController.onRemove(id, index, true); - }, - ); + return _buildItem(theme, latest[index], (id) { + _likeMeController.onRemove(id, index, true); + }); }, itemCount: latest.length, separatorBuilder: (context, index) => divider, @@ -116,13 +113,9 @@ class _LikeMePageState extends State { if (index == total.length - 1) { _likeMeController.onLoadMore(); } - return _buildItem( - theme, - total[index], - (id) { - _likeMeController.onRemove(id, index, false); - }, - ); + return _buildItem(theme, total[index], (id) { + _likeMeController.onRemove(id, index, false); + }); }, itemCount: total.length, separatorBuilder: (context, index) => divider, @@ -200,51 +193,43 @@ class _LikeMePageState extends State { context: context, builder: (context) { final isNotice = item.noticeState == 0; - return AlertDialog( + return SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - onTap: () { - Get.back(); + children: [ + DialogOption( + onPressed: () { + Get.back(); + showConfirmDialog( + context: context, + title: const Text('删除'), + content: const Text('该条通知删除后,当有新点赞时会重新出现在列表,是否继续?'), + onConfirm: () => onRemove(item.id), + ); + }, + child: const Text('删除', style: TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () { + Get.back(); + if (isNotice) { showConfirmDialog( context: context, - title: const Text('删除'), - content: const Text('该条通知删除后,当有新点赞时会重新出现在列表,是否继续?'), - onConfirm: () => onRemove(item.id), + title: const Text('不再通知'), + content: const Text('这条内容的点赞将不再通知,但仍可在列表内查看,是否继续?'), + onConfirm: () => + _likeMeController.onSetNotice(item, isNotice), ); - }, - dense: true, - title: const Text( - '删除', - style: TextStyle(fontSize: 14), - ), - ), - ListTile( - onTap: () { - Get.back(); - if (isNotice) { - showConfirmDialog( - context: context, - title: const Text('不再通知'), - content: const Text('这条内容的点赞将不再通知,但仍可在列表内查看,是否继续?'), - onConfirm: () => - _likeMeController.onSetNotice(item, isNotice), - ); - } else { - _likeMeController.onSetNotice(item, isNotice); - } - }, - dense: true, - title: Text( - isNotice ? '不再通知' : '接收通知', - style: const TextStyle(fontSize: 14), - ), + } else { + _likeMeController.onSetNotice(item, isNotice); + } + }, + child: Text( + isNotice ? '不再通知' : '接收通知', + style: const TextStyle(fontSize: 14), ), - ], - ), + ), + ], ); }, ); @@ -336,9 +321,7 @@ class _LikeMePageState extends State { width: 45, height: 45, src: item.item!.image, - borderRadius: const BorderRadius.all( - Radius.circular(8), - ), + borderRadius: const BorderRadius.all(Radius.circular(8)), ), if (item.noticeState == 1) ...[ if (item.item?.image?.isNotEmpty == true) const SizedBox(width: 4), diff --git a/lib/pages/music/view.dart b/lib/pages/music/view.dart index e637c6bfe3..b2e204fb67 100644 --- a/lib/pages/music/view.dart +++ b/lib/pages/music/view.dart @@ -7,9 +7,9 @@ import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/hero.dart'; import 'package:PiliPlus/common/widgets/marquee.dart'; +import 'package:PiliPlus/common/widgets/sliver/sliver_to_box_adapter.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/music.dart'; -import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/models/common/image_type.dart'; import 'package:PiliPlus/models_new/music/bgm_detail.dart'; @@ -17,6 +17,7 @@ import 'package:PiliPlus/pages/common/dyn/common_dyn_page.dart'; import 'package:PiliPlus/pages/music/controller.dart'; import 'package:PiliPlus/pages/music/video/view.dart'; import 'package:PiliPlus/utils/accounts.dart'; +import 'package:PiliPlus/utils/android/android_helper.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/extension/get_ext.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; @@ -25,6 +26,7 @@ import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; +import 'package:PiliPlus/utils/share_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; @@ -51,8 +53,7 @@ class _MusicDetailPageState extends CommonDynPageState { @override Widget build(BuildContext context) { - final theme = Theme.of(context); - return Scaffold( + final child = Scaffold( resizeToAvoidBottomInset: false, appBar: _buildAppBar(), body: Padding( @@ -60,11 +61,12 @@ class _MusicDetailPageState extends CommonDynPageState { child: isPortrait ? refreshIndicator( onRefresh: controller.onRefresh, - child: _buildBody(theme), + child: _buildBody(), ) - : _buildBody(theme), + : _buildBody(), ), ); + return fabAnimWrapper(child); } PreferredSizeWidget _buildAppBar() => AppBar( @@ -106,7 +108,7 @@ class _MusicDetailPageState extends CommonDynPageState { ], ); - Widget _buildBody(ThemeData theme) => Obx(() { + Widget _buildBody() => Obx(() { switch (controller.infoState.value) { case Success(:final response): double padding = max(maxWidth / 2 - Grid.smallCardWidth, 0); @@ -115,17 +117,18 @@ class _MusicDetailPageState extends CommonDynPageState { child = Padding( padding: EdgeInsets.symmetric(horizontal: padding), child: CustomScrollView( - controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ - SliverToBoxAdapter( - child: _buildCard(theme, response, maxWidth), + SliverToBoxWithOffsetAdapter( + offset: 45, + onVisibilityChanged: controller.showTitle.call, + child: _buildCard(response, maxWidth), ), SliverToBoxAdapter( - child: _buildChart(theme, response, maxWidth), + child: _buildChart(response, maxWidth), ), - buildReplyHeader(theme), - Obx(() => replyList(theme, controller.loadingState.value)), + buildReplyHeader(), + Obx(() => replyList(controller.loadingState.value)), ], ), ); @@ -141,7 +144,6 @@ class _MusicDetailPageState extends CommonDynPageState { Expanded( flex: flex, child: CustomScrollView( - controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ SliverPadding( @@ -149,7 +151,7 @@ class _MusicDetailPageState extends CommonDynPageState { left: padding, ), sliver: SliverToBoxAdapter( - child: _buildCard(theme, response, leftWidth), + child: _buildCard(response, leftWidth), ), ), SliverPadding( @@ -158,7 +160,7 @@ class _MusicDetailPageState extends CommonDynPageState { bottom: this.padding.bottom + 100, ), sliver: SliverToBoxAdapter( - child: _buildChart(theme, response, leftWidth), + child: _buildChart(response, leftWidth), ), ), ], @@ -174,13 +176,11 @@ class _MusicDetailPageState extends CommonDynPageState { body: refreshIndicator( onRefresh: controller.onRefresh, child: CustomScrollView( - controller: scrollController, physics: const AlwaysScrollableScrollPhysics(), slivers: [ - buildReplyHeader(theme), + buildReplyHeader(), Obx( - () => - replyList(theme, controller.loadingState.value), + () => replyList(controller.loadingState.value), ), ], ), @@ -195,7 +195,7 @@ class _MusicDetailPageState extends CommonDynPageState { clipBehavior: Clip.none, children: [ child, - _buildBottom(theme, response), + _buildBottom(response), ], ); default: @@ -203,7 +203,7 @@ class _MusicDetailPageState extends CommonDynPageState { } }); - Widget _buildBottom(ThemeData theme, MusicDetail item) { + Widget _buildBottom(MusicDetail item) { if (!controller.showDynActionBar) { return Positioned( right: kFloatingActionButtonMargin, @@ -307,7 +307,8 @@ class _MusicDetailPageState extends CommonDynPageState { child: textIconButton( icon: CustomIcons.share_node, text: '分享', - onPressed: () => Utils.shareText(controller.shareUrl), + onPressed: () => + ShareUtils.shareText(controller.shareUrl), ), ), Expanded( @@ -385,8 +386,7 @@ class _MusicDetailPageState extends CommonDynPageState { Widget _buildRank( int? rank, - String name, - ThemeData theme, [ + String name, [ VoidCallback? onTap, ]) { final outline = theme.colorScheme.outline; @@ -422,7 +422,7 @@ class _MusicDetailPageState extends CommonDynPageState { ); } - Widget _buildCard(ThemeData theme, MusicDetail item, double maxWidth) { + Widget _buildCard(MusicDetail item, double maxWidth) { final textTheme = theme.textTheme; return SizedBox( width: maxWidth, @@ -486,35 +486,35 @@ class _MusicDetailPageState extends CommonDynPageState { Wrap( spacing: 16, children: [ - if (!item.musicRank.isNullOrEmpty) - PBadge( - text: item.musicRank, - type: PBadgeType.secondary, - isStack: false, - fontSize: 11, - ), - if (item.mvCid != null && item.mvCid != 0) + if (item.achievement.isNotEmpty) + for (var i in item.achievement) + if (i.isNotEmpty) + PBadge( + text: i, + type: .secondary, + isStack: false, + fontSize: 11, + ), + if (item.mvCid != 0) GestureDetector( onTap: () => PageUtils.toVideoPage( bvid: item.mvBvid, - cid: item.mvCid!, + cid: item.mvCid, aid: item.mvAid, ), child: DecoratedBox( decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(4), - ), + borderRadius: const .all(.circular(4)), color: theme.colorScheme.secondaryContainer .withValues(alpha: 0.5), ), child: Padding( - padding: const EdgeInsets.symmetric( + padding: const .symmetric( vertical: 3, horizontal: 4, ), child: Row( - mainAxisSize: MainAxisSize.min, + mainAxisSize: .min, children: [ Icon( Icons.play_circle_outline, @@ -567,12 +567,11 @@ class _MusicDetailPageState extends CommonDynPageState { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ const Text('热歌榜排名'), - _buildRank(item.hotSongHeat?.lastHeat, '热度', theme), - _buildRank(item.listenPv, '总播放量', theme), + _buildRank(item.hotSongHeat?.lastHeat, '热度'), + _buildRank(item.listenPv, '总播放量'), _buildRank( item.musicRelation, '使用稿件量', - theme, () => Get.to( const MusicRecommendPage(), arguments: (id: controller.musicId, item: item), @@ -587,7 +586,7 @@ class _MusicDetailPageState extends CommonDynPageState { ); } - Widget? _buildChart(ThemeData theme, MusicDetail item, double maxWidth) { + Widget? _buildChart(MusicDetail item, double maxWidth) { final heat = item.hotSongHeat?.songHeat; if (heat == null || heat.isEmpty) return null; final colorScheme = theme.colorScheme; @@ -689,16 +688,13 @@ class _MusicDetailPageState extends CommonDynPageState { ); } - Future _searchMusic(MusicDetail item) async { - final res = - Platform.isAndroid && - (await Utils.channel.invokeMethod('music', { - 'title': item.musicTitle, - 'artist': item.originArtist ?? item.originArtistList, - 'album': item.album, - }) ?? - false); - if (!res) { + void _searchMusic(MusicDetail item) { + if (!Platform.isAndroid || + !PiliAndroidHelper.openMusic( + item.musicTitle!, + item.originArtist ?? item.originArtistList, + item.album, + )) { Utils.copyText(item.musicTitle!); } } diff --git a/lib/pages/music/widget/music_video_card_h.dart b/lib/pages/music/widget/music_video_card_h.dart index b9cc608911..0ac1ff9a14 100644 --- a/lib/pages/music/widget/music_video_card_h.dart +++ b/lib/pages/music/widget/music_video_card_h.dart @@ -1,6 +1,5 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/marquee.dart'; @@ -13,7 +12,7 @@ import 'package:PiliPlus/models_new/video/video_detail/dimension.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; class MusicVideoCardH extends StatelessWidget { final BgmRecommend videoItem; diff --git a/lib/pages/my_reply/view.dart b/lib/pages/my_reply/view.dart index a10553dc7e..1136d85174 100644 --- a/lib/pages/my_reply/view.dart +++ b/lib/pages/my_reply/view.dart @@ -15,9 +15,7 @@ import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/waterfall.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; -import 'package:get/get_core/src/get_main.dart'; -import 'package:get/get_navigation/src/extension_navigation.dart'; -import 'package:get/get_rx/get_rx.dart'; +import 'package:get/get.dart'; import 'package:waterfall_flow/waterfall_flow.dart'; class MyReply extends StatefulWidget { diff --git a/lib/pages/pgc/widgets/pgc_card_v.dart b/lib/pages/pgc/widgets/pgc_card_v.dart index 20774983d3..1732d32138 100644 --- a/lib/pages/pgc/widgets/pgc_card_v.dart +++ b/lib/pages/pgc/widgets/pgc_card_v.dart @@ -1,13 +1,12 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models_new/fav/fav_pgc/list.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; // 视频卡片 - 垂直布局 class PgcCardV extends StatelessWidget { diff --git a/lib/pages/pgc/widgets/pgc_card_v_timeline.dart b/lib/pages/pgc/widgets/pgc_card_v_timeline.dart index d7b8410e56..b690992de8 100644 --- a/lib/pages/pgc/widgets/pgc_card_v_timeline.dart +++ b/lib/pages/pgc/widgets/pgc_card_v_timeline.dart @@ -1,13 +1,12 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models_new/pgc/pgc_timeline/episode.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; // 视频卡片 - 垂直布局 class PgcCardVTimeline extends StatelessWidget { diff --git a/lib/pages/pgc_index/widgets/pgc_card_v_pgc_index.dart b/lib/pages/pgc_index/widgets/pgc_card_v_pgc_index.dart index 33ba163225..92d68566e3 100644 --- a/lib/pages/pgc_index/widgets/pgc_card_v_pgc_index.dart +++ b/lib/pages/pgc_index/widgets/pgc_card_v_pgc_index.dart @@ -1,13 +1,12 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models_new/pgc/pgc_index_result/list.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; // 视频卡片 - 垂直布局 class PgcCardVPgcIndex extends StatelessWidget { diff --git a/lib/pages/pgc_review/child/controller.dart b/lib/pages/pgc_review/child/controller.dart index 92b5daa293..91eec433b0 100644 --- a/lib/pages/pgc_review/child/controller.dart +++ b/lib/pages/pgc_review/child/controller.dart @@ -14,9 +14,9 @@ class PgcReviewController final PgcReviewType type; final dynamic mediaId; - Rx count = Rx(null); + final count = RxnInt(); String? next; - Rx sortType = PgcReviewSortType.def.obs; + final sortType = PgcReviewSortType.def.obs; @override void onInit() { diff --git a/lib/pages/pgc_review/child/view.dart b/lib/pages/pgc_review/child/view.dart index a8536acef7..70d5f7173f 100644 --- a/lib/pages/pgc_review/child/view.dart +++ b/lib/pages/pgc_review/child/view.dart @@ -2,6 +2,7 @@ import 'package:PiliPlus/common/skeleton/video_reply.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; @@ -13,11 +14,10 @@ import 'package:PiliPlus/models_new/pgc/pgc_review/list.dart'; import 'package:PiliPlus/pages/pgc_review/child/controller.dart'; import 'package:PiliPlus/pages/pgc_review/post/view.dart'; import 'package:PiliPlus/utils/accounts.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; +import 'package:PiliPlus/utils/bili_utils.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:get/get.dart'; @@ -121,71 +121,56 @@ class _PgcReviewChildPageState extends State Widget _itemWidget(ThemeData theme, int index, PgcReviewItemModel item) { void showMore() => showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - if (item.author!.mid == Accounts.main.mid) ...[ - ListTile( - dense: true, - title: const Text( - '编辑', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - showModalBottomSheet( - context: context, - useSafeArea: true, - isScrollControlled: true, - builder: (context) { - return PgcReviewPostPanel( - name: widget.name, - mediaId: widget.mediaId, - reviewId: item.reviewId, - content: item.content, - score: item.score, - ); - }, - ); - }, - ), - ListTile( - dense: true, - title: const Text( - '删除', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - showConfirmDialog( - context: context, - title: const Text('删除短评,同时删除评分?'), - onConfirm: () => _controller.onDel(index, item.reviewId!), - ); - }, - ), - ], - ListTile( - dense: true, - title: const Text( - '举报', - style: TextStyle(fontSize: 14), - ), - onTap: () => Get - ..back() - ..toNamed( - '/webview', - parameters: { - 'url': - 'https://www.bilibili.com/appeal/?reviewId=${item.reviewId}&type=shortComment&mediaId=${widget.mediaId}', + children: [ + if (item.author!.mid == Accounts.main.mid) ...[ + DialogOption( + child: const Text('编辑', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + showModalBottomSheet( + context: context, + useSafeArea: true, + isScrollControlled: true, + builder: (context) { + return PgcReviewPostPanel( + name: widget.name, + mediaId: widget.mediaId, + reviewId: item.reviewId, + content: item.content, + score: item.score, + ); }, - ), + ); + }, + ), + DialogOption( + child: const Text('删除', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + showConfirmDialog( + context: context, + title: const Text('删除短评,同时删除评分?'), + onConfirm: () => _controller.onDel(index, item.reviewId!), + ); + }, ), ], - ), + DialogOption( + child: const Text('举报', style: TextStyle(fontSize: 14)), + onPressed: () => Get + ..back() + ..toNamed( + '/webview', + parameters: { + 'url': + 'https://www.bilibili.com/appeal/?reviewId=${item.reviewId}&type=shortComment&mediaId=${widget.mediaId}', + }, + ), + ), + ], ), ); @@ -244,10 +229,9 @@ class _PgcReviewChildPageState extends State fontSize: 13, ), ), - Image.asset( - Utils.levelName(item.author!.level!), + BiliUtils.levelPicture( + item.author!.level!, height: 11, - cacheHeight: 11.cacheSize(context), ), ], ), @@ -325,7 +309,12 @@ class _PgcReviewChildPageState extends State SizedBox( height: 32, child: TextButton( - style: style, + style: const ButtonStyle( + visualDensity: .compact, + tapTargetSize: .shrinkWrap, + padding: WidgetStatePropertyAll(.zero), + minimumSize: WidgetStatePropertyAll(.square(40)), + ), onPressed: () => _controller.onDislike( item, isDislike, diff --git a/lib/pages/pgc_review/view.dart b/lib/pages/pgc_review/view.dart index 1b3446a349..62fd58ef42 100644 --- a/lib/pages/pgc_review/view.dart +++ b/lib/pages/pgc_review/view.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/models/common/pgc_review_type.dart'; import 'package:PiliPlus/pages/pgc_review/child/controller.dart'; import 'package:PiliPlus/pages/pgc_review/child/view.dart'; @@ -115,51 +116,40 @@ class _PgcReviewPageState extends State child: FloatingActionButton( onPressed: () => showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - title: const Text( - '写短评', - style: TextStyle(fontSize: 14), + children: [ + DialogOption( + child: const Text('写短评', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + showModalBottomSheet( + context: context, + useSafeArea: true, + isScrollControlled: true, + builder: (context) { + return PgcReviewPostPanel( + name: widget.name, + mediaId: widget.mediaId, + ); + }, + ); + }, + ), + DialogOption( + child: const Text('写长评', style: TextStyle(fontSize: 14)), + onPressed: () => Get + ..back() + ..toNamed( + '/webview', + parameters: { + 'url': + 'https://member.bilibili.com/article-text/mobile?theme=${theme.isDark ? 1 : 0}&media_id=${widget.mediaId}', + }, ), - onTap: () { - Get.back(); - showModalBottomSheet( - context: context, - useSafeArea: true, - isScrollControlled: true, - builder: (context) { - return PgcReviewPostPanel( - name: widget.name, - mediaId: widget.mediaId, - ); - }, - ); - }, - ), - ListTile( - dense: true, - title: const Text( - '写长评', - style: TextStyle(fontSize: 14), - ), - onTap: () => Get - ..back() - ..toNamed( - '/webview', - parameters: { - 'url': - 'https://member.bilibili.com/article-text/mobile?theme=${theme.brightness.isDark ? 1 : 0}&media_id=${widget.mediaId}', - }, - ), - ), - ], - ), + ), + ], ), ), child: const Icon(Icons.edit), diff --git a/lib/pages/popular_series/controller.dart b/lib/pages/popular_series/controller.dart index cf58a5451e..1657fb66a3 100644 --- a/lib/pages/popular_series/controller.dart +++ b/lib/pages/popular_series/controller.dart @@ -14,7 +14,7 @@ class PopularSeriesController with ReloadMixin { late int number; - final Rx config = Rx(null); + final config = Rxn(); String? reminder; List? seriesList; diff --git a/lib/pages/rank/zone/widget/pgc_rank_item.dart b/lib/pages/rank/zone/widget/pgc_rank_item.dart index d821e50651..392f690548 100644 --- a/lib/pages/rank/zone/widget/pgc_rank_item.dart +++ b/lib/pages/rank/zone/widget/pgc_rank_item.dart @@ -1,5 +1,4 @@ import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; @@ -7,7 +6,7 @@ import 'package:PiliPlus/models/common/stat_type.dart'; import 'package:PiliPlus/models_new/pgc/pgc_rank/pgc_rank_item_model.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; class PgcRankItem extends StatelessWidget { const PgcRankItem({super.key, required this.item}); diff --git a/lib/pages/rcmd/controller.dart b/lib/pages/rcmd/controller.dart index 061e6d69c9..2fa55d6c3a 100644 --- a/lib/pages/rcmd/controller.dart +++ b/lib/pages/rcmd/controller.dart @@ -10,6 +10,9 @@ class RcmdController extends CommonListController { int? lastRefreshAt; late bool savedRcmdTip = Pref.savedRcmdTip; + @override + bool get isEnd => false; + @override void onInit() { super.onInit(); @@ -24,6 +27,11 @@ class RcmdController extends CommonListController { : VideoHttp.rcmdVideoList(freshIdx: page, ps: 20); } + @override + bool handleError(String? errMsg) { + return enableSaveLastData; + } + @override void handleListResponse(List dataList) { if (enableSaveLastData && page == 0) { diff --git a/lib/pages/save_panel/view.dart b/lib/pages/save_panel/view.dart index fd2af540a0..ec485597f4 100644 --- a/lib/pages/save_panel/view.dart +++ b/lib/pages/save_panel/view.dart @@ -19,6 +19,7 @@ import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:PiliPlus/utils/share_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; @@ -90,7 +91,7 @@ class _SavePanelState extends State { final currentRoute = Get.currentRoute; late final hasRoot = reply.hasRoot(); - if (currentRoute.startsWith('/video')) { + if (currentRoute == '/videoV') { final rootId = hasRoot ? reply.root : reply.id; uri = @@ -312,7 +313,7 @@ class _SavePanelState extends State { mimeType: 'image/png', ), ], - sharePositionOrigin: await Utils.sharePositionOrigin, + sharePositionOrigin: await ShareUtils.sharePositionOrigin, ), ); } else { @@ -490,7 +491,7 @@ class _SavePanelState extends State { height: 88, margin: const .all(12), padding: const .all(3), - color: theme.brightness.isDark + color: theme.isDark ? Colors.white : theme.colorScheme.surface, child: PrettyQrView.data( diff --git a/lib/pages/search/view.dart b/lib/pages/search/view.dart index 7f7500deee..6afb0bfa46 100644 --- a/lib/pages/search/view.dart +++ b/lib/pages/search/view.dart @@ -14,7 +14,7 @@ import 'package:PiliPlus/utils/extension/size_ext.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; class SearchPage extends StatefulWidget { diff --git a/lib/pages/search/widgets/hot_keyword.dart b/lib/pages/search/widgets/hot_keyword.dart index 20cf6a7e1f..d0e53adeb7 100644 --- a/lib/pages/search/widgets/hot_keyword.dart +++ b/lib/pages/search/widgets/hot_keyword.dart @@ -2,7 +2,7 @@ import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/models_new/search/search_trending/list.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show @@ -135,12 +135,10 @@ class _RenderHotKeywordGrid extends RenderBox ContainerRenderObjectMixin, RenderBoxContainerDefaultsMixin { _RenderHotKeywordGrid({ - required int crossAxisCount, - required double mainAxisSpacing, - required double crossAxisSpacing, - }) : _crossAxisCount = crossAxisCount, - _mainAxisSpacing = mainAxisSpacing, - _crossAxisSpacing = crossAxisSpacing; + required this._crossAxisCount, + required this._mainAxisSpacing, + required this._crossAxisSpacing, + }); int _crossAxisCount; int get crossAxisCount => _crossAxisCount; diff --git a/lib/pages/search_panel/all/view.dart b/lib/pages/search_panel/all/view.dart index 5e346822d9..fdee2e4a5e 100644 --- a/lib/pages/search_panel/all/view.dart +++ b/lib/pages/search_panel/all/view.dart @@ -104,7 +104,7 @@ class _SearchAllPanelState @override Widget get buildLoading => SliverGrid.builder( - gridDelegate: Grid.videoCardHDelegate(context), + gridDelegate: Grid.videoCardHDelegate(), itemBuilder: (context, index) => const VideoCardHSkeleton(), itemCount: 10, ); diff --git a/lib/pages/search_panel/all/widgets/pgc_card_v_search.dart b/lib/pages/search_panel/all/widgets/pgc_card_v_search.dart index 6ef7c92925..0645851472 100644 --- a/lib/pages/search_panel/all/widgets/pgc_card_v_search.dart +++ b/lib/pages/search_panel/all/widgets/pgc_card_v_search.dart @@ -1,12 +1,11 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/search/result.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; // 视频卡片 - 垂直布局 class PgcCardVSearch extends StatelessWidget { diff --git a/lib/pages/search_panel/article/widgets/item.dart b/lib/pages/search_panel/article/widgets/item.dart index 0d42347b80..85dda2ecc1 100644 --- a/lib/pages/search_panel/article/widgets/item.dart +++ b/lib/pages/search_panel/article/widgets/item.dart @@ -1,11 +1,10 @@ import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/search/result.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; class SearchArticleItem extends StatelessWidget { diff --git a/lib/pages/search_panel/live/widgets/item.dart b/lib/pages/search_panel/live/widgets/item.dart index b52ac14e4f..1e2794e1f8 100644 --- a/lib/pages/search_panel/live/widgets/item.dart +++ b/lib/pages/search_panel/live/widgets/item.dart @@ -1,12 +1,11 @@ import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models/search/result.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; class LiveItem extends StatelessWidget { final SearchLiveItemModel liveItem; diff --git a/lib/pages/search_panel/pgc/view.dart b/lib/pages/search_panel/pgc/view.dart index 154f41098e..bfa411ceea 100644 --- a/lib/pages/search_panel/pgc/view.dart +++ b/lib/pages/search_panel/pgc/view.dart @@ -70,7 +70,6 @@ class _SearchPgcPanelState mainAxisSpacing: 2, maxCrossAxisExtent: Grid.smallCardWidth * 2, childAspectRatio: Style.aspectRatio * 1.5, - minHeight: MediaQuery.textScalerOf(context).scale(155), ), itemBuilder: (context, index) => const MediaPgcSkeleton(), itemCount: 10, diff --git a/lib/pages/search_panel/user/widgets/item.dart b/lib/pages/search_panel/user/widgets/item.dart index 97e0a86df6..4fc1d7ac9a 100644 --- a/lib/pages/search_panel/user/widgets/item.dart +++ b/lib/pages/search_panel/user/widgets/item.dart @@ -1,8 +1,7 @@ import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; import 'package:PiliPlus/models/search/result.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; +import 'package:PiliPlus/utils/bili_utils.dart'; import 'package:PiliPlus/utils/num_utils.dart'; -import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get_core/src/get_main.dart'; import 'package:get/get_navigation/src/extension_navigation.dart'; @@ -52,14 +51,10 @@ class SearchUserItem extends StatelessWidget { ), ), const SizedBox(width: 6), - Image.asset( - Utils.levelName( - item.level!, - isSeniorMember: item.isSeniorMember == 1, - ), + BiliUtils.levelPicture( + item.level!, + isSeniorMember: item.isSeniorMember == 1, height: 11, - cacheHeight: 11.cacheSize(context), - semanticLabel: '等级${item.level}', ), ], ), diff --git a/lib/pages/search_trending/view.dart b/lib/pages/search_trending/view.dart index d557566711..0d33f60fea 100644 --- a/lib/pages/search_trending/view.dart +++ b/lib/pages/search_trending/view.dart @@ -8,13 +8,13 @@ import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/models_new/search/search_trending/list.dart'; import 'package:PiliPlus/pages/search_trending/controller.dart'; +import 'package:PiliPlus/utils/color_utils.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; import 'package:PiliPlus/utils/extension/get_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; -import 'package:PiliPlus/utils/utils.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' hide ListTile; import 'package:flutter/services.dart' show SystemUiOverlayStyle; @@ -181,7 +181,7 @@ class _SearchTrendingPageState extends State { '${index + 1 - _controller.topCount}', style: TextStyle( fontWeight: FontWeight.bold, - color: Utils.index2Color( + color: ColourUtils.index2Color( index - _controller.topCount, theme.colorScheme.outline, ), diff --git a/lib/pages/setting/common_setting.dart b/lib/pages/setting/common_setting.dart new file mode 100644 index 0000000000..abb6052843 --- /dev/null +++ b/lib/pages/setting/common_setting.dart @@ -0,0 +1,65 @@ +import 'package:PiliPlus/models/common/setting_type.dart'; +import 'package:PiliPlus/pages/setting/models/model.dart'; +import 'package:flutter/material.dart'; + +class CommonSetting extends StatefulWidget { + const CommonSetting({ + super.key, + required this.settingType, + this.showAppBar = true, + }); + + final bool showAppBar; + final SettingType settingType; + + @override + State createState() => _CommonSettingState(); +} + +class _CommonSettingState extends State { + late EdgeInsets padding; + late List settings; + + void _initSetting() { + settings = widget.settingType.settings; + } + + @override + void initState() { + super.initState(); + _initSetting(); + } + + @override + void didUpdateWidget(CommonSetting oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.settingType != oldWidget.settingType) { + _initSetting(); + } + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + padding = MediaQuery.viewPaddingOf(context); + } + + @override + Widget build(BuildContext context) { + final showAppBar = widget.showAppBar; + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: showAppBar ? AppBar(title: Text(widget.settingType.title)) : null, + body: ListView.builder( + key: ValueKey(widget.settingType), + padding: EdgeInsets.only( + left: showAppBar ? padding.left : 0, + right: showAppBar ? padding.right : 0, + bottom: padding.bottom + 100, + ), + itemCount: settings.length, + itemBuilder: (context, index) => settings[index].widget, + ), + ); + } +} diff --git a/lib/pages/setting/extra_setting.dart b/lib/pages/setting/extra_setting.dart deleted file mode 100644 index 0c6343e0c4..0000000000 --- a/lib/pages/setting/extra_setting.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:PiliPlus/pages/setting/models/extra_settings.dart'; -import 'package:flutter/material.dart'; - -class ExtraSetting extends StatefulWidget { - const ExtraSetting({super.key, this.showAppBar = true}); - - final bool showAppBar; - - @override - State createState() => _ExtraSettingState(); -} - -class _ExtraSettingState extends State { - final settings = extraSettings; - - @override - Widget build(BuildContext context) { - final showAppBar = widget.showAppBar; - final padding = MediaQuery.viewPaddingOf(context); - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: showAppBar ? AppBar(title: const Text('其它设置')) : null, - body: ListView.builder( - padding: EdgeInsets.only( - left: showAppBar ? padding.left : 0, - right: showAppBar ? padding.right : 0, - bottom: padding.bottom + 100, - ), - itemCount: settings.length, - itemBuilder: (context, index) => settings[index].widget, - ), - ); - } -} diff --git a/lib/pages/setting/models/extra_settings.dart b/lib/pages/setting/models/extra_settings.dart index d49caa3dd2..809c8bb59f 100644 --- a/lib/pages/setting/models/extra_settings.dart +++ b/lib/pages/setting/models/extra_settings.dart @@ -1,10 +1,11 @@ import 'dart:io'; -import 'dart:math' show pi, max; +import 'dart:math' show max; import 'package:PiliPlus/common/widgets/custom_icon.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/flutter/refresh_indicator.dart'; import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart' - show touchSlopH; + show deviceTouchSlop, touchSlopH; import 'package:PiliPlus/common/widgets/image_grid/image_grid_view.dart' show ImageGridView, ImageModel; import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; @@ -32,6 +33,7 @@ import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/cache_manager.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/feed_back.dart'; +import 'package:PiliPlus/utils/filtering_text.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/path_utils.dart'; @@ -74,14 +76,7 @@ List get extraSettings => [ normalModel: const NormalModel.split( title: '空降助手', subtitle: '点击配置', - leading: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - Icon(Icons.shield_outlined), - Icon(Icons.play_arrow_rounded, size: 15), - ], - ), + leading: Icon(CustomIcons.shield_play_arrow), ), switchModel: SwitchModel.split( defaultVal: false, @@ -111,12 +106,9 @@ List get extraSettings => [ onTap: _showDynDialog, ), ), - SwitchModel( + const SwitchModel( title: '显示视频分段信息', - leading: Transform.rotate( - angle: pi / 2, - child: const Icon(MdiIcons.viewHeadline), - ), + leading: Icon(CustomIcons.view_headline_rotate_90), setKey: SettingBoxKey.showViewPoints, defaultVal: true, ), @@ -246,7 +238,7 @@ List get extraSettings => [ ), NormalModel( title: '横向滑动阈值', - getSubtitle: () => '当前:「${Pref.touchSlopH}」', + getSubtitle: () => '当前:「${Pref.touchSlopH}」,系统默认值: $deviceTouchSlop', onTap: _showTouchSlopDialog, leading: const Icon(Icons.pan_tool_alt_outlined), ), @@ -380,14 +372,7 @@ List get extraSettings => [ const SwitchModel( title: '发评反诈', subtitle: '发送评论后检查评论是否可见', - leading: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - Icon(Icons.shield_outlined), - Icon(Icons.reply, size: 14), - ], - ), + leading: Icon(CustomIcons.shield_reply), setKey: SettingBoxKey.enableCommAntifraud, defaultVal: false, ), @@ -404,51 +389,27 @@ List get extraSettings => [ const SwitchModel( title: '发布/转发动态反诈', subtitle: '发布/转发动态后检查动态是否可见', - leading: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - Icon(Icons.shield_outlined), - Icon(Icons.motion_photos_on, size: 12), - ], - ), + leading: Icon(CustomIcons.shield_published), setKey: SettingBoxKey.enableCreateDynAntifraud, defaultVal: false, ), SwitchModel( title: '屏蔽带货动态', - leading: const Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - Icon(Icons.shopping_bag_outlined, size: 14), - Icon(Icons.not_interested), - ], - ), + leading: const Icon(CustomIcons.shopping_bag_not_interested), setKey: SettingBoxKey.antiGoodsDyn, defaultVal: false, onChanged: (value) => DynamicsDataModel.antiGoodsDyn = value, ), SwitchModel( title: '屏蔽带货评论', - leading: const Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - Icon(Icons.shopping_bag_outlined, size: 14), - Icon(Icons.not_interested), - ], - ), + leading: const Icon(CustomIcons.shopping_bag_not_interested), setKey: SettingBoxKey.antiGoodsReply, defaultVal: false, onChanged: (value) => ReplyGrpc.antiGoodsReply = value, ), SwitchModel( title: '侧滑关闭二级页面', - leading: Transform.rotate( - angle: pi * 1.5, - child: const Icon(Icons.touch_app), - ), + leading: const Icon(CustomIcons.touch_app_rotate_270), setKey: SettingBoxKey.slideDismissReplyPage, defaultVal: Platform.isIOS, onChanged: (value) => CommonSlideMixin.slideDismissReplyPage = value, @@ -635,19 +596,10 @@ List get extraSettings => [ onTap: _showProxyDialog, ), ), - const SwitchModel( - title: '自动清除缓存', - subtitle: '每次启动时清除缓存', - leading: Icon(Icons.auto_delete_outlined), - setKey: SettingBoxKey.autoClearCache, - defaultVal: false, - ), NormalModel( title: '最大缓存大小', - getSubtitle: () { - final num = Pref.maxCacheSize; - return '当前最大缓存大小: 「${num == 0 ? '无限' : CacheManager.formatSize(Pref.maxCacheSize)}」'; - }, + getSubtitle: () => + '当前最大缓存大小: 「${CacheManager.formatSize(Pref.maxCacheSize)}」', leading: const Icon(Icons.delete_outlined), onTap: _showCacheDialog, ), @@ -761,48 +713,42 @@ Future audioNormalization( void _showDownPathDialog(BuildContext context, VoidCallback setState) { showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - onTap: () { - Get.back(); - Utils.copyText(downloadPath); - }, - dense: true, - title: const Text('复制', style: TextStyle(fontSize: 14)), - ), - ListTile( - onTap: () { - Get.back(); - final defPath = defDownloadPath; - if (downloadPath == defPath) return; - downloadPath = defPath; - setState(); - Get.find().initDownloadList(); - GStorage.setting.delete(SettingBoxKey.downloadPath); - }, - dense: true, - title: const Text('重置', style: TextStyle(fontSize: 14)), - ), - ListTile( - onTap: () async { - Get.back(); - final path = await FilePicker.getDirectoryPath(); - if (path == null || path == downloadPath) return; - downloadPath = path; - setState(); - Get.find().initDownloadList(); - GStorage.setting.put(SettingBoxKey.downloadPath, path); - }, - dense: true, - title: const Text('设置新路径', style: TextStyle(fontSize: 14)), - ), - ], - ), + children: [ + DialogOption( + onPressed: () { + Get.back(); + Utils.copyText(downloadPath); + }, + child: const Text('复制', style: TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () { + Get.back(); + final defPath = defDownloadPath; + if (downloadPath == defPath) return; + downloadPath = defPath; + setState(); + Get.find().initDownloadList(); + GStorage.setting.delete(SettingBoxKey.downloadPath); + }, + child: const Text('重置', style: TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () async { + Get.back(); + final path = await FilePicker.getDirectoryPath(); + if (path == null || path == downloadPath) return; + downloadPath = path; + setState(); + Get.find().initDownloadList(); + GStorage.setting.put(SettingBoxKey.downloadPath, path); + }, + child: const Text('设置新路径', style: TextStyle(fontSize: 14)), + ), + ], ), ); } @@ -899,9 +845,7 @@ void _showDmHeightDialog(BuildContext context, VoidCallback setState) { initialValue: danmakuLineHeight, keyboardType: const .numberWithOptions(decimal: true), onChanged: (value) => danmakuLineHeight = value, - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'[\d\.]+')), - ], + inputFormatters: FilteringText.decimal, ), actions: [ TextButton( @@ -943,9 +887,7 @@ void _showTouchSlopDialog(BuildContext context, VoidCallback setState) { initialValue: initialValue, keyboardType: const .numberWithOptions(decimal: true), onChanged: (value) => initialValue = value, - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'[\d\.]+')), - ], + inputFormatters: FilteringText.decimal, ), actions: [ TextButton( @@ -981,7 +923,7 @@ Future _showRefreshDragDialog( final res = await showDialog( context: context, builder: (context) => SliderDialog( - title: '刷新滑动距离', + title: const Text('刷新滑动距离'), min: 0.1, max: 0.5, divisions: 8, @@ -1004,7 +946,7 @@ Future _showRefreshDialog( final res = await showDialog( context: context, builder: (context) => SliderDialog( - title: '刷新指示器高度', + title: const Text('刷新指示器高度'), min: 10.0, max: 100.0, divisions: 9, @@ -1104,7 +1046,7 @@ Future _showReplyCountDialog( final res = await showDialog( context: context, builder: (context) => SliderDialog( - title: '连接重试次数', + title: const Text('连接重试次数'), min: 0, max: 8, divisions: 8, @@ -1126,7 +1068,7 @@ Future _showReplyDelayDialog( final res = await showDialog( context: context, builder: (context) => SliderDialog( - title: '连接重试间隔', + title: const Text('连接重试间隔'), min: 0, max: 1000, divisions: 10, @@ -1229,9 +1171,7 @@ void _showProxyDialog(BuildContext context) { decoration: const InputDecoration( isDense: true, labelText: '请输入Port', - border: OutlineInputBorder( - borderRadius: BorderRadius.all(Radius.circular(6)), - ), + border: OutlineInputBorder(borderRadius: .all(.circular(6))), ), inputFormatters: [FilteringTextInputFormatter.digitsOnly], onChanged: (e) => systemProxyPort = e, @@ -1275,9 +1215,7 @@ void _showCacheDialog(BuildContext context, VoidCallback setState) { autofocus: true, onChanged: (value) => valueStr = value, keyboardType: TextInputType.number, - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'[\d\.]+')), - ], + inputFormatters: FilteringText.decimal, decoration: const InputDecoration(suffixText: 'MB'), ), actions: [ diff --git a/lib/pages/setting/models/play_settings.dart b/lib/pages/setting/models/play_settings.dart index c21977fb30..b5943d815b 100644 --- a/lib/pages/setting/models/play_settings.dart +++ b/lib/pages/setting/models/play_settings.dart @@ -1,15 +1,18 @@ -import 'dart:io'; +import 'dart:io' show Platform; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/models/common/super_chat_type.dart'; import 'package:PiliPlus/models/common/video/subtitle_pref_type.dart'; import 'package:PiliPlus/pages/main/controller.dart'; import 'package:PiliPlus/pages/setting/models/model.dart'; +import 'package:PiliPlus/pages/setting/pages/fullscreen_sc_size.dart'; import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart'; +import 'package:PiliPlus/pages/setting/widgets/slider_dialog.dart'; import 'package:PiliPlus/plugin/pl_player/models/bottom_progress_behavior.dart'; import 'package:PiliPlus/plugin/pl_player/models/fullscreen_mode.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; import 'package:PiliPlus/services/service_locator.dart'; +import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; @@ -41,6 +44,13 @@ List get playSettings => [ title: '倍速设置', subtitle: '设置视频播放速度', ), + if (Platform.isAndroid) + NormalModel( + onTap: _showAngleDegreesDialog, + leading: const Icon(MdiIcons.angleAcute), + title: '倾斜角度阈值', + getSubtitle: () => '当前:「${Pref.angleDegrees}°」', + ), const SwitchModel( title: '自动播放', subtitle: '进入详情页自动播放', @@ -92,6 +102,20 @@ List get playSettings => [ setKey: SettingBoxKey.enableSlideFS, defaultVal: true, ), + if (PlatformUtils.isMobile) + NormalModel( + title: '播放器音量', + leading: const Icon(Icons.volume_up), + getSubtitle: () => '当前:「${Pref.playerVolume.toStringAsFixed(0)}%」', + onTap: showPlayerVolumeDialog, + ) + else + NormalModel( + title: '最高音量', + leading: const Icon(Icons.volume_up), + getSubtitle: () => '当前:「${(Pref.maxVolume * 100).toStringAsFixed(0)}%」', + onTap: _showMaxVolumeDialog, + ), getVideoFilterSelectModel( title: '双击快进/快退时长', suffix: 's', @@ -145,6 +169,12 @@ List get playSettings => [ getSubtitle: () => '当前:「${Pref.superChatType.title}」', onTap: _showSuperChatDialog, ), + NormalModel( + title: '全屏 SC 大小', + subtitle: 'SuperChat (醒目留言) 大小设置', + leading: const Icon(Icons.open_in_full), + onTap: (_, _) => Get.to(const FullScreenScSize()), + ), const SwitchModel( title: '竖屏扩大展示', subtitle: '小屏竖屏视频宽高比由16:9扩大至1:1(不支持收起);横屏适配时,扩大至9:16', @@ -339,3 +369,89 @@ Future _showProgressBehaviorDialog( setState(); } } + +Future _showAngleDegreesDialog( + BuildContext context, + VoidCallback setState, +) async { + final res = await showDialog( + context: context, + builder: (context) => SliderDialog( + title: const Text('倾斜角度阈值'), + min: 10.0, + max: 90.0, + divisions: 90, + precise: 0, + value: Pref.angleDegrees.toDouble(), + suffix: '°', + ), + ); + if (res != null) { + await GStorage.setting.put(SettingBoxKey.angleDegrees, res.toInt()); + setState(); + } +} + +Future showPlayerVolumeDialog( + BuildContext context, + VoidCallback setState, { + ValueChanged? onChanged, +}) { + return showVolumeDialog( + context, + title: const Text('播放器音量'), + value: Pref.playerVolume, + onChanged: (value) => GStorage.setting + .put(SettingBoxKey.playerVolume, value) + .whenComplete(() { + setState(); + onChanged?.call(value); + }), + ); +} + +Future _showMaxVolumeDialog( + BuildContext context, + VoidCallback setState, +) { + return showVolumeDialog( + context, + title: const Text('最高音量'), + value: Pref.maxVolume * 100, + onChanged: (rawValue) { + final maxVolume = (rawValue / 100).toPrecision(2); + if (Pref.desktopVolume > maxVolume) { + GStorage.setting.put(SettingBoxKey.desktopVolume, maxVolume); + } + GStorage.setting + .put(SettingBoxKey.maxVolume, maxVolume) + .whenComplete(setState); + }, + ); +} + +const kMinVolume = 100.0; +const kMaxVolume = 300.0; + +Future showVolumeDialog( + BuildContext context, { + required Widget title, + required double value, + required ValueChanged onChanged, +}) async { + final res = await showDialog( + context: context, + builder: (context) => SliderDialog( + title: title, + min: kMinVolume, + max: kMaxVolume, + divisions: 40, + precise: 0, + value: value, + suffix: '%', + ), + ); + if (res != null) { + onChanged(res); + } +} diff --git a/lib/pages/setting/models/privacy_settings.dart b/lib/pages/setting/models/privacy_settings.dart index 9aeaf3e751..3734b72bfc 100644 --- a/lib/pages/setting/models/privacy_settings.dart +++ b/lib/pages/setting/models/privacy_settings.dart @@ -1,5 +1,4 @@ import 'package:PiliPlus/models/common/account_type.dart'; -import 'package:PiliPlus/pages/mine/controller.dart'; import 'package:PiliPlus/pages/setting/models/model.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts/api_type.dart'; @@ -20,26 +19,13 @@ List get privacySettings => [ subtitle: '已拉黑用户', leading: const Icon(Icons.block), ), - NormalModel( - onTap: (context, setState) { - MineController.onChangeAnonymity(); - setState(); - }, - leading: const Icon(Icons.privacy_tip_outlined), - getTitle: () => MineController.anonymity.value ? '退出无痕模式' : '进入无痕模式', - getSubtitle: () => MineController.anonymity.value - ? '已进入无痕模式,搜索、观看视频/直播不携带Cookie与CSRF,其余操作不受影响' - : '未开启无痕模式,将使用账户信息提供完整服务', - ), NormalModel( onTap: (context, setState) { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('账号模式详情'), - content: SingleChildScrollView( - child: _getAccountDetail(context), - ), + content: SingleChildScrollView(child: _getAccountDetail(context)), actions: [ TextButton( onPressed: Get.back, diff --git a/lib/pages/setting/models/recommend_settings.dart b/lib/pages/setting/models/recommend_settings.dart index 256d274682..84f4b584e1 100644 --- a/lib/pages/setting/models/recommend_settings.dart +++ b/lib/pages/setting/models/recommend_settings.dart @@ -93,8 +93,8 @@ List get recommendSettings => [ onChanged: (value) => RecommendFilter.exemptFilterForFollowed = value, ), SwitchModel( - title: '过滤器也应用于相关视频', - subtitle: '视频详情页的相关视频也进行过滤¹', + title: '过滤器也应用于详情页相关视频', + subtitle: '其它(如热门视频、搜索等)均不受过滤器影响,无法豁免相关视频中的已关注UP', leading: const Icon(Icons.explore_outlined), setKey: SettingBoxKey.applyFilterToRelatedVideos, defaultVal: true, diff --git a/lib/pages/setting/models/style_settings.dart b/lib/pages/setting/models/style_settings.dart index 45bdf28263..5be370a614 100644 --- a/lib/pages/setting/models/style_settings.dart +++ b/lib/pages/setting/models/style_settings.dart @@ -7,7 +7,6 @@ import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/scale_app.dart'; import 'package:PiliPlus/common/widgets/stateful_builder.dart'; -import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/models/common/bar_hide_type.dart'; import 'package:PiliPlus/models/common/dynamic/dynamic_badge_mode.dart'; import 'package:PiliPlus/models/common/dynamic/up_panel_position.dart'; @@ -35,6 +34,7 @@ import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:flutter/material.dart' hide StatefulBuilder; import 'package:flutter/services.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -142,16 +142,11 @@ List get styleSettings => [ setKey: SettingBoxKey.removeSafeArea, defaultVal: false, ), - SwitchModel( + const SwitchModel( title: '视频播放页使用深色主题', - leading: const Icon(Icons.dark_mode_outlined), + leading: Icon(Icons.dark_mode_outlined), setKey: SettingBoxKey.darkVideoPage, defaultVal: false, - onChanged: (value) { - if (value && MyApp.darkThemeData == null) { - Get.updateMyAppTheme(); - } - }, ), SwitchModel( title: '动态页启用瀑布流', @@ -225,7 +220,7 @@ List get styleSettings => [ NormalModel( onTap: (context, setState) => _showQualityDialog( context: context, - title: '图片质量', + title: const Text('图片质量'), initValue: Pref.picQuality, onChanged: (picQuality) async { GlobalData().imgQuality = picQuality; @@ -244,7 +239,7 @@ List get styleSettings => [ NormalModel( onTap: (context, setState) => _showQualityDialog( context: context, - title: '查看大图质量', + title: const Text('查看大图质量'), initValue: Pref.previewQ, onChanged: (picQuality) async { await GStorage.setting.put(SettingBoxKey.previewQuality, picQuality); @@ -295,7 +290,7 @@ List get styleSettings => [ setKey: SettingBoxKey.isPureBlackTheme, defaultVal: false, onChanged: (value) { - if (Get.isDarkMode || Pref.darkVideoPage) { + if (ThemeUtils.isDarkMode || Pref.darkVideoPage) { Get.updateMyAppTheme(); } }, @@ -386,7 +381,7 @@ List get styleSettings => [ void _showQualityDialog({ required BuildContext context, - required String title, + required Widget title, required int initValue, required ValueChanged onChanged, }) { @@ -447,9 +442,7 @@ void _showUiScaleDialog( ), TextFormField( controller: textController, - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - ), + keyboardType: const .numberWithOptions(decimal: true), inputFormatters: [ LengthLimitingTextInputFormatter(4), FilteringTextInputFormatter.allow(RegExp(r'[\d.]+')), @@ -642,7 +635,7 @@ Future _showFontWeightDialog(BuildContext context) async { final res = await showDialog( context: context, builder: (context) => SliderDialog( - title: 'App字体字重', + title: const Text('App字体字重'), value: Pref.appFontWeight.toDouble() + 1, min: 1, max: FontWeight.values.length.toDouble(), @@ -668,8 +661,8 @@ Future _showTransitionDialog( ), ); if (res != null) { + Get.rootController.defaultTransition = res; await GStorage.setting.put(SettingBoxKey.pageTransition, res.index); - SmartDialog.showToast('重启生效'); setState(); } } @@ -681,11 +674,11 @@ Future _showCardWidthDialog( final res = await showDialog<(double, double)>( context: context, builder: (context) => DualSliderDialog( - title: '列表最大列宽度(默认240dp)', + title: const Text('列表最大列宽度(默认240dp)'), value1: Pref.recommendCardWidth, value2: Pref.smallCardWidth, - description1: '主页推荐流', - description2: '其他', + description1: const Text('主页推荐流'), + description2: const Text('其他'), min: 150.0, max: 500.0, divisions: 35, @@ -858,7 +851,7 @@ Future _showToastDialog( final res = await showDialog( context: context, builder: (context) => SliderDialog( - title: 'Toast不透明度', + title: const Text('Toast不透明度'), value: CustomToast.toastOpacity, min: 0.0, max: 1.0, @@ -890,7 +883,7 @@ Future _showThemeTypeDialog( Get.find().themeType.value = res; } catch (_) {} GStorage.setting.put(SettingBoxKey.themeMode, res.index); - Get.changeThemeMode(res.toThemeMode); + Get.changeThemeMode(ThemeUtils.themeMode = res.toThemeMode); setState(); } } diff --git a/lib/pages/setting/models/video_settings.dart b/lib/pages/setting/models/video_settings.dart index 138dadcd1e..2136518ea2 100644 --- a/lib/pages/setting/models/video_settings.dart +++ b/lib/pages/setting/models/video_settings.dart @@ -10,6 +10,7 @@ import 'package:PiliPlus/pages/setting/widgets/ordered_multi_select_dialog.dart' import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart'; import 'package:PiliPlus/plugin/pl_player/models/audio_output_type.dart'; import 'package:PiliPlus/plugin/pl_player/models/hwdec_type.dart'; +import 'package:PiliPlus/utils/filtering_text.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; @@ -126,16 +127,11 @@ List get videoSettings => [ NormalModel( title: '首选解码格式', leading: const Icon(Icons.movie_creation_outlined), - getSubtitle: () => - '首选解码格式:${VideoDecodeFormatType.fromCode(Pref.defaultDecode).description},请根据设备支持情况与需求调整', - onTap: _showDecodeDialog, - ), - NormalModel( - title: '次选解码格式', - getSubtitle: () => - '非杜比视频次选:${VideoDecodeFormatType.fromCode(Pref.secondDecode).description},仍无则选择首个提供的解码格式', - leading: const Icon(Icons.swap_horizontal_circle_outlined), - onTap: _showSecondDecodeDialog, + getSubtitle: () { + final list = Pref.preferCodecs; + return '首选解码格式:${(list.isEmpty ? '第一个可用' : list.map((i) => i.name).join(","))},请根据设备支持情况与需求调整'; + }, + onTap: _showCodecsDialog, ), if (kDebugMode || Platform.isAndroid) NormalModel( @@ -144,12 +140,19 @@ List get videoSettings => [ getSubtitle: () => '当前:${Pref.audioOutput}', onTap: _showAudioOutputDialog, ), - const SwitchModel( - title: '扩大缓冲区', - leading: Icon(Icons.storage_outlined), - subtitle: '默认缓冲区为视频4MB/直播16MB,开启后为32MB/64MB,加载时间变长', - setKey: SettingBoxKey.expandBuffer, - defaultVal: false, + NormalModel( + title: '缓冲大小', + leading: const Icon(Icons.storage_outlined), + getSubtitle: () => + '当前:${Pref.bufferSize}MB。同时为前向和后向缓冲区大小。对于直播流,无后向缓冲大小,全部转给前向(此选项即mpv的--demuxer-max-bytes,--demuxer-max-back-bytes)', + onTap: _showBufferSizeDialog, + ), + NormalModel( + title: '缓冲时长', + leading: const Icon(Icons.av_timer), + getSubtitle: () => + '当前:${Pref.bufferSec}s。实际缓冲为二者最小值。对于直播流,该选项无效(此选项即mpv的--cache-secs)', + onTap: _showBufferSecDialog, ), NormalModel( title: '自动同步', @@ -341,42 +344,25 @@ Future _showLiveCellularQaDialog( } } -Future _showDecodeDialog( +Future _showCodecsDialog( BuildContext context, VoidCallback setState, ) async { - final res = await showDialog( + final res = await showDialog>( context: context, - builder: (context) => SelectDialog( - title: '默认解码格式', - value: Pref.defaultDecode, - values: VideoDecodeFormatType.values - .map((e) => (e.codes.first, e.description)) - .toList(), + builder: (context) => OrderedMultiSelectDialog( + title: '首选解码格式', + initValues: Pref.preferCodecs, + values: {for (final e in VideoDecodeFormatType.values) e: e.name}, ), ); if (res != null) { - await GStorage.setting.put(SettingBoxKey.defaultDecode, res); - setState(); - } -} - -Future _showSecondDecodeDialog( - BuildContext context, - VoidCallback setState, -) async { - final res = await showDialog( - context: context, - builder: (context) => SelectDialog( - title: '次选解码格式', - value: Pref.secondDecode, - values: VideoDecodeFormatType.values - .map((e) => (e.codes.first, e.description)) - .toList(), - ), - ); - if (res != null) { - await GStorage.setting.put(SettingBoxKey.secondDecode, res); + await (res.isEmpty + ? GStorage.setting.delete(SettingBoxKey.preferCodecs) + : GStorage.setting.put( + SettingBoxKey.preferCodecs, + res.map((i) => i.name).toList(), + )); setState(); } } @@ -494,3 +480,70 @@ void _showAutoSyncDialog(BuildContext context, VoidCallback setState) { ), ); } + +void _showDecimalDialog( + BuildContext context, + VoidCallback setState, { + required String key, + required double defVal, + required String title, + required String? suffix, +}) { + String value = (GStorage.setting.get(key) ?? defVal).toString(); + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text(title), + content: TextFormField( + autofocus: true, + initialValue: value, + keyboardType: const .numberWithOptions(decimal: true), + onChanged: (val) => value = val, + inputFormatters: FilteringText.decimal, + decoration: suffix == null ? null : InputDecoration(suffixText: suffix), + ), + actions: [ + TextButton( + onPressed: Get.back, + child: Text( + '取消', + style: TextStyle(color: ColorScheme.of(context).outline), + ), + ), + TextButton( + onPressed: () async { + try { + final val = double.parse(value); + Get.back(); + await GStorage.setting.put(key, val); + setState(); + } catch (e) { + SmartDialog.showToast(e.toString()); + } + }, + child: const Text('确定'), + ), + ], + ), + ); +} + +void _showBufferSizeDialog(BuildContext context, VoidCallback setState) => + _showDecimalDialog( + context, + setState, + key: SettingBoxKey.bufferSize, + defVal: Pref.bufferSize, + title: '缓冲大小', + suffix: 'MB', + ); + +void _showBufferSecDialog(BuildContext context, VoidCallback setState) => + _showDecimalDialog( + context, + setState, + key: SettingBoxKey.bufferSec, + defVal: Pref.bufferSec, + title: '缓冲时长', + suffix: 's', + ); diff --git a/lib/pages/setting/pages/bar_set.dart b/lib/pages/setting/pages/bar_set.dart index 0dfaa95f34..422fdfb4f6 100644 --- a/lib/pages/setting/pages/bar_set.dart +++ b/lib/pages/setting/pages/bar_set.dart @@ -1,4 +1,5 @@ import 'package:PiliPlus/common/widgets/pair.dart'; +import 'package:PiliPlus/common/widgets/reorder_mixin.dart'; import 'package:PiliPlus/models/common/enum_with_label.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:flutter/material.dart'; @@ -12,10 +13,11 @@ class BarSetPage extends StatefulWidget { State createState() => _BarSetPageState(); } -class _BarSetPageState extends State { +class _BarSetPageState extends State with ReorderMixin { late final String key; late final String title; late final List> list; + late EdgeInsets padding; @override void initState() { @@ -28,7 +30,7 @@ class _BarSetPageState extends State { .map((e) => Pair(first: e, second: cache?.contains(e.index) ?? true)) .toList(); if (cache != null && cache.isNotEmpty) { - final cacheIndex = {for (final (k, v) in cache.indexed) v: k}; + final cacheIndex = {for (int i = 0; i < cache.length; i++) cache[i]: i}; list.sort((a, b) { final indexA = cacheIndex[a.first.index] ?? cacheIndex.length; final indexB = cacheIndex[b.first.index] ?? cacheIndex.length; @@ -37,6 +39,13 @@ class _BarSetPageState extends State { } } + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final viewPad = MediaQuery.viewPaddingOf(context); + padding = .only(top: 10, right: viewPad.right + 34, bottom: viewPad.bottom); + } + void saveEdit() { GStorage.setting.put( key, @@ -51,8 +60,7 @@ class _BarSetPageState extends State { SmartDialog.showToast('重置成功,下次启动时生效'); } - void onReorder(int oldIndex, int newIndex) { - if (newIndex > oldIndex) newIndex -= 1; + void onReorderItem(int oldIndex, int newIndex) { list.insert(newIndex, list.removeAt(oldIndex)); setState(() {}); } @@ -70,11 +78,10 @@ class _BarSetPageState extends State { ], ), body: ReorderableListView( - onReorder: onReorder, + onReorderItem: onReorderItem, + proxyDecorator: proxyDecorator, footer: Padding( - padding: - MediaQuery.viewPaddingOf(context).copyWith(top: 0, left: 0) + - const EdgeInsets.only(right: 34, top: 10), + padding: padding, child: const Align( alignment: Alignment.centerRight, child: Text('*长按拖动排序'), diff --git a/lib/pages/setting/pages/color_select.dart b/lib/pages/setting/pages/color_select.dart index 3bb7476bf7..b80766ea90 100644 --- a/lib/pages/setting/pages/color_select.dart +++ b/lib/pages/setting/pages/color_select.dart @@ -14,6 +14,8 @@ import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; +import 'package:collection/collection.dart'; import 'package:flex_seed_scheme/flex_seed_scheme.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -85,7 +87,7 @@ class _ColorSelectPageState extends State { } catch (_) {} ctr.themeType.value = result; GStorage.setting.put(SettingBoxKey.themeMode, result.index); - Get.changeThemeMode(result.toThemeMode); + Get.changeThemeMode(ThemeUtils.themeMode = result.toThemeMode); } }, leading: const Icon(Icons.flashlight_on_outlined), @@ -147,10 +149,8 @@ class _ColorSelectPageState extends State { alignment: WrapAlignment.center, spacing: 22, runSpacing: 18, - children: colorThemeTypes.indexed.map( - (e) { - final index = e.$1; - final item = e.$2; + children: colorThemeTypes.mapIndexed( + (index, item) { return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { diff --git a/lib/pages/setting/pages/display_mode.dart b/lib/pages/setting/pages/display_mode.dart index 7b5e25802d..d5d3c0905c 100644 --- a/lib/pages/setting/pages/display_mode.dart +++ b/lib/pages/setting/pages/display_mode.dart @@ -1,6 +1,6 @@ -import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show PlatformException; diff --git a/lib/pages/setting/pages/fullscreen_sc_size.dart b/lib/pages/setting/pages/fullscreen_sc_size.dart new file mode 100644 index 0000000000..220da117e4 --- /dev/null +++ b/lib/pages/setting/pages/fullscreen_sc_size.dart @@ -0,0 +1,139 @@ +import 'dart:io' show Platform; +import 'dart:math' as math; + +import 'package:PiliPlus/common/widgets/custom_icon.dart'; +import 'package:PiliPlus/common/widgets/extra_hittest_stack.dart'; +import 'package:PiliPlus/models_new/live/live_superchat/item.dart'; +import 'package:PiliPlus/pages/live_room/superchat/superchat_card.dart'; +import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart'; +import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:PiliPlus/utils/storage.dart'; +import 'package:PiliPlus/utils/storage_key.dart'; +import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:flutter/material.dart'; + +const kFullScreenSCWidth = 255.0; + +class FullScreenScSize extends StatefulWidget { + const FullScreenScSize({super.key}); + + @override + State createState() => _FullScreenScSizeState(); +} + +class _FullScreenScSizeState extends State { + double _width = Pref.fullScreenSCWidth; + final _randomSC = SuperChatItem.random; + late EdgeInsets _padding; + late ColorScheme _colorScheme; + + @override + void initState() { + super.initState(); + if (Platform.isAndroid) { + landscapeLeftMode(); + } else if (Platform.isIOS) { + landscapeRightMode(); + } + } + + @override + void dispose() { + if (PlatformUtils.isMobile) { + if (Pref.horizontalScreen) { + fullMode(); + } else { + portraitUpMode(); + } + } + super.dispose(); + } + + @override + void didChangeDependencies() { + super.didChangeDependencies(); + final padding = MediaQuery.viewPaddingOf(context); + _padding = .only( + right: padding.right + 17, + left: padding.left + 25, + bottom: padding.bottom + 25, + ); + _colorScheme = ColorScheme.of(context); + } + + void _onReset() { + _width = kFullScreenSCWidth; + GStorage.setting.delete(SettingBoxKey.fullScreenSCWidth); + setState(() {}); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + resizeToAvoidBottomInset: false, + appBar: AppBar( + title: const Text('全屏 SC 大小设置'), + actions: [ + TextButton(onPressed: _onReset, child: const Text('重置')), + ], + ), + body: Padding(padding: _padding, child: _buildBody), + ); + } + + Widget get _buildBody { + return Align( + alignment: .bottomLeft, + child: ExtraHitTestStack( + clipBehavior: .none, + children: [ + SizedBox( + width: _width, + child: IgnorePointer( + child: SuperChatCard( + item: _randomSC, + persistentSC: true, + ), + ), + ), + Positioned( + top: 0, + bottom: 0, + right: -17, + width: 34, + child: MouseRegion( + cursor: SystemMouseCursors.resizeRight, + child: GestureDetector( + behavior: .opaque, + onHorizontalDragUpdate: _onHorizontalDragUpdate, + onHorizontalDragEnd: _onHorizontalDragEnd, + child: DecoratedBox( + decoration: BoxDecoration( + shape: .circle, + color: _colorScheme.secondaryContainer.withValues( + alpha: .8, + ), + ), + child: Icon( + size: 18, + CustomIcons.open_in_full_rotate_45, + color: _colorScheme.onSecondaryContainer, + ), + ), + ), + ), + ), + ], + ), + ); + } + + void _onHorizontalDragUpdate(DragUpdateDetails details) { + _width = math.max(25.0, _width + details.delta.dx); + setState(() {}); + } + + void _onHorizontalDragEnd(DragEndDetails details) { + GStorage.setting.put(SettingBoxKey.fullScreenSCWidth, _width); + } +} diff --git a/lib/pages/setting/pages/logs.dart b/lib/pages/setting/pages/logs.dart index 040b266211..101ac3bad2 100644 --- a/lib/pages/setting/pages/logs.dart +++ b/lib/pages/setting/pages/logs.dart @@ -11,8 +11,8 @@ import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:catcher_2/model/platform_type.dart'; -import 'package:catcher_2/model/report.dart' as catcher; +import 'package:catcher_2/catcher_2.dart'; +import 'package:catcher_2/utils/log_printer.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -27,25 +27,25 @@ class LogsPage extends StatefulWidget { } class _LogsPageState extends State { - List logsContent = []; - Report? latestLog; + List<_ExpandedItem> logsContent = []; + _ExpandedItem<_DeviceInfo>? _deviceInfo; late bool enableLog = Pref.enableLog; @override void initState() { + _initDeviceInfo(); getLog(); super.initState(); } - @override - void dispose() { - if (latestLog != null) { - final time = latestLog!.dateTime; - if (DateTime.now().difference(time) >= const Duration(days: 14)) { - LoggerUtils.clearLogs(); - } + void _initDeviceInfo() { + if (Catcher2.instance case final c?) { + _deviceInfo = _ExpandedItem(( + c.deviceParameters, + c.applicationParameters, + c.customParameters, + )); } - super.dispose(); } Future getLog() async { @@ -53,19 +53,18 @@ class _LogsPageState extends State { logsContent = (await logsPath.readAsLines()).reversed.map((i) { try { final log = Report.fromJson(jsonDecode(i)); - latestLog ??= log.copyWith(); - return log; + return _ExpandedItem(log); } catch (e, s) { - return Report( - 'Parse log failed: $e\n\n\n$i', - s, - DateTime.now(), - const {}, - const {}, - const {}, - null, - PlatformType.unknown, - null, + return _ExpandedItem( + Report( + 'Parse log failed: $e\n\n\n$i', + s, + DateTime.now(), + const {}, + const {}, + const {}, + null, + ), ); } }).toList(); @@ -119,7 +118,7 @@ class _LogsPageState extends State { onTap: () => Timer.periodic( const Duration(milliseconds: 3500), (timer) { - Utils.reportError('Manual'); + Utils.reportError('Manual', StackTrace.current); if (timer.tick > 3) { timer.cancel(); if (mounted) getLog(); @@ -146,10 +145,7 @@ class _LogsPageState extends State { child: const Text('错误反馈'), ), PopupMenuItem( - onTap: () { - latestLog = null; - clearLogs(); - }, + onTap: clearLogs, child: const Text('清空日志'), ), ], @@ -157,7 +153,7 @@ class _LogsPageState extends State { const SizedBox(width: 6), ], ), - body: logsContent.isNotEmpty + body: logsContent.isNotEmpty || _deviceInfo != null ? Padding( padding: EdgeInsets.only( left: padding.left + 12, @@ -165,11 +161,11 @@ class _LogsPageState extends State { ), child: CustomScrollView( slivers: [ - if (latestLog != null) + if (_deviceInfo != null) SliverToBoxAdapter( child: Padding( padding: const .only(bottom: 12), - child: InfoCard(report: latestLog!), + child: _InfoCard(info: _deviceInfo!), ), ), SliverPadding( @@ -177,7 +173,7 @@ class _LogsPageState extends State { sliver: SliverList.separated( itemCount: logsContent.length, itemBuilder: (context, index) => - ReportCard(report: logsContent[index]), + _ReportCard(report: logsContent[index]), separatorBuilder: (_, _) => const SizedBox(height: 12), ), ), @@ -189,10 +185,16 @@ class _LogsPageState extends State { } } -class InfoCard extends StatelessWidget { - final Report report; +typedef _DeviceInfo = ( + Map, + Map, + Map, +); + +class _InfoCard extends StatelessWidget { + final _ExpandedItem<_DeviceInfo> info; - const InfoCard({super.key, required this.report}); + const _InfoCard({required this.info}); Widget _buildMapSection( Color color, @@ -210,11 +212,7 @@ class InfoCard extends StatelessWidget { const SizedBox(height: 8), Text( title, - style: TextStyle( - fontWeight: FontWeight.bold, - color: color, - fontSize: 15, - ), + style: TextStyle(fontWeight: .bold, color: color, fontSize: 15), ), ...map.entries.map( (entry) => Text.rich( @@ -250,58 +248,47 @@ class InfoCard extends StatelessWidget { const Expanded( child: Text( '相关信息', - style: TextStyle( - fontWeight: FontWeight.bold, - fontSize: 15, - ), + style: TextStyle(fontWeight: .bold, fontSize: 15), maxLines: 1, - overflow: TextOverflow.ellipsis, + overflow: .ellipsis, ), ), iconButton( size: 34, iconSize: 22, + tooltip: info.isExpanded ? '收起' : '展开', icon: Icon( - report.isExpanded ? Icons.expand_less : Icons.expand_more, + info.isExpanded ? Icons.expand_less : Icons.expand_more, ), onPressed: () { - report.isExpanded = !report.isExpanded; + info.isExpanded = !info.isExpanded; (context as Element).markNeedsBuild(); }, ), ], ), - if (report.isExpanded) ...[ - _buildMapSection( - colorScheme.primary, - '设备信息', - report.deviceParameters, - ), - _buildMapSection( - colorScheme.primary, - '应用信息', - report.applicationParameters, - ), - _buildMapSection( - colorScheme.primary, - '编译信息', - report.customParameters, - ), + if (info.isExpanded) ...[ + _buildMapSection(colorScheme.primary, '设备信息', info.item.$1), + _buildMapSection(colorScheme.primary, '应用信息', info.item.$2), + _buildMapSection(colorScheme.primary, '编译信息', info.item.$3), ], ]); } } -class ReportCard extends StatelessWidget { - final Report report; +class _ReportCard extends StatelessWidget { + final _ExpandedItem report; - const ReportCard({super.key, required this.report}); + const _ReportCard({required this.report}); @override Widget build(BuildContext context) { final colorScheme = ColorScheme.of(context); - late final stackTrace = report.stackTrace.toString().trim(); - final dateTime = DateFormatUtils.longFormatDs.format(report.dateTime); + late final stackTrace = PrettyLogPrinter.formatStackString( + report.item.stackTrace?.toString(), + -1, + ); + final dateTime = DateFormatUtils.longFormatDs.format(report.item.dateTime); return _card([ Row( crossAxisAlignment: .start, @@ -312,13 +299,10 @@ class ReportCard extends StatelessWidget { crossAxisAlignment: .start, children: [ Text( - report.error.toString(), - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 15, - ), + report.item.error.toString(), + style: const TextStyle(fontWeight: .bold, fontSize: 15), maxLines: 2, - overflow: TextOverflow.ellipsis, + overflow: .ellipsis, ), Text( dateTime, @@ -334,6 +318,7 @@ class ReportCard extends StatelessWidget { iconButton( size: 34, iconSize: 22, + tooltip: '复制', onPressed: () { Utils.copyText('```\n$report```', needToast: false); ScaffoldMessenger.of(context).showSnackBar( @@ -343,14 +328,12 @@ class ReportCard extends StatelessWidget { ), ); }, - icon: const Icon( - Icons.copy_outlined, - size: 16, - ), + icon: const Icon(Icons.copy_outlined, size: 16), ), iconButton( size: 34, iconSize: 22, + tooltip: report.isExpanded ? '收起' : '展开', icon: Icon( report.isExpanded ? Icons.expand_less : Icons.expand_more, ), @@ -373,16 +356,14 @@ class ReportCard extends StatelessWidget { ), const SizedBox(height: 8), Container( - padding: const EdgeInsets.all(12), + padding: const .all(12), decoration: BoxDecoration( color: colorScheme.surface, - borderRadius: const BorderRadius.all(Radius.circular(8)), - border: Border.all( - color: colorScheme.outline.withValues(alpha: 0.5), - ), + borderRadius: const .all(.circular(8)), + border: .all(color: colorScheme.outline.withValues(alpha: 0.5)), ), child: SelectableText( - report.error.toString(), + report.item.error.toString(), style: TextStyle( fontFamily: 'Monospace', color: colorScheme.onSurfaceVariant, @@ -390,7 +371,7 @@ class ReportCard extends StatelessWidget { ), ), // stackTrace may be null or String("null") or blank - if (stackTrace.isNotEmpty && stackTrace != 'null') ...[ + if (stackTrace != null && stackTrace.isNotEmpty) ...[ const SizedBox(height: 16), Text( '堆栈跟踪', @@ -402,21 +383,29 @@ class ReportCard extends StatelessWidget { ), const SizedBox(height: 8), Container( - padding: const EdgeInsets.all(12), + padding: const .all(12), decoration: BoxDecoration( color: colorScheme.surface, - borderRadius: const BorderRadius.all(Radius.circular(8)), - border: Border.all( - color: colorScheme.outline.withValues(alpha: 0.5), - ), + borderRadius: const .all(.circular(8)), + border: .all(color: colorScheme.outline.withValues(alpha: 0.5)), ), - child: SelectableText( - stackTrace, - style: TextStyle( - fontFamily: 'Monospace', - fontSize: 13, - color: colorScheme.onSurfaceVariant, + child: SelectableText.rich( + TextSpan( + children: stackTrace + .map( + (i) => TextSpan( + text: '$i\n', + style: i.contains('(package:${Constants.appName}') + ? TextStyle( + color: colorScheme.onSurface, + fontWeight: .w600, + ) + : TextStyle(color: colorScheme.onSurfaceVariant), + ), + ) + .toList(), ), + style: const TextStyle(fontFamily: 'Monospace', fontSize: 13), ), ), ], @@ -437,68 +426,12 @@ Widget _card(List contents) { ); } -class Report extends catcher.Report { - Report( - super.error, - super.stackTrace, - super.dateTime, - super.deviceParameters, - super.applicationParameters, - super.customParameters, - super.errorDetails, - super.platformType, - super.screenshot, - ); - +class _ExpandedItem { bool isExpanded = false; + final T item; - factory Report.fromJson(Map json) => Report( - json['error'], - json['stackTrace'], - DateTime.tryParse(json['dateTime'] ?? '') ?? DateTime(1970), - json['deviceParameters'] ?? const {}, - json['applicationParameters'] ?? const {}, - json['customParameters'] ?? const {}, - null, - PlatformType.values.byName(json['platformType']), - null, - ); - - Report copyWith({ - dynamic error, - dynamic stackTrace, - DateTime? dateTime, - Map? deviceParameters, - Map? applicationParameters, - Map? customParameters, - FlutterErrorDetails? errorDetails, - PlatformType? platformType, - }) { - return Report( - error ?? this.error, - stackTrace ?? this.stackTrace, - dateTime ?? this.dateTime, - deviceParameters ?? this.deviceParameters, - applicationParameters ?? this.applicationParameters, - customParameters ?? this.customParameters, - errorDetails ?? this.errorDetails, - platformType ?? this.platformType, - null, - ); - } - - String _params2String(Map params) { - return params.entries - .map((entry) => '${entry.key}: ${entry.value}\n') - .join(); - } + _ExpandedItem(this.item); @override - String toString() { - return '------- DEVICE INFO -------\n${_params2String(deviceParameters)}' - '------- APP INFO -------\n${_params2String(applicationParameters)}' - '------- ERROR -------\n$error\n' - '------- STACK TRACE -------\n${stackTrace.toString().trim()}\n' - '------- CUSTOM INFO -------\n${_params2String(customParameters)}'; - } + String toString() => item.toString(); } diff --git a/lib/pages/setting/pages/play_speed_set.dart b/lib/pages/setting/pages/play_speed_set.dart index aa67061b38..16f3000591 100644 --- a/lib/pages/setting/pages/play_speed_set.dart +++ b/lib/pages/setting/pages/play_speed_set.dart @@ -4,11 +4,11 @@ import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/pages/setting/widgets/switch_item.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; +import 'package:PiliPlus/utils/filtering_text.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/material.dart' hide ListTile; -import 'package:flutter/services.dart' show FilteringTextInputFormatter; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:hive_ce/hive.dart'; @@ -74,9 +74,7 @@ class _PlaySpeedPageState extends State { border: OutlineInputBorder(borderRadius: .all(.circular(6))), ), onChanged: (value) => initialValue = value, - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'[\d\.]+')), - ], + inputFormatters: FilteringText.decimal, ), ], ), diff --git a/lib/pages/setting/play_setting.dart b/lib/pages/setting/play_setting.dart deleted file mode 100644 index 6c97855a5d..0000000000 --- a/lib/pages/setting/play_setting.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:PiliPlus/pages/setting/models/play_settings.dart'; -import 'package:flutter/material.dart'; - -class PlaySetting extends StatefulWidget { - const PlaySetting({super.key, this.showAppBar = true}); - - final bool showAppBar; - - @override - State createState() => _PlaySettingState(); -} - -class _PlaySettingState extends State { - final settings = playSettings; - - @override - Widget build(BuildContext context) { - final showAppBar = widget.showAppBar; - final padding = MediaQuery.viewPaddingOf(context); - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: showAppBar ? AppBar(title: const Text('播放器设置')) : null, - body: ListView.builder( - padding: EdgeInsets.only( - left: showAppBar ? padding.left : 0, - right: showAppBar ? padding.right : 0, - bottom: padding.bottom + 100, - ), - itemCount: settings.length, - itemBuilder: (context, index) => settings[index].widget, - ), - ); - } -} diff --git a/lib/pages/setting/privacy_setting.dart b/lib/pages/setting/privacy_setting.dart deleted file mode 100644 index f2ff30bb3e..0000000000 --- a/lib/pages/setting/privacy_setting.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:PiliPlus/pages/setting/models/privacy_settings.dart'; -import 'package:flutter/material.dart'; - -class PrivacySetting extends StatefulWidget { - const PrivacySetting({super.key, this.showAppBar = true}); - - final bool showAppBar; - - @override - State createState() => _PrivacySettingState(); -} - -class _PrivacySettingState extends State { - final settings = privacySettings; - - @override - Widget build(BuildContext context) { - final showAppBar = widget.showAppBar; - final padding = MediaQuery.viewPaddingOf(context); - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: showAppBar ? AppBar(title: const Text('隐私设置')) : null, - body: ListView( - padding: EdgeInsets.only( - left: showAppBar ? padding.left : 0, - right: showAppBar ? padding.right : 0, - bottom: padding.bottom + 100, - ), - children: settings.map((item) => item.widget).toList(), - ), - ); - } -} diff --git a/lib/pages/setting/recommend_setting.dart b/lib/pages/setting/recommend_setting.dart deleted file mode 100644 index d77315fe36..0000000000 --- a/lib/pages/setting/recommend_setting.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; -import 'package:PiliPlus/pages/setting/models/recommend_settings.dart'; -import 'package:flutter/material.dart' hide ListTile; - -class RecommendSetting extends StatefulWidget { - const RecommendSetting({super.key, this.showAppBar = true}); - - final bool showAppBar; - - @override - State createState() => _RecommendSettingState(); -} - -class _RecommendSettingState extends State { - final list = recommendSettings; - - @override - Widget build(BuildContext context) { - final showAppBar = widget.showAppBar; - final padding = MediaQuery.viewPaddingOf(context); - final theme = Theme.of(context); - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: widget.showAppBar ? AppBar(title: const Text('推荐流设置')) : null, - body: ListView( - padding: EdgeInsets.only( - left: showAppBar ? padding.left : 0, - right: showAppBar ? padding.right : 0, - bottom: padding.bottom + 100, - ), - children: [ - ...list.take(4).map((item) => item.widget), - const Divider(height: 1), - ...list.skip(4).map((item) => item.widget), - ListTile( - dense: true, - subtitle: Text( - '¹ 由于接口未提供关注信息,无法豁免相关视频中的已关注Up。\n\n' - '* 其它(如热门视频、手动搜索、链接跳转等)均不受过滤器影响。\n' - '* 设定较严苛的条件可导致推荐项数锐减或多次请求,请酌情选择。\n' - '* 后续可能会增加更多过滤条件,敬请期待。', - style: theme.textTheme.labelSmall!.copyWith( - color: theme.colorScheme.outline.withValues(alpha: 0.7), - ), - ), - ), - ], - ), - ); - } -} diff --git a/lib/pages/setting/style_setting.dart b/lib/pages/setting/style_setting.dart deleted file mode 100644 index 1b13350d7b..0000000000 --- a/lib/pages/setting/style_setting.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:PiliPlus/pages/setting/models/style_settings.dart'; -import 'package:flutter/material.dart'; - -class StyleSetting extends StatefulWidget { - const StyleSetting({super.key, this.showAppBar = true}); - - final bool showAppBar; - - @override - State createState() => _StyleSettingState(); -} - -class _StyleSettingState extends State { - final settings = styleSettings; - - @override - Widget build(BuildContext context) { - final showAppBar = widget.showAppBar; - final padding = MediaQuery.viewPaddingOf(context); - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: showAppBar ? AppBar(title: const Text('外观设置')) : null, - body: ListView.builder( - padding: EdgeInsets.only( - left: showAppBar ? padding.left : 0, - right: showAppBar ? padding.right : 0, - bottom: padding.bottom + 100, - ), - itemCount: settings.length, - itemBuilder: (context, index) => settings[index].widget, - ), - ); - } -} diff --git a/lib/pages/setting/video_setting.dart b/lib/pages/setting/video_setting.dart deleted file mode 100644 index 495720aa64..0000000000 --- a/lib/pages/setting/video_setting.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:PiliPlus/pages/setting/models/video_settings.dart'; -import 'package:flutter/material.dart'; - -class VideoSetting extends StatefulWidget { - const VideoSetting({super.key, this.showAppBar = true}); - - final bool showAppBar; - - @override - State createState() => _VideoSettingState(); -} - -class _VideoSettingState extends State { - final settings = videoSettings; - - @override - Widget build(BuildContext context) { - final showAppBar = widget.showAppBar; - final padding = MediaQuery.viewPaddingOf(context); - return Scaffold( - resizeToAvoidBottomInset: false, - appBar: showAppBar ? AppBar(title: const Text('音视频设置')) : null, - body: ListView.builder( - padding: EdgeInsets.only( - left: showAppBar ? padding.left : 0, - right: showAppBar ? padding.right : 0, - bottom: padding.bottom + 100, - ), - itemCount: settings.length, - itemBuilder: (context, index) => settings[index].widget, - ), - ); - } -} diff --git a/lib/pages/setting/view.dart b/lib/pages/setting/view.dart index fb94fa9c59..633dd7317e 100644 --- a/lib/pages/setting/view.dart +++ b/lib/pages/setting/view.dart @@ -4,12 +4,7 @@ import 'package:PiliPlus/http/login.dart'; import 'package:PiliPlus/models/common/setting_type.dart'; import 'package:PiliPlus/pages/about/view.dart'; import 'package:PiliPlus/pages/login/controller.dart'; -import 'package:PiliPlus/pages/setting/extra_setting.dart'; -import 'package:PiliPlus/pages/setting/play_setting.dart'; -import 'package:PiliPlus/pages/setting/privacy_setting.dart'; -import 'package:PiliPlus/pages/setting/recommend_setting.dart'; -import 'package:PiliPlus/pages/setting/style_setting.dart'; -import 'package:PiliPlus/pages/setting/video_setting.dart'; +import 'package:PiliPlus/pages/setting/common_setting.dart'; import 'package:PiliPlus/pages/setting/widgets/multi_select_dialog.dart'; import 'package:PiliPlus/pages/webdav/view.dart'; import 'package:PiliPlus/utils/accounts.dart'; @@ -43,11 +38,12 @@ class _SettingPageState extends State { late SettingType _type = SettingType.privacySetting; final RxBool _noAccount = Accounts.account.isEmpty.obs; late bool _isPortrait; + late ThemeData theme; static const List<_SettingsModel> _items = [ _SettingsModel( type: SettingType.privacySetting, - subtitle: '黑名单、无痕模式', + subtitle: '黑名单', icon: Icon(Icons.privacy_tip_outlined), ), _SettingsModel( @@ -86,9 +82,15 @@ class _SettingPageState extends State { ]; @override - Widget build(BuildContext context) { - final theme = Theme.of(context); + void didChangeDependencies() { + super.didChangeDependencies(); + + theme = Theme.of(context); _isPortrait = MediaQuery.sizeOf(context).isPortrait; + } + + @override + Widget build(BuildContext context) { return Scaffold( resizeToAvoidBottomInset: false, appBar: AppBar( @@ -111,28 +113,19 @@ class _SettingPageState extends State { Expanded( flex: 6, child: switch (_type) { - SettingType.privacySetting => const PrivacySetting( - showAppBar: false, - ), - SettingType.recommendSetting => const RecommendSetting( + .privacySetting || + .recommendSetting || + .videoSetting || + .playSetting || + .styleSetting || + .extraSetting => CommonSetting( + settingType: _type, showAppBar: false, ), - SettingType.videoSetting => const VideoSetting( + .webdavSetting => const WebDavSettingPage( showAppBar: false, ), - SettingType.playSetting => const PlaySetting( - showAppBar: false, - ), - SettingType.styleSetting => const StyleSetting( - showAppBar: false, - ), - SettingType.extraSetting => const ExtraSetting( - showAppBar: false, - ), - SettingType.webdavSetting => const WebDavSettingPage( - showAppBar: false, - ), - SettingType.about => const AboutPage(showAppBar: false), + .about => const AboutPage(showAppBar: false), }, ), ], @@ -149,7 +142,18 @@ class _SettingPageState extends State { void _toPage(SettingType type) { if (_isPortrait) { - Get.toNamed('/${type.name}'); + Get.to( + () => switch (type) { + .privacySetting || + .recommendSetting || + .videoSetting || + .playSetting || + .styleSetting || + .extraSetting => CommonSetting(settingType: type), + .webdavSetting => const WebDavSettingPage(), + .about => const AboutPage(), + }, + ); } else { _type = type; setState(() {}); @@ -190,7 +194,7 @@ class _SettingPageState extends State { ListTile( onTap: () => LoginPageController.switchAccountDialog(context), leading: const Icon(Icons.switch_account_outlined), - title: Text('设置账号模式', style: titleStyle), + title: Text('切换账号', style: titleStyle), ), Obx( () => _noAccount.value diff --git a/lib/pages/setting/widgets/dual_slider_dialog.dart b/lib/pages/setting/widgets/dual_slider_dialog.dart index 95beac98d6..ba3b0b679a 100644 --- a/lib/pages/setting/widgets/dual_slider_dialog.dart +++ b/lib/pages/setting/widgets/dual_slider_dialog.dart @@ -4,9 +4,9 @@ import 'package:flutter/material.dart'; class DualSliderDialog extends StatefulWidget { final double value1; final double value2; - final String title; - final String description1; - final String description2; + final Widget title; + final Widget description1; + final Widget description2; final double min; final double max; final int? divisions; @@ -45,7 +45,7 @@ class _DualSliderDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(widget.title), + title: widget.title, contentPadding: const EdgeInsets.only( top: 20, left: 8, @@ -55,7 +55,7 @@ class _DualSliderDialogState extends State { content: Column( mainAxisSize: .min, children: [ - Text(widget.description1), + widget.description1, Builder( builder: (context) { return Slider( @@ -72,7 +72,7 @@ class _DualSliderDialogState extends State { ); }, ), - Text(widget.description2), + widget.description2, Builder( builder: (context) { return Slider( diff --git a/lib/pages/setting/widgets/ordered_multi_select_dialog.dart b/lib/pages/setting/widgets/ordered_multi_select_dialog.dart index 6d8900c16e..7f1452173c 100644 --- a/lib/pages/setting/widgets/ordered_multi_select_dialog.dart +++ b/lib/pages/setting/widgets/ordered_multi_select_dialog.dart @@ -88,7 +88,7 @@ class _OrderedMultiSelectDialogState ), TextButton( onPressed: () { - assert(_tempValues.values.isSorted((a, b) => a.compareTo(b))); + assert(_tempValues.values.isSorted(Comparable.compare)); Get.back(result: _tempValues.keys.toList()); }, child: const Text('确定'), diff --git a/lib/pages/setting/widgets/select_dialog.dart b/lib/pages/setting/widgets/select_dialog.dart index 5097e31737..a01aa23a87 100644 --- a/lib/pages/setting/widgets/select_dialog.dart +++ b/lib/pages/setting/widgets/select_dialog.dart @@ -35,7 +35,7 @@ class SelectDialog extends StatelessWidget { clipBehavior: Clip.hardEdge, title: Text(title), constraints: subtitleBuilder != null - ? const BoxConstraints(maxWidth: 320, minWidth: 320) + ? const BoxConstraints.tightFor(width: 320) : null, contentPadding: const EdgeInsets.symmetric(vertical: 12), content: Material( diff --git a/lib/pages/setting/widgets/slider_dialog.dart b/lib/pages/setting/widgets/slider_dialog.dart index b1e18f7499..7cf64475f4 100644 --- a/lib/pages/setting/widgets/slider_dialog.dart +++ b/lib/pages/setting/widgets/slider_dialog.dart @@ -2,14 +2,6 @@ import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:flutter/material.dart'; class SliderDialog extends StatefulWidget { - final double value; - final String title; - final double min; - final double max; - final int? divisions; - final String suffix; - final int precise; - const SliderDialog({ super.key, required this.value, @@ -21,6 +13,14 @@ class SliderDialog extends StatefulWidget { this.precise = 1, }); + final double value; + final Widget title; + final double min; + final double max; + final int? divisions; + final String suffix; + final int precise; + @override State createState() => _SliderDialogState(); } @@ -37,13 +37,8 @@ class _SliderDialogState extends State { @override Widget build(BuildContext context) { return AlertDialog( - title: Text(widget.title), - contentPadding: const EdgeInsets.only( - top: 20, - left: 8, - right: 8, - bottom: 8, - ), + title: widget.title, + contentPadding: const .only(top: 20, left: 8, right: 8, bottom: 8), content: SizedBox( height: 40, child: Slider( diff --git a/lib/pages/sponsor_block/block_mixin.dart b/lib/pages/sponsor_block/block_mixin.dart index 907908545e..c554fb8a07 100644 --- a/lib/pages/sponsor_block/block_mixin.dart +++ b/lib/pages/sponsor_block/block_mixin.dart @@ -1,6 +1,7 @@ import 'dart:async' show StreamSubscription, Timer; import 'dart:math' as math; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/progress_bar/segment_progress_bar.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/sponsor_block.dart'; @@ -285,40 +286,32 @@ mixin BlockMixin on GetxController { void _showVoteDialog(SegmentModel segment) { showDialog( context: Get.context!, - builder: (context) => AlertDialog( - clipBehavior: Clip.hardEdge, - contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - title: const Text('赞成票', style: TextStyle(fontSize: 14)), - onTap: () { - Get.back(); - _doVote(segment.uuid, 1); - }, - ), - ListTile( - dense: true, - title: const Text('反对票', style: TextStyle(fontSize: 14)), - onTap: () { - Get.back(); - _doVote(segment.uuid, 0); - }, - ), - ListTile( - dense: true, - title: const Text('更改类别', style: TextStyle(fontSize: 14)), - onTap: () { - Get.back(); - _showCategoryDialog(segment); - }, - ), - ], + builder: (context) => SimpleDialog( + clipBehavior: .hardEdge, + contentPadding: const .symmetric(vertical: 10), + children: [ + DialogOption( + child: const Text('赞成票', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + _doVote(segment.uuid, 1); + }, + ), + DialogOption( + child: const Text('反对票', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + _doVote(segment.uuid, 0); + }, ), - ), + DialogOption( + child: const Text('更改类别', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + _showCategoryDialog(segment); + }, + ), + ], ), ); } @@ -331,54 +324,49 @@ mixin BlockMixin on GetxController { void _showCategoryDialog(SegmentModel segment) { showDialog( context: Get.context!, - builder: (context) => AlertDialog( - clipBehavior: Clip.hardEdge, - contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: SegmentType.values - .map( - (item) => ListTile( - dense: true, - onTap: () { - Get.back(); - SponsorBlock.voteOnSponsorTime( - uuid: segment.uuid, - category: item, - ).then((i) { - SmartDialog.showToast( - '类别更改${i.isSuccess ? '成功' : '失败: $i'}', - ); - }); - }, - title: Text.rich( - TextSpan( - children: [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Container( - height: 10, - width: 10, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: blockConfig._getColor(item), - ), - ), - style: const TextStyle(fontSize: 14, height: 1), - ), - TextSpan( - text: ' ${item.title}', - style: const TextStyle(fontSize: 14, height: 1), + builder: (context) => SimpleDialog( + clipBehavior: .hardEdge, + contentPadding: const .symmetric(vertical: 10), + children: SegmentType.values + .map( + (item) => ListTile( + dense: true, + onTap: () { + Get.back(); + SponsorBlock.voteOnSponsorTime( + uuid: segment.uuid, + category: item, + ).then((i) { + SmartDialog.showToast( + '类别更改${i.isSuccess ? '成功' : '失败: $i'}', + ); + }); + }, + title: Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Container( + height: 10, + width: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: blockConfig._getColor(item), ), - ], + ), + style: const TextStyle(fontSize: 14, height: 1), ), - ), + TextSpan( + text: ' ${item.title}', + style: const TextStyle(fontSize: 14, height: 1), + ), + ], ), - ) - .toList(), - ), - ), + ), + ), + ) + .toList(), ), ); } @@ -386,96 +374,91 @@ mixin BlockMixin on GetxController { void showSBDetail() { showDialog( context: Get.context!, - builder: (context) => AlertDialog( - clipBehavior: Clip.hardEdge, - contentPadding: const EdgeInsets.fromLTRB(0, 10, 0, 10), - content: SingleChildScrollView( - child: Column( - mainAxisSize: MainAxisSize.min, - children: _segmentList - .map( - (item) => ListTile( - onTap: () { - Get.back(); - if (isBlock) { - _showVoteDialog(item); - } - }, - dense: true, - title: Text.rich( - TextSpan( - children: [ - WidgetSpan( - alignment: PlaceholderAlignment.middle, - child: Container( - height: 10, - width: 10, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: blockConfig._getColor(item.segmentType), - ), - ), - style: const TextStyle(fontSize: 14, height: 1), - ), - TextSpan( - text: ' ${item.segmentType.title}', - style: const TextStyle(fontSize: 14, height: 1), + builder: (context) => SimpleDialog( + clipBehavior: .hardEdge, + contentPadding: const .symmetric(vertical: 10), + children: _segmentList + .map( + (item) => ListTile( + onTap: () { + Get.back(); + if (isBlock) { + _showVoteDialog(item); + } + }, + dense: true, + title: Text.rich( + TextSpan( + children: [ + WidgetSpan( + alignment: PlaceholderAlignment.middle, + child: Container( + height: 10, + width: 10, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: blockConfig._getColor(item.segmentType), ), - ], + ), + style: const TextStyle(fontSize: 14, height: 1), ), - ), - contentPadding: const EdgeInsets.only(left: 16, right: 8), - subtitle: Text( - '${DurationUtils.formatDuration(item.segment.$1 / 1000)} 至 ${DurationUtils.formatDuration(item.segment.$2 / 1000)}', + TextSpan( + text: ' ${item.segmentType.title}', + style: const TextStyle(fontSize: 14, height: 1), + ), + ], + ), + ), + contentPadding: const EdgeInsets.only(left: 16, right: 8), + subtitle: Text( + '${DurationUtils.formatDuration(item.segment.$1 / 1000)} 至 ${DurationUtils.formatDuration(item.segment.$2 / 1000)}', + style: const TextStyle(fontSize: 13), + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + item.skipType.label, style: const TextStyle(fontSize: 13), ), - trailing: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - item.skipType.label, - style: const TextStyle(fontSize: 13), + if (item.segment.$2 != 0) + SizedBox( + width: 36, + height: 36, + child: IconButton( + tooltip: item.skipType == SkipType.showOnly + ? '跳至此片段' + : '跳过此片段', + onPressed: () { + Get.back(); + onSkip( + item, + isSkip: item.skipType != SkipType.showOnly, + isSeek: false, + ); + }, + style: IconButton.styleFrom( + padding: EdgeInsets.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + icon: Icon( + item.skipType == SkipType.showOnly + ? Icons.my_location + : MdiIcons.debugStepOver, + size: 18, + color: ColorScheme.of( + context, + ).onSurface.withValues(alpha: 0.7), + ), ), - if (item.segment.$2 != 0) - SizedBox( - width: 36, - height: 36, - child: IconButton( - tooltip: item.skipType == SkipType.showOnly - ? '跳至此片段' - : '跳过此片段', - onPressed: () { - Get.back(); - onSkip( - item, - isSkip: item.skipType != SkipType.showOnly, - isSeek: false, - ); - }, - style: IconButton.styleFrom( - padding: EdgeInsets.zero, - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - ), - icon: Icon( - item.skipType == SkipType.showOnly - ? Icons.my_location - : MdiIcons.debugStepOver, - size: 18, - color: Theme.of( - context, - ).colorScheme.onSurface.withValues(alpha: 0.7), - ), - ), - ) - else - const SizedBox(width: 10), - ], - ), - ), - ) - .toList(), - ), - ), + ) + else + const SizedBox(width: 10), + ], + ), + ), + ) + .toList(), ), ); } diff --git a/lib/pages/sponsor_block/view.dart b/lib/pages/sponsor_block/view.dart index b372b532b6..b053bbb722 100644 --- a/lib/pages/sponsor_block/view.dart +++ b/lib/pages/sponsor_block/view.dart @@ -7,6 +7,7 @@ import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart'; import 'package:PiliPlus/models/common/sponsor_block/skip_type.dart'; import 'package:PiliPlus/models_new/sponsor_block/user_info.dart'; import 'package:PiliPlus/pages/setting/slide_color_picker.dart'; +import 'package:PiliPlus/utils/filtering_text.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; @@ -82,15 +83,11 @@ class _SponsorBlockPageState extends State { builder: (_) => AlertDialog( title: Text('最短片段时长', style: titleStyle), content: TextFormField( - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - ), + keyboardType: const .numberWithOptions(decimal: true), controller: _textController, autofocus: true, decoration: const InputDecoration(suffixText: 's'), - inputFormatters: [ - FilteringTextInputFormatter.allow(RegExp(r'[\d\.]+')), - ], + inputFormatters: FilteringText.decimal, ), actions: [ TextButton( diff --git a/lib/pages/subscription/widgets/item.dart b/lib/pages/subscription/widgets/item.dart index 66bee5feb3..da47c042fc 100644 --- a/lib/pages/subscription/widgets/item.dart +++ b/lib/pages/subscription/widgets/item.dart @@ -1,13 +1,12 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/models_new/sub/sub/list.dart'; import 'package:PiliPlus/pages/subscription_detail/view.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; diff --git a/lib/pages/subscription_detail/widget/sub_video_card.dart b/lib/pages/subscription_detail/widget/sub_video_card.dart index 99826c9b9c..152e1cf0fc 100644 --- a/lib/pages/subscription_detail/widget/sub_video_card.dart +++ b/lib/pages/subscription_detail/widget/sub_video_card.dart @@ -1,6 +1,5 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; @@ -12,7 +11,7 @@ import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; // 收藏视频卡片 - 水平布局 class SubVideoCardH extends StatelessWidget { diff --git a/lib/pages/video/controller.dart b/lib/pages/video/controller.dart index 83577969d3..efaf5c92bf 100644 --- a/lib/pages/video/controller.dart +++ b/lib/pages/video/controller.dart @@ -1,5 +1,4 @@ import 'dart:async'; -import 'dart:io'; import 'dart:math' show min; import 'dart:ui'; @@ -8,12 +7,12 @@ import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/progress_bar/segment_progress_bar.dart'; import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pbenum.dart' show PlaylistSource; +import 'package:PiliPlus/grpc/dm.dart'; import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/init.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/user.dart'; import 'package:PiliPlus/http/video.dart'; -import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/models/common/account_type.dart'; import 'package:PiliPlus/models/common/sponsor_block/action_type.dart'; import 'package:PiliPlus/models/common/sponsor_block/post_segment_model.dart'; @@ -21,7 +20,6 @@ import 'package:PiliPlus/models/common/sponsor_block/segment_model.dart'; import 'package:PiliPlus/models/common/sponsor_block/segment_type.dart'; import 'package:PiliPlus/models/common/video/audio_quality.dart'; import 'package:PiliPlus/models/common/video/source_type.dart'; -import 'package:PiliPlus/models/common/video/subtitle_pref_type.dart'; import 'package:PiliPlus/models/common/video/video_decode_type.dart'; import 'package:PiliPlus/models/common/video/video_quality.dart'; import 'package:PiliPlus/models/common/video/video_type.dart'; @@ -53,18 +51,20 @@ import 'package:PiliPlus/plugin/pl_player/models/heart_beat_type.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; import 'package:PiliPlus/services/download/download_service.dart'; import 'package:PiliPlus/utils/accounts.dart'; +import 'package:PiliPlus/utils/connectivity_utils.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; -import 'package:PiliPlus/utils/extension/file_ext.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; +import 'package:PiliPlus/utils/extension/nested_scroll_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart'; import 'package:PiliPlus/utils/page_utils.dart'; -import 'package:PiliPlus/utils/path_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/video_utils.dart'; +import 'package:collection/collection.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; @@ -73,7 +73,6 @@ import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:get/get.dart'; import 'package:hive_ce/hive.dart'; import 'package:media_kit/media_kit.dart' hide Subtitle; -import 'package:path/path.dart' as path; class VideoDetailController extends GetxController with GetTickerProviderStateMixin, BlockMixin { @@ -125,6 +124,7 @@ class VideoDetailController extends GetxController ..brightness.value = -1; bool get setSystemBrightness => plPlayerController.setSystemBrightness; bool get removeSafeArea => plPlayerController.removeSafeArea; + double get uiScale => plPlayerController.uiScale; late VideoItem firstVideo; String? videoUrl; @@ -144,8 +144,7 @@ class VideoDetailController extends GetxController Box setting = GStorage.setting; // 预设的解码格式 - late String cacheDecode = Pref.defaultDecode; // def avc - late String cacheSecondDecode = Pref.secondDecode; // def av1 + late List preferCodecs = Pref.preferCodecs; bool get showReply => isFileSource ? false @@ -189,9 +188,7 @@ class VideoDetailController extends GetxController )..addListener(_animListener)); void refreshPage() { - if (scrollKey.currentState?.mounted ?? false) { - (scrollKey.currentState!.context as Element).markNeedsBuild(); - } + scrollKey.currentState?.refresh(); } void _animListener() { @@ -219,14 +216,7 @@ class VideoDetailController extends GetxController } void animToTop() { - final outerController = scrollKey.currentState!.outerController; - if (outerController.hasClients) { - outerController.animateTo( - outerController.offset, - duration: const Duration(milliseconds: 500), - curve: Curves.easeInOut, - ); - } + scrollKey.currentState?.animToTop(); } bool _needAnimOnDimensionChanged(bool isVertical) { @@ -247,13 +237,15 @@ class VideoDetailController extends GetxController if (width == null || height == null) { if (isUgc && !isFileSource) { final ugcIntroCtr = Get.find(tag: heroTag); - final data = ugcIntroCtr.videoDetail.value; - if (data.cid == cid.value) { - final dimension = data.dimension!; + final cid = this.cid.value; + final part = ugcIntroCtr.videoDetail.value.pages?.firstWhereOrNull( + (e) => e.cid == cid, + ); + if (part != null) { + final dimension = part.dimension!; width = dimension.width!; height = dimension.height!; } else { - ugcIntroCtr.queryVideoIntro().whenComplete(_setVideoHeight); return; } } else { @@ -263,7 +255,10 @@ class VideoDetailController extends GetxController final isVertical = height > width; if (_scrollCtr?.hasClients != true) { videoHeight = isVertical ? maxVideoHeight : minVideoHeight; - this.isVertical.value = isVertical; + if (this.isVertical.value != isVertical) { + this.isVertical.value = isVertical; + _needAnimOnDimensionChanged(isVertical); + } return; } if (this.isVertical.value != isVertical) { @@ -285,11 +280,11 @@ class VideoDetailController extends GetxController .toPrecision(2); double minVideoHeightPrecise = minVideoHeight.toPrecision(2); if (currentHeight == minVideoHeightPrecise) { + this.videoHeight = minVideoHeight; if (_needAnimOnDimensionChanged(isVertical)) { isExpanding = true; - this.videoHeight = minVideoHeight; + animationController.forward(from: 1); } - animationController.forward(from: 1); } else if (currentHeight < minVideoHeightPrecise) { // expand if (_needAnimOnDimensionChanged(isVertical)) { @@ -472,7 +467,6 @@ class VideoDetailController extends GetxController } } - // 稍后再看面板展开 void showMediaListPanel(BuildContext context) { if (mediaList.isNotEmpty) { Widget panel() => MediaListPanel( @@ -523,13 +517,9 @@ class VideoDetailController extends GetxController if (plPlayerController.isFullScreen.value || showVideoSheet) { PageUtils.showVideoBottomSheet( context, - child: plPlayerController.darkVideoPage && MyApp.darkThemeData != null - ? Theme( - data: MyApp.darkThemeData!, - child: panel(), - ) + child: plPlayerController.darkVideoPage + ? Theme(data: ThemeUtils.darkTheme, child: panel()) : panel(), - isFullScreen: () => plPlayerController.isFullScreen.value, ); } else { childKey.currentState?.showBottomSheet( @@ -567,14 +557,14 @@ class VideoDetailController extends GetxController bool get preInitPlayer => plPlayerController.preInitPlayer; @override int get currPosInMilliseconds => - defaultST?.inMilliseconds ?? plPlayerController.position.inMilliseconds; + defaultST?.inMilliseconds ?? plPlayerController.positionInMilliseconds; @override Future seekTo(Duration duration, {required bool isSeek}) => plPlayerController.seekTo(duration, isSeek: isSeek); @override Widget buildItem(Object item, Animation animation) { - final theme = Get.theme; + final theme = ThemeUtils.theme; return Align( alignment: Alignment.centerLeft, child: SlideTransition( @@ -645,20 +635,23 @@ class VideoDetailController extends GetxController await Get.key.currentState!.push( PublishRoute( pageBuilder: (buildContext, animation, secondaryAnimation) { - return SendDanmakuPanel( + final child = SendDanmakuPanel( cid: cid.value, bvid: bvid, - progress: plPlayerController.position.inMilliseconds, + progress: plPlayerController.positionInMilliseconds, initialValue: savedDanmaku, onSave: (danmaku) => savedDanmaku = danmaku, onSuccess: (danmakuModel) { savedDanmaku = null; plPlayerController.danmakuController?.addDanmaku(danmakuModel); }, - darkVideoPage: plPlayerController.darkVideoPage, dmConfig: dmConfig, onSaveDmConfig: (dmConfig) => this.dmConfig = dmConfig, ); + if (plPlayerController.darkVideoPage) { + return Theme(data: ThemeUtils.darkTheme, child: child); + } + return child; }, ), ); @@ -667,31 +660,38 @@ class VideoDetailController extends GetxController } } - VideoItem findVideoByQa(int qa) { + VideoItem findVideoByQa(int qa, {bool setCodecs = false}) { /// 根据currentVideoQa和currentDecodeFormats 重新设置videoUrl final videoList = data.dash!.video!.where((i) => i.id == qa).toList(); - final currentDecodeFormats = this.currentDecodeFormats.codes; - final defaultDecodeFormats = VideoDecodeFormatType.fromString( - cacheDecode, - ).codes; - final secondDecodeFormats = VideoDecodeFormatType.fromString( - cacheSecondDecode, - ).codes; - - VideoItem? video; - for (final i in videoList) { - final codec = i.codecs!; - if (currentDecodeFormats.any(codec.startsWith)) { - video = i; - break; - } else if (defaultDecodeFormats.any(codec.startsWith)) { - video = i; - } else if (video == null && secondDecodeFormats.any(codec.startsWith)) { - video = i; + final currentCodes = currentDecodeFormats.codes; + VideoItem? bestVideo; + int bestIndex = preferCodecs.length; + for (final video in videoList) { + final c = video.codecs!; + if (currentCodes.any(c.startsWith)) { + return video; + } + for (int i = 0; i < bestIndex; i++) { + if (preferCodecs[i].codes.any(c.startsWith)) { + bestIndex = i; + bestVideo = video; + break; + } } } - return video ?? videoList.first; + + if (setCodecs) { + if (bestIndex < preferCodecs.length) { + currentDecodeFormats = preferCodecs[bestIndex]; + } else { + currentDecodeFormats = VideoDecodeFormatType.fromString( + videoList.first.codecs!, + ); + } + } + + return bestVideo ?? videoList.first; } /// 更新画质、音质 @@ -699,16 +699,12 @@ class VideoDetailController extends GetxController final currentVideoQa = this.currentVideoQa.value; if (currentVideoQa == null) return; _autoPlay.value = true; - playedTime = plPlayerController.position; + playedTime = plPlayerController.videoPlayerController?.state.position; plPlayerController ..isBuffering.value = false - ..buffered.value = Duration.zero; + ..buffered.value = 0; - final video = findVideoByQa(currentVideoQa.code); - if (firstVideo.codecs != video.codecs) { - currentDecodeFormats = VideoDecodeFormatType.fromString(video.codecs!); - } - firstVideo = video; + firstVideo = findVideoByQa(currentVideoQa.code, setCodecs: true); videoUrl = VideoUtils.getCdnUrl(firstVideo.playUrls); /// 根据currentAudioQa 重新设置audioUrl @@ -737,15 +733,10 @@ class VideoDetailController extends GetxController } Future playerInit({ - String? video, - String? audio, - Duration? seekToTime, - Duration? duration, bool? autoplay, - Volume? volume, bool autoFullScreenFlag = false, }) async { - Duration? seek = seekToTime ?? defaultST ?? playedTime; + Duration? seek = defaultST ?? playedTime; if (seek == null || seek == Duration.zero) { seek = getFirstSegment(); } @@ -758,15 +749,13 @@ class VideoDetailController extends GetxController hasDashAudio: entry.hasDashAudio, ) : NetworkSource( - videoSource: video ?? videoUrl!, - audioSource: audio ?? audioUrl, + videoSource: videoUrl!, + audioSource: audioUrl, ), seekTo: seek, - duration: - duration ?? - (data.timeLength == null - ? null - : Duration(milliseconds: data.timeLength!)), + duration: data.timeLength == null + ? null + : Duration(milliseconds: data.timeLength!), isVertical: isVertical.value, aid: aid, bvid: bvid, @@ -782,7 +771,7 @@ class VideoDetailController extends GetxController }, width: firstVideo.width, height: firstVideo.height, - volume: volume ?? this.volume, + volume: volume, autoFullScreenFlag: autoFullScreenFlag, ); @@ -807,8 +796,8 @@ class VideoDetailController extends GetxController bool isQuerying = false; - final Rx?> languages = Rx?>(null); - final Rx currLang = Rx(null); + final languages = Rxn>(); + final currLang = Rxn(); void setLanguage(String language) { if (currLang.value == language) return; if (!isLoginVideo) { @@ -816,14 +805,14 @@ class VideoDetailController extends GetxController return; } currLang.value = language; - queryVideoUrl(defaultST: playedTime); + queryVideoUrl(fromReset: true); } Volume? volume; // 视频链接 + /// TODO: merge [DownloadHttp.getVideoUrl]. Future queryVideoUrl({ - Duration? defaultST, bool fromReset = false, bool autoFullScreenFlag = false, }) async { @@ -838,7 +827,7 @@ class VideoDetailController extends GetxController querySponsorBlock(bvid: bvid, cid: cid.value); } if (plPlayerController.cacheVideoQa == null) { - final isWiFi = await Utils.isWiFi; + final isWiFi = await ConnectivityUtils.isWiFi; plPlayerController ..cacheVideoQa = isWiFi ? Pref.defaultVideoQa @@ -856,6 +845,7 @@ class VideoDetailController extends GetxController tryLook: plPlayerController.tryLook, videoType: _actualVideoType ?? videoType, language: currLang.value, + voiceBalance: plPlayerController.enableAudioNormalization, ); if (result case Success(:final response)) { @@ -866,11 +856,13 @@ class VideoDetailController extends GetxController volume = data.volume; - final progress = args.remove('progress'); - if (progress != null) { - this.defaultST = Duration(milliseconds: progress); - } else if (defaultST == null && data.lastPlayTime != null) { - this.defaultST = Duration(milliseconds: data.lastPlayTime!); + if (!fromReset) { + final progress = args.remove('progress'); + if (progress != null) { + defaultST = Duration(milliseconds: progress); + } else { + defaultST = Duration(milliseconds: data.lastPlayTime); + } } if (!isUgc && !fromReset && plPlayerController.enablePgcSkip) { @@ -900,7 +892,7 @@ class VideoDetailController extends GetxController quality: videoQuality, ); _setVideoHeight(); - currentDecodeFormats = VideoDecodeFormatType.fromString('avc1'); + currentDecodeFormats = VideoDecodeFormatType.AVC; currentVideoQa.value = videoQuality; await _initPlayerIfNeeded(autoFullScreenFlag); isQuerying = false; @@ -932,42 +924,25 @@ class VideoDetailController extends GetxController } currentVideoQa.value = VideoQuality.fromCode(targetVideoQa); + /// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式 + final supportFormats = data.supportFormats!; + + // 根据画质选编码格式 + currentDecodeFormats = VideoUtils.selectCodec( + supportFormats + .firstWhere( + (e) => e.quality == targetVideoQa, + orElse: () => supportFormats.first, + ) + .codecs!, + preferCodecs, + ); + /// 取出符合当前画质的videoList - final List videosList = videoList + final videosList = videoList .where((e) => e.quality.code == targetVideoQa) .toList(); - /// 优先顺序 设置中指定解码格式 -> 当前可选的首个解码格式 - final List supportFormats = data.supportFormats!; - // 根据画质选编码格式 - final List supportDecodeFormats = supportFormats - .firstWhere( - (e) => e.quality == targetVideoQa, - orElse: () => supportFormats.first, - ) - .codecs!; - // 默认从设置中取AV1 - currentDecodeFormats = VideoDecodeFormatType.fromString(cacheDecode); - VideoDecodeFormatType secondDecodeFormats = - VideoDecodeFormatType.fromString(cacheSecondDecode); - // 当前视频没有对应格式返回第一个 - int flag = 0; - for (final e in supportDecodeFormats) { - if (currentDecodeFormats.codes.any(e.startsWith)) { - flag = 1; - break; - } else if (secondDecodeFormats.codes.any(e.startsWith)) { - flag = 2; - } - } - if (flag == 2) { - currentDecodeFormats = secondDecodeFormats; - } else if (flag == 0) { - currentDecodeFormats = VideoDecodeFormatType.fromString( - supportDecodeFormats.first, - ); - } - /// 取出符合当前解码格式的videoItem firstVideo = videosList.firstWhere( (e) => currentDecodeFormats.codes.any(e.codecs!.startsWith), @@ -1020,7 +995,7 @@ class VideoDetailController extends GetxController PostSegmentModel( segment: Pair( first: 0, - second: plPlayerController.position.inMilliseconds / 1000, + second: plPlayerController.positionInMilliseconds / 1000, ), category: SegmentType.sponsor, actionType: ActionType.skip, @@ -1028,23 +1003,16 @@ class VideoDetailController extends GetxController ); } if (plPlayerController.isFullScreen.value || showVideoSheet) { + final child = PostPanel( + enableSlide: false, + videoDetailController: this, + plPlayerController: plPlayerController, + ); PageUtils.showVideoBottomSheet( context, - child: plPlayerController.darkVideoPage && MyApp.darkThemeData != null - ? Theme( - data: MyApp.darkThemeData!, - child: PostPanel( - enableSlide: false, - videoDetailController: this, - plPlayerController: plPlayerController, - ), - ) - : PostPanel( - enableSlide: false, - videoDetailController: this, - plPlayerController: plPlayerController, - ), - isFullScreen: () => plPlayerController.isFullScreen.value, + child: plPlayerController.darkVideoPage + ? Theme(data: ThemeUtils.darkTheme, child: child) + : child, ); } else { childKey.currentState?.showBottomSheet( @@ -1060,16 +1028,14 @@ class VideoDetailController extends GetxController RxList subtitles = RxList(); final Map vttSubtitles = {}; - late final RxInt vttSubtitlesIndex = (-1).obs; - late final RxBool showVP = true.obs; - late final RxList viewPointList = [].obs; + late final vttSubtitlesIndex = (-1).obs; + late final showVP = true.obs; + late final viewPointList = [].obs; // 设定字幕轨道 Future setSubtitle(int index) async { if (index <= 0) { - await plPlayerController.videoPlayerController?.setSubtitleTrack( - SubtitleTrack.no(), - ); + await plPlayerController.videoPlayerController?.setSubtitleTrack(.no()); vttSubtitlesIndex.value = index; return; } @@ -1078,19 +1044,8 @@ class VideoDetailController extends GetxController final sub = subtitles[index - 1]; String subUri = subtitle.id; - File? file; if (subtitle.isData) { - subUri = path.join(tmpDirPath, '${cid.value}-${sub.lan}.vtt'); - file = File(subUri); - if (!file.existsSync()) { - await file.writeAsString(subtitle.id); - if (plPlayerController.videoPlayerController?.disposed == false) { - plPlayerController.videoPlayerController!.release.add(file.tryDel); - } else { - file.tryDel(); - return; - } - } + subUri = 'memory://$subUri'; } await plPlayerController.videoPlayerController?.setSubtitleTrack( SubtitleTrack(subUri, sub.lanDoc, sub.lan, uri: true), @@ -1157,9 +1112,9 @@ class VideoDetailController extends GetxController ); if (res case Success(:final response)) { // interactive video + late final introCtr = Get.find(tag: heroTag); if (isUgc && graphVersion == null) { try { - final introCtr = Get.find(tag: heroTag); if (introCtr.videoDetail.value.rights?.isSteinGate == 1) { graphVersion = response.interaction?.graphVersion; getSteinEdgeInfo(); @@ -1171,22 +1126,18 @@ class VideoDetailController extends GetxController if (isUgc && continuePlayingPart) { continuePlayingPart = false; - try { - UgcIntroController ugcIntroController = Get.find( - tag: heroTag, - ); - if ((ugcIntroController.videoDetail.value.pages?.length ?? 0) > 1 && - response.lastPlayCid != null && - response.lastPlayCid != 0) { - if (response.lastPlayCid != cid.value) { - int index = ugcIntroController.videoDetail.value.pages! - .indexWhere((item) => item.cid == response.lastPlayCid); + final lastCid = response.lastPlayCid; + if (lastCid != null && lastCid != 0 && lastCid != cid.value) { + try { + final pages = introCtr.videoDetail.value.pages; + if (pages != null && pages.length > 1) { + final index = pages.indexWhere((item) => item.cid == lastCid); if (index != -1) { onAddItem(index); } } - } - } catch (_) {} + } catch (_) {} + } } if (plPlayerController.showViewPoints && @@ -1205,27 +1156,53 @@ class VideoDetailController extends GetxController } catch (_) {} } - if (response.subtitle?.subtitles?.isNotEmpty == true) { - subtitles.value = response.subtitle!.subtitles!; - - final idx = switch (Pref.subtitlePreferenceV2) { - SubtitlePrefType.off => 0, - SubtitlePrefType.on => 1, - SubtitlePrefType.withoutAi => - subtitles.first.lan.startsWith('ai') ? 0 : 1, - SubtitlePrefType.auto => - !subtitles.first.lan.startsWith('ai') || - (PlatformUtils.isMobile && - (await FlutterVolumeController.getVolume() ?? 0.0) <= - 0.0) - ? 1 - : 0, - }; - await setSubtitle(idx); + if (response.subtitle?.subtitles case final sub? when (sub.isNotEmpty)) { + _setSubtitle(sub); + } else if (!Accounts.main.isLogin) { + final res = await DmGrpc.dmView(aid, cid.value); + if (res case Success(:final response)) { + if (response.hasSubtitle() && + response.subtitle.subtitles.isNotEmpty) { + _setSubtitle( + response.subtitle.subtitles + .map( + (i) => Subtitle( + lan: i.lan, + lanDoc: i.lanDoc, + subtitleUrl: i.subtitleUrl.replaceFirst( + RegExp('^https?:'), + '', + ), + isAi: i.type == .AI, + ), + ) + .toList() + ..sort(), + ); + } + } else { + res.toast(); + } } } } + Future _setSubtitle(List sub) async { + subtitles.value = sub; + final idx = switch (Pref.subtitlePreferenceV2) { + .off => 0, + .on => 1, + .withoutAi => sub.first.lan.startsWith('ai') ? 0 : 1, + .auto => + !sub.first.lan.startsWith('ai') || + (PlatformUtils.isMobile && + (await FlutterVolumeController.getVolume() ?? 0.0) <= 0.0) + ? 1 + : 0, + }; + await setSubtitle(idx); + } + void updateMediaListHistory(int aid) { if (args['sortField'] != null) { VideoHttp.medialistHistory( @@ -1367,27 +1344,18 @@ class VideoDetailController extends GetxController ).videoDetail.value.title; } catch (_) {} if (plPlayerController.isFullScreen.value || showVideoSheet) { + final child = NoteListPage( + oid: aid, + enableSlide: false, + heroTag: heroTag, + isStein: graphVersion != null, + title: title, + ); PageUtils.showVideoBottomSheet( context, - child: plPlayerController.darkVideoPage && MyApp.darkThemeData != null - ? Theme( - data: MyApp.darkThemeData!, - child: NoteListPage( - oid: aid, - enableSlide: false, - heroTag: heroTag, - isStein: graphVersion != null, - title: title, - ), - ) - : NoteListPage( - oid: aid, - enableSlide: false, - heroTag: heroTag, - isStein: graphVersion != null, - title: title, - ), - isFullScreen: () => plPlayerController.isFullScreen.value, + child: plPlayerController.darkVideoPage + ? Theme(data: ThemeUtils.darkTheme, child: child) + : child, ); } else { childKey.currentState?.showBottomSheet( diff --git a/lib/pages/video/download_panel/view.dart b/lib/pages/video/download_panel/view.dart index 9f14590b5c..01298cf097 100644 --- a/lib/pages/video/download_panel/view.dart +++ b/lib/pages/video/download_panel/view.dart @@ -383,7 +383,7 @@ class _DownloadPanelState extends State { return Padding( padding: const EdgeInsets.only(bottom: 2), child: SizedBox( - height: 98, + height: 110, child: Builder( builder: (context) { return Material( @@ -412,8 +412,8 @@ class _DownloadPanelState extends State { children: [ NetworkImgLayer( src: cover, - width: 140.8, - height: 88, + width: 160, + height: 100, cacheWidth: cacheWidth, ), if (duration != null && duration > 0) diff --git a/lib/pages/video/introduction/local/controller.dart b/lib/pages/video/introduction/local/controller.dart index c7af976e6b..299469576a 100644 --- a/lib/pages/video/introduction/local/controller.dart +++ b/lib/pages/video/introduction/local/controller.dart @@ -15,7 +15,7 @@ class LocalIntroController extends CommonIntroController { void queryVideoIntro() {} @override - void actionCoinVideo() {} + int get copyright => throw UnimplementedError(); @override void actionLikeVideo() {} @@ -71,9 +71,9 @@ class LocalIntroController extends CommonIntroController { if (index != 0) { SchedulerBinding.instance.addPostFrameCallback((_) { try { - if (videoDetailCtr.scrollKey.currentState?.mounted ?? false) { - (videoDetailCtr.scrollKey.currentState!.innerController - as ExtendedNestedScrollController) + final state = videoDetailCtr.scrollKey.currentState; + if (state != null && state.mounted) { + (state.innerController as ExtendedNestedScrollController) .nestedPositions .first .localJumpTo(_offset); @@ -88,7 +88,7 @@ class LocalIntroController extends CommonIntroController { } final index = (-1).obs; - double get _offset => index * 100 + 7 - 35; + double get _offset => index * 112 + 7 - 35; final list = RxList(); @override diff --git a/lib/pages/video/introduction/local/view.dart b/lib/pages/video/introduction/local/view.dart index bf8f16dd69..6f5629ae2f 100644 --- a/lib/pages/video/introduction/local/view.dart +++ b/lib/pages/video/introduction/local/view.dart @@ -42,7 +42,7 @@ class _LocalIntroPanelState extends State final item = _controller.list[index]; return _buildItem(theme, currIndex == index, index, item); }, - itemExtent: 100, + itemExtent: 112, ); }); } @@ -59,7 +59,7 @@ class _LocalIntroPanelState extends State return Padding( padding: const EdgeInsets.only(bottom: 2), child: SizedBox( - height: 98, + height: 110, child: Material( type: MaterialType.transparency, child: InkWell( @@ -85,15 +85,15 @@ class _LocalIntroPanelState extends State borderRadius: Style.mdRadius, child: Image.file( cover, - width: 140.8, - height: 88, + width: 160, + height: 100, fit: BoxFit.cover, cacheWidth: cacheWidth - ? 140.8.cacheSize(context) + ? 160.cacheSize(context) : null, cacheHeight: cacheWidth ? null - : 88.cacheSize(context), + : 100.cacheSize(context), colorBlendMode: NetworkImgLayer.reduce ? BlendMode.modulate : null, @@ -104,8 +104,8 @@ class _LocalIntroPanelState extends State ) : NetworkImgLayer( src: entry.cover, - width: 140.8, - height: 88, + width: 160, + height: 100, ), PBadge( text: DurationUtils.formatDuration( @@ -185,7 +185,7 @@ class _LocalIntroPanelState extends State ), Align( alignment: Alignment.bottomRight, - child: entry.moreBtn(theme), + child: entry.moreBtn(theme.colorScheme), ), ], ), diff --git a/lib/pages/video/introduction/pgc/controller.dart b/lib/pages/video/introduction/pgc/controller.dart index 6426cc5f92..523ccee7c8 100644 --- a/lib/pages/video/introduction/pgc/controller.dart +++ b/lib/pages/video/introduction/pgc/controller.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math' show max; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/http/fav.dart'; import 'package:PiliPlus/http/loading_state.dart'; @@ -16,17 +17,17 @@ import 'package:PiliPlus/models_new/video/video_detail/episode.dart' import 'package:PiliPlus/models_new/video/video_detail/stat_detail.dart'; import 'package:PiliPlus/pages/common/common_intro_controller.dart'; import 'package:PiliPlus/pages/dynamics_repost/view.dart'; -import 'package:PiliPlus/pages/video/pay_coins/view.dart'; import 'package:PiliPlus/pages/video/reply/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; import 'package:PiliPlus/services/service_locator.dart'; -import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:PiliPlus/utils/share_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -113,29 +114,8 @@ class PgcIntroController extends CommonIntroController { } } - // 投币 @override - void actionCoinVideo() { - if (!isLogin) { - SmartDialog.showToast('账号未登录'); - return; - } - - if (coinNum.value >= 2) { - SmartDialog.showToast('达到投币上限啦~'); - return; - } - - if (GlobalData().coins != null && GlobalData().coins! < 1) { - SmartDialog.showToast('硬币不足'); - // return; - } - - PayCoinsPage.toPayCoinsPage( - onPayCoin: coinVideo, - hasCoin: coinNum.value == 1, - ); - } + int get copyright => 1; // 分享视频 @override @@ -144,55 +124,44 @@ class PgcIntroController extends CommonIntroController { '${HttpString.baseUrl}/bangumi/play/ep$epId${videoDetailCtr.playedTimePos}'; showDialog( context: context, - builder: (_) => AlertDialog( + builder: (_) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - title: const Text( - '复制链接', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - Utils.copyText(videoUrl); - }, - ), - ListTile( - dense: true, - title: const Text( - '其它app打开', - style: TextStyle(fontSize: 14), - ), - onTap: () { + children: [ + DialogOption( + child: const Text('复制链接', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + Utils.copyText(videoUrl); + }, + ), + DialogOption( + child: const Text('其它app打开', style: TextStyle(fontSize: 14)), + onPressed: () { + Get.back(); + PageUtils.launchURL(videoUrl); + }, + ), + if (PlatformUtils.isMobile) + DialogOption( + child: const Text('分享视频', style: TextStyle(fontSize: 14)), + onPressed: () { + final item = pgcItem.episodes?.firstWhereOrNull( + (item) => item.epId == epId, + ); Get.back(); - PageUtils.launchURL(videoUrl); + ShareUtils.shareText( + '${pgcItem.title}${item != null ? ' ${item.showTitle}' : ''}' + ' - $videoUrl', + ); }, ), - if (PlatformUtils.isMobile) - ListTile( - dense: true, - title: const Text( - '分享视频', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - Utils.shareText(videoUrl); - }, - ), - ListTile( - dense: true, - title: const Text( - '分享至动态', - style: TextStyle(fontSize: 14), - ), - onTap: () { + if (isLogin) + DialogOption( + child: const Text('分享至动态', style: TextStyle(fontSize: 14)), + onPressed: () { Get.back(); - EpisodeItem? item = pgcItem.episodes?.firstWhereOrNull( + final item = pgcItem.episodes?.firstWhereOrNull( (item) => item.epId == epId, ); showModalBottomSheet( @@ -201,15 +170,15 @@ class PgcIntroController extends CommonIntroController { useSafeArea: true, builder: (context) => RepostPanel( rid: epId, - /** - * 1:番剧 // 4097 - 2:电影 // 4098 - 3:纪录片 // 4101 - 4:国创 // 4100 - 5:电视剧 // 4099 - 6:漫画 - 7:综艺 // 4099 - */ + /* + 1:番剧 // 4097 + 2:电影 // 4098 + 3:纪录片 // 4101 + 4:国创 // 4100 + 5:电视剧 // 4099 + 6:漫画 + 7:综艺 // 4099 + */ dynType: switch (pgcItem.type) { 1 => 4097, 2 => 4098, @@ -226,16 +195,16 @@ class PgcIntroController extends CommonIntroController { ); }, ), - ListTile( - dense: true, - title: const Text( + if (isLogin) + DialogOption( + child: const Text( '分享至消息', style: TextStyle(fontSize: 14), ), - onTap: () { + onPressed: () { Get.back(); try { - EpisodeItem item = pgcItem.episodes!.firstWhere( + final item = pgcItem.episodes!.firstWhere( (item) => item.epId == epId, ); final title = @@ -267,8 +236,7 @@ class PgcIntroController extends CommonIntroController { } }, ), - ], - ), + ], ), ); } diff --git a/lib/pages/video/introduction/pgc/view.dart b/lib/pages/video/introduction/pgc/view.dart index 4a124dc5d9..8d94dab634 100644 --- a/lib/pages/video/introduction/pgc/view.dart +++ b/lib/pages/video/introduction/pgc/view.dart @@ -9,7 +9,6 @@ import 'package:PiliPlus/common/widgets/image_viewer/hero.dart'; import 'package:PiliPlus/common/widgets/stat/stat.dart'; import 'package:PiliPlus/models/common/image_preview_type.dart'; import 'package:PiliPlus/models/common/image_type.dart'; -import 'package:PiliPlus/models/common/stat_type.dart'; import 'package:PiliPlus/models_new/pgc/pgc_info_model/result.dart'; import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/introduction/pgc/controller.dart'; @@ -60,7 +59,7 @@ class _PgcIntroPageState extends State { @override Widget build(BuildContext context) { - final ThemeData theme = Theme.of(context); + final colorScheme = ColorScheme.of(context); final item = introController.pgcItem; final isLandscape = widget.isLandscape; Widget sliver = SliverToBoxAdapter( @@ -71,13 +70,13 @@ class _PgcIntroPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, spacing: 10, children: [ - _buildCover(theme, isLandscape, item), - Expanded(child: _buildInfoPanel(isLandscape, theme, item)), + _buildCover(colorScheme, isLandscape, item), + Expanded(child: _buildInfoPanel(isLandscape, colorScheme, item)), ], ), const SizedBox(height: 6), // 点赞收藏转发 布局样式2 - if (introController.isPgc) actionGrid(theme, item, introController), + if (introController.isPgc) actionGrid(item, introController), // 番剧分集 if (item.episodes?.isNotEmpty == true) PgcPanel( @@ -94,18 +93,16 @@ class _PgcIntroPageState extends State { if (!introController.isPgc) { final brief = _buildBrief(item); if (brief != null) { - sliver = SliverMainAxisGroup( - slivers: [ - sliver, - brief, - ], - ); + sliver = SliverMainAxisGroup(slivers: [sliver, brief]); } } return SliverPadding( - padding: - const EdgeInsets.all(Style.safeSpace) + - const EdgeInsets.only(bottom: 50), + padding: const .fromLTRB( + Style.safeSpace, + Style.safeSpace, + Style.safeSpace, + Style.safeSpace + 50, + ), sliver: sliver, ); } @@ -118,11 +115,7 @@ class _PgcIntroPageState extends State { final imgWidth = maxWidth - padding; padding = padding / 2; return SliverPadding( - padding: EdgeInsetsGeometry.only( - top: 10, - left: padding, - right: padding, - ), + padding: .only(top: 10, left: padding, right: padding), sliver: SliverMainAxisGroup( slivers: img.map((e) { return SliverToBoxAdapter( @@ -140,7 +133,11 @@ class _PgcIntroPageState extends State { return null; } - Widget _buildCover(ThemeData theme, bool isLandscape, PgcInfoModel item) { + Widget _buildCover( + ColorScheme colorScheme, + bool isLandscape, + PgcInfoModel item, + ) { return Stack( clipBehavior: Clip.none, children: [ @@ -180,11 +177,11 @@ class _PgcIntroPageState extends State { ? const Icon(Icons.star_rounded) : const Icon(Icons.star_border_rounded), bgColor: isFav - ? theme.colorScheme.secondaryContainer - : theme.colorScheme.onInverseSurface, + ? colorScheme.secondaryContainer + : colorScheme.onInverseSurface, iconColor: isFav - ? theme.colorScheme.onSecondaryContainer - : theme.colorScheme.onSurfaceVariant, + ? colorScheme.onSecondaryContainer + : colorScheme.onSurfaceVariant, ); }), ), @@ -192,7 +189,11 @@ class _PgcIntroPageState extends State { ); } - Widget _buildInfoPanel(bool isLandscape, ThemeData theme, PgcInfoModel item) { + Widget _buildInfoPanel( + bool isLandscape, + ColorScheme colorScheme, + PgcInfoModel item, + ) { if (introController.isPgc) { Widget subBtn() => Obx( () { @@ -206,10 +207,8 @@ class _PgcIntroPageState extends State { vertical: 10, ), visualDensity: VisualDensity.compact, - foregroundColor: isFollowed ? theme.colorScheme.outline : null, - backgroundColor: isFollowed - ? theme.colorScheme.onInverseSurface - : null, + foregroundColor: isFollowed ? colorScheme.outline : null, + backgroundColor: isFollowed ? colorScheme.onInverseSurface : null, ), onPressed: followStatus == -1 ? null @@ -259,10 +258,7 @@ class _PgcIntroPageState extends State { List desc() => [ Text( item.newEp!.desc!, - style: TextStyle( - fontSize: 12, - color: theme.colorScheme.outline, - ), + style: TextStyle(fontSize: 12, color: colorScheme.outline), ), Text.rich( TextSpan( @@ -274,24 +270,15 @@ class _PgcIntroPageState extends State { ), ], ), - style: TextStyle( - fontSize: 12, - color: theme.colorScheme.outline, - ), + style: TextStyle(fontSize: 12, color: colorScheme.outline), ), ]; Widget stat() => Wrap( spacing: 6, runSpacing: 2, children: [ - StatWidget( - type: StatType.play, - value: item.stat!.view, - ), - StatWidget( - type: StatType.danmaku, - value: item.stat!.danmaku, - ), + StatWidget(type: .play, value: item.stat!.view), + StatWidget(type: .danmaku, value: item.stat!.danmaku), if (isLandscape) ...desc(), ], ); @@ -314,10 +301,7 @@ class _PgcIntroPageState extends State { Expanded( child: Text( '简介:${item.evaluate}', - style: TextStyle( - fontSize: 13, - color: theme.colorScheme.outline, - ), + style: TextStyle(fontSize: 13, color: colorScheme.outline), ), ), ], @@ -351,7 +335,7 @@ class _PgcIntroPageState extends State { role, style: TextStyle( fontSize: 12, - color: theme.colorScheme.outline, + color: colorScheme.outline, ), ), ], @@ -391,7 +375,7 @@ class _PgcIntroPageState extends State { item.subtitle!, style: TextStyle( fontSize: 13, - color: theme.colorScheme.onSurfaceVariant, + color: colorScheme.onSurfaceVariant, ), ), ], @@ -400,7 +384,6 @@ class _PgcIntroPageState extends State { } Widget actionGrid( - ThemeData theme, PgcInfoModel item, PgcIntroController introController, ) { diff --git a/lib/pages/video/introduction/pgc/widgets/intro_detail.dart b/lib/pages/video/introduction/pgc/widgets/intro_detail.dart index 514609431f..a3beecb1f8 100644 --- a/lib/pages/video/introduction/pgc/widgets/intro_detail.dart +++ b/lib/pages/video/introduction/pgc/widgets/intro_detail.dart @@ -147,17 +147,17 @@ class _IntroDetailState extends State ), const SizedBox(height: 4), Row( + spacing: 6, children: [ - Text( - widget.item.areas!.first.name!, - style: smallTitle, - ), - const SizedBox(width: 6), + if (widget.item.areas?.isNotEmpty ?? false) + Text( + widget.item.areas!.first.name!, + style: smallTitle, + ), Text( widget.item.publish!.pubTimeShow!, style: smallTitle, ), - const SizedBox(width: 6), Text( widget.item.newEp!.desc!, style: smallTitle, diff --git a/lib/pages/video/introduction/pgc/widgets/pgc_panel.dart b/lib/pages/video/introduction/pgc/widgets/pgc_panel.dart index 2fec611e35..da1d1d6109 100644 --- a/lib/pages/video/introduction/pgc/widgets/pgc_panel.dart +++ b/lib/pages/video/introduction/pgc/widgets/pgc_panel.dart @@ -13,6 +13,7 @@ import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:get/get.dart'; class PgcPanel extends StatefulWidget { @@ -213,11 +214,10 @@ class _PgcPanelState extends State { if (item.badge?.isNotEmpty == true) ...[ const SizedBox(width: 2), if (item.badge == '会员') - Image.asset( + SvgPicture.asset( Assets.vipIcon, height: 16, - cacheHeight: 16.cacheSize(context), - semanticLabel: "大会员", + semanticsLabel: "大会员", ) else Text( diff --git a/lib/pages/video/introduction/ugc/controller.dart b/lib/pages/video/introduction/ugc/controller.dart index 866caeb2fe..cbfcc2eda8 100644 --- a/lib/pages/video/introduction/ugc/controller.dart +++ b/lib/pages/video/introduction/ugc/controller.dart @@ -24,13 +24,13 @@ import 'package:PiliPlus/models_new/video/video_detail/stat_detail.dart'; import 'package:PiliPlus/models_new/video/video_detail/ugc_season.dart'; import 'package:PiliPlus/pages/common/common_intro_controller.dart'; import 'package:PiliPlus/pages/dynamics_repost/view.dart'; -import 'package:PiliPlus/pages/video/pay_coins/view.dart'; import 'package:PiliPlus/pages/video/related/controller.dart'; import 'package:PiliPlus/pages/video/reply/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; import 'package:PiliPlus/services/service_locator.dart'; import 'package:PiliPlus/utils/accounts.dart'; -import 'package:PiliPlus/utils/extension/context_ext.dart'; +import 'package:PiliPlus/utils/device_utils.dart'; +import 'package:PiliPlus/utils/extension/size_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/global_data.dart'; @@ -38,6 +38,7 @@ import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/request_utils.dart'; +import 'package:PiliPlus/utils/share_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:expandable/expandable.dart'; @@ -77,7 +78,7 @@ class UgcIntroController extends CommonIntroController with ReloadMixin { ); if (!alwaysExpandIntroPanel && Pref.expandIntroPanelH) { WidgetsBinding.instance.addPostFrameCallback((_) { - if (!expandableCtr.expanded && Get.context!.isLandscape) { + if (!expandableCtr.expanded && !DeviceUtils.size.isPortrait) { expandableCtr.toggle(); } }); @@ -274,31 +275,8 @@ class UgcIntroController extends CommonIntroController with ReloadMixin { } } - // 投币 @override - void actionCoinVideo() { - if (!isLogin) { - SmartDialog.showToast('账号未登录'); - return; - } - - int copyright = videoDetail.value.copyright ?? 1; - if ((copyright != 1 && coinNum.value >= 1) || coinNum.value >= 2) { - SmartDialog.showToast('达到投币上限啦~'); - return; - } - - if (GlobalData().coins != null && GlobalData().coins! < 1) { - SmartDialog.showToast('硬币不足'); - // return; - } - - PayCoinsPage.toPayCoinsPage( - onPayCoin: coinVideo, - copyright: copyright, - hasCoin: coinNum.value == 1, - ); - } + int get copyright => videoDetail.value.copyright ?? 1; @override (Object, int) get getFavRidType => (IdUtils.bv2av(bvid), 2); @@ -314,60 +292,59 @@ class UgcIntroController extends CommonIntroController with ReloadMixin { String videoUrl = '${HttpString.baseUrl}/video/$bvid'; showDialog( context: context, - builder: (_) => AlertDialog( + builder: (_) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - title: const Text( - '复制链接', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - Utils.copyText(videoUrl); - }, - trailing: playedTimePos.isNotEmpty - ? iconButton( - tooltip: '精确分享', - icon: const Icon(Icons.timer_outlined), - onPressed: () { - Get.back(); - Utils.copyText('$videoUrl$playedTimePos'); - }, - ) - : null, + children: [ + ListTile( + dense: true, + title: const Text( + '复制链接', + style: TextStyle(fontSize: 14), ), + onTap: () { + Get.back(); + Utils.copyText(videoUrl); + }, + trailing: playedTimePos.isNotEmpty + ? iconButton( + tooltip: '精确分享', + icon: const Icon(Icons.timer_outlined), + onPressed: () { + Get.back(); + Utils.copyText('$videoUrl$playedTimePos'); + }, + ) + : null, + ), + ListTile( + dense: true, + title: const Text( + '其它app打开', + style: TextStyle(fontSize: 14), + ), + onTap: () { + Get.back(); + PageUtils.launchURL(videoUrl); + }, + ), + if (PlatformUtils.isMobile) ListTile( dense: true, title: const Text( - '其它app打开', + '分享视频', style: TextStyle(fontSize: 14), ), onTap: () { Get.back(); - PageUtils.launchURL(videoUrl); + ShareUtils.shareText( + '${videoDetail.title} ' + 'UP主: ${videoDetail.owner!.name!}' + ' - $videoUrl', + ); }, ), - if (PlatformUtils.isMobile) - ListTile( - dense: true, - title: const Text( - '分享视频', - style: TextStyle(fontSize: 14), - ), - onTap: () { - Get.back(); - Utils.shareText( - '${videoDetail.title} ' - 'UP主: ${videoDetail.owner!.name!}' - ' - $videoUrl', - ); - }, - ), + if (isLogin) ListTile( dense: true, title: const Text( @@ -390,6 +367,7 @@ class UgcIntroController extends CommonIntroController with ReloadMixin { ); }, ), + if (isLogin) ListTile( dense: true, title: const Text( @@ -416,8 +394,7 @@ class UgcIntroController extends CommonIntroController with ReloadMixin { } }, ), - ], - ), + ], ), ); } diff --git a/lib/pages/video/introduction/ugc/view.dart b/lib/pages/video/introduction/ugc/view.dart index 32c642bf04..8e4dd7f3e4 100644 --- a/lib/pages/video/introduction/ugc/view.dart +++ b/lib/pages/video/introduction/ugc/view.dart @@ -650,9 +650,7 @@ class _UgcIntroPanelState extends State { videoDetailCtr.data.timeLength ?? videoDetailCtr .plPlayerController - .duration - .value - .inMilliseconds; + .durationInMilliseconds; if (duration > 0) { final ytbId = youtubeRegExp .firstMatch(matchStr) diff --git a/lib/pages/video/introduction/ugc/widgets/action_item.dart b/lib/pages/video/introduction/ugc/widgets/action_item.dart index d224812728..db61cdd2fa 100644 --- a/lib/pages/video/introduction/ugc/widgets/action_item.dart +++ b/lib/pages/video/introduction/ugc/widgets/action_item.dart @@ -44,6 +44,7 @@ class ActionItem extends StatelessWidget { selectStatus ? selectIcon!.icon! : icon.icon, size: 18, color: selectStatus ? primary : icon.color ?? colorScheme.outline, + semanticLabel: semanticsLabel, ); if (animation != null) { @@ -53,11 +54,8 @@ class ActionItem extends StatelessWidget { children: [ AnimatedBuilder( animation: animation!, - builder: (context, child) => Arc( - size: 28, - color: primary, - progress: -animation!.value, - ), + builder: (context, child) => + Arc(size: 28, color: primary, progress: -animation!.value), ), child, ], @@ -69,7 +67,7 @@ class ActionItem extends StatelessWidget { child = Material( type: .transparency, child: InkWell( - borderRadius: const BorderRadius.all(Radius.circular(6)), + borderRadius: const .all(.circular(6)), onTap: _isThumbsUp ? null : onTap, onLongPress: _isThumbsUp ? null : onLongPress, onSecondaryTap: PlatformUtils.isMobile || _isThumbsUp @@ -104,9 +102,8 @@ class ActionItem extends StatelessWidget { if (hasText) { return AnimatedSwitcher( duration: const Duration(milliseconds: 300), - transitionBuilder: (Widget child, Animation animation) { - return ScaleTransition(scale: animation, child: child); - }, + transitionBuilder: (child, animation) => + ScaleTransition(scale: animation, child: child), child: child, ); } diff --git a/lib/pages/video/introduction/ugc/widgets/triple_mixin.dart b/lib/pages/video/introduction/ugc/widgets/triple_mixin.dart index 9f98e03a19..4fdd8ff32d 100644 --- a/lib/pages/video/introduction/ugc/widgets/triple_mixin.dart +++ b/lib/pages/video/introduction/ugc/widgets/triple_mixin.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:PiliPlus/pages/video/pay_coins/view.dart'; +import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -18,6 +20,46 @@ mixin TripleMixin on GetxController, TickerProvider { bool get hasTriple => hasLike.value && hasCoin && hasFav.value; + bool get isLogin; + + bool isHasCopyright(int copyright) { + return copyright != 2; + } + + bool reachCoinLimit(bool hasCopyRight, num coinNum) { + return (!hasCopyRight && coinNum >= 1) || coinNum >= 2; + } + + int get copyright; + + void onPayCoin(int coin, bool coinWithLike); + + void actionCoinVideo() { + if (!isLogin) { + SmartDialog.showToast('账号未登录'); + return; + } + + final coinNum = this.coinNum.value; + final copyright = this.copyright; + final hasCopyright = isHasCopyright(copyright); + if (reachCoinLimit(hasCopyright, coinNum)) { + SmartDialog.showToast('达到投币上限啦~'); + return; + } + + if (GlobalData().coins != null && GlobalData().coins! < 1) { + SmartDialog.showToast('硬币不足'); + // return; + } + + PayCoinsPage.toPayCoinsPage( + onPayCoin: onPayCoin, + hasCoin: coinNum == 1, + hasCopyright: hasCopyright, + ); + } + void actionTriple(); void actionLikeVideo(); diff --git a/lib/pages/video/medialist/view.dart b/lib/pages/video/medialist/view.dart index d322d73161..654ce0b001 100644 --- a/lib/pages/video/medialist/view.dart +++ b/lib/pages/video/medialist/view.dart @@ -57,7 +57,7 @@ class _MediaListPanelState extends State final bvid = widget.bvid; final bvIndex = widget.mediaList.indexWhere((item) => item.bvid == bvid); _controller = ScrollController( - initialScrollOffset: bvIndex <= 0 ? 0 : bvIndex * 100.0 + 7, + initialScrollOffset: bvIndex <= 0 ? 0 : bvIndex * 112.0 + 7, ); } @@ -131,7 +131,7 @@ class _MediaListPanelState extends State ), sliver: Obx( () => SliverFixedExtentList.builder( - itemExtent: 100, + itemExtent: 112, itemCount: widget.mediaList.length, itemBuilder: (context, index) { if (index == widget.mediaList.length - 1 && @@ -166,7 +166,7 @@ class _MediaListPanelState extends State return Padding( padding: const EdgeInsets.only(bottom: 2), child: SizedBox( - height: 98, + height: 110, child: Material( type: MaterialType.transparency, child: InkWell( @@ -196,8 +196,8 @@ class _MediaListPanelState extends State children: [ NetworkImgLayer( src: item.cover, - width: 140.8, - height: 88, + width: 160, + height: 100, ), if (item.badge?.isNotEmpty == true) PBadge( diff --git a/lib/pages/video/member/view.dart b/lib/pages/video/member/view.dart index 8816a62e3b..18cf3ccfd0 100644 --- a/lib/pages/video/member/view.dart +++ b/lib/pages/video/member/view.dart @@ -19,7 +19,7 @@ import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/introduction/ugc/controller.dart'; import 'package:PiliPlus/pages/video/member/controller.dart'; import 'package:PiliPlus/utils/accounts.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; +import 'package:PiliPlus/utils/bili_utils.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; @@ -66,7 +66,7 @@ class _HorizontalMemberPageState extends State { final index = res.response?.indexWhere((e) => e.bvid == _bvid) ?? -1; if (index != -1) { WidgetsBinding.instance.addPostFrameCallback((_) { - _controller.scrollController.jumpTo(100.0 * index); + _controller.scrollController.jumpTo(112.0 * index); }); } } @@ -177,7 +177,7 @@ class _HorizontalMemberPageState extends State { Loading() => SliverFixedExtentList.builder( itemCount: 10, itemBuilder: (_, _) => const VideoCardHSkeleton(), - itemExtent: 100, + itemExtent: 112, ), Success(:final response) => response != null && response.isNotEmpty @@ -209,7 +209,7 @@ class _HorizontalMemberPageState extends State { ); }, itemCount: response.length, - itemExtent: 100, + itemExtent: 112, ), ], ) @@ -256,13 +256,10 @@ class _HorizontalMemberPageState extends State { ), ), const SizedBox(width: 8), - Image.asset( - Utils.levelName( - memberInfoModel.level!, - isSeniorMember: memberInfoModel.isSeniorMember == 1, - ), + BiliUtils.levelPicture( + memberInfoModel.level!, + isSeniorMember: memberInfoModel.isSeniorMember == 1, height: 11, - cacheHeight: 11.cacheSize(context), ), ], ), diff --git a/lib/pages/video/note/view.dart b/lib/pages/video/note/view.dart index f8ac82f274..ca8bdab954 100644 --- a/lib/pages/video/note/view.dart +++ b/lib/pages/video/note/view.dart @@ -9,9 +9,8 @@ import 'package:PiliPlus/pages/common/slide/common_slide_page.dart'; import 'package:PiliPlus/pages/video/note/controller.dart'; import 'package:PiliPlus/pages/webview/view.dart'; import 'package:PiliPlus/utils/accounts.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; +import 'package:PiliPlus/utils/bili_utils.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; -import 'package:PiliPlus/utils/utils.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -263,13 +262,10 @@ class _NoteListPageState extends State ), ), const SizedBox(width: 6), - Image.asset( - Utils.levelName( - item.author!.level!, - isSeniorMember: item.author!.isSeniorMember == 1, - ), + BiliUtils.levelPicture( + item.author!.level!, + isSeniorMember: item.author!.isSeniorMember == 1, height: 11, - cacheHeight: 11.cacheSize(context), ), ], ), diff --git a/lib/pages/video/pay_coins/view.dart b/lib/pages/video/pay_coins/view.dart index 69c79a416b..1b5861a537 100644 --- a/lib/pages/video/pay_coins/view.dart +++ b/lib/pages/video/pay_coins/view.dart @@ -15,33 +15,35 @@ import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; +typedef OnPayCoin = Function(int coin, bool coinWithLike); + class PayCoinsPage extends StatefulWidget { const PayCoinsPage({ super.key, required this.onPayCoin, - int copyright = 1, - this.hasCoin = false, - }) : hasCopyright = copyright == 1; + required this.hasCoin, + required this.hasCopyright, + }); - final Function(int coin, bool coinWithLike) onPayCoin; - final bool hasCopyright; + final OnPayCoin onPayCoin; final bool hasCoin; + final bool hasCopyright; @override State createState() => _PayCoinsPageState(); static void toPayCoinsPage({ - required Function(int coin, bool coinWithLike) onPayCoin, - int copyright = 1, - bool hasCoin = false, + required OnPayCoin onPayCoin, + required bool hasCoin, + required bool hasCopyright, }) { Get.key.currentState!.push( PublishRoute( pageBuilder: (buildContext, animation, secondaryAnimation) { return PayCoinsPage( onPayCoin: onPayCoin, - copyright: copyright, hasCoin: hasCoin, + hasCopyright: hasCopyright, ); }, transitionDuration: const Duration(milliseconds: 225), @@ -105,8 +107,8 @@ class _PayCoinsPageState extends State return index == 0 ? Assets.mario : Assets.gunSister; } - late final color = Colors.black.withValues(alpha: 0.4); - Color _getPayFilter(int index) => _canPay(index) ? Colors.transparent : color; + Color _getPayFilter(int index) => + _canPay(index) ? Colors.transparent : const Color(0x66000000); @override void initState() { @@ -199,38 +201,39 @@ class _PayCoinsPageState extends State } Widget _buildCoinWidget(int index, double factor) { - return Center( - child: SizedBox( - height: 70 + (factor * 30), - width: 70 + (factor * 30), - child: ColorFiltered( - colorFilter: ColorFilter.mode( - _getPayFilter(index), - BlendMode.srcATop, + final filter = _getPayFilter(index); + final boxSize = 70 + (factor * 30); + final coinSize = 35 + (factor * 15); + return Stack( + alignment: .center, + clipBehavior: .none, + children: [ + SlideTransition( + position: _boxAnim, + child: Image.asset( + Assets.payBox, + color: filter, + width: boxSize, + height: boxSize, + cacheWidth: 100.cacheSize(context), + colorBlendMode: .srcATop, ), - child: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - SlideTransition( - position: _boxAnim, - child: Image.asset(Assets.payBox), - ), - SlideTransition( - position: _coinSlideAnim, - child: FadeTransition( - opacity: _coinFadeAnim, - child: Image.asset( - height: 35 + (factor * 15), - width: 35 + (factor * 15), - index == 0 ? Assets.coinsOne : Assets.coinsTwo, - ), - ), - ), - ], + ), + SlideTransition( + position: _coinSlideAnim, + child: FadeTransition( + opacity: _coinFadeAnim, + child: Image.asset( + height: coinSize, + width: coinSize, + cacheWidth: 50.cacheSize(context), + color: filter, + colorBlendMode: .srcATop, + index == 0 ? Assets.coinsOne : Assets.coinsTwo, + ), ), ), - ), + ], ); } @@ -384,8 +387,13 @@ class _PayCoinsPageState extends State ], ) else - SizedBox(height: 100, child: _buildCoinWidget(0, 1)), - SizedBox(height: isV ? 25 : 10), + Center( + child: SizedBox(height: 100, child: _buildCoinWidget(0, 1)), + ), + if (isV) + const SizedBox(height: 25) + else + const SizedBox(height: 10), if (_hasCopyright) GestureDetector( behavior: HitTestBehavior.opaque, diff --git a/lib/pages/video/post_panel/view.dart b/lib/pages/video/post_panel/view.dart index b2b6c6153e..71417dcf0e 100644 --- a/lib/pages/video/post_panel/view.dart +++ b/lib/pages/video/post_panel/view.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:math'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; -import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; +import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/sponsor_block.dart'; @@ -183,10 +183,9 @@ class _PostPanelState extends State late final PlPlayerController plPlayerController = widget.plPlayerController; late final List list = videoDetailController.postList; - late final double videoDuration = - plPlayerController.duration.value.inMilliseconds / 1000; + double get videoDuration => plPlayerController.durationInMilliseconds / 1000; - double currentPos() => plPlayerController.position.inMilliseconds / 1000; + double currentPos() => plPlayerController.positionInMilliseconds / 1000; @override Widget buildPage(ThemeData theme) { @@ -249,7 +248,7 @@ class _PostPanelState extends State @override Widget buildList(ThemeData theme) { if (list.isEmpty) { - return const HttpError(isSliver: false); + return scrollableError; } final bottom = MediaQuery.viewPaddingOf(context).bottom; Widget child = ListView.builder( @@ -467,14 +466,21 @@ class _PostPanelState extends State final player = plPlayerController.videoPlayerController; if (player != null) { final start = (item.segment.first * 1000).round(); + Future seekTo() => player.seek( + Duration(milliseconds: (item.segment.second * 1000).round()), + ); + if (start <= 0) { + seekTo(); + if (!player.state.playing) { + await player.play(); + } + return; + } final seek = max(0, start - 2000); await player.seek(Duration(milliseconds: seek)); if (!player.state.playing) { await player.play(); } - Future seekTo() => player.seek( - Duration(milliseconds: (item.segment.second * 1000).round()), - ); if (start > seek) { final posSub = player.stream.position.listen( null, diff --git a/lib/pages/video/reply/controller.dart b/lib/pages/video/reply/controller.dart index be234c4a4d..9746848f70 100644 --- a/lib/pages/video/reply/controller.dart +++ b/lib/pages/video/reply/controller.dart @@ -33,7 +33,7 @@ class VideoReplyController extends ReplyController { Future> customGetData() => ReplyGrpc.mainList( oid: isPugv ? videoCtr.epId! : aid, type: videoType.replyType, - mode: mode.value, + mode: mode, cursorNext: cursorNext, offset: paginationReply?.nextOffset, ); diff --git a/lib/pages/video/reply/view.dart b/lib/pages/video/reply/view.dart index ad86d5cd28..72e3e45106 100644 --- a/lib/pages/video/reply/view.dart +++ b/lib/pages/video/reply/view.dart @@ -36,6 +36,7 @@ class _VideoReplyPanelState extends State with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin, + BaseFabMixin, FabMixin { late VideoReplyController _videoReplyController; @@ -238,11 +239,10 @@ class _VideoReplyPanelState extends State EasyThrottle.throttle('replyReply', const Duration(milliseconds: 500), () { int oid = replyItem.oid.toInt(); int rpid = replyItem.id.toInt(); - showBottomSheet( - context: context, + Scaffold.of(context).showBottomSheet( backgroundColor: Colors.transparent, constraints: const BoxConstraints(), - builder: (context) => VideoReplyReplyPanel( + (context) => VideoReplyReplyPanel( id: id, oid: oid, rpid: rpid, diff --git a/lib/pages/video/reply/widgets/reply_item_grpc.dart b/lib/pages/video/reply/widgets/reply_item_grpc.dart index 9f17ab1d74..7da217e0c6 100644 --- a/lib/pages/video/reply/widgets/reply_item_grpc.dart +++ b/lib/pages/video/reply/widgets/reply_item_grpc.dart @@ -4,6 +4,8 @@ import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; +import 'package:PiliPlus/common/widgets/custom_icon.dart'; +import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/dialog/report.dart'; import 'package:PiliPlus/common/widgets/extra_hit_test_widget.dart'; import 'package:PiliPlus/common/widgets/flutter/text/text.dart' as custom_text; @@ -26,6 +28,8 @@ import 'package:PiliPlus/pages/video/controller.dart'; import 'package:PiliPlus/pages/video/reply/widgets/zan_grpc.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; +import 'package:PiliPlus/utils/bili_utils.dart'; +import 'package:PiliPlus/utils/color_utils.dart'; import 'package:PiliPlus/utils/danmaku_utils.dart'; import 'package:PiliPlus/utils/date_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; @@ -38,10 +42,11 @@ import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; +import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/url_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:cached_network_image/cached_network_image.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; @@ -179,13 +184,10 @@ class ReplyItemGrpc extends StatelessWidget { ), ), ), - Image.asset( - Utils.levelName( - member.level, - isSeniorMember: member.isSeniorMember == 1, - ), + BiliUtils.levelPicture( + member.level.toInt(), + isSeniorMember: member.isSeniorMember == 1, height: 11, - cacheHeight: 11.cacheSize(context), ), if (replyItem.mid == upMid) const PBadge( @@ -275,7 +277,7 @@ class ReplyItemGrpc extends StatelessWidget { style: TextStyle( fontSize: 8, fontFamily: Assets.digitalNum, - color: Utils.parseColor(garb.cardFanColor), + color: ColourUtils.parseColor(garb.cardFanColor), ), ), ), @@ -451,6 +453,27 @@ class ReplyItemGrpc extends StatelessWidget { tapTargetSize: .shrinkWrap, padding: WidgetStatePropertyAll(.zero), ); + + Widget? dialogBtn; + if (replyLevel == 2 && needDivider && replyItem.id != replyItem.dialog) { + dialogBtn = SizedBox( + height: 32, + child: TextButton( + onPressed: showDialogue, + style: buttonStyle, + child: Text('查看对话', style: textStyle), + ), + ); + } else if (replyLevel == 3 && replyItem.parent != replyItem.root) { + dialogBtn = SizedBox( + height: 32, + child: TextButton( + onPressed: jumpToDialogue, + style: buttonStyle, + child: Text('跳转回复', style: textStyle), + ), + ); + } return Row( children: [ const SizedBox(width: 36), @@ -487,35 +510,16 @@ class ReplyItemGrpc extends StatelessWidget { buttonStyle, ), const SizedBox(width: 2), - ] else if (replyItem.replyControl.cardLabels.isNotEmpty) ...[ + ] else if (replyControl.cardLabels.isNotEmpty) ...[ Text( - replyItem.replyControl.cardLabels - .map((e) => e.textContent) - .join(' '), + dialogBtn != null + ? replyControl.cardLabels.first.textContent + : replyControl.cardLabels.map((e) => e.textContent).join(' '), style: textStyle.copyWith(color: theme.colorScheme.secondary), ), const SizedBox(width: 2), ], - if (replyLevel == 2 && needDivider && replyItem.id != replyItem.dialog) - SizedBox( - height: 32, - child: TextButton( - onPressed: showDialogue, - style: buttonStyle, - child: Text('查看对话', style: textStyle), - ), - ) - else if (replyLevel == 3 && - needDivider && - replyItem.parent != replyItem.root) - SizedBox( - height: 32, - child: TextButton( - onPressed: jumpToDialogue, - style: buttonStyle, - child: Text('跳转回复', style: textStyle), - ), - ), + ?dialogBtn, const Spacer(), ZanButtonGrpc(replyItem: replyItem), const SizedBox(width: 5), @@ -908,16 +912,16 @@ class ReplyItemGrpc extends StatelessWidget { }, ); - if (urlKeys.isNotEmpty) { - List unmatchedItems = urlKeys - .where((url) => !matchedUrls.contains(url)) - .toList(); - if (unmatchedItems.isNotEmpty) { - for (final patternStr in unmatchedItems) { - addUrl(patternStr, content.urls[patternStr]!); - } - } - } + // if (urlKeys.isNotEmpty) { + // List unmatchedItems = urlKeys + // .where((url) => !matchedUrls.contains(url)) + // .toList(); + // if (unmatchedItems.isNotEmpty) { + // for (final patternStr in unmatchedItems) { + // addUrl(patternStr, content.urls[patternStr]!); + // } + // } + // } if (!hasNote && replyControl.isNote && replyControl.isNoteV2) { final Color color; @@ -1162,6 +1166,8 @@ class ReplyItemGrpc extends StatelessWidget { child: SelectableText( message, style: const TextStyle(fontSize: 15, height: 1.7), + contextMenuBuilder: (_, editableTextState) => + _filterMenuBuilder(context, editableTextState), ), ), ), @@ -1187,18 +1193,63 @@ class ReplyItemGrpc extends StatelessWidget { onCheckReply?.call(item); }, minLeadingWidth: 0, - leading: const Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - Icon(Icons.shield_outlined, size: 19), - Icon(Icons.reply, size: 12), - ], - ), + leading: const Icon(CustomIcons.shield_reply, size: 19), title: Text('检查评论', style: style), ), ], ), ); } + + static Widget _filterMenuBuilder( + BuildContext context, + EditableTextState editableTextState, + ) { + final items = editableTextState.contextMenuButtonItems; + if (!editableTextState.textEditingValue.selection.isCollapsed) { + items.add( + ContextMenuButtonItem( + onPressed: () { + Navigator.of(context).pop(); + final select = editableTextState.textEditingValue; + String text = RegExp.escape( + select.selection.textInside(select.text), + ); + if (ReplyGrpc.enableFilter) text = '|$text'; + + showConfirmDialog( + context: context, + title: const Text('是否确认评论过滤的变更:'), + content: Text.rich( + TextSpan( + text: ReplyGrpc.replyRegExp.pattern, + children: [ + TextSpan( + text: text, + style: const TextStyle( + color: Colors.green, + fontWeight: .bold, + ), + ), + ], + ), + ), + onConfirm: () { + final filter = ReplyGrpc.replyRegExp.pattern + text; + ReplyGrpc.replyRegExp = RegExp(filter, caseSensitive: true); + ReplyGrpc.enableFilter = true; + GStorage.setting.put(SettingBoxKey.banWordForReply, filter); + SmartDialog.showToast('已保存'); + }, + ); + }, + label: '加入过滤', + ), + ); + } + return AdaptiveTextSelectionToolbar.buttonItems( + buttonItems: items, + anchors: editableTextState.contextMenuAnchors, + ); + } } diff --git a/lib/pages/video/reply/widgets/zan_grpc.dart b/lib/pages/video/reply/widgets/zan_grpc.dart index b6218051c8..9fdd8055ff 100644 --- a/lib/pages/video/reply/widgets/zan_grpc.dart +++ b/lib/pages/video/reply/widgets/zan_grpc.dart @@ -119,7 +119,12 @@ class ZanButtonGrpc extends StatelessWidget { SizedBox( height: 32, child: TextButton( - style: style, + style: const ButtonStyle( + visualDensity: .compact, + tapTargetSize: .shrinkWrap, + padding: WidgetStatePropertyAll(.zero), + minimumSize: WidgetStatePropertyAll(.square(40)), + ), onPressed: () => onHateReply( context, isProcessing, diff --git a/lib/pages/video/reply_new/view.dart b/lib/pages/video/reply_new/view.dart index 30efcdbcbb..42d8d28b7c 100644 --- a/lib/pages/video/reply_new/view.dart +++ b/lib/pages/video/reply_new/view.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:math' show max; import 'package:PiliPlus/common/widgets/button/toolbar_icon_button.dart'; +import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/flutter/text_field/controller.dart' show RichTextType; import 'package:PiliPlus/common/widgets/flutter/text_field/text_field.dart'; @@ -11,7 +12,6 @@ import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/http/video.dart'; -import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/models/common/publish_panel_type.dart'; import 'package:PiliPlus/models/dynamics/result.dart' show FilePicModel; import 'package:PiliPlus/pages/common/publish/common_rich_text_pub_page.dart'; @@ -25,6 +25,7 @@ import 'package:PiliPlus/utils/extension/context_ext.dart'; import 'package:PiliPlus/utils/grid.dart'; import 'package:PiliPlus/utils/path_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/material.dart' hide TextField; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -72,13 +73,11 @@ class _ReplyPageState extends CommonRichTextPubPageState { @override void didChangeDependencies() { super.didChangeDependencies(); - themeData = darkVideoPage - ? MyApp.darkThemeData ?? Theme.of(context) - : Theme.of(context); + themeData = darkVideoPage ? ThemeUtils.darkTheme : Theme.of(context); } late final darkVideoPage = - Get.currentRoute.startsWith('/video') && Pref.darkVideoPage; + Get.currentRoute == '/videoV' && Pref.darkVideoPage; late ThemeData themeData; @override @@ -196,49 +195,22 @@ class _ReplyPageState extends CommonRichTextPubPageState { atBtn, const SizedBox(width: 8), moreBtn, - Expanded( - child: Center( - child: Obx( - () { - final syncToDynamic = _syncToDynamic.value; - return TextButton( - style: TextButton.styleFrom( - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - padding: const EdgeInsets.all(13), - visualDensity: VisualDensity.compact, - foregroundColor: syncToDynamic - ? themeData.colorScheme.secondary - : themeData.colorScheme.outline, - ), - onPressed: () => _syncToDynamic.value = !syncToDynamic, - child: Row( - spacing: 4, - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - syncToDynamic - ? Icons.check_box - : Icons.check_box_outline_blank, - size: 22, - ), - const Flexible( - child: Text( - '转到动态', - maxLines: 1, - style: TextStyle(height: 1), - strutStyle: StrutStyle(leading: 0, height: 1), - ), - ), - ], - ), - ); - }, + const SizedBox(width: 8), + Obx( + () => ToolbarIconButton( + tooltip: '转到动态', + onPressed: _syncToDynamic.toggle, + icon: const Icon( + CustomIcons.repeat_rounded_rotate_90, + size: 22, ), + selected: _syncToDynamic.value, ), ), + const Spacer(), Obx( () => FilledButton.tonal( - onPressed: enablePublish.value ? onPublish : null, + onPressed: enablePublish.value ? onPublishThrottle : null, style: FilledButton.styleFrom( tapTargetSize: MaterialTapTargetSize.shrinkWrap, padding: const EdgeInsets.symmetric( @@ -318,7 +290,6 @@ class _ReplyPageState extends CommonRichTextPubPageState { children: [ item( onTap: () async { - controller.keepChatPanel(); final ({String title, String url})? res = await Get.to( ReplySearchPage(type: widget.replyType, oid: widget.oid), ); @@ -329,7 +300,6 @@ class _ReplyPageState extends CommonRichTextPubPageState { rawText: '${res.url} ', ); } - controller.restoreChatPanel(); }, icon: Icon(Icons.post_add, size: 28, color: color), title: '插入内容', @@ -380,12 +350,16 @@ class _ReplyPageState extends CommonRichTextPubPageState { final res = await plPlayerController .plPlayerController .videoPlayerController - ?.screenshot(format: .png); + ?.screenshot(); if (res != null) { - final path = - '$tmpDirPath/${Utils.generateRandomString(8)}.png'; - await File(path).writeAsBytes(res); - imageList.add(FilePicModel(path: path)); + final png = await res.toByteData(format: .png); + if (png != null) { + final path = + '$tmpDirPath/${Utils.generateRandomString(8)}.png'; + await File(path).writeAsBytes(png.buffer.asUint8List()); + imageList.add(FilePicModel(path: path)); + } + res.dispose(); } else { debugPrint('null screenshot'); } diff --git a/lib/pages/video/reply_reply/controller.dart b/lib/pages/video/reply_reply/controller.dart index 29ebcded39..e2d7b3b2ed 100644 --- a/lib/pages/video/reply_reply/controller.dart +++ b/lib/pages/video/reply_reply/controller.dart @@ -33,7 +33,7 @@ class VideoReplyReplyController extends ReplyController int replyType; bool hasRoot = false; - final Rx firstFloor = Rx(null); + final firstFloor = Rxn(); final index = RxnInt(); @@ -53,7 +53,7 @@ class VideoReplyReplyController extends ReplyController @override void onInit() { super.onInit(); - mode.value = Mode.MAIN_LIST_TIME; + mode = Mode.MAIN_LIST_TIME; queryData(); } @@ -132,19 +132,10 @@ class VideoReplyReplyController extends ReplyController oid: oid, root: rpid, rpid: id ?? 0, - mode: mode.value, + mode: mode, offset: paginationReply?.nextOffset, ); - @override - void queryBySort() { - if (isLoading) return; - mode.value = mode.value == Mode.MAIN_LIST_HOT - ? Mode.MAIN_LIST_TIME - : Mode.MAIN_LIST_HOT; - onReload(); - } - @override Future onReload() { if (loadingState.value.isSuccess) { diff --git a/lib/pages/video/reply_reply/view.dart b/lib/pages/video/reply_reply/view.dart index efa174847c..ed7e3d9108 100644 --- a/lib/pages/video/reply_reply/view.dart +++ b/lib/pages/video/reply_reply/view.dart @@ -6,7 +6,7 @@ import 'package:PiliPlus/common/widgets/loading_widget/http_error.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_header.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' - show ReplyInfo, Mode; + show ReplyInfo; import 'package:PiliPlus/http/loading_state.dart'; import 'package:PiliPlus/pages/common/slide/common_slide_page.dart'; import 'package:PiliPlus/pages/video/reply/widgets/reply_item_grpc.dart'; @@ -203,17 +203,16 @@ class _VideoReplyReplyPanelState extends State } return _header(theme, firstFloor); }), - _sortWidget(theme), + _sortWidget(theme.colorScheme), ], - Obx(() => _buildBody(theme, _controller.loadingState.value)), + Obx( + () => _buildBody(theme.colorScheme, _controller.loadingState.value), + ), ], ), ); if (widget.isNested) { - return ExtendedVisibilityDetector( - uniqueKey: Key(_tag), - child: child, - ); + return ExtendedVisibilityDetector(uniqueKey: Key(_tag), child: child); } return child; } @@ -243,9 +242,9 @@ class _VideoReplyReplyPanelState extends State ); } - Widget _sortWidget(ThemeData theme) { + Widget _sortWidget(ColorScheme colorScheme) { return SliverPinnedHeader( - backgroundColor: theme.colorScheme.surface, + backgroundColor: colorScheme.surface, child: Padding( padding: const EdgeInsets.fromLTRB(12, 2.5, 6, 2.5), child: Row( @@ -265,18 +264,11 @@ class _VideoReplyReplyPanelState extends State TextButton.icon( style: Style.buttonStyle, onPressed: _controller.queryBySort, - icon: Icon( - Icons.sort, - size: 16, - color: theme.colorScheme.secondary, - ), + icon: Icon(Icons.sort, size: 16, color: colorScheme.secondary), label: Obx( () => Text( - _controller.mode.value == Mode.MAIN_LIST_HOT ? '按热度' : '按时间', - style: TextStyle( - fontSize: 13, - color: theme.colorScheme.secondary, - ), + _controller.sortType.value.text!, + style: TextStyle(fontSize: 13, color: colorScheme.secondary), ), ), ), @@ -287,7 +279,7 @@ class _VideoReplyReplyPanelState extends State } Widget _buildBody( - ThemeData theme, + ColorScheme colorScheme, LoadingState?> loadingState, ) { final jumpIndex = _controller.index.value; @@ -305,15 +297,13 @@ class _VideoReplyReplyPanelState extends State return Container( height: 125, alignment: Alignment.center, - margin: EdgeInsets.only( - bottom: MediaQuery.viewPaddingOf(context).bottom, - ), + margin: .only(bottom: MediaQuery.viewPaddingOf(context).bottom), child: Text( _controller.isEnd ? '没有更多了' : '加载中...', textAlign: TextAlign.center, style: TextStyle( fontSize: 12, - color: theme.colorScheme.outline, + color: colorScheme.outline, ), ), ); @@ -323,13 +313,10 @@ class _VideoReplyReplyPanelState extends State return ColoredBoxTransition( color: _colorAnimation ??= _controller.animController.drive( ColorTween( - begin: theme.colorScheme.onInverseSurface, - end: theme.colorScheme.surface, - ).chain( - CurveTween( - curve: const Interval(0.8, 1.0), // 前0.8s不变, 后0.2s开始动画 - ), - ), + begin: colorScheme.onInverseSurface, + end: colorScheme.surface, + // 前0.8s不变, 后0.2s开始动画 + ).chain(CurveTween(curve: const Interval(0.8, 1.0))), ), child: child, ); diff --git a/lib/pages/video/reply_search_item/child/widgets/item.dart b/lib/pages/video/reply_search_item/child/widgets/item.dart index ae6ac2fdd7..8fa693f98b 100644 --- a/lib/pages/video/reply_search_item/child/widgets/item.dart +++ b/lib/pages/video/reply_search_item/child/widgets/item.dart @@ -1,6 +1,5 @@ import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/image/image_save.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' @@ -9,7 +8,7 @@ import 'package:PiliPlus/models/common/badge_type.dart'; import 'package:PiliPlus/models/common/reply/reply_search_type.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:flutter/material.dart'; import 'package:get/get.dart'; class ReplySearchItem extends StatelessWidget { diff --git a/lib/pages/video/send_danmaku/view.dart b/lib/pages/video/send_danmaku/view.dart index 68018e228c..d34049ebdc 100644 --- a/lib/pages/video/send_danmaku/view.dart +++ b/lib/pages/video/send_danmaku/view.dart @@ -4,7 +4,6 @@ import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/http/danmaku.dart'; import 'package:PiliPlus/http/loading_state.dart'; -import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/models/common/publish_panel_type.dart'; import 'package:PiliPlus/pages/common/publish/common_text_pub_page.dart'; import 'package:PiliPlus/pages/danmaku/danmaku_model.dart'; @@ -24,7 +23,6 @@ class SendDanmakuPanel extends CommonTextPubPage { final dynamic progress; final ValueChanged> onSuccess; - final bool darkVideoPage; // config final ({int? mode, int? fontSize, Color? color})? dmConfig; @@ -38,7 +36,6 @@ class SendDanmakuPanel extends CommonTextPubPage { this.bvid, this.progress, required this.onSuccess, - required this.darkVideoPage, this.dmConfig, this.onSaveDmConfig, }); @@ -138,16 +135,14 @@ class _SendDanmakuPanelState extends CommonTextPubPageState { @override void didChangeDependencies() { super.didChangeDependencies(); - themeData = widget.darkVideoPage - ? MyApp.darkThemeData ?? Theme.of(context) - : Theme.of(context); + themeData = Theme.of(context); } late ThemeData themeData; @override Widget build(BuildContext context) { - Widget child = ViewSafeArea( + return ViewSafeArea( child: Align( alignment: Alignment.bottomCenter, child: Container( @@ -166,7 +161,6 @@ class _SendDanmakuPanelState extends CommonTextPubPageState { ), ), ); - return widget.darkVideoPage ? Theme(data: themeData, child: child) : child; } @override @@ -421,7 +415,7 @@ class _SendDanmakuPanelState extends CommonTextPubPageState { iconColor: enablePublish.value ? themeData.colorScheme.primary : themeData.colorScheme.outline, - onPressed: enablePublish.value ? onPublish : null, + onPressed: enablePublish.value ? onPublishThrottle : null, icon: const Icon(Icons.send), ), ), @@ -430,9 +424,8 @@ class _SendDanmakuPanelState extends CommonTextPubPageState { ); } - Future _showColorPicker() async { - controller.keepChatPanel(); - await showDialog( + void _showColorPicker() { + showDialog( context: context, builder: (context) => AlertDialog( clipBehavior: Clip.hardEdge, @@ -448,7 +441,6 @@ class _SendDanmakuPanelState extends CommonTextPubPageState { ), ), ); - controller.restoreChatPanel(); } @override diff --git a/lib/pages/video/view.dart b/lib/pages/video/view.dart index 33ee64b066..6e20e1aa36 100644 --- a/lib/pages/video/view.dart +++ b/lib/pages/video/view.dart @@ -12,7 +12,7 @@ import 'package:PiliPlus/common/widgets/keep_alive_wrapper.dart'; import 'package:PiliPlus/common/widgets/route_aware_mixin.dart'; import 'package:PiliPlus/common/widgets/scroll_physics.dart'; import 'package:PiliPlus/common/widgets/sliver/sliver_pinned_dynamic_header.dart'; -import 'package:PiliPlus/main.dart'; +import 'package:PiliPlus/common/widgets/svg/play_icon.dart'; import 'package:PiliPlus/models/common/episode_panel_type.dart'; import 'package:PiliPlus/models_new/pgc/pgc_info_model/result.dart'; import 'package:PiliPlus/models_new/video/video_detail/episode.dart' as ugc; @@ -51,17 +51,18 @@ import 'package:PiliPlus/services/service_locator.dart'; import 'package:PiliPlus/services/shutdown_timer_service.dart' show shutdownTimerService; import 'package:PiliPlus/utils/accounts.dart'; -import 'package:PiliPlus/utils/extension/num_ext.dart'; +import 'package:PiliPlus/utils/android/bindings.g.dart'; import 'package:PiliPlus/utils/extension/scroll_controller_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; +import 'package:PiliPlus/utils/max_screen_size.dart'; import 'package:PiliPlus/utils/mobile_observer.dart'; import 'package:PiliPlus/utils/num_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; -import 'package:floating/floating.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show SystemUiOverlayStyle; @@ -78,11 +79,7 @@ class VideoDetailPageV extends StatefulWidget { } class _VideoDetailPageVState extends State - with - TickerProviderStateMixin, - RouteAware, - RouteAwareMixin, - WidgetsBindingObserver { + with RouteAware, RouteAwareMixin, WidgetsBindingObserver { final heroTag = Get.arguments['heroTag']; late final VideoDetailController videoDetailController; @@ -141,7 +138,7 @@ class _VideoDetailPageVState extends State videoDetailController = Get.put(VideoDetailController(), tag: heroTag); if (videoDetailController.removeSafeArea) { - hideStatusBar(); + hideSystemBar(); } if (videoDetailController.showReply) { @@ -199,6 +196,11 @@ class _VideoDetailPageVState extends State } Future? playCallBack() { + if (!isShowing) { + plPlayerController + ?..addStatusLister(playerListener) + ..addPositionListener(positionListener); + } return plPlayerController?.play(); } @@ -263,16 +265,14 @@ class _VideoDetailPageVState extends State } if (exitFlag) { - // 结束播放退出全屏 if (autoExitFullscreen) { plPlayerController!.triggerFullScreen(status: false); if (plPlayerController!.controlsLock.value) { plPlayerController!.onLockControl(false); } - } - // 播放完展示控制栏 - if (Platform.isAndroid) { - if (await Floating().pipStatus == PiPStatus.disabled) { + } else { + if (plPlayerController!.controlsLock.value && + (!Platform.isAndroid || !AndroidHelper.isPipMode)) { plPlayerController!.onLockControl(false); } } @@ -330,13 +330,6 @@ class _VideoDetailPageVState extends State tag: videoDetailController.heroTag, ); - if (!Get.previousRoute.startsWith('/video')) { - if (Platform.isAndroid && !videoDetailController.setSystemBrightness) { - ScreenBrightnessPlatform.instance.resetApplicationScreenBrightness(); - } - PlPlayerController.setPlayCallBack(null); - } - if (!videoDetailController.isFileSource) { if (videoDetailController.isUgc) { ugcIntroController @@ -348,7 +341,7 @@ class _VideoDetailPageVState extends State } if (!videoDetailController.removeSafeArea) { - showStatusBar(); + showSystemBar(); } if (!videoDetailController.plPlayerController.isCloseAll) { @@ -460,6 +453,10 @@ class _VideoDetailPageVState extends State final size = MediaQuery.sizeOf(context); maxWidth = size.width; maxHeight = size.height; + isWindowMode = MaxScreenSize.isWindowMode( + width: maxWidth * videoDetailController.uiScale, + height: maxHeight * videoDetailController.uiScale, + ); videoDetailController.plPlayerController.screenRatio = maxHeight / maxWidth; final shortestSide = size.shortestSide; @@ -474,12 +471,13 @@ class _VideoDetailPageVState extends State : minVideoHeight; themeData = videoDetailController.plPlayerController.darkVideoPage - ? MyApp.darkThemeData ?? Theme.of(context) + ? ThemeUtils.darkTheme : Theme.of(context); } - bool get removeAppBar => - videoDetailController.removeSafeArea || (isFullScreen && !isPortrait); + bool removeAppBar(bool isFullScreen) => + videoDetailController.removeSafeArea || + (isWindowMode && isFullScreen && !isPortrait); Widget get childWhenDisabled { return Obx( @@ -487,7 +485,7 @@ class _VideoDetailPageVState extends State final isFullScreen = this.isFullScreen; return Scaffold( resizeToAvoidBottomInset: false, - appBar: removeAppBar + appBar: removeAppBar(isFullScreen) ? null : PreferredSize( preferredSize: const Size.fromHeight(0), @@ -527,7 +525,7 @@ class _VideoDetailPageVState extends State onlyOneScrollInBody: true, pinnedHeaderSliverHeightBuilder: () { double pinnedHeight = this.isFullScreen || !isPortrait - ? maxHeight - (isPortrait ? padding.top : 0) + ? maxHeight - (isWindowMode && !isPortrait ? 0 : padding.top) : videoDetailController.isExpanding || videoDetailController.isCollapsing ? videoDetailController.animHeight @@ -553,7 +551,7 @@ class _VideoDetailPageVState extends State }, headerSliverBuilder: (context, innerBoxIsScrolled) { final height = isFullScreen || !isPortrait - ? maxHeight - (isPortrait ? padding.top : 0) + ? maxHeight - (isWindowMode && !isPortrait ? 0 : padding.top) : videoDetailController.isExpanding || videoDetailController.isCollapsing ? videoDetailController.animHeight @@ -637,7 +635,7 @@ class _VideoDetailPageVState extends State Text( '${videoDetailController.playedTime == null ? '立即' - : plPlayerController!.playerStatus.isCompleted + : plPlayerController!.isCompleted ? '重新' : '继续'}播放', style: TextStyle( @@ -695,13 +693,11 @@ class _VideoDetailPageVState extends State : Positioned.fill( bottom: -2, child: GestureDetector( - onTap: () async { + onTap: () { if (!videoDetailController.isFileSource) { if (videoDetailController.isQuerying) { if (kDebugMode) { - debugPrint( - 'handlePlay: querying', - ); + debugPrint('handlePlay: querying'); } return; } @@ -725,21 +721,7 @@ class _VideoDetailPageVState extends State null) { handlePlay(); } else { - if (plPlayerController! - .videoPlayerController! - .state - .completed) { - await plPlayerController! - .videoPlayerController! - .seek(Duration.zero); - plPlayerController! - .videoPlayerController! - .play(); - } else { - plPlayerController! - .videoPlayerController! - .playOrPause(); - } + plPlayerController!.onDoubleTapCenter(); } }, behavior: HitTestBehavior.opaque, @@ -789,7 +771,7 @@ class _VideoDetailPageVState extends State final isFullScreen = this.isFullScreen; return Scaffold( resizeToAvoidBottomInset: false, - appBar: removeAppBar + appBar: removeAppBar(isFullScreen) ? null : AppBar(backgroundColor: Colors.black, toolbarHeight: 0), body: Padding( @@ -930,7 +912,7 @@ class _VideoDetailPageVState extends State final videoWidth = isFullScreen ? maxWidth : width; final double height = width / Style.aspectRatio16x9; final videoHeight = isFullScreen - ? maxHeight - (isPortrait ? padding.top : 0) + ? maxHeight - (isWindowMode && !isPortrait ? 0 : padding.top) : height; if (height > maxHeight) { return childSplit(Style.aspectRatio16x9); @@ -1024,7 +1006,7 @@ class _VideoDetailPageVState extends State final isFullScreen = this.isFullScreen; return Scaffold( resizeToAvoidBottomInset: false, - appBar: removeAppBar + appBar: removeAppBar(isFullScreen) ? null : AppBar(backgroundColor: Colors.black, toolbarHeight: 0), body: Padding( @@ -1056,7 +1038,7 @@ class _VideoDetailPageVState extends State final shouldShowSeasonPanel = _shouldShowSeasonPanel; final double height = maxHeight / 2.5; final videoHeight = isFullScreen - ? maxHeight - (isPortrait ? padding.top : 0) + ? maxHeight - (isWindowMode && !isPortrait ? 0 : padding.top) : height; final bottomHeight = maxHeight - height - padding.top; return Column( @@ -1191,12 +1173,7 @@ class _VideoDetailPageVState extends State child: IconButton( tooltip: '播放', onPressed: handlePlay, - icon: Image.asset( - Assets.play, - width: 60, - height: 60, - cacheHeight: 60.cacheSize(context), - ), + icon: const PlayIcon(), ), ), ], @@ -1305,6 +1282,7 @@ class _VideoDetailPageVState extends State late bool isPortrait; late double maxWidth; late double maxHeight; + bool isWindowMode = false; late EdgeInsets padding; @override @@ -1357,7 +1335,7 @@ class _VideoDetailPageVState extends State if (videoDetailController.tabCtr.length != tabs.length) { videoDetailController.tabCtr.dispose(); videoDetailController.tabCtr = TabController( - vsync: this, + vsync: videoDetailController, length: tabs.length, initialIndex: tabs.isEmpty ? 0 @@ -1611,9 +1589,7 @@ class _VideoDetailPageVState extends State return FilledButton.tonal( style: FilledButton.styleFrom( shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(6), - ), + borderRadius: .all(.circular(6)), ), backgroundColor: themeData .colorScheme @@ -1631,9 +1607,7 @@ class _VideoDetailPageVState extends State item, isStein: true, ); - videoDetailController.getSteinEdgeInfo( - item.id, - ); + videoDetailController.getSteinEdgeInfo(item.id); }, child: Text(item.option!), ); @@ -1988,15 +1962,12 @@ class _VideoDetailPageVState extends State }, ); if (isFullScreen || videoDetailController.showVideoSheet) { + final child = listSheetContent(enableSlide: false); PageUtils.showVideoBottomSheet( context, - isFullScreen: () => isFullScreen, child: videoDetailController.plPlayerController.darkVideoPage - ? Theme( - data: themeData, - child: listSheetContent(enableSlide: false), - ) - : listSheetContent(enableSlide: false), + ? Theme(data: themeData, child: child) + : child, ); } else { videoDetailController.childKey.currentState?.showBottomSheet( @@ -2068,23 +2039,16 @@ class _VideoDetailPageVState extends State void showViewPoints() { if (isFullScreen || videoDetailController.showVideoSheet) { + final child = ViewPointsPage( + enableSlide: false, + videoDetailController: videoDetailController, + plPlayerController: plPlayerController, + ); PageUtils.showVideoBottomSheet( context, - isFullScreen: () => isFullScreen, child: videoDetailController.plPlayerController.darkVideoPage - ? Theme( - data: themeData, - child: ViewPointsPage( - enableSlide: false, - videoDetailController: videoDetailController, - plPlayerController: plPlayerController, - ), - ) - : ViewPointsPage( - enableSlide: false, - videoDetailController: videoDetailController, - plPlayerController: plPlayerController, - ), + ? Theme(data: themeData, child: child) + : child, ); } else { videoDetailController.childKey.currentState?.showBottomSheet( diff --git a/lib/pages/video/view_point/view.dart b/lib/pages/video/view_point/view.dart index 26d54a555d..c33d2cafd4 100644 --- a/lib/pages/video/view_point/view.dart +++ b/lib/pages/video/view_point/view.dart @@ -103,14 +103,14 @@ class _ViewPointsPageState extends State final segment = videoDetailController.viewPointList[index]; if (currentIndex == -1 && segment.from != null && segment.to != null) { final positionSeconds = - videoDetailController.plPlayerController.positionSeconds.value; + videoDetailController.plPlayerController.position.value; if (positionSeconds >= segment.from! && positionSeconds < segment.to!) { currentIndex = index; } } final isCurr = currentIndex == index; - return _buildItem(theme, segment, isCurr); + return _buildItem(theme.colorScheme, segment, isCurr); }, ); if (_isNested) { @@ -122,8 +122,11 @@ class _ViewPointsPageState extends State return child; } - Widget _buildItem(ThemeData theme, ViewPointSegment segment, bool isCurr) { - final theme = Theme.of(context); + Widget _buildItem( + ColorScheme colorScheme, + ViewPointSegment segment, + bool isCurr, + ) { return Material( type: MaterialType.transparency, child: InkWell( @@ -137,17 +140,14 @@ class _ViewPointsPageState extends State } : null, child: Padding( - padding: const EdgeInsets.symmetric( - horizontal: Style.safeSpace, - vertical: 5, - ), + padding: const .symmetric(horizontal: Style.safeSpace, vertical: 5), child: Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ NetworkImgLayer( src: segment.url, - width: 140.8, - height: 88, + width: 160, + height: 100, ), const SizedBox(width: 10), Expanded( @@ -163,14 +163,14 @@ class _ViewPointsPageState extends State style: isCurr ? TextStyle( fontWeight: FontWeight.bold, - color: theme.colorScheme.primary, + color: colorScheme.primary, ) : null, ), Text( '${segment.from != null ? DurationUtils.formatDuration(segment.from) : ''} - ' '${segment.to != null ? DurationUtils.formatDuration(segment.to) : ''}', - style: TextStyle(color: theme.colorScheme.outline), + style: TextStyle(color: colorScheme.outline), ), ], ), diff --git a/lib/pages/video/widgets/header_control.dart b/lib/pages/video/widgets/header_control.dart index bd615448e0..b407fda3b5 100644 --- a/lib/pages/video/widgets/header_control.dart +++ b/lib/pages/video/widgets/header_control.dart @@ -1,12 +1,13 @@ -import 'dart:async'; +import 'dart:async' show Timer; import 'dart:convert' show jsonDecode, utf8; -import 'dart:io'; -import 'dart:math'; +import 'dart:io' show Platform, File; +import 'dart:typed_data' show Uint8List; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/button/icon_button.dart'; import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/dialog/report.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/marquee.dart'; import 'package:PiliPlus/http/danmaku.dart'; import 'package:PiliPlus/http/danmaku_block.dart'; @@ -23,6 +24,8 @@ import 'package:PiliPlus/models/video/play/url.dart'; import 'package:PiliPlus/models_new/video/video_play_info/subtitle.dart'; import 'package:PiliPlus/pages/common/common_intro_controller.dart'; import 'package:PiliPlus/pages/danmaku/danmaku_model.dart'; +import 'package:PiliPlus/pages/setting/models/play_settings.dart' + show showPlayerVolumeDialog; import 'package:PiliPlus/pages/setting/widgets/popup_item.dart'; import 'package:PiliPlus/pages/setting/widgets/select_dialog.dart'; import 'package:PiliPlus/pages/video/controller.dart'; @@ -35,12 +38,12 @@ import 'package:PiliPlus/pages/video/widgets/header_mixin.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/data_source.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; -import 'package:PiliPlus/services/service_locator.dart'; import 'package:PiliPlus/services/shutdown_timer_service.dart' show shutdownTimerService; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts/account.dart'; -import 'package:PiliPlus/utils/extension/iterable_ext.dart'; +import 'package:PiliPlus/utils/android/bindings.g.dart'; +import 'package:PiliPlus/utils/connectivity_utils.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/image_utils.dart'; @@ -49,15 +52,16 @@ import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:PiliPlus/utils/storage_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:PiliPlus/utils/video_utils.dart'; import 'package:battery_plus/battery_plus.dart'; import 'package:canvas_danmaku/canvas_danmaku.dart'; +import 'package:collection/collection.dart'; import 'package:dio/dio.dart'; import 'package:easy_debounce/easy_throttle.dart'; import 'package:file_picker/file_picker.dart'; -import 'package:floating/floating.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' show compute; import 'package:flutter/material.dart' hide showBottomSheet; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -65,6 +69,7 @@ import 'package:get/get.dart'; import 'package:hive_ce/hive.dart'; import 'package:intl/intl.dart' show DateFormat; import 'package:material_design_icons_flutter/material_design_icons_flutter.dart'; +import 'package:media_kit/media_kit.dart' show NativePlayer; mixin TimeBatteryMixin on State { PlPlayerController get plPlayerController; @@ -445,10 +450,7 @@ class HeaderControlState extends State dense: true, onTap: () { Get.back(); - videoDetailCtr.queryVideoUrl( - defaultST: videoDetailCtr.playedTime, - fromReset: true, - ); + videoDetailCtr.queryVideoUrl(fromReset: true); }, leading: const Icon(Icons.refresh_outlined, size: 20), title: const Text('重载视频', style: titleStyle), @@ -475,6 +477,24 @@ class HeaderControlState extends State descFontSize: 12, descPosType: .subtitle, ), + if (PlatformUtils.isMobile) + if (plPlayerController.videoPlayerController + case final player?) + Builder( + builder: (context) => ListTile( + dense: true, + leading: const Icon(Icons.volume_up, size: 20), + title: const Text('播放器音量'), + subtitle: Text( + '当前: ${Pref.playerVolume.toStringAsFixed(0)}%', + ), + onTap: () => showPlayerVolumeDialog( + context, + () => (context as Element).markNeedsBuild(), + onChanged: player.setVolume, + ), + ), + ), if (!isFileSource) ListTile( dense: true, @@ -496,10 +516,7 @@ class HeaderControlState extends State VideoUtils.cdnService = result; setting.put(SettingBoxKey.CDNService, result.name); SmartDialog.showToast('已设置为 ${result.desc},正在重载视频'); - videoDetailCtr.queryVideoUrl( - defaultST: videoDetailCtr.playedTime, - fromReset: true, - ); + videoDetailCtr.queryVideoUrl(fromReset: true); } }, ), @@ -525,15 +542,12 @@ class HeaderControlState extends State () { final flipY = plPlayerController.flipY.value; return ActionRowLineItem( - icon: Transform.rotate( - angle: pi / 2, - child: Icon( - Icons.flip, - size: 13, - color: flipY - ? theme.colorScheme.onSecondaryContainer - : theme.colorScheme.outline, - ), + icon: Icon( + CustomIcons.flip_rotate_90, + size: 13, + color: flipY + ? theme.colorScheme.onSecondaryContainer + : theme.colorScheme.outline, ), onTap: () { plPlayerController.flipY.value = !flipY; @@ -669,48 +683,49 @@ class HeaderControlState extends State onTap: () async { Get.back(); try { - final result = await FilePicker.pickFiles(); + final result = await FilePicker.pickFile( + type: .custom, + allowedExtensions: const ['json', 'vtt', 'srt', 'ass'], + ); if (result != null) { - final file = result.files.single; + final file = result.xFile; final path = file.path; - if (path != null) { - final name = file.name; - final length = videoDetailCtr.subtitles.length; - if (name.endsWith('.json')) { - final file = File(path); - final stream = file.openRead().transform( - utf8.decoder, - ); - final buffer = StringBuffer(); - await for (final chunk in stream) { - if (!mounted) return; - buffer.write(chunk); - } - if (!mounted) return; - String sub = buffer.toString(); - sub = await compute( - VideoHttp.processList, - jsonDecode(sub)['body'], - ); + final name = file.name; + final length = videoDetailCtr.subtitles.length; + if (name.endsWith('.json')) { + final file = File(path); + final stream = file.openRead().transform( + utf8.decoder, + ); + final buffer = StringBuffer(); + await for (final chunk in stream) { if (!mounted) return; - videoDetailCtr.vttSubtitles[length] = ( - isData: true, - id: sub, - ); - } else { - videoDetailCtr.vttSubtitles[length] = ( - isData: false, - id: path, - ); + buffer.write(chunk); } - videoDetailCtr.subtitles.add( - Subtitle( - lan: '', - lanDoc: name.split('.').firstOrNull ?? name, - ), + if (!mounted) return; + String sub = buffer.toString(); + sub = await compute( + VideoHttp.processList, + jsonDecode(sub)['body'], + ); + if (!mounted) return; + videoDetailCtr.vttSubtitles[length] = ( + isData: true, + id: sub, + ); + } else { + videoDetailCtr.vttSubtitles[length] = ( + isData: false, + id: path, ); - await videoDetailCtr.setSubtitle(length + 1); } + videoDetailCtr.subtitles.add( + Subtitle( + lan: '', + lanDoc: name.split('.').firstOrNull ?? name, + ), + ); + await videoDetailCtr.setSubtitle(length + 1); } } catch (e) { SmartDialog.showToast('加载失败: $e'); @@ -730,15 +745,13 @@ class HeaderControlState extends State leading: const Icon(Icons.download_outlined, size: 20), title: const Text('保存字幕', style: titleStyle), ), - ListTile( - dense: true, - title: const Text('播放信息', style: titleStyle), - leading: const Icon(Icons.info_outline, size: 20), - onTap: () => showPlayerInfo( - context, - plPlayerController: plPlayerController, + if (plPlayerController.videoPlayerController case final player?) + ListTile( + dense: true, + title: const Text('播放信息', style: titleStyle), + leading: const Icon(Icons.info_outline, size: 20), + onTap: () => showPlayerInfo(context, player: player), ), - ), ListTile( dense: true, onTap: () { @@ -762,14 +775,10 @@ class HeaderControlState extends State static void showPlayerInfo( BuildContext context, { - required PlPlayerController plPlayerController, + required NativePlayer player, }) { - final player = plPlayerController.videoPlayerController; - if (player == null) { - SmartDialog.showToast('播放器未初始化'); - return; - } final hwdec = player.getProperty('hwdec-current'); + final volume = player.getProperty('volume').subLength(3); showDialog( context: context, builder: (context) { @@ -781,18 +790,14 @@ class HeaderControlState extends State content: Material( type: MaterialType.transparency, child: ListTileTheme( - contentPadding: const EdgeInsets.symmetric( - horizontal: 24, - ), + contentPadding: const .symmetric(horizontal: 24), child: SingleChildScrollView( child: Column( children: [ ListTile( dense: true, title: const Text("Resolution"), - subtitle: Text( - '${state.width}x${state.height}', - ), + subtitle: Text('${state.width}x${state.height}'), onTap: () => Utils.copyText( 'Resolution\n${state.width}x${state.height}', ), @@ -800,60 +805,36 @@ class HeaderControlState extends State ListTile( dense: true, title: const Text("VideoParams"), - subtitle: Text( - state.videoParams.toString(), - ), - onTap: () => Utils.copyText( - 'VideoParams\n${state.videoParams}', - ), + subtitle: Text(state.videoParams.toString()), + onTap: () => + Utils.copyText('VideoParams\n${state.videoParams}'), ), ListTile( dense: true, title: const Text("AudioParams"), - subtitle: Text( - state.audioParams.toString(), - ), - onTap: () => Utils.copyText( - 'AudioParams\n${state.audioParams}', - ), + subtitle: Text(state.audioParams.toString()), + onTap: () => + Utils.copyText('AudioParams\n${state.audioParams}'), ), ListTile( dense: true, title: const Text("Media"), - subtitle: Text( - state.playlist.toString(), - ), - onTap: () => Utils.copyText( - 'Media\n${state.playlist}', - ), + subtitle: Text(state.playlist.toString()), + onTap: () => Utils.copyText('Media\n${state.playlist}'), ), ListTile( dense: true, title: const Text("AudioTrack"), - subtitle: Text( - state.track.audio.toString(), - ), - onTap: () => Utils.copyText( - 'AudioTrack\n${state.track.audio}', - ), + subtitle: Text(state.track.audio.toString()), + onTap: () => + Utils.copyText('AudioTrack\n${state.track.audio}'), ), ListTile( dense: true, title: const Text("VideoTrack"), - subtitle: Text( - state.track.video.toString(), - ), - onTap: () => Utils.copyText( - 'VideoTrack\n${state.track.audio}', - ), - ), - ListTile( - dense: true, - title: const Text("pitch"), - subtitle: Text(state.pitch.toString()), - onTap: () => Utils.copyText( - 'pitch\n${state.pitch}', - ), + subtitle: Text(state.track.video.toString()), + onTap: () => + Utils.copyText('VideoTrack\n${state.track.audio}'), ), ListTile( dense: true, @@ -864,12 +845,8 @@ class HeaderControlState extends State ListTile( dense: true, title: const Text("Volume"), - subtitle: Text( - state.volume.toString(), - ), - onTap: () => Utils.copyText( - 'Volume\n${state.volume}', - ), + subtitle: Text(volume.toString()), + onTap: () => Utils.copyText('Volume\n$volume'), ), ListTile( dense: true, @@ -979,7 +956,7 @@ class HeaderControlState extends State // update if (!plPlayerController.tempPlayerConf) { setting.put( - await Utils.isWiFi + await ConnectivityUtils.isWiFi ? SettingBoxKey.defaultVideoQa : SettingBoxKey.defaultVideoQaCellular, quality, @@ -1059,7 +1036,7 @@ class HeaderControlState extends State // update if (!plPlayerController.tempPlayerConf) { setting.put( - await Utils.isWiFi + await ConnectivityUtils.isWiFi ? SettingBoxKey.defaultAudioQa : SettingBoxKey.defaultAudioQaCellular, quality, @@ -1093,28 +1070,26 @@ class HeaderControlState extends State // 选择解码格式 void showSetDecodeFormats() { - final VideoItem firstVideo = videoDetailCtr.firstVideo; + final firstCode = videoDetailCtr.firstVideo.quality.code; // 当前视频可用的解码格式 - final List videoFormat = videoInfo.supportFormats!; - final List? list = videoFormat - .firstWhere((FormatItem e) => e.quality == firstVideo.quality.code) - .codecs; + final videoFormat = videoInfo.supportFormats!; + + final list = videoFormat.firstWhere((e) => e.quality == firstCode).codecs; if (list == null) { SmartDialog.showToast('当前视频不支持选择解码格式'); return; } // 当前选中的解码格式 - final VideoDecodeFormatType currentDecodeFormats = - videoDetailCtr.currentDecodeFormats; + final curCodecs = videoDetailCtr.currentDecodeFormats.codes; showBottomSheet( (context, setState) { - final theme = Theme.of(context); + final colorScheme = ColorScheme.of(context); return Padding( padding: const EdgeInsets.all(12), child: Material( clipBehavior: Clip.hardEdge, - color: theme.colorScheme.surface, + color: colorScheme.surface, borderRadius: const BorderRadius.all(Radius.circular(12)), child: Column( children: [ @@ -1132,30 +1107,22 @@ class HeaderControlState extends State itemBuilder: (context, index) { final item = list[index]; final format = VideoDecodeFormatType.fromString(item); - final isCurr = currentDecodeFormats.codes.any( - item.startsWith, - ); + final isCurr = curCodecs.any(item.startsWith); return ListTile( dense: true, onTap: () { - if (isCurr) { - return; - } + if (isCurr) return; Get.back(); videoDetailCtr ..currentDecodeFormats = format ..updatePlayer(); + SmartDialog.showToast("解码已变为:${format.name}"); }, - contentPadding: const EdgeInsets.symmetric( - horizontal: 20, - ), + contentPadding: const .symmetric(horizontal: 20), title: Text(format.description), subtitle: Text(item, style: subTitleStyle), trailing: isCurr - ? Icon( - Icons.done, - color: theme.colorScheme.primary, - ) + ? Icon(Icons.done, color: colorScheme.primary) : null, ); }, @@ -1171,69 +1138,100 @@ class HeaderControlState extends State ); } + Future<_SubtitleFormat?> _showFormatDialog() { + return showDialog<_SubtitleFormat>( + context: context, + builder: (context) => SimpleDialog( + title: const Text('选择格式'), + children: [ + DialogOption( + onPressed: () => Get.back(result: _SubtitleFormat.json), + child: const Text('JSON'), + ), + DialogOption( + onPressed: () => Get.back(result: _SubtitleFormat.vtt), + child: const Text('WEBVTT'), + ), + ], + ), + ); + } + void onExportSubtitle() { showDialog( context: context, - builder: (context) => AlertDialog( - clipBehavior: Clip.hardEdge, - contentPadding: const EdgeInsets.fromLTRB(0, 12, 0, 12), - title: const Text('保存字幕'), - content: SingleChildScrollView( - child: Column( - children: videoDetailCtr.subtitles - .map( - (item) => ListTile( - dense: true, - onTap: () async { - Get.back(); - final url = item.subtitleUrl; - if (url == null || url.isEmpty) return; - try { - final res = await Request.dio.get( - url.http2https, - options: Options( - responseType: ResponseType.bytes, - headers: Constants.baseHeaders, - extra: {'account': const NoAccount()}, - ), + builder: (context) { + final subtitles = videoDetailCtr.subtitles; + return SimpleDialog( + clipBehavior: Clip.hardEdge, + contentPadding: const .only(bottom: 12), + titlePadding: const .fromLTRB(20, 20, 20, 12), + title: const Text('保存字幕'), + children: List.generate(subtitles.length, (i) { + final item = subtitles[i]; + return DialogOption( + onPressed: () async { + Get.back(); + final url = item.subtitleUrl; + if (url == null || url.isEmpty) return; + final format = await _showFormatDialog(); + if (format == null) return; + try { + final Uint8List bytes; + switch (format) { + case .vtt: + var subtitle = videoDetailCtr.vttSubtitles[i]; + if (subtitle == null) { + final res = await VideoHttp.vttSubtitles( + item.subtitleUrl!, ); - if (res.statusCode == 200) { - final bytes = Uint8List.fromList( - Request.responseBytesDecoder( - res.data!, - res.headers.map, - ), - ); - String name = - '${introController.videoDetail.value.title}-${videoDetailCtr.bvid}-${videoDetailCtr.cid.value}-${item.lanDoc}.json'; - if (Platform.isWindows) { - // Reserved characters may not be used in file names. See: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions - name = name.replaceAll( - RegExp(r'[<>:/\\|?*"]'), - '', - ); - } - Utils.saveBytes2File( - name: name, - bytes: bytes, - allowedExtensions: const ['json'], - ); - } - } catch (e, s) { - Utils.reportError(e, s); - SmartDialog.showToast(e.toString()); + if (res == null) return; + subtitle = (isData: true, id: res); + videoDetailCtr.vttSubtitles[i] = subtitle; } - }, - title: Text( - item.lanDoc!, - style: const TextStyle(fontSize: 14), - ), - ), - ) - .toList(), - ), - ), - ), + bytes = utf8.encode(subtitle.id); + case .json: + final res = await Request.dio.get( + url.http2https, + options: Options( + responseType: ResponseType.bytes, + headers: Constants.baseHeaders, + extra: {'account': const NoAccount()}, + ), + ); + if (res.statusCode != 200) return; + bytes = Uint8List.fromList( + Request.responseBytesDecoder( + res.data!, + res.headers.map, + ), + ); + } + String name = + '${introController.videoDetail.value.title}-${videoDetailCtr.bvid}-${videoDetailCtr.cid.value}-${item.lanDoc}.${format.name}'; + // Reserved characters may not be used in file names. See: https://docs.microsoft.com/en-us/windows/win32/fileio/naming-a-file#naming-conventions + name = name.replaceAll( + Platform.isWindows ? RegExp(r'[<>:/\\|?*"]') : '/', + '_', + ); + StorageUtils.saveBytes2File( + name: name, + bytes: bytes, + allowedExtensions: [format.name], + ); + } catch (e, s) { + Utils.reportError(e, s); + SmartDialog.showToast(e.toString()); + } + }, + child: Text( + item.lanDoc ?? item.lan, + style: const TextStyle(fontSize: 14), + ), + ); + }), + ); + }, ); } @@ -1248,7 +1246,7 @@ class HeaderControlState extends State /// 字幕设置 void showSetSubtitle() { showBottomSheet( - padding: isFullScreen ? 70 : null, + padding: () => isFullScreen ? const .only(bottom: 70) : .zero, (context, setState) { final theme = Theme.of(context); @@ -1844,21 +1842,10 @@ class HeaderControlState extends State tooltip: '提交片段', style: btnStyle, onPressed: () => videoDetailCtr.onBlock(context), - icon: const Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - Icon( - Icons.shield_outlined, - size: 19, - color: Colors.white, - ), - Icon( - Icons.play_arrow_rounded, - size: 13, - color: Colors.white, - ), - ], + icon: const Icon( + CustomIcons.shield_play_arrow, + size: 20, + color: Colors.white, ), ), ), @@ -1954,87 +1941,12 @@ class HeaderControlState extends State child: IconButton( tooltip: '画中画', style: btnStyle, - onPressed: () async { + onPressed: () { if (PlatformUtils.isDesktop) { plPlayerController.toggleDesktopPip(); return; } - if (await Floating().isPipAvailable) { - if (context.mounted && - !videoPlayerServiceHandler!.enableBackgroundPlay) { - final theme = Theme.of(context); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Column( - children: [ - const Row( - children: [ - Icon( - Icons.check, - color: Colors.green, - ), - SizedBox(width: 10), - Text( - '画中画', - style: TextStyle( - fontSize: 15, - height: 1.5, - ), - ), - ], - ), - const SizedBox(height: 10), - const Text( - '建议开启【后台音频服务】\n' - '避免画中画没有暂停按钮', - style: TextStyle( - fontSize: 12.5, - height: 1.5, - ), - ), - Row( - children: [ - TextButton( - style: ButtonStyle( - foregroundColor: - WidgetStatePropertyAll( - theme - .snackBarTheme - .actionTextColor, - ), - ), - onPressed: () { - plPlayerController.setBackgroundPlay( - true, - ); - SmartDialog.showToast("请重新载入本页面刷新"); - }, - child: const Text('启用后台音频服务'), - ), - const SizedBox(width: 10), - TextButton( - style: ButtonStyle( - foregroundColor: - WidgetStatePropertyAll( - theme - .snackBarTheme - .actionTextColor, - ), - ), - onPressed: () {}, - child: const Text('不启用'), - ), - ], - ), - ], - ), - duration: const Duration(seconds: 2), - showCloseIcon: true, - ), - ); - await Future.delayed(const Duration(seconds: 3)); - } - if (!context.mounted) return; + if (AndroidHelper.isPipAvailable) { plPlayerController.enterPip(); } }, @@ -2076,9 +1988,7 @@ class HeaderControlState extends State FontAwesomeIcons.thumbsUp, color: Colors.white, ), - selectIcon: const Icon( - FontAwesomeIcons.solidThumbsUp, - ), + selectIcon: const Icon(FontAwesomeIcons.solidThumbsUp), selectStatus: introController.hasLike.value, semanticsLabel: '点赞', animation: introController.tripleAnimation, @@ -2175,3 +2085,5 @@ class HeaderControlState extends State ); } } + +enum _SubtitleFormat { json, vtt } diff --git a/lib/pages/video/widgets/header_mixin.dart b/lib/pages/video/widgets/header_mixin.dart index 84a48f6082..697bb2e0cd 100644 --- a/lib/pages/video/widgets/header_mixin.dart +++ b/lib/pages/video/widgets/header_mixin.dart @@ -4,6 +4,7 @@ import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/utils/danmaku_options.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/page_utils.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; @@ -12,21 +13,32 @@ mixin HeaderMixin on State { bool get isFullScreen => plPlayerController.isFullScreen.value; + ThemeData? get theme { + if (plPlayerController.darkVideoPage) { + return ThemeUtils.darkTheme; + } + return null; + } + Future? showBottomSheet( StatefulWidgetBuilder builder, { - double? padding, + ValueGetter? padding, }) { return PageUtils.showVideoBottomSheet( context, - isFullScreen: () => isFullScreen, + maxWidth: 512, padding: padding, child: StatefulBuilder( - builder: (context, setState) => plPlayerController.darkVideoPage - ? Theme( - data: Theme.of(this.context), - child: builder(this.context, setState), - ) - : builder(context, setState), + builder: (context, setState) { + final theme = this.theme; + if (theme != null) { + return Theme( + data: theme, + child: builder(this.context, setState), + ); + } + return builder(context, setState); + }, ), ); } diff --git a/lib/pages/video/widgets/player_focus.dart b/lib/pages/video/widgets/player_focus.dart index 34b0493d93..8c98da4172 100644 --- a/lib/pages/video/widgets/player_focus.dart +++ b/lib/pages/video/widgets/player_focus.dart @@ -63,7 +63,7 @@ class PlayerFocus extends StatelessWidget { void _setVolume({required bool isIncrease}) { final volume = isIncrease ? math.min( - PlPlayerController.maxVolume, + plPlayerController.maxVolume, plPlayerController.volume.value + 0.1, ) : math.max(0.0, plPlayerController.volume.value - 0.1); diff --git a/lib/pages/webdav/webdav.dart b/lib/pages/webdav/webdav.dart index 4a5e6aadae..bd05361b70 100644 --- a/lib/pages/webdav/webdav.dart +++ b/lib/pages/webdav/webdav.dart @@ -2,12 +2,10 @@ import 'dart:convert'; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; -import 'package:PiliPlus/utils/extension/context_ext.dart'; +import 'package:PiliPlus/utils/device_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:get/get_core/src/get_main.dart'; -import 'package:get/get_navigation/src/extension_navigation.dart'; import 'package:webdav_client/webdav_client.dart' as webdav; class WebDav { @@ -53,7 +51,7 @@ class WebDav { } String _getFileName() { - return 'piliplus_settings_${Get.context!.platformName}.json'; + return 'piliplus_settings_${DeviceUtils.platformName}.json'; } Future backup() async { diff --git a/lib/pages/whisper/view.dart b/lib/pages/whisper/view.dart index 04e8fa9871..5a1ea50dfa 100644 --- a/lib/pages/whisper/view.dart +++ b/lib/pages/whisper/view.dart @@ -7,7 +7,7 @@ import 'package:PiliPlus/pages/whisper/controller.dart'; import 'package:PiliPlus/pages/whisper/widgets/item.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/extension/three_dot_ext.dart'; -import 'package:PiliPlus/utils/utils.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -37,7 +37,7 @@ class _WhisperPageState extends State { '/webview', parameters: { 'url': - 'https://www.bilibili.com/h5/follow/newFans?navhide=1&${Utils.themeUrl(theme.colorScheme.isDark)}', + 'https://www.bilibili.com/h5/follow/newFans?navhide=1&${ThemeUtils.themeUrl(theme.isDark)}', }, ), icon: const Icon(Icons.account_circle_outlined), diff --git a/lib/pages/whisper/widgets/item.dart b/lib/pages/whisper/widgets/item.dart index 86c5ad6546..98f2d9b534 100644 --- a/lib/pages/whisper/widgets/item.dart +++ b/lib/pages/whisper/widgets/item.dart @@ -3,6 +3,7 @@ import 'dart:convert'; import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/flutter/list_tile.dart'; import 'package:PiliPlus/common/widgets/pendant_avatar.dart'; import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart' @@ -51,53 +52,44 @@ class WhisperSessionItem extends StatelessWidget { safeArea: true, tileColor: item.isPinned ? theme.colorScheme.onInverseSurface.withValues( - alpha: theme.brightness.isDark ? 0.4 : 0.8, + alpha: theme.isDark ? 0.4 : 0.8, ) : null, onLongPress: () => showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: DefaultTextStyle( - style: const TextStyle(fontSize: 14), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - onTap: () { - Get.back(); - onSetTop(item.isPinned, item.id); - }, - title: Text(item.isPinned ? '移除置顶' : '置顶'), - ), - if (item.id.privateId.hasTalkerUid()) - ListTile( - dense: true, - onTap: () { - Get.back(); - onSetMute(item.isMuted, item.id.privateId.talkerUid); - }, - title: Text('${item.isMuted ? '关闭' : '开启'}免打扰'), - ), - if (item.id.privateId.hasTalkerUid()) - ListTile( - dense: true, - onTap: () { - Get.back(); - showConfirmDialog( - context: context, - title: const Text('确定删除该对话?'), - onConfirm: () => - onRemove(item.id.privateId.talkerUid.toInt()), - ); - }, - title: const Text('删除'), - ), - ], + children: [ + DialogOption( + onPressed: () { + Get.back(); + onSetTop(item.isPinned, item.id); + }, + child: Text(item.isPinned ? '移除置顶' : '置顶'), ), - ), + if (item.id.privateId.hasTalkerUid()) + DialogOption( + onPressed: () { + Get.back(); + onSetMute(item.isMuted, item.id.privateId.talkerUid); + }, + child: Text('${item.isMuted ? '关闭' : '开启'}免打扰'), + ), + if (item.id.privateId.hasTalkerUid()) + DialogOption( + onPressed: () { + Get.back(); + showConfirmDialog( + context: context, + title: const Text('确定删除该对话?'), + onConfirm: () => + onRemove(item.id.privateId.talkerUid.toInt()), + ); + }, + child: const Text('删除'), + ), + ], ), ), onSecondaryTapUp: PlatformUtils.isDesktop diff --git a/lib/pages/whisper_block/view.dart b/lib/pages/whisper_block/view.dart index 2db4864870..2f060bc5e8 100644 --- a/lib/pages/whisper_block/view.dart +++ b/lib/pages/whisper_block/view.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/assets.dart'; import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart' @@ -114,7 +115,7 @@ class _WhisperBlockPageState extends State { spacing: 6, mainAxisSize: MainAxisSize.min, children: [ - SvgPicture.asset("assets/images/error.svg", height: 156), + SvgPicture.asset(Assets.error, height: 156), const Text( '还未添加屏蔽词', style: TextStyle( diff --git a/lib/pages/whisper_detail/controller.dart b/lib/pages/whisper_detail/controller.dart index 3422a01a88..fc10a9678b 100644 --- a/lib/pages/whisper_detail/controller.dart +++ b/lib/pages/whisper_detail/controller.dart @@ -19,11 +19,11 @@ import 'package:get/get.dart'; class WhisperDetailController extends CommonListController { late final account = Accounts.main; - final int talkerId = Get.arguments['talkerId']; - final String name = Get.arguments['name']; - final String face = Get.arguments['face']; - final int? mid = Get.arguments['mid']; - final bool isLive = Get.arguments['isLive'] ?? false; + late final int talkerId; + late final String name; + late final String face; + late final int? mid; + late final bool isLive; Int64? msgSeqno; @@ -33,6 +33,12 @@ class WhisperDetailController extends CommonListController { @override void onInit() { super.onInit(); + final args = Get.arguments; + talkerId = args['talkerId']; + name = args['name']; + face = args['face']; + mid = args['mid']; + isLive = args['isLive'] ?? false; queryData(); } diff --git a/lib/pages/whisper_detail/view.dart b/lib/pages/whisper_detail/view.dart index 9962334d8a..593ae5a3af 100644 --- a/lib/pages/whisper_detail/view.dart +++ b/lib/pages/whisper_detail/view.dart @@ -344,9 +344,10 @@ class _WhisperDetailPageState ); } else { try { - final XFile? pickedFile = await imagePicker.pickImage( + final pickedFile = await imagePicker.pickImage( source: ImageSource.gallery, imageQuality: 100, + requestFullMetadata: false, ); if (pickedFile != null) { final path = pickedFile.path; diff --git a/lib/pages/whisper_detail/widget/chat_item.dart b/lib/pages/whisper_detail/widget/chat_item.dart index d87b0922c7..70a070d811 100644 --- a/lib/pages/whisper_detail/widget/chat_item.dart +++ b/lib/pages/whisper_detail/widget/chat_item.dart @@ -4,7 +4,6 @@ import 'dart:math' as math; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/common/widgets/badge.dart'; -import 'package:PiliPlus/common/widgets/flutter/layout_builder.dart'; import 'package:PiliPlus/common/widgets/gesture/tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/image/network_img_layer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/hero.dart'; @@ -22,8 +21,8 @@ import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; -import 'package:cached_network_image/cached_network_image.dart'; -import 'package:flutter/material.dart' hide LayoutBuilder; +import 'package:cached_network_image_ce/cached_network_image.dart'; +import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; @@ -178,7 +177,7 @@ class ChatItem extends StatelessWidget { return msgTypeTipMessage_18(theme, content); case MsgType.EN_MSG_TYPE_TEXT: return msgTypeText_1(theme, content: content, textColor: textColor); - case MsgType.EN_MSG_TYPE_PIC: + case MsgType.EN_MSG_TYPE_PIC || MsgType.EN_MSG_TYPE_CUSTOM_FACE: return msgTypePic_2(content); case MsgType.EN_MSG_TYPE_SHARE_V2: return msgTypeShareV2_7(content, textColor); @@ -201,44 +200,45 @@ class ChatItem extends StatelessWidget { Widget msgTypeCommonShareCard_14(dynamic content, Color textColor) { if (content['source'] == '直播') { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: () { - dynamic roomId = content['sourceID']; - if (roomId is String) { - roomId = int.parse(roomId); - } - PageUtils.toLiveRoom(roomId); - }, - child: NetworkImgLayer( + return GestureDetector( + behavior: .opaque, + onTap: () { + dynamic roomId = content['sourceID']; + if (roomId is String) { + roomId = int.parse(roomId); + } + PageUtils.toLiveRoom(roomId); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetworkImgLayer( width: 220, height: 123.75, src: content['cover'], ), - ), - const SizedBox(height: 6), - Text( - content['title'] ?? "", - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor, - fontWeight: FontWeight.bold, + const SizedBox(height: 6), + Text( + content['title'] ?? "", + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor, + fontWeight: FontWeight.bold, + ), ), - ), - const SizedBox(height: 1), - Text( - '${content['author']} · 直播', - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor.withValues(alpha: 0.6), - fontSize: 12, + const SizedBox(height: 1), + Text( + '${content['author']} · 直播', + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor.withValues(alpha: 0.6), + fontSize: 12, + ), ), - ), - ], + ], + ), ); } else { return def(textColor); @@ -247,6 +247,7 @@ class ChatItem extends StatelessWidget { Widget msgTypeArticleCard_12(dynamic content, Color textColor) { return GestureDetector( + behavior: .opaque, onTap: () => Get.toNamed( '/articlePage', parameters: { @@ -268,7 +269,7 @@ class ChatItem extends StatelessWidget { ], ), const SizedBox(height: 6), - SelectableText( + Text( content['title'] ?? "", style: TextStyle( letterSpacing: 0.6, @@ -279,8 +280,7 @@ class ChatItem extends StatelessWidget { ), if (content['summary'] != null && content['summary'] != '') ...[ const SizedBox(height: 1), - SelectableText( - scrollPhysics: const NeverScrollableScrollPhysics(), + Text( content['summary'], style: TextStyle( letterSpacing: 0.6, @@ -571,32 +571,20 @@ class ChatItem extends StatelessWidget { 'unsupported source type: ${content['source']}', ); } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - GestureDetector( - onTap: onTap, - child: NetworkImgLayer( + return GestureDetector( + onTap: onTap, + behavior: .opaque, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + NetworkImgLayer( width: 220, height: 123.75, src: content['thumb'], ), - ), - const SizedBox(height: 6), - Text( - content['title'] ?? "", - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor, - fontWeight: FontWeight.bold, - ), - ), - if (content['source'] == 6 && - (content['headline'] as String?)?.isNotEmpty == true) ...[ - const SizedBox(height: 1), + const SizedBox(height: 6), Text( - content['headline'], + content['title'] ?? "", style: TextStyle( letterSpacing: 0.6, height: 1.5, @@ -604,20 +592,33 @@ class ChatItem extends StatelessWidget { fontWeight: FontWeight.bold, ), ), - ], - if (content['author'] != null) ...[ - const SizedBox(height: 1), - Text( - '${content['author']}${type != null ? ' · $type' : ''}', - style: TextStyle( - letterSpacing: 0.6, - height: 1.5, - color: textColor.withValues(alpha: 0.6), - fontSize: 12, + if (content['source'] == 6 && + (content['headline'] as String?)?.isNotEmpty == true) ...[ + const SizedBox(height: 1), + Text( + content['headline'], + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor, + fontWeight: FontWeight.bold, + ), ), - ), + ], + if (content['author'] != null) ...[ + const SizedBox(height: 1), + Text( + '${content['author']}${type != null ? ' · $type' : ''}', + style: TextStyle( + letterSpacing: 0.6, + height: 1.5, + color: textColor.withValues(alpha: 0.6), + fontSize: 12, + ), + ), + ], ], - ], + ), ); } @@ -750,7 +751,7 @@ class ChatItem extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - SelectableText( + Text( content['title'], style: theme.textTheme.titleMedium!.copyWith( fontWeight: FontWeight.bold, diff --git a/lib/pages/whisper_settings/view.dart b/lib/pages/whisper_settings/view.dart index 070a983f65..ae1a777032 100644 --- a/lib/pages/whisper_settings/view.dart +++ b/lib/pages/whisper_settings/view.dart @@ -1,3 +1,4 @@ +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/common/widgets/loading_widget/loading_widget.dart'; import 'package:PiliPlus/grpc/bilibili/app/im/v1.pb.dart' show IMSettingType, Setting; @@ -82,49 +83,45 @@ class _WhisperSettingsPageState extends State { String? selected; showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: item.redirect.windowSelect.item.map( - (e) { - if (e.selected) { - selected ??= e.text; - } - return ListTile( - dense: true, - onTap: () async { - if (!e.selected) { - Get.back(); + children: item.redirect.windowSelect.item.map( + (e) { + if (e.selected) { + selected ??= e.text; + } + return DialogOption( + onPressed: () async { + if (!e.selected) { + Get.back(); + for (final j in item.redirect.windowSelect.item) { + j.selected = false; + } + item.redirect.selectedSummary = e.text; + e.selected = true; + _controller.loadingState.refresh(); + final settings = {key: item}; + final res = await _controller.onSet(settings); + if (!res) { for (final j in item.redirect.windowSelect.item) { - j.selected = false; + j.selected = j.text == selected; } - item.redirect.selectedSummary = e.text; - e.selected = true; + item.redirect.selectedSummary = selected!; _controller.loadingState.refresh(); - final settings = {key: item}; - final res = await _controller.onSet(settings); - if (!res) { - for (final j in item.redirect.windowSelect.item) { - j.selected = j.text == selected; - } - item.redirect.selectedSummary = selected!; - _controller.loadingState.refresh(); - } } - }, - title: Text( - e.text, - style: TextStyle( - fontSize: 14, - color: e.selected ? theme.colorScheme.primary : null, - ), + } + }, + child: Text( + e.text, + style: TextStyle( + fontSize: 14, + color: e.selected ? theme.colorScheme.primary : null, ), - ); - }, - ).toList(), - ), + ), + ); + }, + ).toList(), ), ); } else if (item.redirect.otherPage.hasUrl()) { diff --git a/lib/pages/whisper_settings/widgets/item.dart b/lib/pages/whisper_settings/widgets/item.dart index f355c567a7..37151a3479 100644 --- a/lib/pages/whisper_settings/widgets/item.dart +++ b/lib/pages/whisper_settings/widgets/item.dart @@ -25,8 +25,8 @@ class ImSettingsItem extends StatelessWidget { } const titleStyle = TextStyle(fontSize: 14); - final theme = Theme.of(context); - final outline = theme.colorScheme.outline; + final colorScheme = ColorScheme.of(context); + final outline = colorScheme.outline; final subtitleStyle = TextStyle(fontSize: 13, color: outline); if (item.hasSwitch_1()) { @@ -156,11 +156,7 @@ class ImSettingsItem extends StatelessWidget { }, title: Text(e.text, style: titleStyle), trailing: e.selected - ? Icon( - size: 20, - Icons.check, - color: theme.colorScheme.primary, - ) + ? Icon(size: 20, Icons.check, color: colorScheme.primary) : null, ); }, diff --git a/lib/plugin/pl_player/controller.dart b/lib/plugin/pl_player/controller.dart index 7d3d9fae9e..b7efe4bcf4 100644 --- a/lib/plugin/pl_player/controller.dart +++ b/lib/plugin/pl_player/controller.dart @@ -17,6 +17,8 @@ import 'package:PiliPlus/models/user/danmaku_rule.dart'; import 'package:PiliPlus/models/video/play/url.dart'; import 'package:PiliPlus/models_new/video/video_shot/data.dart'; import 'package:PiliPlus/pages/danmaku/danmaku_model.dart'; +import 'package:PiliPlus/pages/setting/models/play_settings.dart' + show kMaxVolume; import 'package:PiliPlus/pages/sponsor_block/block_mixin.dart'; import 'package:PiliPlus/plugin/pl_player/models/data_source.dart'; import 'package:PiliPlus/plugin/pl_player/models/data_status.dart'; @@ -30,7 +32,11 @@ import 'package:PiliPlus/plugin/pl_player/models/video_fit_type.dart'; import 'package:PiliPlus/plugin/pl_player/utils/fullscreen.dart'; import 'package:PiliPlus/services/service_locator.dart'; import 'package:PiliPlus/utils/accounts.dart'; +import 'package:PiliPlus/utils/android/android_helper.dart'; +import 'package:PiliPlus/utils/android/bindings.g.dart'; import 'package:PiliPlus/utils/asset_utils.dart'; +import 'package:PiliPlus/utils/device_utils.dart'; +import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/extension/box_ext.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/feed_back.dart'; @@ -45,7 +51,6 @@ import 'package:PiliPlus/utils/utils.dart'; import 'package:archive/archive.dart' show getCrc32; import 'package:canvas_danmaku/canvas_danmaku.dart'; import 'package:easy_debounce/easy_throttle.dart'; -import 'package:floating/floating.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter/services.dart' show HapticFeedback, DeviceOrientation; @@ -57,6 +62,7 @@ import 'package:media_kit/media_kit.dart'; import 'package:media_kit_video/media_kit_video.dart'; import 'package:native_device_orientation/native_device_orientation.dart'; import 'package:path/path.dart' as path; +import 'package:screen_brightness_platform_interface/screen_brightness_platform_interface.dart'; import 'package:wakelock_plus/wakelock_plus.dart'; import 'package:window_manager/window_manager.dart'; @@ -66,38 +72,31 @@ class PlPlayerController with BlockConfigMixin { Player? _videoPlayerController; VideoController? _videoController; - // 添加一个私有静态变量来保存实例 static PlPlayerController? _instance; - // 流事件 监听播放状态变化 - // StreamSubscription? _playerEventSubs; + final playerStatus = PlPlayerStatus(.playing); - /// [playerStatus] has a [status] observable - final playerStatus = PlPlayerStatus(PlayerStatus.playing); + final Rx dataStatus = Rx(.none); - /// - final Rx dataStatus = Rx(DataStatus.none); + Duration? seekToPos; + bool hasToasted = false; + final RxBool isSeeking = false.obs; - // bool controlsEnabled = false; + final RxInt position = RxInt(0); - /// 响应数据 - /// 带有Seconds的变量只在秒数更新时更新,以避免频繁触发重绘 - // 播放位置 - Duration position = Duration.zero; - final RxInt positionSeconds = 0.obs; + int get positionInMilliseconds => + videoPlayerController?.state.position.inMilliseconds ?? 0; - /// 进度条位置 - Duration sliderPosition = Duration.zero; - final RxInt sliderPositionSeconds = 0.obs; - // 展示使用 - final Rx sliderTempPosition = Rx(Duration.zero); + final RxInt buffered = RxInt(0); - /// 视频时长 - final Rx duration = Rx(Duration.zero); + final RxInt duration = RxInt(0); - /// 视频缓冲 - final Rx buffered = Rx(Duration.zero); - final RxInt bufferedSeconds = 0.obs; + int durationInMilliseconds = 0; + + void updateDuration(Duration value) { + duration.value = value.inSeconds; + durationInMilliseconds = value.inMilliseconds; + } int _playerCount = 0; @@ -105,44 +104,31 @@ class PlPlayerController with BlockConfigMixin { final RxDouble _playbackSpeed = Pref.playSpeedDefault.obs; late final RxDouble _longPressSpeed = Pref.longPressSpeedDefault.obs; - /// 音量控制条 final RxDouble volume = RxDouble( PlatformUtils.isDesktop ? Pref.desktopVolume : 1.0, ); final setSystemBrightness = Pref.setSystemBrightness; - /// 亮度控制条 final RxDouble brightness = (-1.0).obs; - /// 是否展示控制条 final RxBool showControls = false.obs; - /// 亮度控制条展示/隐藏 final RxBool showBrightnessStatus = false.obs; - /// 是否长按倍速 final RxBool longPressStatus = false.obs; - /// 屏幕锁 为true时,关闭控制栏 final RxBool controlsLock = false.obs; - /// 全屏状态 final RxBool isFullScreen = false.obs; - // 默认投稿视频格式 bool isLive = false; bool _isVertical = false; - /// 视频比例 - final Rx videoFit = Rx(VideoFitType.contain); + final Rx videoFit = Rx(.contain); - /// 后台播放 late final RxBool continuePlayInBackground = Pref.continuePlayInBackground.obs; - /// - final RxBool isSliderMoving = false.obs; - bool _autoPlay = false; // 记录历史记录 @@ -162,7 +148,7 @@ class PlPlayerController with BlockConfigMixin { late DataSource dataSource; Timer? _timer; - StreamSubscription? _subForSeek; + StreamSubscription? _subForSeek; Box setting = GStorage.setting; @@ -205,7 +191,7 @@ class PlPlayerController with BlockConfigMixin { late final bool autoPiP = Pref.autoPiP; bool get isPipMode => - (Platform.isAndroid && Floating().isPipMode) || + (Platform.isAndroid && AndroidHelper.isPipMode) || (PlatformUtils.isDesktop && isDesktopPip); late bool isDesktopPip = false; late Rect _lastWindowBounds; @@ -271,47 +257,37 @@ class PlPlayerController with BlockConfigMixin { } } - late bool _shouldSetPip = false; + late bool _isAutoEnterPip = false; + bool get isAutoEnterPip => _isAutoEnterPip; - bool get _isCurrVideoPage { + static bool get _isCurrVideoPage { final routing = Get.routing; if (routing.route is! GetPageRoute) { return false; } - final currentRoute = routing.current; - return currentRoute.startsWith('/video') || - currentRoute.startsWith('/liveRoom'); + return _isVideoPage(routing.current); } - bool get _isPreviousVideoPage { - final previousRoute = Get.previousRoute; - return previousRoute.startsWith('/video') || - previousRoute.startsWith('/liveRoom'); + static bool _isVideoPage(String routeName) { + return routeName == '/videoV' || routeName == '/liveRoom'; } - void enterPip({bool isAuto = false}) { + void enterPip({bool autoEnter = false}) { if (videoPlayerController != null) { - controls = false; final state = videoPlayerController!.state; PageUtils.enterPip( - isAuto: isAuto, + autoEnter: autoEnter, width: state.width == 0 ? width : state.width, height: state.height == 0 ? height : state.height, + isLive: isLive, + isPlaying: playerStatus.isPlaying, ); } } - void _disableAutoEnterPipIfNeeded() { - if (!_isPreviousVideoPage) { - _disableAutoEnterPip(); - } - } - void _disableAutoEnterPip() { - if (_shouldSetPip) { - Utils.channel.invokeMethod('setPipAutoEnterEnabled', { - 'autoEnable': false, - }); + if (_isAutoEnterPip) { + PiliAndroidHelper.disableAutoEnterPip(); } } @@ -367,6 +343,7 @@ class PlPlayerController with BlockConfigMixin { late final showFsScreenshotBtn = Pref.showFsScreenshotBtn; late final showFsLockBtn = Pref.showFsLockBtn; late final keyboardControl = Pref.keyboardControl; + late final uiScale = Pref.uiScale; late final bool autoEnterFullScreen = Pref.autoEnterFullScreen; late final bool autoExitFullscreen = Pref.autoExitFullscreen; @@ -390,8 +367,7 @@ class PlPlayerController with BlockConfigMixin { ? Pref.sliderDuration / 100 : Pref.sliderDuration * 1000; - num get sliderScale => - isRelative ? duration.value.inMilliseconds * offset : offset; + num get sliderScale => isRelative ? durationInMilliseconds * offset : offset; // 播放顺序相关 late PlayRepeat playRepeat = Pref.playRepeat; @@ -444,27 +420,6 @@ class PlPlayerController with BlockConfigMixin { putSubtitleSettings(); } - void updateSliderPositionSecond() { - int newSecond = sliderPosition.inSeconds; - if (sliderPositionSeconds.value != newSecond) { - sliderPositionSeconds.value = newSecond; - } - } - - void updatePositionSecond() { - int newSecond = position.inSeconds; - if (positionSeconds.value != newSecond) { - positionSeconds.value = newSecond; - } - } - - void updateBufferedSecond() { - int newSecond = buffered.value.inSeconds; - if (bufferedSeconds.value != newSecond) { - bufferedSeconds.value = newSecond; - } - } - static PlPlayerController? get instance => _instance; static bool instanceExists() { @@ -478,7 +433,6 @@ class PlPlayerController with BlockConfigMixin { static PlayCallback? _playCallBack; static Future? playIfExists() { - // await _instance?.play(repeat: repeat, hideControls: hideControls); return _playCallBack?.call(); } @@ -529,7 +483,7 @@ class PlPlayerController with BlockConfigMixin { void _onOrientationChanged(OrientationParams param) { _orientation = param.orientation; - if (!visible) return; + if (Platform.isIOS && !visible) return; final orientation = param.orientation; final isFullScreen = this.isFullScreen.value; if (checkIsAutoRotate && @@ -574,8 +528,8 @@ class PlPlayerController with BlockConfigMixin { if (PlatformUtils.isMobile) { _orientationListener = NativeDeviceOrientationPlatform.instance .onOrientationChanged( - useSensor: Platform.isAndroid, checkIsAutoRotate: checkIsAutoRotate, + angleDegrees: Platform.isAndroid ? Pref.angleDegrees : null, ) .listen(_onOrientationChanged); } @@ -585,20 +539,22 @@ class PlPlayerController with BlockConfigMixin { } if (Platform.isAndroid && autoPiP) { - if (Utils.sdkInt < 36) { - Utils.channel.setMethodCallHandler((call) async { - if (call.method == 'onUserLeaveHint') { - if (playerStatus.isPlaying && _isCurrVideoPage) { - enterPip(); - } - } - }); + if (DeviceUtils.sdkInt < 31) { + AndroidHelper$ToDart.onUserLeaveHint = Runnable.implement( + $Runnable(run: _onUserLeaveHint), + ); } else { - _shouldSetPip = true; + _isAutoEnterPip = true; } } } + void _onUserLeaveHint() { + if (playerStatus.isPlaying && _isCurrVideoPage) { + enterPip(); + } + } + // 获取实例 传参 static PlPlayerController getInstance({bool isLive = false}) { // 如果实例尚未创建,则创建一个新实例 @@ -613,6 +569,12 @@ class PlPlayerController with BlockConfigMixin { // offline bool get isFileSource => dataSource is FileSource; + late final _audioNormalization = Pref.audioNormalization; + late final enableAudioNormalization = + Platform.isAndroid && _audioNormalization != '0'; + late final String _audioNormalizationParam = + AudioNormalization.getParamFromConfig(_audioNormalization); + // 初始化资源 Future setDataSource( DataSource dataSource, { @@ -683,14 +645,10 @@ class PlPlayerController with BlockConfigMixin { return; } - // 获取视频时长 00:00 - this.duration.value = duration ?? _videoPlayerController!.state.duration; - position = buffered.value = sliderPosition = seekTo ?? Duration.zero; - updatePositionSecond(); - updateSliderPositionSecond(); - updateBufferedSecond(); - // 数据加载完成 - dataStatus.value = DataStatus.loaded; + updateDuration(duration ?? _videoPlayerController!.state.duration); + position.value = buffered.value = seekTo?.inSeconds ?? 0; + + dataStatus.value = .loaded; if (autoFullScreenFlag && autoEnterFullScreen) { triggerFullScreen(status: true); @@ -767,13 +725,12 @@ class PlPlayerController with BlockConfigMixin { assert(_videoPlayerController == null); final opt = { 'video-sync': Pref.videoSync, + if (Platform.isAndroid) 'ao': Pref.audioOutput, + 'volume': + (PlatformUtils.isMobile ? Pref.playerVolume : volume.value * 100) + .toString(), + 'volume-max': kMaxVolume.toString(), }; - if (Platform.isAndroid) { - opt['volume-max'] = '100'; - opt['ao'] = Pref.audioOutput; - } else if (PlatformUtils.isDesktop) { - opt['volume'] = (volume.value * 100).toString(); - } final autosync = Pref.autosync; if (autosync != '0') { opt['autosync'] = autosync; @@ -781,9 +738,6 @@ class PlPlayerController with BlockConfigMixin { final player = await Player.create( configuration: PlayerConfiguration( - bufferSize: Pref.expandBuffer - ? (isLive ? 64 * 1024 * 1024 : 32 * 1024 * 1024) - : (isLive ? 16 * 1024 * 1024 : 4 * 1024 * 1024), logLevel: kDebugMode ? .warn : .error, options: opt, ), @@ -800,17 +754,19 @@ class PlPlayerController with BlockConfigMixin { ), ); - player.setMediaHeader( - userAgent: BrowserUa.pc, - referer: HttpString.baseUrl, - ); - // await player.setAudioTrack(.auto()); + player.setMediaHeader(userAgent: BrowserUa.pc, referer: HttpString.baseUrl); _startListeners(player); return player; } + Map? _buffer; + Map get buffer => + _buffer ??= Pref.initBuffer(_playbackSpeed.value); + Map? _liveBuffer; + Map get liveBuffer => _liveBuffer ??= Pref.initLiveBuffer(); + // 配置播放器 Future _createVideoController( DataSource dataSource, @@ -818,10 +774,7 @@ class PlPlayerController with BlockConfigMixin { Volume? volume, ) async { isBuffering.value = false; - buffered.value = Duration.zero; _heartDuration = 0; - position = Duration.zero; - // 初始化时清空弹幕,防止上次重叠 danmakuController?.clear(); var player = _videoPlayerController; @@ -843,6 +796,16 @@ class PlPlayerController with BlockConfigMixin { final Map extras = {}; + if (dataSource is FileSource) { + extras['cache'] = 'no'; + } else { + if (isLive) { + extras.addAll(liveBuffer); + } else { + extras.addAll(buffer); + } + } + String video = dataSource.videoSource; if (dataSource.audioSource case final audio? when (audio.isNotEmpty)) { if (onlyPlayAudio.value) { @@ -851,12 +814,10 @@ class PlPlayerController with BlockConfigMixin { extras['audio-files'] = '"${Platform.isWindows ? audio.replaceAll(';', r'\;') : audio.replaceAll(':', r'\:')}"'; } - if (kDebugMode || Platform.isAndroid) { - String audioNormalization = AudioNormalization.getParamFromConfig( - Pref.audioNormalization, - ); + if (enableAudioNormalization) { + final String audioNormalization; if (volume != null && volume.isNotEmpty) { - audioNormalization = audioNormalization.replaceFirstMapped( + audioNormalization = _audioNormalizationParam.replaceFirstMapped( loudnormRegExp, (i) => 'loudnorm=${volume.format( @@ -869,7 +830,7 @@ class PlPlayerController with BlockConfigMixin { )}', ); } else { - audioNormalization = audioNormalization.replaceFirst( + audioNormalization = _audioNormalizationParam.replaceFirst( loudnormRegExp, AudioNormalization.getParamFromConfig(Pref.fallbackNormalization), ); @@ -894,9 +855,9 @@ class PlPlayerController with BlockConfigMixin { if (dataSource is FileSource) { return null; } - if (_videoPlayerController?.current.isNotEmpty ?? false) { - return _videoPlayerController!.open( - _videoPlayerController!.current.last.copyWith(start: position), + if (_videoPlayerController case final ctr? when (ctr.current.isNotEmpty)) { + return ctr.open( + ctr.current.last.copyWith(start: ctr.state.position), play: true, ); } @@ -940,74 +901,79 @@ class PlPlayerController with BlockConfigMixin { assert(_subscriptions == null); final stream = player.stream; _subscriptions = [ - stream.playing.listen((event) { - WakelockPlus.toggle(enable: event); - if (event) { - if (_shouldSetPip) { + /// playing + stream.playing.listen((bool playing) { + WakelockPlus.toggle(enable: playing); + if (playing) { + if (_isAutoEnterPip) { if (_isCurrVideoPage) { - enterPip(isAuto: true); + enterPip(autoEnter: true); } else { _disableAutoEnterPip(); } } - playerStatus.value = PlayerStatus.playing; + playerStatus.value = .playing; } else { _disableAutoEnterPip(); - playerStatus.value = PlayerStatus.paused; + playerStatus.value = .paused; } + videoPlayerServiceHandler?.onStatusChange( playerStatus.value, isBuffering.value, isLive, ); - /// 触发回调事件 for (final element in _statusListeners) { - element(event ? PlayerStatus.playing : PlayerStatus.paused); + element(playing ? .playing : .paused); } - if (videoPlayerController!.state.position.inSeconds != 0) { - makeHeartBeat(positionSeconds.value, type: HeartBeatType.status); + + final seconds = videoPlayerController!.state.position.inSeconds; + if (seconds != 0) { + makeHeartBeat(seconds, type: .status); } }), - stream.completed.listen((event) { - if (event) { - playerStatus.value = PlayerStatus.completed; - /// 触发回调事件 + ///completed + stream.completed.listen((bool completed) { + if (completed) { + playerStatus.value = .completed; + for (final element in _statusListeners) { - element(PlayerStatus.completed); + element(.completed); } - } else { - // playerStatus.value = PlayerStatus.playing; + + makeHeartBeat(-1, type: .completed); } - makeHeartBeat(positionSeconds.value, type: HeartBeatType.completed); }), - stream.position.listen((event) { - position = event; - updatePositionSecond(); - if (!isSliderMoving.value) { - sliderPosition = event; - updateSliderPositionSecond(); + + /// position + stream.position.listen((Duration position) { + final posInSeconds = position.inSeconds; + + if (posInSeconds != this.position.value) { + if (!isSeeking.value) { + this.position.value = posInSeconds; + } + + videoPlayerServiceHandler?.onPositionChange(position); + + makeHeartBeat(posInSeconds); } - /// 触发回调事件 for (final element in _positionListeners) { - element(event); + element(position); } - makeHeartBeat(event.inSeconds); - }), - stream.duration.listen((Duration event) { - duration.value = event; }), - stream.buffer.listen((Duration event) { - buffered.value = event; - updateBufferedSecond(); + stream.duration.listen(updateDuration), + stream.buffer.listen((Duration buffer) { + buffered.value = buffer.inSeconds; }), - stream.buffering.listen((bool event) { - isBuffering.value = event; + stream.buffering.listen((bool buffering) { + isBuffering.value = buffering; videoPlayerServiceHandler?.onStatusChange( playerStatus.value, - event, + buffering, isLive, ); }), @@ -1048,7 +1014,7 @@ class PlPlayerController with BlockConfigMixin { // if (kDebugMode) { // debugPrint("_buffered.value: ${_buffered.value}"); // } - if (isBuffering.value && buffered.value == Duration.zero) { + if (isBuffering.value && buffered.value == 0) { SmartDialog.showToast( '视频链接打开失败,重试中', displayTime: const Duration(milliseconds: 500), @@ -1071,24 +1037,6 @@ class PlPlayerController with BlockConfigMixin { // SmartDialog.showToast('视频加载错误, $event'); } }), - // controllerStream.volume.listen((event) { - // if (!mute.value && _volumeBeforeMute != event) { - // _volumeBeforeMute = event / 100; - // } - // }), - // 媒体通知监听 - if (videoPlayerServiceHandler != null) ...[ - playerStatus.listen((PlayerStatus event) { - videoPlayerServiceHandler!.onStatusChange( - event, - isBuffering.value, - isLive, - ); - }), - positionSeconds.listen((int event) { - videoPlayerServiceHandler!.onPositionChange(Duration(seconds: event)); - }), - ], ]; } @@ -1108,17 +1056,12 @@ class PlPlayerController with BlockConfigMixin { /// 跳转至指定位置 Future seekTo(Duration position, {bool isSeek = true}) async { - // if (position >= duration.value) { - // position = duration.value - const Duration(milliseconds: 100); - // } if (_playerCount == 0) { return; } if (position < Duration.zero) { position = Duration.zero; } - this.position = position; - updatePositionSecond(); _heartDuration = position.inSeconds; Future seek() async { @@ -1134,7 +1077,7 @@ class PlPlayerController with BlockConfigMixin { } } - if (duration.value != Duration.zero) { + if (duration.value != 0) { seek(); } else { // if (kDebugMode) debugPrint('seek duration else'); @@ -1214,42 +1157,22 @@ class PlPlayerController with BlockConfigMixin { void hideTaskControls() { _timer?.cancel(); _timer = Timer(showControlDuration, () { - if (!isSliderMoving.value && !tripling) { + if (!isSeeking.value && !tripling) { controls = false; } _timer = null; }); } - /// 调整播放时间 - void onChangedSlider(int v) { - sliderPosition = Duration(seconds: v); - updateSliderPositionSecond(); - } - - void onChangedSliderStart([Duration? value]) { - if (value != null) { - sliderTempPosition.value = value; - } - isSliderMoving.value = true; - } - - bool? cancelSeek; - bool? hasToast; - - void onUpdatedSliderProgress(Duration value) { - sliderTempPosition.value = value; - sliderPosition = value; - updateSliderPositionSecond(); - } - - void onChangedSliderEnd() { - if (cancelSeek != true) { + void onSeekEnd() { + if (seekToPos != null) { feedBack(); } - cancelSeek = null; - hasToast = null; - isSliderMoving.value = false; + if (showSeekPreview) { + showPreview.value = false; + } + hasToasted = false; + isSeeking.value = false; hideTaskControls(); } @@ -1257,13 +1180,13 @@ class PlPlayerController with BlockConfigMixin { Timer? volumeTimer; bool volumeInterceptEventStream = false; - static final double maxVolume = PlatformUtils.isDesktop ? 2.0 : 1.0; + final double maxVolume = PlatformUtils.isDesktop ? Pref.maxVolume : 1.0; Future setVolume(double volume, {bool showIndicator = true}) async { if (this.volume.value != volume) { this.volume.value = volume; try { if (PlatformUtils.isDesktop) { - _videoPlayerController!.setVolume(volume * 100); + await _videoPlayerController!.setVolume(volume * 100); } else { FlutterVolumeController.updateShowSystemUI(false); await FlutterVolumeController.setVolume(volume); @@ -1350,13 +1273,13 @@ class PlPlayerController with BlockConfigMixin { } } - bool get _isCompleted => + bool get isCompleted => videoPlayerController!.state.completed || - (duration.value - position).inMilliseconds <= 50; + durationInMilliseconds - positionInMilliseconds <= 50; // 双击播放、暂停 Future onDoubleTapCenter() async { - if (!isLive && _isCompleted) { + if (!isLive && isCompleted) { await videoPlayerController!.seek(Duration.zero); videoPlayerController!.play(); } else { @@ -1376,11 +1299,11 @@ class PlPlayerController with BlockConfigMixin { } void onForward(Duration duration) { - onForwardBackward(position + duration); + onForwardBackward(videoPlayerController!.state.position + duration); } void onBackward(Duration duration) { - onForwardBackward(position - duration); + onForwardBackward(videoPlayerController!.state.position - duration); } void onForwardBackward(Duration duration) { @@ -1479,7 +1402,7 @@ class PlPlayerController with BlockConfigMixin { try { if (status) { if (PlatformUtils.isMobile) { - hideStatusBar(); + hideSystemBar(); await changeOrientation( isVertical: isVertical, orientation: orientation, @@ -1490,26 +1413,12 @@ class PlPlayerController with BlockConfigMixin { } else { if (PlatformUtils.isMobile) { if (!removeSafeArea) { - showStatusBar(); + showSystemBar(); } if (orientation == null && mode == .none) { return; } - if (!horizontalScreen) { - await portraitUpMode(); - } else { - switch (_orientation) { - case .portraitUp: - await portraitUpMode(); - case .landscapeLeft: - await landscapeLeftMode(); - case .portraitDown: - await portraitDownMode(); - case .landscapeRight: - await landscapeRightMode(); - case _: - } - } + await resetScreenRotation(); } else { await exitDesktopFullScreen(); } @@ -1582,7 +1491,7 @@ class PlPlayerController with BlockConfigMixin { } case .completed: if (playerStatus.isCompleted && - (duration.value - position).inMilliseconds <= 1000) { + (durationInMilliseconds - positionInMilliseconds) <= 1000) { progress = -1; } return send(); @@ -1610,11 +1519,11 @@ class PlPlayerController with BlockConfigMixin { bool _isCloseAll = false; bool get isCloseAll => _isCloseAll; - void resetScreenRotation() { + Future? resetScreenRotation() { if (horizontalScreen) { - fullMode(); + return fullMode(); } else { - portraitUpMode(); + return portraitUpMode(); } } @@ -1632,15 +1541,12 @@ class PlPlayerController with BlockConfigMixin { if (!_isCloseAll && _playerCount > 1) { _playerCount -= 1; _heartDuration = 0; - if (!_isPreviousVideoPage) { - pause(); - } return; } _playerCount = 0; if (removeSafeArea) { - showStatusBar(); + showSystemBar(); } danmakuController = null; _stopOrientationListener(); @@ -1650,7 +1556,10 @@ class PlPlayerController with BlockConfigMixin { if (showSeekPreview) { _clearPreview(); } - Utils.channel.setMethodCallHandler(null); + if (Platform.isAndroid) { + AndroidHelper$ToDart.onUserLeaveHint?.release(); + AndroidHelper$ToDart.onUserLeaveHint = null; + } _timer?.cancel(); // _position.close(); // _playerEventSubs?.cancel(); @@ -1745,59 +1654,75 @@ class PlPlayerController with BlockConfigMixin { videoShot = await VideoHttp.videoshot(bvid: bvid, cid: cid!); } - void takeScreenshot() { + Future takeScreenshot() async { SmartDialog.showToast('截图中'); - videoPlayerController?.screenshot(format: .png).then((value) { - if (value != null) { - SmartDialog.showToast('点击弹窗保存截图'); - showDialog( - context: Get.context!, - builder: (context) => GestureDetector( - onTap: () { - Get.back(); + final time = DurationUtils.formatDuration( + positionInMilliseconds / 1000, + ).replaceAll(':', '-'); + final image = await videoPlayerController?.screenshot(); + if (image != null) { + SmartDialog.showToast('点击弹窗保存截图'); + showDialog( + context: Get.context!, + builder: (context) => GestureDetector( + onTap: () async { + final bytes = await image.toByteData(format: .png); + if (bytes != null) { ImageUtils.saveByteImg( - bytes: value, - fileName: 'screenshot_${ImageUtils.time}', + bytes: bytes.buffer.asUint8List(), + fileName: 'screenshot_${cid}_$time', ); - }, - child: Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.only(right: 12), - child: ConstrainedBox( - constraints: BoxConstraints( - maxWidth: min(Get.width / 3, 350), - ), - child: DecoratedBox( - decoration: BoxDecoration( - border: Border.all( - width: 5, - color: Get.theme.colorScheme.surface, - ), - ), - child: Padding( - padding: const EdgeInsets.all(5), - child: Image.memory(value), + } + Get.back(); + }, + child: Align( + alignment: Alignment.centerRight, + child: Padding( + padding: const EdgeInsets.only(right: 12), + child: ConstrainedBox( + constraints: BoxConstraints( + maxWidth: min(MediaQuery.widthOf(context) / 3, 350), + ), + child: DecoratedBox( + decoration: BoxDecoration( + border: Border.all( + width: 5, + color: ColorScheme.of(context).surface, ), ), + child: Padding( + padding: const EdgeInsets.all(5), + child: RawImage(image: image), + ), ), ), ), ), - ); - } else { - SmartDialog.showToast('截图失败'); - } - }); + ), + ).whenComplete(image.dispose); + } else { + SmartDialog.showToast('截图失败'); + } } void onPopInvokedWithResult(bool didPop, Object? result) { if (didPop) { - if (Platform.isAndroid) { - _disableAutoEnterPipIfNeeded(); + if (playerStatus.isPlaying) { + pause(); + } + + setPlayCallBack(null); + + if (Platform.isAndroid && _playerCount <= 1) { + _disableAutoEnterPip(); + if (!setSystemBrightness) { + ScreenBrightnessPlatform.instance.resetApplicationScreenBrightness(); + } } + return; } + if (controlsLock.value) { onLockControl(false); return; diff --git a/lib/plugin/pl_player/models/hwdec_type.dart b/lib/plugin/pl_player/models/hwdec_type.dart index ae1b4671d9..2fecfdd53b 100644 --- a/lib/plugin/pl_player/models/hwdec_type.dart +++ b/lib/plugin/pl_player/models/hwdec_type.dart @@ -37,4 +37,9 @@ enum HwDecType { final String hwdec; final String desc; const HwDecType(this.hwdec, this.desc); + + static final String androidDefault = [ + HwDecType.mediacodec.hwdec, + HwDecType.autoSafe.hwdec, + ].join(','); } diff --git a/lib/plugin/pl_player/utils/fullscreen.dart b/lib/plugin/pl_player/utils/fullscreen.dart index 3c7065f2f4..15783f4490 100644 --- a/lib/plugin/pl_player/utils/fullscreen.dart +++ b/lib/plugin/pl_player/utils/fullscreen.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'dart:io' show Platform; -import 'package:PiliPlus/utils/utils.dart'; +import 'package:PiliPlus/utils/device_utils.dart'; import 'package:flutter/services.dart' show SystemChrome, MethodChannel, SystemUiOverlay, DeviceOrientation; @@ -62,23 +62,24 @@ Future? fullMode() { ); } -bool _showStatusBar = true; -Future? hideStatusBar() { - if (!_showStatusBar) { +bool _showSystemBar = true; +bool get showSystemBar_ => _showSystemBar; +Future? hideSystemBar() { + if (!_showSystemBar) { return null; } - _showStatusBar = false; + _showSystemBar = false; return SystemChrome.setEnabledSystemUIMode(.immersiveSticky); } //退出全屏显示 -Future? showStatusBar() { - if (_showStatusBar) { +Future? showSystemBar() { + if (_showSystemBar) { return null; } - _showStatusBar = true; + _showSystemBar = true; return SystemChrome.setEnabledSystemUIMode( - Platform.isAndroid && Utils.sdkInt < 29 ? .manual : .edgeToEdge, + Platform.isAndroid && DeviceUtils.sdkInt < 29 ? .manual : .edgeToEdge, overlays: SystemUiOverlay.values, ); } diff --git a/lib/plugin/pl_player/view/view.dart b/lib/plugin/pl_player/view/view.dart index 3de04686c8..d472358e5d 100644 --- a/lib/plugin/pl_player/view/view.dart +++ b/lib/plugin/pl_player/view/view.dart @@ -11,6 +11,7 @@ import 'package:PiliPlus/common/widgets/custom_icon.dart'; import 'package:PiliPlus/common/widgets/disabled_icon.dart'; import 'package:PiliPlus/common/widgets/gesture/immediate_tap_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/gesture/mouse_interactive_viewer.dart'; +import 'package:PiliPlus/common/widgets/gesture/player_gesture_recognizer.dart'; import 'package:PiliPlus/common/widgets/loading_widget.dart'; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/common/widgets/player_bar.dart'; @@ -24,8 +25,6 @@ import 'package:PiliPlus/models/common/super_resolution_type.dart'; import 'package:PiliPlus/models/common/video/video_quality.dart'; import 'package:PiliPlus/models/video/play/url.dart'; import 'package:PiliPlus/models_new/video/video_detail/episode.dart' as ugc; -import 'package:PiliPlus/models_new/video/video_detail/episode.dart'; -import 'package:PiliPlus/models_new/video/video_detail/section.dart'; import 'package:PiliPlus/models_new/video/video_detail/ugc_season.dart'; import 'package:PiliPlus/pages/common/common_intro_controller.dart'; import 'package:PiliPlus/pages/danmaku/danmaku_model.dart'; @@ -51,6 +50,9 @@ import 'package:PiliPlus/plugin/pl_player/widgets/common_btn.dart'; import 'package:PiliPlus/plugin/pl_player/widgets/forward_seek.dart'; import 'package:PiliPlus/plugin/pl_player/widgets/mpv_convert_webp.dart'; import 'package:PiliPlus/plugin/pl_player/widgets/play_pause_btn.dart'; +import 'package:PiliPlus/utils/android/bindings.g.dart'; +import 'package:PiliPlus/utils/cache_manager.dart'; +import 'package:PiliPlus/utils/connectivity_utils.dart'; import 'package:PiliPlus/utils/duration_utils.dart'; import 'package:PiliPlus/utils/extension/num_ext.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; @@ -72,7 +74,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show RenderProxyBox, SemanticsConfiguration; import 'package:flutter/services.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_volume_controller/flutter_volume_controller.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -128,7 +129,7 @@ class PLVideoPlayer extends StatefulWidget { class _PLVideoPlayerState extends State with WidgetsBindingObserver, TickerProviderStateMixin { - late AnimationController animationController; + late AnimationController _animationController; late VideoController videoController; late final CommonIntroController introController = widget.introController!; late final VideoDetailController videoDetailController = @@ -146,19 +147,59 @@ class _PLVideoPlayerState extends State late final RxBool showRestoreScaleBtn = false.obs; GestureType? _gestureType; + Offset? _initialFocalPoint; - Offset initialFocalPoint = Offset.zero; + bool _pauseDueToPauseUponEnteringBackgroundMode = false; - //播放器放缩 - bool interacting = false; + StreamSubscription? _brightnessListener; + void _onBrightnessChanged(double value) { + if (mounted && _gestureType != .left) { + _brightnessValue.value = value; + } + } - // 阅读器限制 - // Timer? _accessibilityDebounce; - // double _lastAnnouncedValue = -1; + void _getSystemBrightness() { + ScreenBrightnessPlatform.instance.system.then((res) { + if (mounted) { + _brightnessValue.value = res; + } + }); + } - bool _pauseDueToPauseUponEnteringBackgroundMode = false; + void _getAppBrightness() { + ScreenBrightnessPlatform.instance.application.then((res) { + if (mounted) { + _brightnessValue.value = res; + } + }); + } - StreamSubscription? _brightnessListener; + void _onVolumeChanged(double value) { + if (mounted && !plPlayerController.volumeInterceptEventStream) { + plPlayerController.volume.value = value; + if (Platform.isIOS && !FlutterVolumeController.showSystemUI) { + plPlayerController + ..volumeIndicator.value = true + ..volumeTimer?.cancel() + ..volumeTimer = Timer( + const Duration(milliseconds: 800), + () { + if (mounted) { + plPlayerController.volumeIndicator.value = false; + } + }, + ); + } + } + } + + void _getCurrVolume() { + FlutterVolumeController.getVolume().then((res) { + if (mounted) { + plPlayerController.volume.value = res!; + } + }); + } int? tmpSubtitlePaddingB; StreamSubscription? _controlsListener; @@ -181,9 +222,9 @@ class _PLVideoPlayerState extends State } if (visible) { - animationController.forward(); + _animationController.forward(); } else { - animationController.reverse(); + _animationController.reverse(); } if (widget.videoDetailController case final controller?) { @@ -217,59 +258,39 @@ class _PLVideoPlayerState extends State _onControlChanged, ); - transformationController = TransformationController(); + _transformationController = TransformationController(); - animationController = AnimationController( + _animationController = AnimationController( vsync: this, duration: const Duration(milliseconds: 100), ); videoController = plPlayerController.videoController!; if (PlatformUtils.isMobile) { - Future.microtask(() async { + Future.microtask(() { try { FlutterVolumeController.updateShowSystemUI(true); - plPlayerController.volume.value = - (await FlutterVolumeController.getVolume())!; - FlutterVolumeController.addListener((double value) { - if (mounted && !plPlayerController.volumeInterceptEventStream) { - plPlayerController.volume.value = value; - if (Platform.isIOS && !FlutterVolumeController.showSystemUI) { - plPlayerController - ..volumeIndicator.value = true - ..volumeTimer?.cancel() - ..volumeTimer = Timer(const Duration(milliseconds: 800), () { - if (mounted) { - plPlayerController.volumeIndicator.value = false; - } - }); - } - } - }, emitOnStart: false); + _getCurrVolume(); + FlutterVolumeController.addListener( + _onVolumeChanged, + emitOnStart: false, + ); } catch (_) {} - }); - Future.microtask(() async { try { - _brightnessValue.value = - await ScreenBrightnessPlatform.instance.application; - - void listener(double value) { - if (mounted) { - _brightnessValue.value = value; - } + if (Platform.isIOS || plPlayerController.setSystemBrightness) { + _getSystemBrightness(); + _brightnessListener = ScreenBrightnessPlatform + .instance + .onSystemScreenBrightnessChanged + .listen(_onBrightnessChanged); + } else { + _getAppBrightness(); + _brightnessListener = ScreenBrightnessPlatform + .instance + .onApplicationScreenBrightnessChanged + .listen(_onBrightnessChanged); } - - _brightnessListener = - Platform.isIOS || plPlayerController.setSystemBrightness - ? ScreenBrightnessPlatform - .instance - .onSystemScreenBrightnessChanged - .listen(listener) - : ScreenBrightnessPlatform - .instance - .onApplicationScreenBrightnessChanged - .listen(listener); } catch (_) {} }); } @@ -293,6 +314,17 @@ class _PLVideoPlayerState extends State _doubleTapGestureRecognizer = DoubleTapGestureRecognizer() ..onDoubleTapDown = _onDoubleTapDown; + + _scaleGestureRecognizer = PlayerScaleGestureRecognizer( + debugOwner: this, + dragStartBehavior: .start, + allowedButtonsFilter: (buttons) => buttons == kPrimaryButton, + trackpadScrollToScaleFactor: const Offset( + 0, + -1 / kDefaultMouseScrollToScaleFactor, + ), + trackpadScrollCausesScale: false, + ); } @override @@ -314,6 +346,7 @@ class _PLVideoPlayerState extends State } Future setBrightness(double value) async { + _brightnessValue.value = value; try { if (Platform.isIOS || plPlayerController.setSystemBrightness) { await ScreenBrightnessPlatform.instance.setSystemScreenBrightness( @@ -342,14 +375,15 @@ class _PLVideoPlayerState extends State _tapGestureRecognizer.dispose(); _longPressRecognizer?.dispose(); _doubleTapGestureRecognizer.dispose(); + _scaleGestureRecognizer.dispose(); _brightnessListener?.cancel(); _controlsListener?.cancel(); - animationController.dispose(); + _animationController.dispose(); + _transformationController.dispose(); + _removeDmAction(); if (PlatformUtils.isMobile) { FlutterVolumeController.removeListener(); } - transformationController.dispose(); - _removeDmAction(); super.dispose(); } @@ -413,10 +447,10 @@ class _PLVideoPlayerState extends State BottomControlType.time => Obx( () => _VideoTime( position: DurationUtils.formatDuration( - plPlayerController.positionSeconds.value, + plPlayerController.position.value, ), duration: DurationUtils.formatDuration( - plPlayerController.duration.value.inSeconds, + plPlayerController.duration.value, ), ), ), @@ -498,13 +532,10 @@ class _PLVideoPlayerState extends State iconSize: 22, color: Colors.white, disable: !show, - child: Transform.rotate( - angle: math.pi / 2, - child: const Icon( - Icons.reorder, - size: 22, - color: Colors.white, - ), + child: const Icon( + CustomIcons.view_headline_rotate_90, + size: 22, + color: Colors.white, ), ), onTap: widget.showViewPoints, @@ -546,9 +577,9 @@ class _PLVideoPlayerState extends State String bvid = plPlayerController.bvid; List episodes = []; if (isSeason) { - final List sections = videoDetail.ugcSeason!.sections!; + final sections = videoDetail.ugcSeason!.sections!; for (int i = 0; i < sections.length; i++) { - final List episodesList = sections[i].episodes!; + final episodesList = sections[i].episodes!; for (final item in episodesList) { if (item.cid == currentCid) { index = i; @@ -693,19 +724,16 @@ class _PLVideoPlayerState extends State ), ), ), - ...videoDetailController.subtitles.indexed.map((e) { + ...videoDetailController.subtitles.mapIndexed((i, e) { return PopupMenuItem( - value: e.$1 + 1, + value: i + 1, height: 35, - onTap: () => videoDetailController.setSubtitle(e.$1 + 1), + onTap: () => videoDetailController.setSubtitle(i + 1), child: Text( - "${e.$2.lanDoc}", + e.lanDoc ?? e.lan, maxLines: 1, overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, - fontSize: 13, - ), + style: const .new(color: Colors.white, fontSize: 13), ), ); }), @@ -778,18 +806,12 @@ class _PLVideoPlayerState extends State if (videoInfo.dash == null) { return const SizedBox.shrink(); } - final List videoFormat = videoInfo.supportFormats!; - final int totalQaSam = videoFormat.length; - int usefulQaSam = 0; - final List video = videoInfo.dash!.video!; - final Set idSet = {}; - for (final VideoItem item in video) { - final int id = item.id!; - if (!idSet.contains(id)) { - idSet.add(id); - usefulQaSam++; - } - } + final videoFormat = videoInfo.supportFormats!; + final totalQaSam = videoFormat.length; + final usefulQaSam = videoInfo.dash!.video! + .map((i) => i.id) + .toSet() + .length; return PopupMenuButton( tooltip: '画质', requestFocus: false, @@ -822,7 +844,7 @@ class _PLVideoPlayerState extends State // update if (!plPlayerController.tempPlayerConf) { GStorage.setting.put( - await Utils.isWiFi + await ConnectivityUtils.isWiFi ? SettingBoxKey.defaultVideoQa : SettingBoxKey.defaultVideoQaCellular, quality, @@ -859,16 +881,8 @@ class _PLVideoPlayerState extends State height: 30, tooltip: isFullScreen ? '退出全屏' : '全屏', icon: isFullScreen - ? const Icon( - Icons.fullscreen_exit, - size: 24, - color: Colors.white, - ) - : const Icon( - Icons.fullscreen, - size: 24, - color: Colors.white, - ), + ? const Icon(Icons.fullscreen_exit, size: 24, color: Colors.white) + : const Icon(Icons.fullscreen, size: 24, color: Colors.white), onTap: () => plPlayerController.triggerFullScreen(status: !isFullScreen), onSecondaryTap: () => plPlayerController.triggerFullScreen( @@ -881,29 +895,24 @@ class _PLVideoPlayerState extends State final isNotFileSource = !plPlayerController.isFileSource; List userSpecifyItemLeft = [ - BottomControlType.playOrPause, - BottomControlType.time, - if (!isNotFileSource || anySeason) ...[ - BottomControlType.pre, - BottomControlType.next, - ], + .playOrPause, + .time, + if (!isNotFileSource || anySeason) ...[.pre, .next], ]; final flag = isFullScreen || plPlayerController.isDesktopPip || maxWidth >= 500; - List userSpecifyItemRight = [ - if (isNotFileSource && plPlayerController.showDmChart) - BottomControlType.dmChart, - if (plPlayerController.isAnim) BottomControlType.superResolution, - if (isNotFileSource && plPlayerController.showViewPoints) - BottomControlType.viewPoints, - if (isNotFileSource && anySeason) BottomControlType.episode, - if (flag) BottomControlType.fit, - if (isNotFileSource) BottomControlType.aiTranslate, - BottomControlType.subtitle, - BottomControlType.speed, - if (isNotFileSource && flag) BottomControlType.qa, - if (!plPlayerController.isDesktopPip) BottomControlType.fullscreen, + final List userSpecifyItemRight = [ + if (isNotFileSource && plPlayerController.showDmChart) .dmChart, + if (plPlayerController.isAnim) .superResolution, + if (isNotFileSource && plPlayerController.showViewPoints) .viewPoints, + if (isNotFileSource && anySeason) .episode, + if (flag) .fit, + if (isNotFileSource) .aiTranslate, + .subtitle, + .speed, + if (isNotFileSource && flag) .qa, + if (!plPlayerController.isDesktopPip) .fullscreen, ]; return PlayerBar( children: [ @@ -923,7 +932,7 @@ class _PLVideoPlayerState extends State bool get isFullScreen => plPlayerController.isFullScreen.value; - late final TransformationController transformationController; + late final TransformationController _transformationController; late ColorScheme colorScheme; late double maxWidth; @@ -935,103 +944,120 @@ class _PLVideoPlayerState extends State colorScheme = ColorScheme.of(context); } - void _onInteractionStart(ScaleStartDetails details) { - if (plPlayerController.controlsLock.value) return; - // 如果起点太靠上则屏蔽 - final localFocalPoint = details.localFocalPoint; - final dx = localFocalPoint.dx; - final dy = localFocalPoint.dy; - if (dx < 40 || dy < 40) return; - if (dx > maxWidth - 40 || dy > maxHeight - 40) return; - if (details.pointerCount > 1) { - interacting = true; + @override + void didUpdateWidget(covariant PLVideoPlayer oldWidget) { + super.didUpdateWidget(oldWidget); + if (Platform.isAndroid && AndroidHelper.isPipMode) { + plPlayerController.controls = false; } - initialFocalPoint = localFocalPoint; - // if (kDebugMode) { - // debugPrint("_initialFocalPoint$_initialFocalPoint"); - // } + } + + void _onPanStart(ScaleStartDetails details) { _gestureType = null; + _initialFocalPoint = details.localFocalPoint; } - void _onInteractionUpdate(ScaleUpdateDetails details) { - showRestoreScaleBtn.value = - transformationController.value.storage[0] != 1.0; - if (interacting || initialFocalPoint == Offset.zero) { - return; - } - Offset cumulativeDelta = details.localFocalPoint - initialFocalPoint; - if (details.pointerCount > 1 && cumulativeDelta.distanceSquared < 2.25) { - interacting = true; - _gestureType = null; - return; + void _onScaleUpdate(double scale) { + showRestoreScaleBtn.value = scale != 1.0; + } + + void _onHorizontalDragStart() { + plPlayerController.isSeeking.value = true; + } + + void _onHorizontalDragUpdate(double dx) { + final curPos = + plPlayerController.seekToPos?.inMilliseconds ?? + plPlayerController.position.value * 1000; + final posDelta = (plPlayerController.sliderScale * dx / maxWidth).round(); + final newPos = (curPos + posDelta).clamp( + 0, + plPlayerController.durationInMilliseconds, + ); + final seconds = newPos ~/ 1000; + plPlayerController + ..seekToPos = Duration(milliseconds: newPos) + ..position.value = seconds; + if (!plPlayerController.isFileSource && + plPlayerController.showSeekPreview) { + plPlayerController.updatePreviewIndex(seconds); } + } - /// 锁定时禁用 - if (plPlayerController.controlsLock.value) return; + void _onHorizontalDragEnd() { + plPlayerController.onSeekEnd(); + if (plPlayerController.seekToPos case final seekToPos?) { + plPlayerController + ..seekTo(seekToPos, isSeek: false) + ..seekToPos = null; + } else { + plPlayerController.position.value = + plPlayerController.videoPlayerController?.state.position.inSeconds ?? + 0; + } + } + void _onPanUpdate(ScaleUpdateDetails details) { if (_gestureType == null) { + final cumulativeDelta = details.localFocalPoint - _initialFocalPoint!; if (cumulativeDelta.distanceSquared < 1) return; final dx = cumulativeDelta.dx.abs(); final dy = cumulativeDelta.dy.abs(); if (dx > 3 * dy) { - _gestureType = GestureType.horizontal; + _onHorizontalDragStart(); + _gestureType = .horizontal; } else if (dy > 3 * dx) { if (!plPlayerController.enableSlideVolumeBrightness && !plPlayerController.enableSlideFS) { return; } - // _gestureType = 'vertical'; - final double tapPosition = details.localFocalPoint.dx; final double sectionWidth = maxWidth / 3; if (tapPosition < sectionWidth) { - if (PlatformUtils.isDesktop || - !plPlayerController.enableSlideVolumeBrightness) { + if (!plPlayerController.enableSlideVolumeBrightness) { return; } // 左边区域 - _gestureType = GestureType.left; + if (PlatformUtils.isDesktop) { + _gestureType = .right; + } else { + _gestureType = .left; + } } else if (tapPosition < sectionWidth * 2) { if (!plPlayerController.enableSlideFS) { return; } // 全屏 - _gestureType = GestureType.center; + _gestureType = .center; } else { if (!plPlayerController.enableSlideVolumeBrightness) { return; } // 右边区域 - _gestureType = GestureType.right; + _gestureType = .right; } - } else { - return; } + return; } Offset delta = details.focalPointDelta; - if (_gestureType == GestureType.horizontal) { + if (_gestureType == .horizontal) { // live模式下禁用 if (plPlayerController.isLive) return; - final int curSliderPosition = - plPlayerController.sliderPosition.inMilliseconds; - final int newPos = - (curSliderPosition + - (plPlayerController.sliderScale * delta.dx / maxWidth) - .round()) - .clamp(0, plPlayerController.duration.value.inMilliseconds); - final Duration result = Duration(milliseconds: newPos); final height = maxHeight * 0.125; if (details.localFocalPoint.dy <= height && (details.localFocalPoint.dx >= maxWidth * 0.875 || details.localFocalPoint.dx <= maxWidth * 0.125)) { - plPlayerController.cancelSeek = true; - plPlayerController.showPreview.value = false; - if (plPlayerController.hasToast != true) { - plPlayerController.hasToast = true; + if (!plPlayerController.hasToasted) { + plPlayerController + ..seekToPos = null + ..hasToasted = true; + if (plPlayerController.showSeekPreview) { + plPlayerController.showPreview.value = false; + } SmartDialog.showAttach( targetContext: context, alignment: Alignment.center, @@ -1040,73 +1066,55 @@ class _PLVideoPlayerState extends State displayTime: const Duration(milliseconds: 1500), maskColor: Colors.transparent, builder: (context) => Container( - padding: const EdgeInsets.symmetric( - horizontal: 8, - vertical: 4, - ), + padding: const .symmetric(horizontal: 8, vertical: 4), decoration: BoxDecoration( - borderRadius: const BorderRadius.all( - Radius.circular(6), - ), + borderRadius: const .all(.circular(6)), color: colorScheme.secondaryContainer, ), child: Text( '松开手指,取消进退', - style: TextStyle( - color: colorScheme.onSecondaryContainer, - ), + style: TextStyle(color: colorScheme.onSecondaryContainer), ), ), ); } - } else { - if (plPlayerController.cancelSeek == true) { - plPlayerController - ..cancelSeek = null - ..hasToast = null; - } - } - plPlayerController - ..onUpdatedSliderProgress(result) - ..onChangedSliderStart(); - if (!plPlayerController.isFileSource && - plPlayerController.showSeekPreview && - plPlayerController.cancelSeek != true) { - plPlayerController.updatePreviewIndex(newPos ~/ 1000); + return; + } else if (plPlayerController.hasToasted) { + plPlayerController.hasToasted = false; } - } else if (_gestureType == GestureType.left) { + + _onHorizontalDragUpdate(delta.dx); + } else if (_gestureType == .left) { // 左边区域 👈 final double level = maxHeight * 3; - final double brightness = _brightnessValue.value - delta.dy / level; - final double result = brightness.clamp(0.0, 1.0); - setBrightness(result); - } else if (_gestureType == GestureType.center) { + final double brightness = (_brightnessValue.value - delta.dy / level) + .clamp(0.0, 1.0); + setBrightness(brightness); + } else if (_gestureType == .center) { // 全屏 const double threshold = 2.5; // 滑动阈值 - double cumulativeDy = details.localFocalPoint.dy - initialFocalPoint.dy; + double cumulativeDy = details.localFocalPoint.dy - _initialFocalPoint!.dy; void fullScreenTrigger(bool status) { plPlayerController.triggerFullScreen(status: status); } if (cumulativeDy > threshold) { - _gestureType = GestureType.center_down; + _gestureType = .center_down; if (isFullScreen ^ plPlayerController.fullScreenGestureReverse) { fullScreenTrigger( plPlayerController.fullScreenGestureReverse, ); } - // if (kDebugMode) debugPrint('center_down:$cumulativeDy'); } else if (cumulativeDy < -threshold) { - _gestureType = GestureType.center_up; + _gestureType = .center_up; if (!isFullScreen ^ plPlayerController.fullScreenGestureReverse) { fullScreenTrigger( !plPlayerController.fullScreenGestureReverse, ); } - // if (kDebugMode) debugPrint('center_up:$cumulativeDy'); } - } else if (_gestureType == GestureType.right) { + } else if (_gestureType == .right) { // 右边区域 final double level = maxHeight * 0.5; EasyThrottle.throttle( @@ -1116,7 +1124,7 @@ class _PLVideoPlayerState extends State final double volume = clampDouble( plPlayerController.volume.value - delta.dy / level, 0.0, - PlPlayerController.maxVolume, + plPlayerController.maxVolume, ); plPlayerController.setVolume(volume); }, @@ -1124,25 +1132,11 @@ class _PLVideoPlayerState extends State } } - void _onInteractionEnd(ScaleEndDetails details) { - if (plPlayerController.showSeekPreview) { - plPlayerController.showPreview.value = false; + void _onPanEnd(ScaleEndDetails details) { + if (_gestureType == .horizontal) { + _onHorizontalDragEnd(); } - if (plPlayerController.isSliderMoving.value) { - if (plPlayerController.cancelSeek == true) { - plPlayerController.onUpdatedSliderProgress( - plPlayerController.position, - ); - } else { - plPlayerController.seekTo( - plPlayerController.sliderPosition, - isSeek: false, - ); - } - plPlayerController.onChangedSliderEnd(); - } - interacting = false; - initialFocalPoint = Offset.zero; + _initialFocalPoint = null; _gestureType = null; } @@ -1163,25 +1157,10 @@ class _PLVideoPlayerState extends State plPlayerController.doubleTapFuc(type); } - void onTapDesktop() { - if (plPlayerController.isLive || plPlayerController.controlsLock.value) { - return; - } - plPlayerController.onDoubleTapCenter(); - } - - void onDoubleTapDesktop() { - if (plPlayerController.controlsLock.value) { - return; - } - plPlayerController.triggerFullScreen(status: !isFullScreen); - } - void _onTapUp(TapUpDetails details) { switch (details.kind) { case ui.PointerDeviceKind.mouse when PlatformUtils.isDesktop: - onTapDesktop(); - break; + plPlayerController.onDoubleTapCenter(); default: if (_suspendedDm == null) { plPlayerController.controls = !plPlayerController.showControls.value; @@ -1190,7 +1169,6 @@ class _PLVideoPlayerState extends State } else { _suspendedDm = null; } - break; } } @@ -1220,11 +1198,9 @@ class _PLVideoPlayerState extends State void _onDoubleTapDown(TapDownDetails details) { switch (details.kind) { case ui.PointerDeviceKind.mouse when PlatformUtils.isDesktop: - onDoubleTapDesktop(); - break; + plPlayerController.triggerFullScreen(status: !isFullScreen); default: onDoubleTapDownMobile(details); - break; } } @@ -1242,8 +1218,21 @@ class _PLVideoPlayerState extends State plPlayerController.setLongPressStatus(false)); late final ImmediateTapGestureRecognizer _tapGestureRecognizer; late final DoubleTapGestureRecognizer _doubleTapGestureRecognizer; + late final PlayerScaleGestureRecognizer _scaleGestureRecognizer; + StreamSubscription? _danmakuListener; + static const _kOffsetThreshold = 25.0; + bool _isPositionAllowed(Offset offset) { + if (offset.dx < _kOffsetThreshold || + offset.dy < _kOffsetThreshold || + offset.dx > maxWidth - _kOffsetThreshold || + offset.dy > maxHeight - _kOffsetThreshold) { + return false; + } + return true; + } + void _onPointerDown(PointerDownEvent event) { if (PlatformUtils.isDesktop) { final buttons = event.buttons; @@ -1255,20 +1244,35 @@ class _PLVideoPlayerState extends State ..controlsLock.value = false ..showControls.value = false; } - plPlayerController - .triggerFullScreen( - status: !isFullScreen, - inAppFullScreen: isSecondaryBtn, - ) - .whenComplete(() => initialFocalPoint = Offset.zero); + plPlayerController.triggerFullScreen( + status: !isFullScreen, + inAppFullScreen: isSecondaryBtn, + ); return; } } - _tapGestureRecognizer.addPointer(event); - _doubleTapGestureRecognizer.addPointer(event); - if (!plPlayerController.isLive) { - longPressRecognizer.addPointer(event); + final controlsUnlock = !plPlayerController.controlsLock.value; + if (PlatformUtils.isMobile) { + _tapGestureRecognizer.addPointer(event); + if (controlsUnlock) { + if (!plPlayerController.isLive) { + _doubleTapGestureRecognizer.addPointer(event); + longPressRecognizer.addPointer(event); + } + _scaleGestureRecognizer + ..isPosAllowed = _isPositionAllowed(event.localPosition) + ..addPointer(event); + } + } else if (controlsUnlock) { + if (plPlayerController.isLive) { + _doubleTapGestureRecognizer.addPointer(event); + } else { + _tapGestureRecognizer.addPointer(event); + _doubleTapGestureRecognizer.addPointer(event); + longPressRecognizer.addPointer(event); + } + _scaleGestureRecognizer.addPointer(event); } } @@ -1280,39 +1284,19 @@ class _PLVideoPlayerState extends State final dx = pan.dx.abs(); final dy = pan.dy.abs(); if (dx > 3 * dy) { - _gestureType = GestureType.horizontal; + _onHorizontalDragStart(); + _gestureType = .horizontal; } else if (dy > 3 * dx) { - _gestureType = GestureType.right; + _gestureType = .right; } return; } - if (_gestureType == GestureType.horizontal) { + if (_gestureType == .horizontal) { if (plPlayerController.isLive) return; - Offset delta = event.localPanDelta; - final int curSliderPosition = - plPlayerController.sliderPosition.inMilliseconds; - final int newPos = - (curSliderPosition + - (plPlayerController.sliderScale * delta.dx / maxWidth) - .round()) - .clamp(0, plPlayerController.duration.value.inMilliseconds); - final Duration result = Duration(milliseconds: newPos); - if (plPlayerController.cancelSeek == true) { - plPlayerController - ..cancelSeek = null - ..hasToast = null; - } - plPlayerController - ..onUpdatedSliderProgress(result) - ..onChangedSliderStart(); - if (!plPlayerController.isFileSource && - plPlayerController.showSeekPreview && - plPlayerController.cancelSeek != true) { - plPlayerController.updatePreviewIndex(newPos ~/ 1000); - } - } else if (_gestureType == GestureType.right) { + _onHorizontalDragUpdate(event.localPanDelta.dx); + } else if (_gestureType == .right) { if (!plPlayerController.enableSlideVolumeBrightness) { return; } @@ -1325,7 +1309,7 @@ class _PLVideoPlayerState extends State final double volume = clampDouble( plPlayerController.volume.value - event.localPanDelta.dy / level, 0.0, - PlPlayerController.maxVolume, + plPlayerController.maxVolume, ); plPlayerController.setVolume(volume); }, @@ -1334,6 +1318,9 @@ class _PLVideoPlayerState extends State } void _onPointerPanZoomEnd(PointerPanZoomEndEvent event) { + if (_gestureType == .horizontal) { + _onHorizontalDragEnd(); + } _gestureType = null; } @@ -1343,7 +1330,7 @@ class _PLVideoPlayerState extends State final volume = clampDouble( plPlayerController.volume.value + offset, 0.0, - PlPlayerController.maxVolume, + plPlayerController.maxVolume, ); plPlayerController.setVolume(volume); } @@ -1455,9 +1442,7 @@ class _PLVideoPlayerState extends State child: Obx( () => AnimatedOpacity( curve: Curves.easeInOut, - opacity: plPlayerController.isSliderMoving.value - ? 1.0 - : 0.0, + opacity: plPlayerController.isSeeking.value ? 1.0 : 0.0, duration: const Duration(milliseconds: 150), child: Container( decoration: const BoxDecoration( @@ -1473,27 +1458,22 @@ class _PLVideoPlayerState extends State mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.center, children: [ - Obx(() { - return Text( + Obx( + () => Text( DurationUtils.formatDuration( - plPlayerController - .sliderTempPosition - .value - .inSeconds, + plPlayerController.position.value, ), style: textStyle, - ); - }), + ), + ), const Text('/', style: textStyle), Obx( - () { - return Text( - DurationUtils.formatDuration( - plPlayerController.duration.value.inSeconds, - ), - style: textStyle, - ); - }, + () => Text( + DurationUtils.formatDuration( + plPlayerController.duration.value, + ), + style: textStyle, + ), ), ], ), @@ -1614,7 +1594,7 @@ class _PLVideoPlayerState extends State children: [ AppBarAni( isTop: true, - controller: animationController, + controller: _animationController, isFullScreen: isFullScreen, removeSafeArea: plPlayerController.removeSafeArea, child: plPlayerController.isDesktopPip @@ -1627,7 +1607,7 @@ class _PLVideoPlayerState extends State ), AppBarAni( isTop: false, - controller: animationController, + controller: _animationController, isFullScreen: isFullScreen, removeSafeArea: plPlayerController.removeSafeArea, child: @@ -1692,12 +1672,12 @@ class _PLVideoPlayerState extends State ); final anim = animController.drive( Matrix4Tween( - begin: transformationController.value, + begin: _transformationController.value, end: Matrix4.identity(), ).chain(CurveTween(curve: Curves.easeOut)), ); void listener() { - transformationController.value = anim.value; + _transformationController.value = anim.value; } animController.addListener(listener); @@ -1727,20 +1707,18 @@ class _PLVideoPlayerState extends State case .alwaysShow: offstage = showControls; case .alwaysHide: - if (!plPlayerController.isSliderMoving.value) { + if (!plPlayerController.isSeeking.value) { return const SizedBox.shrink(); } offstage = showControls; case .onlyShowFullScreen: offstage = showControls || - (!isFullScreen && - !plPlayerController.isSliderMoving.value); + (!isFullScreen && !plPlayerController.isSeeking.value); case .onlyHideFullScreen: offstage = showControls || - (isFullScreen && - !plPlayerController.isSliderMoving.value); + (isFullScreen && !plPlayerController.isSeeking.value); } return Offstage( offstage: offstage, @@ -1748,17 +1726,11 @@ class _PLVideoPlayerState extends State clipBehavior: Clip.none, alignment: Alignment.bottomCenter, children: [ - Obx(() { - final int value = - plPlayerController.sliderPositionSeconds.value; - final int max = - plPlayerController.duration.value.inSeconds; - final int buffer = - plPlayerController.bufferedSeconds.value; - return ProgressBar( - progress: Duration(seconds: value), - buffered: Duration(seconds: buffer), - total: Duration(seconds: max), + Obx( + () => ProgressBar( + progress: plPlayerController.position.value, + buffered: plPlayerController.buffered.value, + total: plPlayerController.duration.value, progressBarColor: primary, baseBarColor: const Color(0x33FFFFFF), bufferedBarColor: bufferedBarColor, @@ -1766,8 +1738,8 @@ class _PLVideoPlayerState extends State thumbGlowColor: thumbGlowColor, barHeight: 3.5, thumbRadius: 2.5, - ); - }), + ), + ), if (plPlayerController.enableBlock && videoDetailController.segmentProgressList.isNotEmpty) Positioned( @@ -1786,10 +1758,16 @@ class _PLVideoPlayerState extends State child: ViewPointSegmentProgressBar( segments: videoDetailController.viewPointList, onSeek: PlatformUtils.isMobile - ? (position) => plPlayerController.seekTo( - position, - isSeek: false, - ) + ? (position) { + if (!plPlayerController + .controlsLock + .value) { + plPlayerController.seekTo( + position, + isSeek: false, + ); + } + } : null, ), ), @@ -1923,7 +1901,8 @@ class _PLVideoPlayerState extends State ), if (plPlayerController.isBuffering.value) Obx(() { - if (plPlayerController.bufferedSeconds.value == 0) { + final buffered = plPlayerController.buffered.value; + if (buffered == 0) { return const Text( '加载中...', style: TextStyle( @@ -1932,10 +1911,8 @@ class _PLVideoPlayerState extends State ), ); } - String bufferStr = plPlayerController.buffered - .toString(); return Text( - bufferStr.substring(0, bufferStr.length - 3), + DurationUtils.formatDuration(buffered), style: const TextStyle( color: Colors.white, fontSize: 12, @@ -2030,7 +2007,7 @@ class _PLVideoPlayerState extends State Widget get _videoWidget { return Container( - clipBehavior: Clip.none, + clipBehavior: .none, width: maxWidth, height: maxHeight, color: widget.fill, @@ -2041,24 +2018,19 @@ class _PLVideoPlayerState extends State onPointerPanZoomUpdate: _onPointerPanZoomUpdate, onPointerPanZoomEnd: _onPointerPanZoomEnd, onPointerDown: _onPointerDown, - onInteractionStart: _onInteractionStart, - onInteractionUpdate: _onInteractionUpdate, - onInteractionEnd: _onInteractionEnd, + onPanStart: _onPanStart, + onPanUpdate: _onPanUpdate, + onPanEnd: _onPanEnd, + onScaleUpdate: _onScaleUpdate, + scaleGestureRecognizer: _scaleGestureRecognizer, panEnabled: false, minScale: plPlayerController.enableShrinkVideoSize ? 0.75 : 1, maxScale: 2.0, boundaryMargin: plPlayerController.enableShrinkVideoSize - ? const EdgeInsets.all(double.infinity) - : EdgeInsets.zero, - panAxis: PanAxis.aligned, - transformationController: transformationController, - onTranslate: () { - final storage = transformationController.value.storage; - showRestoreScaleBtn.value = - storage[12].abs() > 2.0 || - storage[13].abs() > 2.0 || - storage[0] != 1.0; - }, + ? const .all(double.infinity) + : .zero, + panAxis: .aligned, + transformationController: _transformationController, childKey: _videoKey, child: RepaintBoundary( key: _videoKey, @@ -2086,11 +2058,6 @@ class _PLVideoPlayerState extends State ); } - late final segment = Pair( - first: plPlayerController.position.inMilliseconds / 1000.0, - second: plPlayerController.position.inMilliseconds / 1000.0, - ); - Future screenshotWebp() async { final videoInfo = videoDetailController.data; final ids = videoInfo.dash!.video!.map((i) => i.id!).toSet(); @@ -2102,8 +2069,9 @@ class _PLVideoPlayerState extends State final ctr = plPlayerController; final theme = Theme.of(context); - final currentPos = ctr.position.inMilliseconds / 1000.0; - final duration = ctr.duration.value.inMilliseconds / 1000.0; + final currentPos = ctr.positionInMilliseconds / 1000.0; + final duration = ctr.durationInMilliseconds / 1000.0; + final segment = Pair(first: currentPos, second: currentPos); final model = PostSegmentModel( segment: segment, category: SegmentType.sponsor, diff --git a/lib/plugin/pl_player/view/widgets.dart b/lib/plugin/pl_player/view/widgets.dart index d1227d719e..a31ad3213d 100644 --- a/lib/plugin/pl_player/view/widgets.dart +++ b/lib/plugin/pl_player/view/widgets.dart @@ -153,15 +153,14 @@ class VideoShotImage extends StatefulWidget { } Future _getImg(String url) async { - final cacheManager = DefaultCacheManager(); final cacheKey = Utils.getFileName(url, fileExt: false); try { - final fileInfo = await cacheManager.getSingleFile( + final fileInfo = await CacheManager.manager.getSingleFile( ImageUtils.safeThumbnailUrl(url), key: cacheKey, headers: Constants.baseHeaders, ); - return _loadImg(fileInfo.path); + return await _loadImg(fileInfo.path); } catch (_) { return null; } @@ -317,8 +316,8 @@ class _DanmakuTip extends SingleChildRenderObjectWidget { class _RenderDanmakuTip extends RenderProxyBox { _RenderDanmakuTip({ - required double offset, - }) : _offset = offset; + required this._offset, + }); double _offset; double get offset => _offset; @@ -407,10 +406,9 @@ class _VideoTime extends LeafRenderObjectWidget { class _RenderVideoTime extends RenderBox { _RenderVideoTime({ - required String position, - required String duration, - }) : _position = position, - _duration = duration; + required this._position, + required this._duration, + }); String _duration; set duration(String value) { diff --git a/lib/plugin/pl_player/widgets/bottom_control.dart b/lib/plugin/pl_player/widgets/bottom_control.dart index ac449c0ec1..c395ceaff9 100644 --- a/lib/plugin/pl_player/widgets/bottom_control.dart +++ b/lib/plugin/pl_player/widgets/bottom_control.dart @@ -27,24 +27,22 @@ class BottomControl extends StatelessWidget { void onDragStart(ThumbDragDetails duration) { feedBack(); - controller.onChangedSliderStart(duration.timeStamp); + controller + ..position.value = duration.seconds + ..isSeeking.value = true; } void onDragUpdate(ThumbDragDetails duration) { if (!controller.isFileSource && controller.showSeekPreview) { - controller.updatePreviewIndex(duration.timeStamp.inSeconds); + controller.updatePreviewIndex(duration.seconds); } - controller.onUpdatedSliderProgress(duration.timeStamp); + controller.position.value = duration.seconds; } - void onSeek(Duration duration) { - if (controller.showSeekPreview) { - controller.showPreview.value = false; - } + void onSeek(int milliseconds) { controller - ..onChangedSliderEnd() - ..onChangedSlider(duration.inSeconds) - ..seekTo(Duration(seconds: duration.inSeconds), isSeek: false); + ..onSeekEnd() + ..seekTo(Duration(milliseconds: milliseconds), isSeek: false); } @override @@ -70,15 +68,11 @@ class BottomControl extends StatelessWidget { clipBehavior: Clip.none, alignment: Alignment.bottomCenter, children: [ - Obx(() { - final int value = controller.sliderPositionSeconds.value; - final int max = controller.duration.value.inSeconds; - return ProgressBar( - progress: Duration(seconds: value), - buffered: Duration( - seconds: controller.bufferedSeconds.value, - ), - total: Duration(seconds: max), + Obx( + () => ProgressBar( + progress: controller.position.value, + buffered: controller.buffered.value, + total: controller.duration.value, progressBarColor: primary, baseBarColor: const Color(0x33FFFFFF), bufferedBarColor: bufferedBarColor, @@ -90,8 +84,8 @@ class BottomControl extends StatelessWidget { onDragStart: onDragStart, onDragUpdate: onDragUpdate, onSeek: onSeek, - ); - }), + ), + ), if (controller.enableBlock && videoDetailController.segmentProgressList.isNotEmpty) Positioned( diff --git a/lib/router/app_pages.dart b/lib/router/app_pages.dart index d26c3d522d..1fd642e44b 100644 --- a/lib/router/app_pages.dart +++ b/lib/router/app_pages.dart @@ -1,4 +1,3 @@ -import 'package:PiliPlus/pages/about/view.dart'; import 'package:PiliPlus/pages/article/view.dart'; import 'package:PiliPlus/pages/article_list/view.dart'; import 'package:PiliPlus/pages/audio/view.dart'; @@ -53,18 +52,12 @@ import 'package:PiliPlus/pages/popular_series/view.dart'; import 'package:PiliPlus/pages/search/view.dart'; import 'package:PiliPlus/pages/search_result/view.dart'; import 'package:PiliPlus/pages/search_trending/view.dart'; -import 'package:PiliPlus/pages/setting/extra_setting.dart'; import 'package:PiliPlus/pages/setting/pages/bar_set.dart'; import 'package:PiliPlus/pages/setting/pages/color_select.dart'; import 'package:PiliPlus/pages/setting/pages/display_mode.dart'; import 'package:PiliPlus/pages/setting/pages/font_size_select.dart'; import 'package:PiliPlus/pages/setting/pages/logs.dart'; import 'package:PiliPlus/pages/setting/pages/play_speed_set.dart'; -import 'package:PiliPlus/pages/setting/play_setting.dart'; -import 'package:PiliPlus/pages/setting/privacy_setting.dart'; -import 'package:PiliPlus/pages/setting/recommend_setting.dart'; -import 'package:PiliPlus/pages/setting/style_setting.dart'; -import 'package:PiliPlus/pages/setting/video_setting.dart'; import 'package:PiliPlus/pages/setting/view.dart'; import 'package:PiliPlus/pages/settings_search/view.dart'; import 'package:PiliPlus/pages/space_setting/view.dart'; @@ -72,7 +65,6 @@ import 'package:PiliPlus/pages/sponsor_block/view.dart'; import 'package:PiliPlus/pages/subscription/view.dart'; import 'package:PiliPlus/pages/subscription_detail/view.dart'; import 'package:PiliPlus/pages/video/view.dart'; -import 'package:PiliPlus/pages/webdav/view.dart'; import 'package:PiliPlus/pages/webview/view.dart'; import 'package:PiliPlus/pages/whisper/view.dart'; import 'package:PiliPlus/pages/whisper_detail/view.dart'; @@ -116,26 +108,12 @@ class Routes { // 用户中心 GetPage(name: '/member', page: () => const MemberPage()), GetPage(name: '/memberSearch', page: () => const MemberSearchPage()), - // 推荐流设置 - GetPage(name: '/recommendSetting', page: () => const RecommendSetting()), - // 音视频设置 - GetPage(name: '/videoSetting', page: () => const VideoSetting()), - // 播放器设置 - GetPage(name: '/playSetting', page: () => const PlaySetting()), - // 外观设置 - GetPage(name: '/styleSetting', page: () => const StyleSetting()), - // 隐私设置 - GetPage(name: '/privacySetting', page: () => const PrivacySetting()), - // 其它设置 - GetPage(name: '/extraSetting', page: () => const ExtraSetting()), // GetPage(name: '/blackListPage', page: () => const BlackListPage()), GetPage(name: '/colorSetting', page: () => const ColorSelectPage()), GetPage(name: '/fontSizeSetting', page: () => const FontSizeSelectPage()), // 屏幕帧率 GetPage(name: '/displayModeSetting', page: () => const SetDisplayMode()), - // 关于 - GetPage(name: '/about', page: () => const AboutPage()), // GetPage(name: '/articlePage', page: () => const ArticlePage()), @@ -174,7 +152,6 @@ class Routes { GetPage(name: '/createFav', page: () => const CreateFavPage()), GetPage(name: '/editProfile', page: () => const EditProfilePage()), GetPage(name: '/settingsSearch', page: () => const SettingsSearchPage()), - GetPage(name: '/webdavSetting', page: () => const WebDavSettingPage()), GetPage(name: '/searchTrending', page: () => const SearchTrendingPage()), GetPage(name: '/dynTopic', page: () => const DynTopicPage()), GetPage(name: '/articleList', page: () => const ArticleListPage()), diff --git a/lib/scripts/bottom_sheet.patch b/lib/scripts/bottom_sheet_android.patch similarity index 100% rename from lib/scripts/bottom_sheet.patch rename to lib/scripts/bottom_sheet_android.patch diff --git a/lib/scripts/bottom_sheet_ios_flutter.patch b/lib/scripts/bottom_sheet_ios_flutter.patch new file mode 100644 index 0000000000..1b44933c0f --- /dev/null +++ b/lib/scripts/bottom_sheet_ios_flutter.patch @@ -0,0 +1,172 @@ +diff --git a/packages/flutter/lib/src/cupertino/route.dart b/packages/flutter/lib/src/cupertino/route.dart +index b6786d2611f..c36901a40bf 100644 +--- a/packages/flutter/lib/src/cupertino/route.dart ++++ b/packages/flutter/lib/src/cupertino/route.dart +@@ -201,7 +201,7 @@ mixin CupertinoRouteTransitionMixin on PageRoute { + // gesture is detected. The returned controller handles all of the subsequent + // drag events. + static _CupertinoBackGestureController _startPopGesture(PageRoute route) { +- assert(route.popGestureEnabled); ++ assert(route.popGestureEnabled || route.popGestureEnabled_); + + return _CupertinoBackGestureController( + navigator: route.navigator!, +@@ -252,6 +252,7 @@ mixin CupertinoRouteTransitionMixin on PageRoute { + linearTransition: linearTransition, + child: _CupertinoBackGestureDetector( + enabledCallback: () => route.popGestureEnabled, ++ popGestureEnabled_: () => route.popGestureEnabled_, + onStartPopGesture: () => _startPopGesture(route), + child: child, + ), +@@ -698,6 +699,7 @@ class _CupertinoBackGestureDetector extends StatefulWidget { + const _CupertinoBackGestureDetector({ + super.key, + required this.enabledCallback, ++ required this.popGestureEnabled_, + required this.onStartPopGesture, + required this.child, + }); +@@ -706,6 +708,8 @@ class _CupertinoBackGestureDetector extends StatefulWidget { + + final ValueGetter enabledCallback; + ++ final ValueGetter popGestureEnabled_; ++ + final ValueGetter<_CupertinoBackGestureController> onStartPopGesture; + + @override +@@ -720,7 +724,7 @@ class _CupertinoBackGestureDetectorState extends State<_CupertinoBackGestureD + @override + void initState() { + super.initState(); +- _recognizer = HorizontalDragGestureRecognizer(debugOwner: this) ++ _recognizer = _HorizontalDragGestureRecognizer(debugOwner: this) + ..onStart = _handleDragStart + ..onUpdate = _handleDragUpdate + ..onEnd = _handleDragEnd +@@ -776,7 +780,13 @@ class _CupertinoBackGestureDetectorState extends State<_CupertinoBackGestureD + + void _handlePointerDown(PointerDownEvent event) { + if (widget.enabledCallback()) { +- _recognizer.addPointer(event); ++ _recognizer ++ ..gestureSettings = null ++ ..addPointer(event); ++ } else if (widget.popGestureEnabled_()) { ++ _recognizer ++ ..gestureSettings = const DeviceGestureSettings(touchSlop: 28) ++ ..addPointer(event); + } + } + +@@ -883,7 +893,7 @@ class _CupertinoBackGestureController { + } else { + if (isCurrent) { + // This route is destined to pop at this point. Reuse navigator's pop. +- navigator.pop(); ++ navigator.pop(gesturePop); + } + + // The popping may have finished inline if already at the target destination. +@@ -1549,6 +1559,20 @@ class CupertinoDialogRoute extends RawDialogRoute { + static final Tween _dialogScaleTween = Tween(begin: 1.3, end: 1.0); + } + ++class _HorizontalDragGestureRecognizer extends HorizontalDragGestureRecognizer { ++ _HorizontalDragGestureRecognizer({ ++ super.debugOwner, ++ super.supportedDevices, ++ super.allowedButtonsFilter, ++ }); ++ ++ @override ++ void didStopTrackingLastPointer(int pointer) { ++ gestureSettings = null; ++ super.didStopTrackingLastPointer(pointer); ++ } ++} ++ + /// A [PageTransitionsBuilder] that provides an iOS-style page transition + /// animation. + /// +diff --git a/packages/flutter/lib/src/material/scaffold.dart b/packages/flutter/lib/src/material/scaffold.dart +index 0233bd1da39..a1ce131c786 100644 +--- a/packages/flutter/lib/src/material/scaffold.dart ++++ b/packages/flutter/lib/src/material/scaffold.dart +@@ -2519,6 +2519,7 @@ class ScaffoldState extends State + final LocalHistoryEntry? entry = isPersistent + ? null + : LocalHistoryEntry( ++ popGestureEnabled: true, + onRemove: () { + if (!removedEntry && _currentBottomSheet?._widget == bottomSheet && !doingDispose) { + removeCurrentBottomSheet(); +diff --git a/packages/flutter/lib/src/widgets/routes.dart b/packages/flutter/lib/src/widgets/routes.dart +index 6d42beb65c0..44c3da72895 100644 +--- a/packages/flutter/lib/src/widgets/routes.dart ++++ b/packages/flutter/lib/src/widgets/routes.dart +@@ -709,7 +709,11 @@ class LocalHistoryEntry { + /// Creates an entry in the history of a [LocalHistoryRoute]. + /// + /// The [impliesAppBarDismissal] defaults to true if not provided. +- LocalHistoryEntry({this.onRemove, this.impliesAppBarDismissal = true}); ++ LocalHistoryEntry({ ++ this.onRemove, ++ this.impliesAppBarDismissal = true, ++ this.popGestureEnabled = false, ++ }); + + /// Called when this entry is removed from the history of its associated [LocalHistoryRoute]. + final VoidCallback? onRemove; +@@ -722,6 +726,8 @@ class LocalHistoryEntry { + /// Defaults to true. + final bool impliesAppBarDismissal; + ++ final bool popGestureEnabled; ++ + /// Remove this entry from the history of its associated [LocalHistoryRoute]. + void remove() { + _owner?.removeLocalHistoryEntry(this); +@@ -949,6 +955,14 @@ mixin LocalHistoryRoute on Route { + @override + bool didPop(T? result) { + if (_localHistory != null && _localHistory!.isNotEmpty) { ++ if (result == gesturePop) { ++ for (var e in _localHistory!) { ++ e._owner = null; ++ } ++ _localHistory!.clear(); ++ _localHistory = null; ++ return super.didPop(result); ++ } + final LocalHistoryEntry entry = _localHistory!.removeLast(); + assert(entry._owner == this); + entry._owner = null; +@@ -970,6 +984,13 @@ mixin LocalHistoryRoute on Route { + bool get willHandlePopInternally { + return _localHistory != null && _localHistory!.isNotEmpty; + } ++ ++ bool get popGestureEnabled_ { ++ if (popDisposition == RoutePopDisposition.doNotPop) { ++ return false; ++ } ++ return _localHistory?.lastOrNull?.popGestureEnabled ?? false; ++ } + } + + class _DismissModalAction extends DismissAction { +@@ -989,6 +1010,12 @@ class _DismissModalAction extends DismissAction { + } + } + ++class _GesturePop { ++ const _GesturePop(); ++} ++ ++const Object gesturePop = _GesturePop(); ++ + enum _ModalRouteAspect { + /// Specifies the aspect corresponding to [ModalRoute.isCurrent]. + isCurrent, diff --git a/lib/scripts/bottom_sheet_ios_piliplus.patch b/lib/scripts/bottom_sheet_ios_piliplus.patch new file mode 100644 index 0000000000..47eeadc273 --- /dev/null +++ b/lib/scripts/bottom_sheet_ios_piliplus.patch @@ -0,0 +1,25 @@ +diff --git a/lib/pages/common/slide/common_slide_page.dart b/lib/pages/common/slide/common_slide_page.dart +index 58ba68b7c..a6bb398ac 100644 +--- a/lib/pages/common/slide/common_slide_page.dart ++++ b/lib/pages/common/slide/common_slide_page.dart +@@ -3,7 +3,8 @@ import 'dart:math' show max; + + import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart'; + import 'package:PiliPlus/utils/storage_pref.dart'; +-import 'package:flutter/gestures.dart' show HorizontalDragGestureRecognizer; ++import 'package:flutter/gestures.dart' ++ show HorizontalDragGestureRecognizer, DeviceGestureSettings; + import 'package:flutter/material.dart'; + import 'package:get/get.dart'; + +@@ -142,6 +143,10 @@ class SlideDragGestureRecognizer extends HorizontalDragGestureRecognizer { + bool isPointerAllowed(PointerEvent event) { + return isDxAllowed(event.localPosition.dx) && super.isPointerAllowed(event); + } ++ ++ @override ++ DeviceGestureSettings get gestureSettings => ++ const DeviceGestureSettings(touchSlop: 5.0); + } + + class TabBarDragGestureRecognizer diff --git a/lib/scripts/geetest_ios.patch b/lib/scripts/geetest_ios.patch new file mode 100644 index 0000000000..edda3ec222 --- /dev/null +++ b/lib/scripts/geetest_ios.patch @@ -0,0 +1,146 @@ +diff --git a/lib/pages/login/geetest/geetest_plugin.dart b/lib/pages/login/geetest/geetest_plugin.dart +new file mode 100644 +index 000000000..2823c2188 +--- /dev/null ++++ b/lib/pages/login/geetest/geetest_plugin.dart +@@ -0,0 +1,110 @@ ++import 'dart:async' show Completer; ++import 'dart:io' show Platform; ++ ++import 'package:flutter/foundation.dart' show kDebugMode, debugPrint; ++import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; ++import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart'; ++ ++abstract final class GeetestPlugin { ++ static Future geetest(String gt, String challenge) { ++ final completer = Completer(); ++ void complete([result]) { ++ if (!completer.isCompleted) { ++ completer.complete(result); ++ } ++ } ++ ++ final registerData = Gt3RegisterData( ++ challenge: challenge, ++ gt: gt, ++ success: true, ++ ); ++ ++ Gt3FlutterPlugin() ++ ..addEventHandler( ++ onShow: (Map message) {}, ++ onClose: (Map message) { ++ SmartDialog.showToast('关闭验证'); ++ complete(); ++ }, ++ onResult: (Map message) { ++ if (kDebugMode) debugPrint("Captcha result: $message"); ++ final String code = message["code"]; ++ if (code == "1") { ++ // 发送 message["result"] 中的数据向 B 端的业务服务接口进行查询 ++ complete(message['result']); ++ return; ++ } else { ++ // 终端用户完成验证失败,自动重试 If the verification fails, it will be automatically retried. ++ if (kDebugMode) debugPrint("Captcha result code : $code"); ++ } ++ complete(); ++ }, ++ onError: (Map message) { ++ SmartDialog.showToast("Captcha onError: $message"); ++ String code = message["code"]; ++ // 处理验证中返回的错误 Handling errors returned in verification ++ if (Platform.isAndroid) { ++ // Android 平台 ++ if (code == "-2") { ++ // Dart 调用异常 Call exception ++ } else if (code == "-1") { ++ // Gt3RegisterData 参数不合法 Parameter is invalid ++ } else if (code == "201") { ++ // 网络无法访问 Network inaccessible ++ } else if (code == "202") { ++ // Json 解析错误 Analysis error ++ } else if (code == "204") { ++ // WebView 加载超时,请检查是否混淆极验 SDK Load timed out ++ } else if (code == "204_1") { ++ // WebView 加载前端页面错误,请查看日志 Error loading front-end page, please check the log ++ } else if (code == "204_2") { ++ // WebView 加载 SSLError ++ } else if (code == "206") { ++ // gettype 接口错误或返回为 null API error or return null ++ } else if (code == "207") { ++ // getphp 接口错误或返回为 null API error or return null ++ } else if (code == "208") { ++ // ajax 接口错误或返回为 null API error or return null ++ } else { ++ // 更多错误码参考开发文档 More error codes refer to the development document ++ // https://docs.geetest.com/sensebot/apirefer/errorcode/android ++ } ++ } ++ ++ if (Platform.isIOS) { ++ // iOS 平台 ++ if (code == "-1009") { ++ // 网络无法访问 Network inaccessible ++ } else if (code == "-1004") { ++ // 无法查找到 HOST Unable to find HOST ++ } else if (code == "-1002") { ++ // 非法的 URL Illegal URL ++ } else if (code == "-1001") { ++ // 网络超时 Network timeout ++ } else if (code == "-999") { ++ // 请求被意外中断, 一般由用户进行取消操作导致 The interrupted request was usually caused by the user cancelling the operation ++ } else if (code == "-21") { ++ // 使用了重复的 challenge Duplicate challenges are used ++ // 检查获取 challenge 是否进行了缓存 Check if the fetch challenge is cached ++ } else if (code == "-20") { ++ // 尝试过多, 重新引导用户触发验证即可 Try too many times, lead the user to request verification again ++ } else if (code == "-10") { ++ // 预判断时被封禁, 不会再进行图形验证 Banned during pre-judgment, and no more image captcha verification ++ } else if (code == "-2") { ++ // Dart 调用异常 Call exception ++ } else if (code == "-1") { ++ // Gt3RegisterData 参数不合法 Parameter is invalid ++ } else { ++ // 更多错误码参考开发文档 More error codes refer to the development document ++ // https://docs.geetest.com/sensebot/apirefer/errorcode/ios ++ } ++ } ++ complete(); ++ }, ++ ) ++ ..startCaptcha(registerData); ++ ++ return completer.future; ++ } ++} +diff --git a/lib/pages/login/geetest/geetest_webview_dialog.dart b/lib/pages/login/geetest/geetest_webview_dialog.dart +index 7a0461170..da032af1a 100644 +--- a/lib/pages/login/geetest/geetest_webview_dialog.dart ++++ b/lib/pages/login/geetest/geetest_webview_dialog.dart +@@ -1,3 +1,5 @@ ++import 'package:PiliPlus/pages/login/geetest/geetest_plugin.dart'; ++ + import 'dart:convert' show base64, jsonDecode, jsonEncode, utf8; + import 'dart:io' show Platform; + +@@ -22,6 +24,7 @@ class GeetestWebviewDialog extends StatefulWidget { + State createState() => _GeetestWebviewDialogState(); + + static Future geetest(String gt, String challenge) { ++ return GeetestPlugin.geetest(gt, challenge); + return showDialog( + context: Get.context!, + builder: (context) => GeetestWebviewDialog(gt, challenge), +diff --git a/pubspec.yaml b/pubspec.yaml +index 1ec95f9f1..e29e474e0 100644 +--- a/pubspec.yaml ++++ b/pubspec.yaml +@@ -34,6 +34,7 @@ dependencies: + sdk: flutter + flutter_localizations: + sdk: flutter ++ gt3_flutter_plugin: ^0.1.0 + + app_links: ^7.1.1 + archive: ^4.0.0 diff --git a/lib/scripts/image_anim.patch b/lib/scripts/image_anim.patch new file mode 100644 index 0000000000..a60005d0b6 --- /dev/null +++ b/lib/scripts/image_anim.patch @@ -0,0 +1,13 @@ +diff --git a/packages/flutter/lib/src/widgets/image.dart b/packages/flutter/lib/src/widgets/image.dart +index 30429ea5e02..b58f4343440 100644 +--- a/packages/flutter/lib/src/widgets/image.dart ++++ b/packages/flutter/lib/src/widgets/image.dart +@@ -1148,7 +1148,7 @@ class _ImageState extends State with WidgetsBindingObserver { + _updateInvertColors(); + _resolveImage(); + +- _isPaused = !TickerMode.of(context) || (MediaQuery.maybeDisableAnimationsOf(context) ?? false); ++ _isPaused = !TickerMode.of(context); + + if (_isPaused && _frameNumber != null) { + _stopListeningToStream(keepStreamAlive: true); diff --git a/lib/scripts/layout_builder.patch b/lib/scripts/layout_builder.patch new file mode 100644 index 0000000000..f0168aaf41 --- /dev/null +++ b/lib/scripts/layout_builder.patch @@ -0,0 +1,79 @@ +diff --git a/packages/flutter/lib/src/widgets/layout_builder.dart b/packages/flutter/lib/src/widgets/layout_builder.dart +index 9dbefb7132d..68af3a95739 100644 +--- a/packages/flutter/lib/src/widgets/layout_builder.dart ++++ b/packages/flutter/lib/src/widgets/layout_builder.dart +@@ -115,43 +115,43 @@ class _LayoutBuilderElement extends RenderObjectElement { + + Element? _child; + +- @override +- BuildScope get buildScope => _buildScope; ++ // @override ++ // BuildScope get buildScope => _buildScope; + +- late final BuildScope _buildScope = BuildScope(scheduleRebuild: _scheduleRebuild); ++ // late final BuildScope _buildScope = BuildScope(scheduleRebuild: _scheduleRebuild); + + // To schedule a rebuild, markNeedsLayout needs to be called on this Element's + // render object (as the rebuilding is done in its performLayout call). However, + // the render tree should typically be kept clean during the postFrameCallbacks + // and the idle phase, so the layout data can be safely read. +- bool _deferredCallbackScheduled = false; +- void _scheduleRebuild() { +- if (_deferredCallbackScheduled) { +- return; +- } +- +- final bool deferMarkNeedsLayout = switch (SchedulerBinding.instance.schedulerPhase) { +- SchedulerPhase.idle || SchedulerPhase.postFrameCallbacks => true, +- SchedulerPhase.transientCallbacks || +- SchedulerPhase.midFrameMicrotasks || +- SchedulerPhase.persistentCallbacks => false, +- }; +- if (!deferMarkNeedsLayout) { +- renderObject.scheduleLayoutCallback(); +- return; +- } +- _deferredCallbackScheduled = true; +- SchedulerBinding.instance.scheduleFrameCallback(_frameCallback); +- } +- +- void _frameCallback(Duration timestamp) { +- _deferredCallbackScheduled = false; +- // This method is only called when the render tree is stable, if the Element +- // is deactivated it will never be reincorporated back to the tree. +- if (mounted) { +- renderObject.scheduleLayoutCallback(); +- } +- } ++ // bool _deferredCallbackScheduled = false; ++ // void _scheduleRebuild() { ++ // if (_deferredCallbackScheduled) { ++ // return; ++ // } ++ ++ // final bool deferMarkNeedsLayout = switch (SchedulerBinding.instance.schedulerPhase) { ++ // SchedulerPhase.idle || SchedulerPhase.postFrameCallbacks => true, ++ // SchedulerPhase.transientCallbacks || ++ // SchedulerPhase.midFrameMicrotasks || ++ // SchedulerPhase.persistentCallbacks => false, ++ // }; ++ // if (!deferMarkNeedsLayout) { ++ // renderObject.scheduleLayoutCallback(); ++ // return; ++ // } ++ // _deferredCallbackScheduled = true; ++ // SchedulerBinding.instance.scheduleFrameCallback(_frameCallback); ++ // } ++ ++ // void _frameCallback(Duration timestamp) { ++ // _deferredCallbackScheduled = false; ++ // // This method is only called when the render tree is stable, if the Element ++ // // is deactivated it will never be reincorporated back to the tree. ++ // if (mounted) { ++ // renderObject.scheduleLayoutCallback(); ++ // } ++ // } + + @override + void visitChildren(ElementVisitor visitor) { diff --git a/lib/scripts/navigation_drawer.patch b/lib/scripts/navigation_drawer.patch new file mode 100644 index 0000000000..c6c7de3679 --- /dev/null +++ b/lib/scripts/navigation_drawer.patch @@ -0,0 +1,13 @@ +diff --git a/packages/flutter/lib/src/material/navigation_drawer.dart b/packages/flutter/lib/src/material/navigation_drawer.dart +index 8a16764cb0c..6b8545c3781 100644 +--- a/packages/flutter/lib/src/material/navigation_drawer.dart ++++ b/packages/flutter/lib/src/material/navigation_drawer.dart +@@ -410,7 +410,7 @@ class _NavigationDestinationBuilder extends StatelessWidget { + child: _NavigationDestinationSemantics( + child: SizedBox( + height: navigationDrawerTheme.tileHeight ?? defaults.tileHeight, +- child: inkWell, ++ child: Material(type: MaterialType.transparency, child: inkWell), + ), + ), + ); diff --git a/lib/scripts/navigator.patch b/lib/scripts/navigator.patch new file mode 100644 index 0000000000..5bd7467282 --- /dev/null +++ b/lib/scripts/navigator.patch @@ -0,0 +1,15 @@ +diff --git a/packages/flutter/lib/src/widgets/navigator.dart b/packages/flutter/lib/src/widgets/navigator.dart +index fb7ccc5da2f..54dbc2b6c1d 100644 +--- a/packages/flutter/lib/src/widgets/navigator.dart ++++ b/packages/flutter/lib/src/widgets/navigator.dart +@@ -5615,7 +5615,9 @@ class NavigatorState extends State with TickerProviderStateMixin, Res + _debugLocked = false; + return true; + }()); +- _afterNavigation(entry.route); ++ if (entry.currentState == _RouteLifecycle.popping) { ++ _afterNavigation(entry.route); ++ } + } + + /// Calls [pop] repeatedly until the predicate returns true. diff --git a/lib/scripts/popup_menu.patch b/lib/scripts/popup_menu.patch new file mode 100644 index 0000000000..47d6add952 --- /dev/null +++ b/lib/scripts/popup_menu.patch @@ -0,0 +1,16 @@ +diff --git a/packages/flutter/lib/src/material/popup_menu.dart b/packages/flutter/lib/src/material/popup_menu.dart +index 8b84b2760d5..6ff222c4541 100644 +--- a/packages/flutter/lib/src/material/popup_menu.dart ++++ b/packages/flutter/lib/src/material/popup_menu.dart +@@ -446,6 +446,11 @@ class PopupMenuItemState> extends State { + data: IconThemeData(opacity: isDark ? 0.5 : 0.38), + child: item, + ); ++ } else { ++ item = IconTheme.merge( ++ data: IconThemeData(color: style.color), ++ child: item, ++ ); + } + + return MergeSemantics( diff --git a/lib/scripts/text_selection.patch b/lib/scripts/text_selection.patch index 5abf357dc0..04b4138c0f 100644 --- a/lib/scripts/text_selection.patch +++ b/lib/scripts/text_selection.patch @@ -1,20 +1,111 @@ diff --git a/packages/flutter/lib/src/gestures/tap_and_drag.dart b/packages/flutter/lib/src/gestures/tap_and_drag.dart -index 2409d19eabd..9460b61c00a 100644 +index 2409d19eabd..3d26609d1ce 100644 --- a/packages/flutter/lib/src/gestures/tap_and_drag.dart +++ b/packages/flutter/lib/src/gestures/tap_and_drag.dart -@@ -1407,6 +1407,7 @@ class TapAndHorizontalDragGestureRecognizer extends BaseTapAndDragGestureRecogni +@@ -1407,6 +1407,9 @@ class TapAndHorizontalDragGestureRecognizer extends BaseTapAndDragGestureRecogni @override bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind) { -+ return false; ++ if (pointerDeviceKind == PointerDeviceKind.touch) { ++ return false; ++ } return _globalDistanceMoved.abs() > computeHitSlop(pointerDeviceKind, gestureSettings); } -@@ -1442,6 +1443,7 @@ class TapAndPanGestureRecognizer extends BaseTapAndDragGestureRecognizer { - - @override - bool _hasSufficientGlobalDistanceToAccept(PointerDeviceKind pointerDeviceKind) { -+ return true; - return _globalDistanceMoved.abs() > computePanSlop(pointerDeviceKind, gestureSettings); - } +diff --git a/packages/flutter/lib/src/widgets/selectable_region.dart b/packages/flutter/lib/src/widgets/selectable_region.dart +index 59de8bae20b..174a224913b 100644 +--- a/packages/flutter/lib/src/widgets/selectable_region.dart ++++ b/packages/flutter/lib/src/widgets/selectable_region.dart +@@ -448,7 +448,7 @@ class SelectableRegionState extends State + void initState() { + super.initState(); + _focusNode.addListener(_handleFocusChanged); +- _initMouseGestureRecognizer(); ++ // _initMouseGestureRecognizer(); + _initTouchGestureRecognizer(); + // Right clicks. + _gestureRecognizers[TapGestureRecognizer] = +@@ -682,12 +682,7 @@ class SelectableRegionState extends State + // ancestor Scrollable gestures in common scenarios like a vertically scrolling list view. + _gestureRecognizers[TapAndHorizontalDragGestureRecognizer] = + GestureRecognizerFactoryWithHandlers( +- () => TapAndHorizontalDragGestureRecognizer( +- debugOwner: this, +- supportedDevices: PointerDeviceKind.values.where((PointerDeviceKind device) { +- return device != PointerDeviceKind.mouse; +- }).toSet(), +- ), ++ () => TapAndHorizontalDragGestureRecognizer(debugOwner: this), + (TapAndHorizontalDragGestureRecognizer instance) { + instance + // iOS does not provide a device specific touch slop +diff --git a/packages/flutter/lib/src/widgets/text_selection.dart b/packages/flutter/lib/src/widgets/text_selection.dart +index 6da7274a82f..78dd5d0e7ec 100644 +--- a/packages/flutter/lib/src/widgets/text_selection.dart ++++ b/packages/flutter/lib/src/widgets/text_selection.dart +@@ -3693,10 +3693,10 @@ class _TextSelectionGestureDetectorState extends State( + () => TapAndHorizontalDragGestureRecognizer(debugOwner: this), +@@ -3716,28 +3716,28 @@ class _TextSelectionGestureDetectorState extends State( +- () => TapAndPanGestureRecognizer(debugOwner: this), +- (TapAndPanGestureRecognizer instance) { +- instance +- // Text selection should start from the position of the first pointer +- // down event. +- ..dragStartBehavior = DragStartBehavior.down +- ..onTapTrackStart = _handleTapTrackStart +- ..onTapTrackReset = _handleTapTrackReset +- ..onTapDown = _handleTapDown +- ..onDragStart = _handleDragStart +- ..onDragUpdate = _handleDragUpdate +- ..onDragEnd = _handleDragEnd +- ..onTapUp = _handleTapUp +- ..onCancel = _handleTapCancel; +- }, +- ); +- } ++ // case TargetPlatform.linux: ++ // case TargetPlatform.macOS: ++ // case TargetPlatform.windows: ++ // gestures[TapAndPanGestureRecognizer] = ++ // GestureRecognizerFactoryWithHandlers( ++ // () => TapAndPanGestureRecognizer(debugOwner: this), ++ // (TapAndPanGestureRecognizer instance) { ++ // instance ++ // // Text selection should start from the position of the first pointer ++ // // down event. ++ // ..dragStartBehavior = DragStartBehavior.down ++ // ..onTapTrackStart = _handleTapTrackStart ++ // ..onTapTrackReset = _handleTapTrackReset ++ // ..onTapDown = _handleTapDown ++ // ..onDragStart = _handleDragStart ++ // ..onDragUpdate = _handleDragUpdate ++ // ..onDragEnd = _handleDragEnd ++ // ..onTapUp = _handleTapUp ++ // ..onCancel = _handleTapCancel; ++ // }, ++ // ); ++ // } + } + if (widget.onForcePressStart != null || widget.onForcePressEnd != null) { diff --git a/lib/services/audio_handler.dart b/lib/services/audio_handler.dart index a58dcaff17..8902016311 100644 --- a/lib/services/audio_handler.dart +++ b/lib/services/audio_handler.dart @@ -1,4 +1,5 @@ -import 'dart:io' show File; +import 'dart:io' show File, Platform; +import 'dart:ui' show PlatformDispatcher; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pb.dart' show DetailItem; @@ -9,11 +10,12 @@ import 'package:PiliPlus/models_new/video/video_detail/data.dart'; import 'package:PiliPlus/models_new/video/video_detail/page.dart'; import 'package:PiliPlus/plugin/pl_player/controller.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_status.dart'; -import 'package:PiliPlus/utils/extension/iterable_ext.dart'; +import 'package:PiliPlus/utils/android/bindings.g.dart'; import 'package:PiliPlus/utils/image_utils.dart'; import 'package:PiliPlus/utils/path_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:audio_service/audio_service.dart'; +import 'package:collection/collection.dart'; import 'package:path/path.dart' as path; Future initAudioService() { @@ -105,13 +107,28 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler { : processingState, controls: [ if (!isLive) - MediaControl.rewind.copyWith( - androidIcon: 'drawable/ic_baseline_replay_10_24', + const MediaControl( + androidIcon: 'drawable/ic_player_rewind_10s', + label: 'Rewind', + action: MediaAction.rewind, + ), + if (playing) + const MediaControl( + androidIcon: 'drawable/ic_player_pause', + label: 'Pause', + action: MediaAction.pause, + ) + else + const MediaControl( + androidIcon: 'drawable/ic_player_play', + label: 'Play', + action: MediaAction.play, ), - if (playing) MediaControl.pause else MediaControl.play, if (!isLive) - MediaControl.fastForward.copyWith( - androidIcon: 'drawable/ic_baseline_forward_10_24', + const MediaControl( + androidIcon: 'drawable/ic_player_fast_forward_10s', + label: 'Fast Forward', + action: MediaAction.fastForward, ), ], playing: playing, @@ -120,6 +137,15 @@ class VideoPlayerServiceHandler extends BaseAudioHandler with SeekHandler { }, ), ); + if (Platform.isAndroid && + (AndroidHelper.isPipMode || + PlPlayerController.instance?.isAutoEnterPip == true)) { + AndroidHelper.updatePipActions( + PlatformDispatcher.instance.engineId!, + isLive, + playing, + ); + } } void onStatusChange(PlayerStatus status, bool isBuffering, isLive) { diff --git a/lib/services/download/download_service.dart b/lib/services/download/download_service.dart index 1d469da0db..6566f5ac13 100644 --- a/lib/services/download/download_service.dart +++ b/lib/services/download/download_service.dart @@ -16,12 +16,12 @@ import 'package:PiliPlus/models_new/video/video_detail/episode.dart' as ugc; import 'package:PiliPlus/models_new/video/video_detail/page.dart'; import 'package:PiliPlus/pages/danmaku/controller.dart'; import 'package:PiliPlus/services/download/download_manager.dart'; +import 'package:PiliPlus/utils/cache_manager.dart'; import 'package:PiliPlus/utils/extension/file_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/path_utils.dart'; import 'package:flutter/foundation.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; import 'package:path/path.dart' as path; @@ -350,7 +350,7 @@ class DownloadService extends GetxService { if (File(filePath).existsSync()) { return true; } - final file = (await DefaultCacheManager().getFileFromCache( + final file = (await CacheManager.manager.getFileFromCache( entry.cover, ))?.file; if (file != null) { diff --git a/lib/services/logger.dart b/lib/services/logger.dart index 039b1afcaa..04588982b9 100644 --- a/lib/services/logger.dart +++ b/lib/services/logger.dart @@ -2,30 +2,19 @@ import 'dart:io'; import 'package:PiliPlus/utils/json_file_handler.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; -import 'package:catcher_2/catcher_2.dart'; +import 'package:catcher_2/utils/log_printer.dart'; +import 'package:flutter/foundation.dart'; import 'package:logger/logger.dart'; import 'package:path/path.dart' as p; import 'package:path_provider/path_provider.dart'; -final logger = PiliLogger(); - -class PiliLogger extends Logger { - PiliLogger() : super(); - - @override - void log( - Level level, - dynamic message, { - Object? error, - StackTrace? stackTrace, - DateTime? time, - }) { - if (level == Level.error || level == Level.fatal) { - Catcher2.reportCheckedError(error, stackTrace); - } - super.log(level, message, error: error, stackTrace: stackTrace, time: time); - } -} +final logger = Logger( + filter: ProductionFilter(), + printer: PrettyLogPrinter( + dateTimeFormat: PrettyLogPrinter.toEncodableFallback, + ), + level: kDebugMode ? .trace : .warning, +); abstract final class LoggerUtils { static File? _logFile; diff --git a/lib/services/shutdown_timer_service.dart b/lib/services/shutdown_timer_service.dart index a169a37d77..a54c85842d 100644 --- a/lib/services/shutdown_timer_service.dart +++ b/lib/services/shutdown_timer_service.dart @@ -108,7 +108,7 @@ class ShutdownTimerService { void _syncProgressAndExit() { if (PlPlayerController.instance case final player?) { final res = player.makeHeartBeat( - player.positionSeconds.value, + player.position.value, type: .completed, isManual: true, ); @@ -147,7 +147,7 @@ class ShutdownTimerService { } PageUtils.showVideoBottomSheet( context, - isFullScreen: () => isFullScreen, + maxWidth: 512, child: StatefulBuilder( builder: (_, setState) { final ThemeData theme = Theme.of(context); @@ -165,7 +165,7 @@ class ShutdownTimerService { const Center(child: Text('定时关闭', style: titleStyle)), const SizedBox(height: 10), ...{...scheduleTimeMinutes, _durationInMinutes} - .sorted((a, b) => a.compareTo(b)) + .sorted(Comparable.compare) .map( (minutes) => ListTile( dense: true, diff --git a/lib/tcp/live.dart b/lib/tcp/live.dart index 520d1847f5..e56ee8d766 100644 --- a/lib/tcp/live.dart +++ b/lib/tcp/live.dart @@ -49,7 +49,7 @@ class PackageHeaderRes extends PackageHeader { static PackageHeaderRes? fromBytesData(Uint8List data) { if (data.length < 10) { - logger.i('数据不足以解析PackageHeader'); + logger.w('数据不足以解析PackageHeader'); return null; } final byteData = ByteData.sublistView(data); diff --git a/lib/utils/accounts.dart b/lib/utils/accounts.dart index a88f531495..3db1b3e910 100644 --- a/lib/utils/accounts.dart +++ b/lib/utils/accounts.dart @@ -31,47 +31,8 @@ abstract final class Accounts { return deletedEntries > 2; }, ); - // await _migrate(); } - // static Future _migrate() async { - // final Directory tempDir = await getApplicationSupportDirectory(); - // final String tempPath = "${tempDir.path}/.plpl/"; - // final Directory dir = Directory(tempPath); - // if (dir.existsSync()) { - // if (kDebugMode) debugPrint('migrating...'); - // final cookieJar = PersistCookieJar( - // ignoreExpires: true, - // storage: FileStorage(tempPath), - // ); - // await cookieJar.forceInit(); - // final cookies = DefaultCookieJar(ignoreExpires: true) - // ..domainCookies.addAll(cookieJar.domainCookies); - // final localAccessKey = GStorage.localCache.get( - // 'accessKey', - // defaultValue: {}, - // ); - - // final isLogin = - // cookies.domainCookies['bilibili.com']?['/']?['SESSDATA'] != null; - - // await Future.wait([ - // GStorage.localCache.delete('accessKey'), - // GStorage.localCache.delete('danmakuFilterRule'), - // GStorage.localCache.delete('blackMidsList'), - // dir.delete(recursive: true), - // if (isLogin) - // LoginAccount( - // cookies, - // localAccessKey['value'], - // localAccessKey['refresh'], - // AccountType.values.toSet(), - // ).onChange(), - // ]); - // if (kDebugMode) debugPrint('migrated successfully'); - // } - // } - static Future refresh() { for (final a in account.values) { for (final t in a.type) { diff --git a/lib/utils/android/android_helper.dart b/lib/utils/android/android_helper.dart new file mode 100644 index 0000000000..c77df635a0 --- /dev/null +++ b/lib/utils/android/android_helper.dart @@ -0,0 +1,121 @@ +import 'dart:convert'; +import 'dart:ui'; + +import 'package:PiliPlus/utils/android/bindings.g.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:jni/jni.dart'; + +abstract final class PiliAndroidHelper { + @pragma('vm:prefer-inline') + static void back() => AndroidHelper.back(); + + static void biliSendCommAntifraud( + int action, + int oid, + int type, + int rpId, + int root, + int parent, + int ctime, + String commentText, + List pictures, + String sourceId, + int uid, + String cookie, + ) { + final jCommentText = commentText.toJString(); + final jSourceId = sourceId.toJString(); + final jCookie = cookie.toJString(); + final jPictures = pictures.isEmpty + ? null + : jsonEncode(pictures).toJString(); + + try { + AndroidHelper.biliSendCommAntifraud( + action, + oid, + type, + rpId, + root, + parent, + ctime, + jCommentText, + jPictures, + jSourceId, + uid, + jCookie, + ); + } catch (e) { + Utils.reportError(e); + } finally { + jCommentText.release(); + jSourceId.release(); + jCookie.release(); + jPictures?.release(); + } + } + + @pragma('vm:prefer-inline') + static void openLinkVerifySettings() => + AndroidHelper.openLinkVerifySettings(); + + static bool openMusic(String title, String? artist, String? album) { + final jTitle = title.toJString(); + final jArtist = artist?.toJString(); + final jAlbum = album?.toJString(); + try { + return AndroidHelper.openMusic(jTitle, jArtist, jAlbum); + } finally { + jTitle.release(); + jArtist?.release(); + jAlbum?.release(); + } + } + + @pragma('vm:prefer-inline') + static void enterPip( + int width, + int height, { + required bool autoEnter, + required bool isLive, + required bool isPlaying, + }) => AndroidHelper.enterPip( + PlatformDispatcher.instance.engineId!, + width, + height, + autoEnter, + isLive, + isPlaying, + ); + + @pragma('vm:prefer-inline') + static void disableAutoEnterPip() => + AndroidHelper.disableAutoEnterPip(PlatformDispatcher.instance.engineId!); + + static (int, int)? maxScreenSize() { + final jIArr = AndroidHelper.maxScreenSize(); + if (jIArr != null) { + try { + return (jIArr[0], jIArr[1]); + } finally { + jIArr.release(); + } + } + return null; + } + + static void createShortcut(String id, String uri, String label, String path) { + final jId = id.toJString(); + final jUri = uri.toJString(); + final jLabel = label.toJString(); + final jPath = path.toJString(); + try { + AndroidHelper.createShortcut(jId, jUri, jLabel, jPath); + } finally { + jId.release(); + jUri.release(); + jLabel.release(); + jPath.release(); + } + } +} diff --git a/lib/utils/android/bindings.g.dart b/lib/utils/android/bindings.g.dart new file mode 100644 index 0000000000..9388d0913e --- /dev/null +++ b/lib/utils/android/bindings.g.dart @@ -0,0 +1,729 @@ +// AUTO GENERATED BY JNIGEN 0.17.0. DO NOT EDIT! + +// ignore_for_file: annotate_overrides +// ignore_for_file: argument_type_not_assignable +// ignore_for_file: camel_case_extensions +// ignore_for_file: camel_case_types +// ignore_for_file: constant_identifier_names +// ignore_for_file: comment_references +// ignore_for_file: doc_directive_unknown +// ignore_for_file: file_names +// ignore_for_file: inference_failure_on_untyped_parameter +// ignore_for_file: invalid_internal_annotation +// ignore_for_file: invalid_use_of_internal_member +// ignore_for_file: library_prefixes +// ignore_for_file: lines_longer_than_80_chars +// ignore_for_file: no_leading_underscores_for_library_prefixes +// ignore_for_file: no_leading_underscores_for_local_identifiers +// ignore_for_file: non_constant_identifier_names +// ignore_for_file: only_throw_errors +// ignore_for_file: overridden_fields +// ignore_for_file: prefer_double_quotes +// ignore_for_file: unintended_html_in_doc_comment +// ignore_for_file: unnecessary_cast +// ignore_for_file: unnecessary_non_null_assertion +// ignore_for_file: unnecessary_parenthesis +// ignore_for_file: unused_element +// ignore_for_file: unused_field +// ignore_for_file: unused_import +// ignore_for_file: unused_local_variable +// ignore_for_file: unused_shown_name +// ignore_for_file: use_super_parameters + +import 'dart:core' as core$_; +import 'dart:core' show Object, String; + +import 'package:jni/_internal.dart' as jni$_; +import 'package:jni/jni.dart' as jni$_; + +const _$jniVersionCheck = jni$_.JniVersionCheck(1, 0); + +/// from: `java.lang.Runnable` +extension type Runnable._(jni$_.JObject _$this) implements jni$_.JObject { + static final _class = jni$_.JClass.forName(r'java/lang/Runnable'); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $Runnable$Type$(); + + /// Maps a specific port to the implemented interface. + static final core$_.Map _$impls = {}; + static jni$_.JObjectPtr _$invoke( + core$_.int port, + jni$_.JObjectPtr descriptor, + jni$_.JObjectPtr args, + ) { + return _$invokeMethod( + port, + jni$_.MethodInvocation.fromAddresses( + 0, + descriptor.address, + args.address, + ), + ); + } + + static final jni$_.Pointer< + jni$_.NativeFunction< + jni$_.JObjectPtr Function(jni$_.Int64, jni$_.JObjectPtr, jni$_.JObjectPtr) + > + > + _$invokePointer = jni$_.Pointer.fromFunction(_$invoke); + + static jni$_.Pointer _$invokeMethod( + core$_.int $p, + jni$_.MethodInvocation $i, + ) { + try { + final $d = $i.methodDescriptor.toDartString(releaseOriginal: true); + final $a = $i.args; + if ($d == r'run()V') { + _$impls[$p]!.run(); + return jni$_.nullptr; + } + } catch (e) { + return jni$_.ProtectedJniExtensions.newDartException(e); + } + return jni$_.nullptr; + } + + static void implementIn( + jni$_.JImplementer implementer, + $Runnable $impl, + ) { + late final jni$_.RawReceivePort $p; + $p = jni$_.RawReceivePort(($m) { + if ($m == null) { + _$impls.remove($p.sendPort.nativePort); + $p.close(); + return; + } + final $i = jni$_.MethodInvocation.fromMessage($m); + final $r = _$invokeMethod($p.sendPort.nativePort, $i); + jni$_.ProtectedJniExtensions.returnResult($i.result, $r); + }); + implementer.add( + r'java.lang.Runnable', + $p, + _$invokePointer, + [ + if ($impl.run$async) r'run()V', + ], + ); + final $a = $p.sendPort.nativePort; + _$impls[$a] = $impl; + } + + factory Runnable.implement( + $Runnable $impl, + ) { + final $i = jni$_.JImplementer(); + implementIn($i, $impl); + return $i.implement(); + } +} + +extension Runnable$$Methods on Runnable { + static final _id_run = Runnable._class.instanceMethodId( + r'run', + r'()V', + ); + + static final _run = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `public abstract void run()` + void run() { + final _$$selfRef = reference; + _run(_$$selfRef.pointer, _id_run.pointer).check(); + } +} + +abstract base mixin class $Runnable { + factory $Runnable({ + required void Function() run, + core$_.bool run$async, + }) = _$Runnable; + + void run(); + core$_.bool get run$async => false; +} + +final class _$Runnable with $Runnable { + _$Runnable({ + required void Function() run, + this.run$async = false, + }) : _run = run; + + final void Function() _run; + final core$_.bool run$async; + + void run() { + return _run(); + } +} + +final class $Runnable$Type$ extends jni$_.JType { + @jni$_.internal + const $Runnable$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Ljava/lang/Runnable;'; +} + +/// from: `com.example.piliplus.AndroidHelper` +extension type AndroidHelper._(jni$_.JObject _$this) implements jni$_.JObject { + static final _class = jni$_.JClass.forName( + r'com/example/piliplus/AndroidHelper', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = $AndroidHelper$Type$(); + static final _id_isFoldable = _class.staticFieldId( + r'isFoldable', + r'Z', + ); + + /// from: `static public final boolean isFoldable` + static core$_.bool get isFoldable => + _id_isFoldable.getNullable(_class, jni$_.jboolean.type) as core$_.bool; + + static final _id_isPipAvailable = _class.staticFieldId( + r'isPipAvailable', + r'Z', + ); + + /// from: `static public final boolean isPipAvailable` + static core$_.bool get isPipAvailable => + _id_isPipAvailable.getNullable(_class, jni$_.jboolean.type) + as core$_.bool; + + static final _id_isPipMode = _class.staticFieldId( + r'isPipMode', + r'Z', + ); + + /// from: `static public volatile boolean isPipMode` + static core$_.bool get isPipMode => + _id_isPipMode.getNullable(_class, jni$_.jboolean.type) as core$_.bool; + + /// from: `static public volatile boolean isPipMode` + static set isPipMode(core$_.bool value) => + _id_isPipMode.set(_class, jni$_.jboolean.type, value); + + static final _id_sdkInt = _class.staticMethodId( + r'sdkInt', + r'()I', + ); + + static final _sdkInt = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallStaticIntMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `static public int sdkInt()` + static core$_.int sdkInt() { + final _$$classRef = _class.reference; + return _sdkInt(_$$classRef.pointer, _id_sdkInt.pointer).integer; + } + + static final _id_back = _class.staticMethodId( + r'back', + r'()V', + ); + + static final _back = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `static public void back()` + static void back() { + final _$$classRef = _class.reference; + _back(_$$classRef.pointer, _id_back.pointer).check(); + } + + static final _id_biliSendCommAntifraud = _class.staticMethodId( + r'biliSendCommAntifraud', + r'(IJIJJJJLjava/lang/String;Ljava/lang/String;Ljava/lang/String;JLjava/lang/String;)V', + ); + + static final _biliSendCommAntifraud = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Int32, + jni$_.Int64, + jni$_.Int32, + jni$_.Int64, + jni$_.Int64, + jni$_.Int64, + jni$_.Int64, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Int64, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + core$_.int, + core$_.int, + core$_.int, + core$_.int, + core$_.int, + core$_.int, + core$_.int, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + core$_.int, + jni$_.Pointer, + ) + >(); + + /// from: `static public void biliSendCommAntifraud(int action, long oid, int type, long rpId, long root, long parent, long ctime, java.lang.String commentText, java.lang.String pictures, java.lang.String sourceId, long uid, java.lang.String cookie)` + static void biliSendCommAntifraud( + core$_.int action, + core$_.int oid, + core$_.int type, + core$_.int rpId, + core$_.int root, + core$_.int parent, + core$_.int ctime, + jni$_.JString commentText, + jni$_.JString? pictures, + jni$_.JString sourceId, + core$_.int uid, + jni$_.JString cookie, + ) { + final _$$classRef = _class.reference; + final _$commentText = commentText.reference; + final _$pictures = pictures?.reference ?? jni$_.jNullReference; + final _$sourceId = sourceId.reference; + final _$cookie = cookie.reference; + _biliSendCommAntifraud( + _$$classRef.pointer, + _id_biliSendCommAntifraud.pointer, + action, + oid, + type, + rpId, + root, + parent, + ctime, + _$commentText.pointer, + _$pictures.pointer, + _$sourceId.pointer, + uid, + _$cookie.pointer, + ).check(); + } + + static final _id_openLinkVerifySettings = _class.staticMethodId( + r'openLinkVerifySettings', + r'()V', + ); + + static final _openLinkVerifySettings = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `static public void openLinkVerifySettings()` + static void openLinkVerifySettings() { + final _$$classRef = _class.reference; + _openLinkVerifySettings( + _$$classRef.pointer, + _id_openLinkVerifySettings.pointer, + ).check(); + } + + static final _id_openMusic = _class.staticMethodId( + r'openMusic', + r'(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Z', + ); + + static final _openMusic = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallStaticBooleanMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public boolean openMusic(java.lang.String title, java.lang.String artist, java.lang.String album)` + static core$_.bool openMusic( + jni$_.JString title, + jni$_.JString? artist, + jni$_.JString? album, + ) { + final _$$classRef = _class.reference; + final _$title = title.reference; + final _$artist = artist?.reference ?? jni$_.jNullReference; + final _$album = album?.reference ?? jni$_.jNullReference; + return _openMusic( + _$$classRef.pointer, + _id_openMusic.pointer, + _$title.pointer, + _$artist.pointer, + _$album.pointer, + ).boolean; + } + + static final _id_enterPip = _class.staticMethodId( + r'enterPip', + r'(JIIZZZ)V', + ); + + static final _enterPip = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Int64, + jni$_.Int32, + jni$_.Int32, + jni$_.Int32, + jni$_.Int32, + jni$_.Int32, + ) + >, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + core$_.int, + core$_.int, + core$_.int, + core$_.int, + core$_.int, + core$_.int, + ) + >(); + + /// from: `static public void enterPip(long engineId, int width, int height, boolean autoEnter, boolean isLive, boolean isPlaying)` + static void enterPip( + core$_.int engineId, + core$_.int width, + core$_.int height, + core$_.bool autoEnter, + core$_.bool isLive, + core$_.bool isPlaying, + ) { + final _$$classRef = _class.reference; + _enterPip( + _$$classRef.pointer, + _id_enterPip.pointer, + engineId, + width, + height, + autoEnter ? 1 : 0, + isLive ? 1 : 0, + isPlaying ? 1 : 0, + ).check(); + } + + static final _id_updatePipActions = _class.staticMethodId( + r'updatePipActions', + r'(JZZ)V', + ); + + static final _updatePipActions = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int64, jni$_.Int32, jni$_.Int32)>, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + core$_.int, + core$_.int, + core$_.int, + ) + >(); + + /// from: `static public void updatePipActions(long engineId, boolean isLive, boolean isPlaying)` + static void updatePipActions( + core$_.int engineId, + core$_.bool isLive, + core$_.bool isPlaying, + ) { + final _$$classRef = _class.reference; + _updatePipActions( + _$$classRef.pointer, + _id_updatePipActions.pointer, + engineId, + isLive ? 1 : 0, + isPlaying ? 1 : 0, + ).check(); + } + + static final _id_disableAutoEnterPip = _class.staticMethodId( + r'disableAutoEnterPip', + r'(J)V', + ); + + static final _disableAutoEnterPip = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs<(jni$_.Int64,)>, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + core$_.int, + ) + >(); + + /// from: `static public void disableAutoEnterPip(long engineId)` + static void disableAutoEnterPip( + core$_.int engineId, + ) { + final _$$classRef = _class.reference; + _disableAutoEnterPip( + _$$classRef.pointer, + _id_disableAutoEnterPip.pointer, + engineId, + ).check(); + } + + static final _id_maxScreenSize = _class.staticMethodId( + r'maxScreenSize', + r'()[I', + ); + + static final _maxScreenSize = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + > + >('globalEnv_CallStaticObjectMethod') + .asFunction< + jni$_.JniResult Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + ) + >(); + + /// from: `static public int[] maxScreenSize()` + /// The returned object must be released after use, by calling the [release] method. + static jni$_.JIntArray? maxScreenSize() { + final _$$classRef = _class.reference; + return _maxScreenSize( + _$$classRef.pointer, + _id_maxScreenSize.pointer, + ).object(); + } + + static final _id_createShortcut = _class.staticMethodId( + r'createShortcut', + r'(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V', + ); + + static final _createShortcut = + jni$_.ProtectedJniExtensions.lookup< + jni$_.NativeFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.VarArgs< + ( + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >, + ) + > + >('globalEnv_CallStaticVoidMethod') + .asFunction< + jni$_.JThrowablePtr Function( + jni$_.Pointer, + jni$_.JMethodIDPtr, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + jni$_.Pointer, + ) + >(); + + /// from: `static public void createShortcut(java.lang.String id, java.lang.String uri, java.lang.String label, java.lang.String icon)` + static void createShortcut( + jni$_.JString id, + jni$_.JString uri, + jni$_.JString label, + jni$_.JString icon, + ) { + final _$$classRef = _class.reference; + final _$id = id.reference; + final _$uri = uri.reference; + final _$label = label.reference; + final _$icon = icon.reference; + _createShortcut( + _$$classRef.pointer, + _id_createShortcut.pointer, + _$id.pointer, + _$uri.pointer, + _$label.pointer, + _$icon.pointer, + ).check(); + } +} + +final class $AndroidHelper$Type$ extends jni$_.JType { + @jni$_.internal + const $AndroidHelper$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Lcom/example/piliplus/AndroidHelper;'; +} + +/// from: `com.example.piliplus.AndroidHelper$ToDart` +extension type AndroidHelper$ToDart._(jni$_.JObject _$this) + implements jni$_.JObject { + static final _class = jni$_.JClass.forName( + r'com/example/piliplus/AndroidHelper$ToDart', + ); + + /// The type which includes information such as the signature of this class. + static const jni$_.JType type = + $AndroidHelper$ToDart$Type$(); + static final _id_onUserLeaveHint = _class.staticFieldId( + r'onUserLeaveHint', + r'Ljava/lang/Runnable;', + ); + + /// from: `static public volatile java.lang.Runnable onUserLeaveHint` + /// The returned object must be released after use, by calling the [release] method. + static Runnable? get onUserLeaveHint => + _id_onUserLeaveHint.getNullable(_class, Runnable.type) as Runnable?; + + /// from: `static public volatile java.lang.Runnable onUserLeaveHint` + /// The returned object must be released after use, by calling the [release] method. + static set onUserLeaveHint(Runnable? value) => + _id_onUserLeaveHint.set(_class, Runnable.type, value); + + static final _id_onConfigurationChanged = _class.staticFieldId( + r'onConfigurationChanged', + r'Ljava/lang/Runnable;', + ); + + /// from: `static public java.lang.Runnable onConfigurationChanged` + /// The returned object must be released after use, by calling the [release] method. + static Runnable? get onConfigurationChanged => + _id_onConfigurationChanged.getNullable(_class, Runnable.type) + as Runnable?; + + /// from: `static public java.lang.Runnable onConfigurationChanged` + /// The returned object must be released after use, by calling the [release] method. + static set onConfigurationChanged(Runnable? value) => + _id_onConfigurationChanged.set(_class, Runnable.type, value); +} + +final class $AndroidHelper$ToDart$Type$ + extends jni$_.JType { + @jni$_.internal + const $AndroidHelper$ToDart$Type$(); + + @jni$_.internal + @core$_.override + String get signature => r'Lcom/example/piliplus/AndroidHelper$ToDart;'; +} diff --git a/lib/utils/app_scheme.dart b/lib/utils/app_scheme.dart index 3343404173..c53cedeed4 100644 --- a/lib/utils/app_scheme.dart +++ b/lib/utils/app_scheme.dart @@ -1,4 +1,6 @@ -import 'dart:async'; +// ignore_for_file: constant_identifier_names + +import 'dart:async' show StreamSubscription; import 'package:PiliPlus/common/widgets/view_safe_area.dart'; import 'package:PiliPlus/grpc/bilibili/app/listener/v1.pbenum.dart' @@ -7,6 +9,7 @@ import 'package:PiliPlus/http/search.dart'; import 'package:PiliPlus/models/common/fav_type.dart'; import 'package:PiliPlus/models/common/video/source_type.dart'; import 'package:PiliPlus/pages/audio/view.dart'; +import 'package:PiliPlus/pages/dynamics/widgets/vote.dart'; import 'package:PiliPlus/pages/fan/view.dart'; import 'package:PiliPlus/pages/follow/view.dart'; import 'package:PiliPlus/pages/follow_type/followed/view.dart'; @@ -323,7 +326,7 @@ abstract final class PiliScheme { return true; } return false; - case 'm.bilibili.com': + case bilibili_m: // bilibili://m.bilibili.com/topic-detail?topic_id=1028161&frommodule=H5&h5awaken=xxx final id = uri.queryParameters['topic_id']; if (id != null) { @@ -433,6 +436,15 @@ abstract final class PiliScheme { } } + static const b23_tv = 'b23.tv'; + static const bilibili = 'bilibili.com'; + static const bilibili_m = 'm.bilibili.com'; + static const bilibili_t = 't.bilibili.com'; + static const bilibili_live = 'live.bilibili.com'; + static const bilibili_space = 'space.bilibili.com'; + static const bilibili_search = 'search.bilibili.com'; + static const bilibili_music = 'music.bilibili.com'; + static Future _fullPathPush( Uri uri, { bool selfHandle = false, @@ -444,48 +456,51 @@ abstract final class PiliScheme { String host = uri.host; - if (selfHandle && - !host.contains('bilibili.com') && - !host.contains('b23.tv')) { - return false; - } - void launchURL() { if (!selfHandle) { _toWebview(uri.toString(), off, parameters); } } - // b23.tv - // bilibili.com - // m.bilibili.com - // www.bilibili.com - // space.bilibili.com - // live.bilibili.com - // search.bilibili.com + if (!host.contains(bilibili) && !host.contains(b23_tv)) { + launchURL(); + return false; + } // redirect - if (host.contains('b23.tv')) { + if (host.contains(b23_tv)) { String? redirectUrl = await UrlUtils.parseRedirectUrl(uri.toString()); if (redirectUrl != null) { uri = Uri.parse(redirectUrl); host = uri.host; } - if (!host.contains('bilibili.com')) { + if (!host.contains(bilibili)) { launchURL(); return false; } } final String path = uri.path; + late final queryParameters = uri.queryParameters; - if (host.contains('t.bilibili.com')) { - bool hasMatch = _onPushDynDetail(uri, off); - if (!hasMatch) { - launchURL(); + if (host.contains(bilibili_t)) { + if (_onPushDynDetail(uri, off)) { + return true; + } else if (path.startsWith('/vote')) { + // t.bilibili.com/vote/h5/index?vote_id={{vote_id}}#/result + if (queryParameters['vote_id'] case final voteIdStr?) { + final voteId = int.tryParse(voteIdStr); + if (voteId != null) { + if (Get.context != null) { + showVoteDialog(Get.context!, voteId); + } + return true; + } + } } - return hasMatch; - } else if (host.contains('live.bilibili.com')) { + launchURL(); + return false; + } else if (host.contains(bilibili_live)) { String? roomId = uriDigitRegExp.firstMatch(path)?.group(1); if (roomId != null) { PageUtils.toLiveRoom(int.parse(roomId), off: off); @@ -493,7 +508,7 @@ abstract final class PiliScheme { } launchURL(); return false; - } else if (host.contains('space.bilibili.com')) { + } else if (host.contains(bilibili_space)) { void toType({ required String mid, required String? type, @@ -513,8 +528,6 @@ abstract final class PiliScheme { } } - late final queryParameters = uri.queryParameters; - // space.bilibili.com/h5/follow?mid={{mid}}&type={{type}} if (path.startsWith('/h5/follow')) { final mid = queryParameters['mid']; @@ -545,7 +558,7 @@ abstract final class PiliScheme { } launchURL(); return false; - } else if (host.contains('search.bilibili.com')) { + } else if (host.contains(bilibili_search)) { String? keyword = uri.queryParameters['keyword']; if (keyword != null) { PageUtils.toDupNamed( @@ -557,7 +570,7 @@ abstract final class PiliScheme { } launchURL(); return false; - } else if (host.contains('music.bilibili.com')) { + } else if (host.contains(bilibili_music)) { // music.bilibili.com/pc/music-detail?music_id=MA*** // music.bilibili.com/h5-music-detail?music_id=MA*** if (path.contains('music-detail')) { diff --git a/lib/utils/app_sign.dart b/lib/utils/app_sign.dart index 9a31623891..c7ebb873f0 100644 --- a/lib/utils/app_sign.dart +++ b/lib/utils/app_sign.dart @@ -1,4 +1,4 @@ -import 'dart:convert'; +import 'dart:convert' show utf8; import 'package:PiliPlus/common/constants.dart'; import 'package:crypto/crypto.dart'; diff --git a/lib/utils/asset_utils.dart b/lib/utils/asset_utils.dart index 9d17a8ff50..7a368cd1f3 100644 --- a/lib/utils/asset_utils.dart +++ b/lib/utils/asset_utils.dart @@ -1,7 +1,7 @@ -import 'dart:async'; -import 'dart:io'; +import 'dart:async' show FutureOr; +import 'dart:io' show Platform, Directory, File; -import 'package:flutter/services.dart'; +import 'package:flutter/services.dart' show rootBundle; import 'package:path/path.dart' as path; abstract final class AssetUtils { @@ -61,7 +61,7 @@ abstract final class AssetUtils { } try { - final data = await rootBundle.load(file); + final data = await rootBundle.load('$src/$file'); await targetFile.writeAsBytes(data.buffer.asUint8List()); } catch (_) {} } diff --git a/lib/utils/bili_utils.dart b/lib/utils/bili_utils.dart new file mode 100644 index 0000000000..1555079c10 --- /dev/null +++ b/lib/utils/bili_utils.dart @@ -0,0 +1,35 @@ +import 'package:PiliPlus/common/widgets/svg/level_icon.dart'; +import 'package:flutter/material.dart'; + +abstract final class BiliUtils { + static bool isDefaultFav(int? attr) { + if (attr == null) { + return false; + } + return (attr & 2) == 0; + } + + static String isPublicFavText(int? attr) { + if (attr == null) { + return ''; + } + return isPublicFav(attr) ? '公开' : '私密'; + } + + static bool isPublicFav(int attr) { + return (attr & 1) == 0; + } + + static bool isCustomFollowTag(int? tagid) { + return tagid != null && tagid != 0 && tagid != -10 && tagid != -2; + } + + // https://s1.hdslb.com/bfs/svg-next/font/2025-10-27/freshspace-zpjpp3aqht.css + static Widget levelPicture( + int level, { + bool isSeniorMember = false, + double height = 11, + }) { + return UserLevel(level, height: height, flash: isSeniorMember); + } +} diff --git a/lib/utils/cache_manager.dart b/lib/utils/cache_manager.dart index 4dc29623c9..6e8c3027c5 100644 --- a/lib/utils/cache_manager.dart +++ b/lib/utils/cache_manager.dart @@ -1,28 +1,29 @@ -import 'dart:async'; -import 'dart:io'; +import 'dart:io' show Directory, File; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:cached_network_image_ce/cached_network_image.dart'; +import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; abstract final class CacheManager { + static late final DefaultCacheManager manager; + + static Future ensureInitialized() => DefaultCacheManager.init( + maxNrOfCacheLength: Pref.maxCacheSize.toInt(), + ).then((i) => manager = i); + // 获取缓存目录 @pragma('vm:notify-debugger-on-exception') - static Future loadApplicationCache([ - final num maxSize = double.infinity, - ]) async { + static Future loadApplicationCache() async { try { - final Directory tempDirectory = await getTemporaryDirectory(); if (PlatformUtils.isDesktop) { - final dir = Directory('${tempDirectory.path}/libCachedImageData'); - if (dir.existsSync()) { - return await getTotalSizeOfFilesInDir(dir, maxSize); - } - return 0; + return manager.getTotalLength(); } + final Directory tempDirectory = await getTemporaryDirectory(); if (tempDirectory.existsSync()) { - return await getTotalSizeOfFilesInDir(tempDirectory, maxSize); + return await getTotalSizeOfFilesInDir(tempDirectory); } } catch (_) {} return 0; @@ -30,16 +31,21 @@ abstract final class CacheManager { // 循环计算文件的大小 @pragma('vm:notify-debugger-on-exception') - static Future getTotalSizeOfFilesInDir( - final Directory file, [ - final num maxSize = double.infinity, - ]) async { - final children = file.list(recursive: true); + static Future getTotalSizeOfFilesInDir(final Directory file) async { int total = 0; - await for (final child in children) { + await for (final child in file.list(recursive: false)) { if (child is File) { total += await child.length(); - if (total >= maxSize) break; + } else if (child is Directory) { + if (path.equals(child.path, manager.cacheDir)) { + total += manager.getTotalLength(); + } else { + await for (final i in child.list(recursive: true)) { + if (i is File) { + total += await i.length(); + } + } + } } } return total; @@ -58,36 +64,21 @@ abstract final class CacheManager { } // 清除 Library/Caches 目录及文件缓存 + @pragma('vm:notify-debugger-on-exception') static Future clearLibraryCache() async { try { - final Directory tempDirectory = await getTemporaryDirectory(); - if (PlatformUtils.isDesktop) { - final dir = Directory('${tempDirectory.path}/libCachedImageData'); - if (dir.existsSync()) { - await dir.delete(recursive: true); - } - return; - } + await manager.emptyCache(); + if (PlatformUtils.isDesktop) return; + + final tempDirectory = await getTemporaryDirectory(); if (tempDirectory.existsSync()) { - final children = tempDirectory.list(recursive: false); - await for (final file in children) { + await for (final file in tempDirectory.list(recursive: false)) { + if (file is Directory && path.equals(file.path, manager.cacheDir)) { + continue; + } await file.delete(recursive: true); } } } catch (_) {} } - - static Future autoClearCache() async { - if (Pref.autoClearCache) { - await clearLibraryCache(); - } else { - final maxCacheSize = Pref.maxCacheSize; - if (maxCacheSize != 0) { - final currCache = await loadApplicationCache(maxCacheSize); - if (currCache >= maxCacheSize) { - await clearLibraryCache(); - } - } - } - } } diff --git a/lib/utils/calc_window_position.dart b/lib/utils/calc_window_position.dart index 1544ce6e57..9f9f4f2066 100644 --- a/lib/utils/calc_window_position.dart +++ b/lib/utils/calc_window_position.dart @@ -1,6 +1,6 @@ import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:collection/collection.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/rendering.dart' show Offset, Size; import 'package:screen_retriever/screen_retriever.dart'; Future calcWindowPosition(Size windowSize) async { diff --git a/lib/utils/color_utils.dart b/lib/utils/color_utils.dart new file mode 100644 index 0000000000..5e1ef2751b --- /dev/null +++ b/lib/utils/color_utils.dart @@ -0,0 +1,20 @@ +import 'package:flutter/rendering.dart' show Color; + +abstract final class ColourUtils { + static int parse2Int(String color) => + 0xFF000000 | int.parse(color.substring(1), radix: 16); + + static Color parseColor(String color) => Color(parse2Int(color)); + + static Color parseMedalColor(String color) { + final rgba = int.parse(color.substring(1), radix: 16); + return Color.fromARGB(rgba, rgba >> 24, rgba >> 16, rgba >> 8); + } + + static Color index2Color(int index, Color color) => switch (index) { + 0 => const Color(0xFFfdad13), + 1 => const Color(0xFF8aace1), + 2 => const Color(0xFFdfa777), + _ => color, + }; +} diff --git a/lib/utils/connectivity_utils.dart b/lib/utils/connectivity_utils.dart new file mode 100644 index 0000000000..ec571f9b84 --- /dev/null +++ b/lib/utils/connectivity_utils.dart @@ -0,0 +1,15 @@ +import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:connectivity_plus/connectivity_plus.dart'; + +abstract final class ConnectivityUtils { + static Future get isWiFi async { + try { + return PlatformUtils.isMobile && + (await Connectivity().checkConnectivity()).contains( + ConnectivityResult.wifi, + ); + } catch (_) { + return true; + } + } +} diff --git a/lib/utils/device_utils.dart b/lib/utils/device_utils.dart new file mode 100644 index 0000000000..c60d26acdd --- /dev/null +++ b/lib/utils/device_utils.dart @@ -0,0 +1,22 @@ +import 'package:PiliPlus/utils/android/bindings.g.dart'; +import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:flutter/widgets.dart' show WidgetsBinding, Size; + +abstract final class DeviceUtils { + static final int sdkInt = AndroidHelper.sdkInt(); + + static bool get isTablet { + return size.shortestSide >= 600; + } + + static Size get size { + final view = WidgetsBinding.instance.platformDispatcher.views.first; + return view.physicalSize / view.devicePixelRatio; + } + + static String get platformName => PlatformUtils.isDesktop + ? 'desktop' + : isTablet + ? 'pad' + : 'phone'; +} diff --git a/lib/utils/em.dart b/lib/utils/em.dart index 40a96dac19..a360943001 100644 --- a/lib/utils/em.dart +++ b/lib/utils/em.dart @@ -1,6 +1,8 @@ abstract final class Em { static final _exp = RegExp('<[^>]*>([^<]*)]*>'); - static final _htmlRegExp = RegExp(r'&(lt|gt|quot|apos|nbsp|amp);'); + static final _htmlRegExp = RegExp( + r'&(lt|gt|quot|apos|nbsp|amp|#x[a-fA-F\d]{2,4});', + ); static String regCate(String origin) { Iterable matches = _exp.allMatches(origin); @@ -29,6 +31,9 @@ abstract final class Em { 'apos' => "'", 'nbsp' => ' ', 'amp' => '&', + var i? when (i.startsWith('#x')) => String.fromCharCode( + int.parse(i.substring(2), radix: 16), + ), _ => m.group(0)!, }, ), diff --git a/lib/utils/extension/context_ext.dart b/lib/utils/extension/context_ext.dart index dfdb01917a..a566a32167 100644 --- a/lib/utils/extension/context_ext.dart +++ b/lib/utils/extension/context_ext.dart @@ -1,4 +1,3 @@ -import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:flutter/material.dart'; /// from Getx @@ -72,10 +71,4 @@ extension ContextExtensions on BuildContext { /// True if the current device is Tablet bool get isTablet => isSmallTablet || isLargeTablet; - - String get platformName => PlatformUtils.isDesktop - ? 'desktop' - : isTablet - ? 'pad' - : 'phone'; } diff --git a/lib/utils/extension/dimension_ext.dart b/lib/utils/extension/dimension_ext.dart index 151d953b6e..d52a551ddd 100644 --- a/lib/utils/extension/dimension_ext.dart +++ b/lib/utils/extension/dimension_ext.dart @@ -3,3 +3,16 @@ import 'package:PiliPlus/grpc/bilibili/app/archive/v1.pb.dart' show Dimension; extension DimensionExt on Dimension { bool get isVertical => rotate == .ONE ? width > height : height > width; } + +extension StringExt on String { + bool get isVerticalFromUri { + try { + final params = Uri.parse(this).queryParameters; + final width = int.parse(params['player_width']!); + final height = int.parse(params['player_height']!); + return params['player_rotate'] == '1' ? width > height : height > width; + } catch (_) { + return false; + } + } +} diff --git a/lib/utils/extension/extension.dart b/lib/utils/extension/extension.dart deleted file mode 100644 index 086c4d8983..0000000000 --- a/lib/utils/extension/extension.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:floating/floating.dart'; - -extension RationalExt on Rational { - /// Checks whether given [Rational] instance fits into Android requirements - /// or not. - /// - /// Android docs specified boundaries as inclusive. - bool get fitsInAndroidRequirements { - final aspectRatio = numerator / denominator; - const min = 1 / 2.39; - const max = 2.39; - return (min <= aspectRatio) && (aspectRatio <= max); - } -} diff --git a/lib/utils/extension/file_ext.dart b/lib/utils/extension/file_ext.dart index 9741181852..4c0168e0d9 100644 --- a/lib/utils/extension/file_ext.dart +++ b/lib/utils/extension/file_ext.dart @@ -1,11 +1,8 @@ -import 'dart:io'; +import 'dart:io' show FileSystemEntity, Directory; extension FileSystemEntityExt on FileSystemEntity { - Future tryDel({bool recursive = false}) async { - try { - await delete(recursive: recursive); - } catch (_) {} - } + Future tryDel({bool recursive = false}) => + delete(recursive: recursive).catchError((_) => this); } extension DirectoryExt on Directory { diff --git a/lib/utils/extension/get_ext.dart b/lib/utils/extension/get_ext.dart index 01639b3cbe..1a532e88b9 100644 --- a/lib/utils/extension/get_ext.dart +++ b/lib/utils/extension/get_ext.dart @@ -6,10 +6,10 @@ extension GetExt on GetInterface { GetInstance().putOrFind(dep, tag: tag); void updateMyAppTheme() { - final (l, d) = MyApp.getAllTheme(); + final (light, dark) = MyApp.getAllTheme(); rootController - ..theme = l - ..darkTheme = d + ..theme = light + ..darkTheme = dark ..update(); } } diff --git a/lib/utils/extension/iterable_ext.dart b/lib/utils/extension/iterable_ext.dart index d30844ac04..036fa1ea18 100644 --- a/lib/utils/extension/iterable_ext.dart +++ b/lib/utils/extension/iterable_ext.dart @@ -14,13 +14,6 @@ extension IterableExt on Iterable { } return value; } - - T? firstWhereOrNull(bool Function(T element) test) { - for (final element in this) { - if (test(element)) return element; - } - return null; - } } extension ListExt on List { @@ -66,4 +59,13 @@ extension ListExt on List { } return min; } + + T? getOrNull(int index) { + if (index < 0 || index >= length) return null; + return this[index]; + } + + T getOrFirst(int index) { + return getOrNull(index) ?? first; + } } diff --git a/lib/utils/extension/nested_scroll_ext.dart b/lib/utils/extension/nested_scroll_ext.dart new file mode 100644 index 0000000000..44e15fcde5 --- /dev/null +++ b/lib/utils/extension/nested_scroll_ext.dart @@ -0,0 +1,26 @@ +import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart' + show ExtendedNestedScrollViewState; +import 'package:flutter/widgets.dart' show Element, Curves; + +extension ExtendedNestedScrollViewStateExt on ExtendedNestedScrollViewState { + void refresh() { + if (mounted) { + (context as Element).markNeedsBuild(); + } + } + + void animToTop() { + if (mounted) { + final position = innerNestedPositions.first; + if (position.pixels >= position.viewportDimension * 7) { + position.localJumpTo(0); + } else { + outerController.animateTo( + outerController.offset, + curve: Curves.easeInOut, + duration: const Duration(milliseconds: 500), + ); + } + } + } +} diff --git a/lib/utils/extension/num_ext.dart b/lib/utils/extension/num_ext.dart index 397eb12f78..5e46e6b2fc 100644 --- a/lib/utils/extension/num_ext.dart +++ b/lib/utils/extension/num_ext.dart @@ -1,6 +1,6 @@ import 'dart:math' show pow; -import 'package:flutter/widgets.dart'; +import 'package:flutter/widgets.dart' show BuildContext, MediaQuery; extension ImageExtension on num { int? cacheSize(BuildContext context) { diff --git a/lib/utils/extension/scroll_controller_ext.dart b/lib/utils/extension/scroll_controller_ext.dart index 5e2a7d2ac8..7e53ec37f8 100644 --- a/lib/utils/extension/scroll_controller_ext.dart +++ b/lib/utils/extension/scroll_controller_ext.dart @@ -1,4 +1,4 @@ -import 'package:flutter/widgets.dart'; +import 'package:flutter/widgets.dart' show ScrollController, Curves; extension ScrollControllerExt on ScrollController { void animToTop() => animTo(0); diff --git a/lib/utils/extension/string_ext.dart b/lib/utils/extension/string_ext.dart index dc039ffd87..4521917353 100644 --- a/lib/utils/extension/string_ext.dart +++ b/lib/utils/extension/string_ext.dart @@ -1,7 +1,14 @@ final _regExp = RegExp("^(http:)?//", caseSensitive: false); -extension StringExt on String? { +extension NullableStringExt on String? { String get http2https => this?.replaceFirst(_regExp, "https://") ?? ''; bool get isNullOrEmpty => this == null || this!.isEmpty; } + +extension StringExt on String { + String subLength(int length) { + if (this.length < length) return this; + return substring(0, length); + } +} diff --git a/lib/utils/extension/theme_ext.dart b/lib/utils/extension/theme_ext.dart index 3f3151a78f..b47191990e 100644 --- a/lib/utils/extension/theme_ext.dart +++ b/lib/utils/extension/theme_ext.dart @@ -1,9 +1,16 @@ import 'package:flex_seed_scheme/flex_seed_scheme.dart'; -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' + show ThemeData, Color, ColorScheme, Brightness, Colors; const _pinkLight = Color(0xFFFF6699); const _pinkDark = Color(0xFFD44E7D); +extension ThemeDataExt on ThemeData { + bool get isLight => brightness.isLight; + + bool get isDark => brightness.isDark; +} + extension ColorSchemeExt on ColorScheme { Color get vipColor => brightness.isLight ? _pinkLight : _pinkDark; diff --git a/lib/utils/fav_utils.dart b/lib/utils/fav_utils.dart deleted file mode 100644 index c013181812..0000000000 --- a/lib/utils/fav_utils.dart +++ /dev/null @@ -1,19 +0,0 @@ -abstract final class FavUtils { - static bool isDefaultFav(int? attr) { - if (attr == null) { - return false; - } - return (attr & 2) == 0; - } - - static String isPublicFavText(int? attr) { - if (attr == null) { - return ''; - } - return isPublicFav(attr) ? '公开' : '私密'; - } - - static bool isPublicFav(int attr) { - return (attr & 1) == 0; - } -} diff --git a/lib/utils/filtering_text.dart b/lib/utils/filtering_text.dart new file mode 100644 index 0000000000..649732b816 --- /dev/null +++ b/lib/utils/filtering_text.dart @@ -0,0 +1,7 @@ +import 'package:flutter/services.dart'; + +abstract final class FilteringText { + static final decimal = [ + FilteringTextInputFormatter.allow(RegExp(r'[\d\.]+')), + ]; +} diff --git a/lib/utils/grid.dart b/lib/utils/grid.dart index 30f178afd6..52b51d144c 100644 --- a/lib/utils/grid.dart +++ b/lib/utils/grid.dart @@ -1,13 +1,12 @@ import 'dart:math'; import 'package:PiliPlus/common/skeleton/video_card_h.dart'; -import 'package:PiliPlus/common/style.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -mixin GridMixin on State { - late final gridDelegate = Grid.videoCardHDelegate(context); +mixin GridMixin { + late final gridDelegate = Grid.videoCardHDelegate(); Widget get gridSkeleton => SliverGrid.builder( gridDelegate: gridDelegate, @@ -19,14 +18,12 @@ mixin GridMixin on State { abstract final class Grid { static final double smallCardWidth = Pref.smallCardWidth; - static SliverGridDelegateWithExtentAndRatio videoCardHDelegate( - BuildContext context, { - double minHeight = 90, - }) => SliverGridDelegateWithExtentAndRatio( + static SliverGridDelegateWithMaxCrossAxisExtent videoCardHDelegate({ + double mainAxisExtent = 110, + }) => SliverGridDelegateWithMaxCrossAxisExtent( mainAxisSpacing: 2, + mainAxisExtent: mainAxisExtent, maxCrossAxisExtent: Grid.smallCardWidth * 2, - childAspectRatio: Style.aspectRatio * 2.2, - minHeight: MediaQuery.textScalerOf(context).scale(minHeight), ); } @@ -43,14 +40,10 @@ class SliverGridDelegateWithExtentAndRatio extends SliverGridDelegate { this.crossAxisSpacing = 0.0, this.childAspectRatio = 1.0, this.mainAxisExtent = 0.0, - this.minHeight = 0.0, }) : assert(maxCrossAxisExtent > 0), assert(mainAxisSpacing >= 0), assert(crossAxisSpacing >= 0), - assert(childAspectRatio > 0), - assert(minHeight >= 0); - - final double minHeight; + assert(childAspectRatio > 0); /// The maximum extent of tiles in the cross axis. /// @@ -111,10 +104,8 @@ class SliverGridDelegateWithExtentAndRatio extends SliverGridDelegate { constraints.crossAxisExtent - crossAxisSpacing * (crossAxisCount - 1), ); final double childCrossAxisExtent = usableCrossAxisExtent / crossAxisCount; - final double childMainAxisExtent = max( - minHeight, - childCrossAxisExtent / childAspectRatio + mainAxisExtent, - ); + final double childMainAxisExtent = + childCrossAxisExtent / childAspectRatio + mainAxisExtent; return layoutCache = SliverGridRegularTileLayout( crossAxisCount: crossAxisCount, mainAxisStride: childMainAxisExtent + mainAxisSpacing, diff --git a/lib/utils/id_utils.dart b/lib/utils/id_utils.dart index 20321f3e3e..953882afbd 100644 --- a/lib/utils/id_utils.dart +++ b/lib/utils/id_utils.dart @@ -1,8 +1,9 @@ // ignore_for_file: constant_identifier_names, non_constant_identifier_names -import 'dart:convert'; +import 'dart:convert' show ascii, base64; import 'package:PiliPlus/utils/utils.dart'; +import 'package:collection/collection.dart'; import 'package:uuid/v4.dart'; abstract final class IdUtils { @@ -24,12 +25,6 @@ abstract final class IdUtils { static final avRegexExact = RegExp(r'^av(\d+)$', caseSensitive: false); static final digitOnlyRegExp = RegExp(r'^\d+$'); - static void swap(List list, int idx1, int idx2) { - final idx1Value = list[idx1]; - list[idx1] = list[idx2]; - list[idx2] = idx1Value; - } - /// av转bv static String av2bv(int aid) { final bytes = ['B', 'V', '1', '0', '0', '0', '0', '0', '0', '0', '0', '0']; @@ -40,18 +35,18 @@ abstract final class IdUtils { tmp ~/= BASE; } - swap(bytes, 3, 9); - swap(bytes, 4, 7); + bytes + ..swap(3, 9) + ..swap(4, 7); return bytes.join(); } /// bv转av static int bv2av(String bvid) { - final bvidArr = bvid.codeUnits.sublist(3); - - swap(bvidArr, 0, 6); - swap(bvidArr, 1, 4); + final bvidArr = bvid.codeUnits.sublist(3) + ..swap(0, 6) + ..swap(1, 4); final tmp = bvidArr.fold(0, (pre, char) => pre * BASE + invData[char]!); return (tmp & MASK_CODE) ^ XOR_CODE; diff --git a/lib/utils/image_utils.dart b/lib/utils/image_utils.dart index 68641dc0cb..41636c47fe 100644 --- a/lib/utils/image_utils.dart +++ b/lib/utils/image_utils.dart @@ -1,50 +1,49 @@ -import 'dart:io'; +import 'dart:async'; +import 'dart:io' show File, Platform; import 'dart:math' as math; -import 'dart:typed_data'; +import 'dart:typed_data' show Uint8List; import 'package:PiliPlus/common/constants.dart'; import 'package:PiliPlus/http/init.dart'; +import 'package:PiliPlus/utils/cache_manager.dart'; +import 'package:PiliPlus/utils/device_utils.dart'; import 'package:PiliPlus/utils/extension/file_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/path_utils.dart'; import 'package:PiliPlus/utils/permission_handler.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:PiliPlus/utils/share_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:dio/dio.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_cache_manager/flutter_cache_manager.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:intl/intl.dart' show DateFormat; import 'package:live_photo_maker/live_photo_maker.dart'; import 'package:saver_gallery/saver_gallery.dart'; import 'package:share_plus/share_plus.dart'; abstract final class ImageUtils { - static String get time => - DateFormat('yyyy-MM-dd_HH-mm-ss').format(DateTime.now()); static bool silentDownImg = Pref.silentDownImg; - static const _androidRelativePath = 'Pictures/${Constants.appName}'; + static final _albumPath = Platform.isAndroid + ? 'Pictures/${Constants.appName}' + : Constants.appName; // 图片分享 static Future onShareImg(String url) async { try { SmartDialog.showLoading(); - final path = '$tmpDirPath/${Utils.getFileName(url)}'; - final res = await Request().downloadFile(url.http2https, path); + final res = await CacheManager.manager.getSingleFile( + url.http2https, + ); SmartDialog.dismiss(); - if (res.statusCode == 200) { - await SharePlus.instance - .share( - ShareParams( - files: [XFile(path)], - sharePositionOrigin: await Utils.sharePositionOrigin, - ), - ) - .whenComplete(File(path).tryDel); - } + await SharePlus.instance.share( + ShareParams( + files: [XFile(res.path)], + sharePositionOrigin: await ShareUtils.sharePositionOrigin, + ), + ); } catch (e) { SmartDialog.showToast(e.toString()); } @@ -78,12 +77,12 @@ abstract final class ImageUtils { } } - static Future checkPermissionDependOnSdkInt() { + static FutureOr checkPermissionDependOnSdkInt() { if (Platform.isAndroid) { - if (Utils.sdkInt < 29) { + if (DeviceUtils.sdkInt < 29) { return requestPer(); } else { - return Future.syncValue(true); + return true; } } return requestPer(); @@ -101,8 +100,6 @@ abstract final class ImageUtils { } if (!silentDownImg) SmartDialog.showLoading(msg: '正在下载'); - late String imageName = "cover_${Utils.getFileName(url)}"; - late String imagePath = '$tmpDirPath/$imageName'; String videoName = "video_${Utils.getFileName(liveUrl)}"; String videoPath = '$tmpDirPath/$videoName'; @@ -110,22 +107,17 @@ abstract final class ImageUtils { if (res.statusCode != 200) throw '${res.statusCode}'; if (Platform.isIOS) { - final res1 = await Request().downloadFile(url.http2https, imagePath); - if (res1.statusCode != 200) throw '${res1.statusCode}'; + final imageFile = await CacheManager.manager.getSingleFile( + url.http2https, + ); if (!silentDownImg) SmartDialog.showLoading(msg: '正在保存'); - bool success = - await LivePhotoMaker.create( - coverImage: imagePath, - imagePath: null, - voicePath: videoPath, - width: width, - height: height, - ).whenComplete( - () { - File(videoPath).tryDel(); - File(imagePath).tryDel(); - }, - ); + bool success = await LivePhotoMaker.create( + coverImage: imageFile.path, + imagePath: null, + voicePath: videoPath, + width: width, + height: height, + ).whenComplete(File(videoPath).tryDel); if (success) { SmartDialog.showToast(' 已保存 '); } else { @@ -150,10 +142,7 @@ abstract final class ImageUtils { } } - static Future downloadImg( - List imgList, [ - CacheManager? manager, - ]) async { + static Future downloadImg(List imgList) async { if (PlatformUtils.isMobile && !await checkPermissionDependOnSdkInt()) { return false; } @@ -170,31 +159,10 @@ abstract final class ImageUtils { final futures = imgList.map((url) async { final name = Utils.getFileName(url); - final file = (await (manager ?? DefaultCacheManager()).getFileFromCache( + final file = await CacheManager.manager.getSingleFile( url.http2https, - ))?.file; - - if (file == null) { - final String filePath = '$tmpDirPath/$name'; - final response = await Request().downloadFile( - url.http2https, - filePath, - cancelToken: cancelToken, - ); - return ( - filePath: filePath, - name: name, - statusCode: response.statusCode, - del: true, - ); - } else { - return ( - filePath: file.path, - name: name, - statusCode: 200, - del: false, - ); - } + ); + return (filePath: file.path, name: name, statusCode: 200); }); final result = await Future.wait(futures, eagerError: true); bool success = true; @@ -202,13 +170,12 @@ abstract final class ImageUtils { final delList = []; final saveList = []; for (final i in result) { - if (i.del) delList.add(i.filePath); if (i.statusCode == 200) { saveList.add( SaveFileData( filePath: i.filePath, fileName: i.name, - androidRelativePath: _androidRelativePath, + albumPath: _albumPath, ), ); } else { @@ -222,11 +189,7 @@ abstract final class ImageUtils { } else { for (final res in result) { if (res.statusCode == 200) { - await saveFileImg( - filePath: res.filePath, - fileName: res.name, - del: res.del, - ); + await saveFileImg(filePath: res.filePath, fileName: res.name); } else { success = false; } @@ -300,7 +263,7 @@ abstract final class ImageUtils { res = await SaverGallery.saveImage( bytes, fileName: fileName, - androidRelativePath: _androidRelativePath, + albumPath: _albumPath, skipIfExists: false, ); SmartDialog.dismiss(); @@ -314,6 +277,7 @@ abstract final class ImageUtils { final savePath = await FilePicker.saveFile( type: FileType.image, fileName: fileName, + bytes: Uint8List(0), ); if (savePath == null) { SmartDialog.showToast("取消保存"); @@ -331,7 +295,6 @@ abstract final class ImageUtils { required String fileName, FileType type = FileType.image, bool needToast = false, - bool del = true, }) async { final file = File(filePath); if (!file.existsSync()) { @@ -343,21 +306,20 @@ abstract final class ImageUtils { res = await SaverGallery.saveFile( filePath: filePath, fileName: fileName, - androidRelativePath: _androidRelativePath, + albumPath: _albumPath, skipIfExists: false, ); - if (del) file.tryDel(); } else { final savePath = await FilePicker.saveFile( type: type, fileName: fileName, + bytes: Uint8List(0), ); if (savePath == null) { SmartDialog.showToast("取消保存"); return; } await file.copy(savePath); - if (del) file.tryDel(); res = SaveResult(true, null); } if (needToast) { diff --git a/lib/utils/json_file_handler.dart b/lib/utils/json_file_handler.dart index d6853fa351..30677c4423 100644 --- a/lib/utils/json_file_handler.dart +++ b/lib/utils/json_file_handler.dart @@ -1,11 +1,8 @@ import 'dart:convert'; import 'dart:io'; -import 'package:PiliPlus/services/logger.dart' show LoggerUtils; -import 'package:catcher_2/model/platform_type.dart'; -import 'package:catcher_2/model/report.dart'; -import 'package:catcher_2/model/report_handler.dart'; -import 'package:flutter/material.dart'; +import 'package:PiliPlus/services/logger.dart'; +import 'package:catcher_2/catcher_2.dart'; class JsonFileHandler extends ReportHandler { final bool enableDeviceParameters; @@ -48,7 +45,7 @@ class JsonFileHandler extends ReportHandler { handleWhenRejected: handleWhenRejected, ); } catch (e, s) { - debugPrintStack(stackTrace: s, label: e.toString()); + logger.e('Init log file', error: e, stackTrace: s); return null; } } @@ -62,18 +59,24 @@ class JsonFileHandler extends ReportHandler { } @override - Future handle(Report report, BuildContext? context) async { + Future handle(Report report) async { try { await _processReport(report); return true; } catch (exc, stackTrace) { - _printLog('Exception occurred: $exc stack: $stackTrace'); + logger.e( + 'Write Json Exception occurred', + error: exc, + stackTrace: stackTrace, + ); return false; } } Future _processReport(Report report) { - _printLog('Writing report to file'); + if (printLogs) { + logger.d('Writing report to file'); + } final json = report.toJson( enableDeviceParameters: enableDeviceParameters, enableApplicationParameters: enableApplicationParameters, @@ -82,22 +85,4 @@ class JsonFileHandler extends ReportHandler { ); return add((raf) => raf.writeString('${jsonEncode(json)}\n')); } - - void _printLog(String log) { - if (printLogs) { - logger.info(log); - } - } - - @override - List getSupportedPlatforms() => const [ - PlatformType.android, - PlatformType.iOS, - PlatformType.linux, - PlatformType.macOS, - PlatformType.windows, - ]; - - @override - bool shouldHandleWhenRejected() => handleWhenRejected; } diff --git a/lib/utils/login_utils.dart b/lib/utils/login_utils.dart index 1ccf8c5815..66ddc760c7 100644 --- a/lib/utils/login_utils.dart +++ b/lib/utils/login_utils.dart @@ -67,11 +67,16 @@ abstract final class LoginUtils { } } else { // 获取用户信息失败 - await Accounts.deleteAll({account}); - SmartDialog.showNotify( - msg: '登录失败,请检查cookie是否正确,${res.toString()}', - notifyType: NotifyType.warning, - ); + final errMsg = res.toString(); + if (errMsg == '账号未登录') { + await Accounts.deleteAll({account}); + SmartDialog.showNotify( + msg: '登录失败,请检查cookie是否正确,$errMsg', + notifyType: .warning, + ); + } else { + SmartDialog.showToast(errMsg); + } } } diff --git a/lib/utils/max_screen_size.dart b/lib/utils/max_screen_size.dart new file mode 100644 index 0000000000..d8f21333c2 --- /dev/null +++ b/lib/utils/max_screen_size.dart @@ -0,0 +1,35 @@ +import 'dart:io' show Platform; + +import 'package:PiliPlus/utils/android/android_helper.dart'; +import 'package:PiliPlus/utils/android/bindings.g.dart'; + +abstract final class MaxScreenSize { + static int? _maxWidth; + static int? _maxHeight; + + static void init() { + _initScreenSize(); + if (AndroidHelper.isFoldable) { + AndroidHelper$ToDart.onConfigurationChanged = Runnable.implement( + $Runnable(run: _initScreenSize), + ); + } + } + + static void _initScreenSize() { + final size = PiliAndroidHelper.maxScreenSize(); + if (size != null) { + _maxWidth = size.$1; + _maxHeight = size.$2; + } + } + + static bool isWindowMode({required num width, required num height}) { + if (!Platform.isAndroid) return false; + width = width.round(); + height = height.round(); + final hasWidthMatch = width == _maxWidth || width == _maxHeight; + final hasHeightMatch = height == _maxWidth || height == _maxHeight; + return !(hasWidthMatch && hasHeightMatch); + } +} diff --git a/lib/utils/page_utils.dart b/lib/utils/page_utils.dart index 3ed12e32ad..464a721687 100644 --- a/lib/utils/page_utils.dart +++ b/lib/utils/page_utils.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:PiliPlus/common/widgets/fractionally_sized_box.dart'; import 'package:PiliPlus/common/widgets/image_viewer/gallery_viewer.dart'; import 'package:PiliPlus/common/widgets/image_viewer/hero_dialog_route.dart'; import 'package:PiliPlus/grpc/im.dart'; @@ -17,10 +18,9 @@ import 'package:PiliPlus/pages/common/publish/publish_route.dart'; import 'package:PiliPlus/pages/contact/view.dart'; import 'package:PiliPlus/pages/fav_panel/view.dart'; import 'package:PiliPlus/pages/share/view.dart'; +import 'package:PiliPlus/utils/android/android_helper.dart'; import 'package:PiliPlus/utils/app_scheme.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; -import 'package:PiliPlus/utils/extension/extension.dart'; -import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; import 'package:PiliPlus/utils/feed_back.dart'; @@ -30,7 +30,7 @@ import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; import 'package:PiliPlus/utils/url_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; -import 'package:floating/floating.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; @@ -187,27 +187,44 @@ abstract final class PageUtils { ); } - static void enterPip({int? width, int? height, bool isAuto = false}) { - if (width != null && height != null) { - Rational aspectRatio = Rational(width, height); - aspectRatio = aspectRatio.fitsInAndroidRequirements - ? aspectRatio - : height > width - ? const Rational.vertical() - : const Rational.landscape(); - Floating().enable( - isAuto - ? AutoEnable(aspectRatio: aspectRatio) - : EnableManual(aspectRatio: aspectRatio), - ); - } else { - Floating().enable(isAuto ? const AutoEnable() : const EnableManual()); + static bool _fitsInAndroidRequirements(int width, int height) { + final aspectRatio = width / height; + const min = 1 / 2.39; + const max = 2.39; + return (min <= aspectRatio) && (aspectRatio <= max); + } + + static void enterPip({ + int? width, + int? height, + bool autoEnter = false, + required bool isLive, + required bool isPlaying, + }) { + if (width != null && + height != null && + !_fitsInAndroidRequirements(width, height)) { + if (height > width) { + width = 9; + height = 16; + } else { + width = 16; + height = 9; + } } + PiliAndroidHelper.enterPip( + width ?? 16, + height ?? 9, + autoEnter: autoEnter, + isLive: isLive, + isPlaying: isPlaying, + ); } static Future pushDynDetail( DynamicItemModel item, { bool isPush = false, + bool viewComment = false, }) async { feedBack(); @@ -229,6 +246,7 @@ abstract final class PageUtils { '/dynamicDetail', arguments: { 'item': item, + if (viewComment) 'viewComment': true, }, ); } @@ -479,8 +497,8 @@ abstract final class PageUtils { static Future? showVideoBottomSheet( BuildContext context, { required Widget child, - required ValueGetter isFullScreen, - double? padding, + ValueGetter? padding, + double maxWidth = 500, }) { if (!context.mounted) { return null; @@ -488,27 +506,17 @@ abstract final class PageUtils { return Get.key.currentState!.push( PublishRoute( pageBuilder: (context, animation, secondaryAnimation) { - if (context.isPortrait) { - return SafeArea( - child: FractionallySizedBox( - heightFactor: 0.7, - widthFactor: 1.0, - alignment: Alignment.bottomCenter, - child: isFullScreen() && padding != null - ? Padding( - padding: EdgeInsets.only(bottom: padding), - child: child, - ) - : child, - ), - ); - } + final isPortrait = context.isPortrait; return SafeArea( - child: FractionallySizedBox( - widthFactor: 0.5, - heightFactor: 1.0, - alignment: Alignment.centerRight, - child: child, + child: CustomFractionallySizedBox( + maxWidth: maxWidth, + widthFactor: isPortrait ? 1.0 : 0.5, + heightFactor: isPortrait ? 0.7 : 1.0, + alignment: isPortrait ? .bottomCenter : .centerRight, + child: Padding( + padding: isPortrait ? padding?.call() ?? .zero : .zero, + child: child, + ), ), ); }, diff --git a/lib/utils/parse_int.dart b/lib/utils/parse_int.dart new file mode 100644 index 0000000000..95c1630631 --- /dev/null +++ b/lib/utils/parse_int.dart @@ -0,0 +1,6 @@ +int? safeToInt(dynamic value) => switch (value) { + int() => value, + String() => int.tryParse(value), + num() => value.toInt(), + _ => null, +}; diff --git a/lib/utils/parse_string.dart b/lib/utils/parse_string.dart index 17da1d0702..bc2684fbd9 100644 --- a/lib/utils/parse_string.dart +++ b/lib/utils/parse_string.dart @@ -1,4 +1,4 @@ -String? noneNullOrEmptyString(String? value) { +String? nonNullOrEmptyString(String? value) { if (value == null || value.isEmpty) return null; return value; } diff --git a/lib/utils/reply_utils.dart b/lib/utils/reply_utils.dart index c1cf9702a3..25962ac563 100644 --- a/lib/utils/reply_utils.dart +++ b/lib/utils/reply_utils.dart @@ -1,4 +1,3 @@ -import 'dart:convert' show jsonEncode; import 'dart:io' show Platform; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' @@ -8,8 +7,11 @@ import 'package:PiliPlus/http/reply.dart'; import 'package:PiliPlus/models/common/reply/reply_sort_type.dart'; import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/accounts/account.dart'; +import 'package:PiliPlus/utils/android/android_helper.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; +import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/id_utils.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; @@ -53,11 +55,11 @@ abstract final class ReplyUtils { required int type, required int id, required String message, - dynamic root, - dynamic parent, - dynamic ctime, - List? pictures, - dynamic mid, + required int root, + required int parent, + required int ctime, + required List pictures, + required int mid, bool isManual = false, required bool biliSendCommAntifraud, required sourceId, @@ -65,27 +67,24 @@ abstract final class ReplyUtils { // biliSendCommAntifraud if (Platform.isAndroid && biliSendCommAntifraud) { try { - final String cookieString = Accounts.main.cookieJar + final cookieString = Accounts.main.cookieJar .toJson() .entries .map((i) => '${i.key}=${i.value}') .join(';'); - Utils.channel.invokeMethod( - 'biliSendCommAntifraud', - { - 'action': 0, - 'oid': oid, - 'type': type, - 'rpid': id, - 'root': root, - 'parent': parent, - 'ctime': ctime, - 'comment_text': message, - if (pictures?.isNotEmpty == true) 'pictures': jsonEncode(pictures), - 'source_id': '$sourceId', - 'uid': mid, - 'cookies': [cookieString], - }, + PiliAndroidHelper.biliSendCommAntifraud( + 0, + oid, + type, + id, + root, + parent, + ctime, + message, + pictures, + sourceId, + mid, + cookieString, ); } catch (e) { if (kDebugMode) debugPrint('biliSendCommAntifraud: $e'); @@ -98,6 +97,7 @@ abstract final class ReplyUtils { await Future.delayed(const Duration(seconds: 8)); } void showReplyCheckResult(String message, {bool isBan = false}) { + final theme = ThemeUtils.theme; final actions = [ if (isBan) TextButton( @@ -117,7 +117,7 @@ abstract final class ReplyUtils { '/webview', parameters: { 'url': - 'https://www.bilibili.com/h5/comment/appeal?${Utils.themeUrl(Get.isDarkMode)}', + 'https://www.bilibili.com/h5/comment/appeal?${ThemeUtils.themeUrl(theme.isDark)}', }, ); }, @@ -128,7 +128,7 @@ abstract final class ReplyUtils { onPressed: Get.back, child: Text( '关闭', - style: TextStyle(color: Get.theme.colorScheme.outline), + style: TextStyle(color: theme.colorScheme.outline), ), ), ]; diff --git a/lib/utils/request_utils.dart b/lib/utils/request_utils.dart index 9d68cf03d0..36a3dc3cdc 100644 --- a/lib/utils/request_utils.dart +++ b/lib/utils/request_utils.dart @@ -1,8 +1,9 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:io'; import 'dart:math'; +import 'package:PiliPlus/common/widgets/dialog/dialog.dart'; +import 'package:PiliPlus/common/widgets/dialog/simple_dialog_option.dart'; import 'package:PiliPlus/grpc/bilibili/im/type.pbenum.dart'; import 'package:PiliPlus/grpc/bilibili/main/community/reply/v1.pb.dart' show ReplyInfo; @@ -29,17 +30,19 @@ import 'package:PiliPlus/utils/accounts.dart'; import 'package:PiliPlus/utils/extension/context_ext.dart'; import 'package:PiliPlus/utils/extension/size_ext.dart'; import 'package:PiliPlus/utils/extension/string_ext.dart'; +import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/feed_back.dart'; import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:PiliPlus/utils/storage.dart'; import 'package:PiliPlus/utils/storage_key.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; +import 'package:PiliPlus/utils/theme_utils.dart'; import 'package:PiliPlus/utils/utils.dart'; import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart' show LengthLimitingTextInputFormatter; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:get/get.dart'; -import 'package:gt3_flutter_plugin/gt3_flutter_plugin.dart'; abstract final class RequestUtils { static Future syncHistoryStatus() async { @@ -99,6 +102,35 @@ abstract final class RequestUtils { } } + static Future createFavTag( + BuildContext context, + ValueChanged<({int tagid, String tagName})> onSuccess, + ) async { + String tagName = ''; + final onCreate = await showConfirmDialog( + context: context, + title: const Text('新建分组'), + content: TextFormField( + autofocus: true, + initialValue: tagName, + onChanged: (value) => tagName = value, + inputFormatters: [ + LengthLimitingTextInputFormatter(16), + ], + decoration: const InputDecoration(border: OutlineInputBorder()), + ), + ); + if (onCreate) { + final res = await MemberHttp.createFollowTag(tagName); + if (res case Success(:final response)) { + onSuccess((tagid: response, tagName: tagName)); + SmartDialog.showToast('创建成功'); + } else { + res.toast(); + } + } + } + static Future actionRelationMod({ required BuildContext context, required dynamic mid, @@ -138,103 +170,84 @@ abstract final class RequestUtils { String text = isSpecialFollowed ? '移除特别关注' : '加入特别关注'; showDialog( context: context, - builder: (context) => AlertDialog( + builder: (context) => SimpleDialog( clipBehavior: Clip.hardEdge, contentPadding: const EdgeInsets.symmetric(vertical: 12), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - ListTile( - dense: true, - onTap: () async { - Get.back(); - final res = await MemberHttp.specialAction( - fid: mid, - isAdd: !isSpecialFollowed, - ); - if (res.isSuccess) { - SmartDialog.showToast('$text成功'); - afterMod?.call(isSpecialFollowed ? 2 : -10); - } else { - res.toast(); - } - }, - title: Text( - text, - style: const TextStyle(fontSize: 14), - ), - ), - ListTile( - dense: true, - onTap: () async { - Get.back(); - final result = await showModalBottomSheet>( - context: context, - useSafeArea: true, - isScrollControlled: true, - constraints: BoxConstraints( - maxWidth: min(640, context.mediaQueryShortestSide), - ), - builder: (BuildContext context) { - final maxChildSize = - PlatformUtils.isMobile && - !context.mediaQuerySize.isPortrait - ? 1.0 - : 0.7; - return DraggableScrollableSheet( - minChildSize: 0, - maxChildSize: 1, - snap: true, - expand: false, - snapSizes: [maxChildSize], - initialChildSize: maxChildSize, - builder: - ( - BuildContext context, - ScrollController scrollController, - ) { - return GroupPanel( - mid: mid, - tags: followStatus!.tag, - scrollController: scrollController, - ); - }, - ); - }, - ); - if (result != null) { - followStatus!.tag = result.toList(); - afterMod?.call(result.contains(-10) ? -10 : 2); - } - }, - title: const Text( - '设置分组', - style: TextStyle(fontSize: 14), - ), - ), - ListTile( - dense: true, - onTap: () async { - Get.back(); - final res = await VideoHttp.relationMod( - mid: mid, - act: 2, - reSrc: 11, - ); - if (res.isSuccess) { - SmartDialog.showToast('取消关注成功'); - afterMod?.call(0); - } else { - res.toast(); - } - }, - title: const Text( - '取消关注', - style: TextStyle(fontSize: 14), - ), - ), - ], - ), + children: [ + DialogOption( + onPressed: () async { + Get.back(); + final res = await MemberHttp.specialAction( + fid: mid, + isAdd: !isSpecialFollowed, + ); + if (res.isSuccess) { + SmartDialog.showToast('$text成功'); + afterMod?.call(isSpecialFollowed ? 2 : -10); + } else { + res.toast(); + } + }, + child: Text(text, style: const TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () async { + Get.back(); + final result = await showModalBottomSheet>( + context: context, + useSafeArea: true, + isScrollControlled: true, + constraints: BoxConstraints( + maxWidth: min(640, context.mediaQueryShortestSide), + ), + builder: (BuildContext context) { + final maxChildSize = + PlatformUtils.isMobile && + !context.mediaQuerySize.isPortrait + ? 1.0 + : 0.7; + return DraggableScrollableSheet( + minChildSize: 0, + maxChildSize: 1, + snap: true, + expand: false, + snapSizes: [maxChildSize], + initialChildSize: maxChildSize, + builder: (context, scrollController) { + return GroupPanel( + mid: mid, + tags: followStatus!.tag, + scrollController: scrollController, + ); + }, + ); + }, + ); + if (result != null) { + followStatus!.tag = result.toList(); + afterMod?.call(result.contains(-10) ? -10 : 2); + } + }, + child: const Text('设置分组', style: TextStyle(fontSize: 14)), + ), + DialogOption( + onPressed: () async { + Get.back(); + final res = await VideoHttp.relationMod( + mid: mid, + act: 2, + reSrc: 11, + ); + if (res.isSuccess) { + SmartDialog.showToast('取消关注成功'); + afterMod?.call(0); + } else { + res.toast(); + } + }, + child: const Text('取消关注', style: TextStyle(fontSize: 14)), + ), + ], ), ); } @@ -317,6 +330,7 @@ abstract final class RequestUtils { clearCookie: true, ); final isSuccess = res.isSuccess; + final theme = ThemeUtils.theme; final actions = [ if (!isSuccess) TextButton( @@ -327,7 +341,7 @@ abstract final class RequestUtils { '/webview', parameters: { 'url': - 'https://www.bilibili.com/h5/comment/appeal?${Utils.themeUrl(Get.isDarkMode)}', + 'https://www.bilibili.com/h5/comment/appeal?${ThemeUtils.themeUrl(theme.isDark)}', }, ); }, @@ -338,7 +352,7 @@ abstract final class RequestUtils { onPressed: Get.back, child: Text( '关闭', - style: TextStyle(color: Get.theme.colorScheme.outline), + style: TextStyle(color: theme.colorScheme.outline), ), ), ]; @@ -544,118 +558,17 @@ abstract final class RequestUtils { } } - if (PlatformUtils.isDesktop) { - final json = await showDialog>( - context: Get.context!, - builder: (context) => GeetestWebviewDialog(gt!, challenge!), - ); - if (json != null) { - captchaData - ..validate = json['geetest_validate'] - ..seccode = json['geetest_seccode'] - ..geetest = GeetestData( - challenge: json['geetest_challenge'], - gt: gt!, - ); - gaiaVgateValidate(); - } - return; + final json = await GeetestWebviewDialog.geetest(gt!, challenge!); + if (json is Map) { + captchaData + ..validate = json['geetest_validate'] + ..seccode = json['geetest_seccode'] + ..geetest = GeetestData( + challenge: json['geetest_challenge'], + gt: gt, + ); + gaiaVgateValidate(); } - - final registerData = Gt3RegisterData( - challenge: challenge, - gt: gt, - success: true, - ); - - Gt3FlutterPlugin() - ..addEventHandler( - onClose: (Map message) { - SmartDialog.showToast('关闭验证'); - }, - onResult: (Map message) { - if (kDebugMode) debugPrint("Captcha result: $message"); - String code = message["code"]; - if (code == "1") { - // 发送 message["result"] 中的数据向 B 端的业务服务接口进行查询 - SmartDialog.showToast('验证成功'); - final result = message['result']; - captchaData - ..validate = result?['geetest_validate'] - ..seccode = result?['geetest_seccode'] - ..geetest = GeetestData( - challenge: result?['geetest_challenge'], - gt: gt!, - ); - gaiaVgateValidate(); - } else { - // 终端用户完成验证失败,自动重试 If the verification fails, it will be automatically retried. - if (kDebugMode) debugPrint("Captcha result code : $code"); - } - }, - onError: (Map message) { - SmartDialog.showToast("Captcha onError: $message"); - String code = message["code"]; - // 处理验证中返回的错误 Handling errors returned in verification - if (Platform.isAndroid) { - // Android 平台 - if (code == "-2") { - // Dart 调用异常 Call exception - } else if (code == "-1") { - // Gt3RegisterData 参数不合法 Parameter is invalid - } else if (code == "201") { - // 网络无法访问 Network inaccessible - } else if (code == "202") { - // Json 解析错误 Analysis error - } else if (code == "204") { - // WebView 加载超时,请检查是否混淆极验 SDK Load timed out - } else if (code == "204_1") { - // WebView 加载前端页面错误,请查看日志 Error loading front-end page, please check the log - } else if (code == "204_2") { - // WebView 加载 SSLError - } else if (code == "206") { - // gettype 接口错误或返回为 null API error or return null - } else if (code == "207") { - // getphp 接口错误或返回为 null API error or return null - } else if (code == "208") { - // ajax 接口错误或返回为 null API error or return null - } else { - // 更多错误码参考开发文档 More error codes refer to the development document - // https://docs.geetest.com/sensebot/apirefer/errorcode/android - } - } - - if (Platform.isIOS) { - // iOS 平台 - if (code == "-1009") { - // 网络无法访问 Network inaccessible - } else if (code == "-1004") { - // 无法查找到 HOST Unable to find HOST - } else if (code == "-1002") { - // 非法的 URL Illegal URL - } else if (code == "-1001") { - // 网络超时 Network timeout - } else if (code == "-999") { - // 请求被意外中断, 一般由用户进行取消操作导致 The interrupted request was usually caused by the user cancelling the operation - } else if (code == "-21") { - // 使用了重复的 challenge Duplicate challenges are used - // 检查获取 challenge 是否进行了缓存 Check if the fetch challenge is cached - } else if (code == "-20") { - // 尝试过多, 重新引导用户触发验证即可 Try too many times, lead the user to request verification again - } else if (code == "-10") { - // 预判断时被封禁, 不会再进行图形验证 Banned during pre-judgment, and no more image captcha verification - } else if (code == "-2") { - // Dart 调用异常 Call exception - } else if (code == "-1") { - // Gt3RegisterData 参数不合法 Parameter is invalid - } else { - // 更多错误码参考开发文档 More error codes refer to the development document - // https://docs.geetest.com/sensebot/apirefer/errorcode/ios - } - } - }, - ) - ..startCaptcha(registerData); } static Future showUserRealName(String mid) async { diff --git a/lib/utils/share_utils.dart b/lib/utils/share_utils.dart new file mode 100644 index 0000000000..c6884eb7ee --- /dev/null +++ b/lib/utils/share_utils.dart @@ -0,0 +1,41 @@ +import 'dart:io' show Platform; + +import 'package:PiliPlus/utils/device_utils.dart'; +import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:PiliPlus/utils/utils.dart'; +import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/rendering.dart' show Rect; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:share_plus/share_plus.dart'; + +abstract final class ShareUtils { + static bool? _isIpad; + static Future get isIpad async { + if (!Platform.isIOS) return false; + return _isIpad ??= (await DeviceInfoPlugin().iosInfo).model + .toLowerCase() + .contains('ipad'); + } + + static Future get sharePositionOrigin async { + if (await isIpad) { + final screenSize = DeviceUtils.size; + return Rect.fromLTRB(0, 0, screenSize.width, screenSize.height / 2); + } + return null; + } + + static Future shareText(String text) async { + if (PlatformUtils.isDesktop) { + Utils.copyText(text); + return; + } + try { + await SharePlus.instance.share( + ShareParams(text: text, sharePositionOrigin: await sharePositionOrigin), + ); + } catch (e) { + SmartDialog.showToast(e.toString()); + } + } +} diff --git a/lib/utils/storage_key.dart b/lib/utils/storage_key.dart index 227cba4606..752859b513 100644 --- a/lib/utils/storage_key.dart +++ b/lib/utils/storage_key.dart @@ -8,13 +8,13 @@ abstract final class SettingBoxKey { defaultAudioQaCellular = 'defaultAudioQaCellular', autoPlayEnable = 'autoPlayEnable', fullScreenMode = 'fullScreenMode', - defaultDecode = 'defaultDecode', - secondDecode = 'secondDecode', + preferCodecs = 'preferCodecs', defaultToastOp = 'defaultToastOp', defaultPicQa = 'defaultPicQa', enableHA = 'enableHA', audioOutput = 'audioOutput', - expandBuffer = 'expandBuffer', + bufferSize = 'bufferSize', + bufferSec = 'bufferSec', hardwareDecoding = 'hardwareDecoding', videoSync = 'videoSync', autosync = 'autosync', @@ -23,11 +23,14 @@ abstract final class SettingBoxKey { enableAutoExit = 'enableAutoExit', enableOnlineTotal = 'enableOnlineTotal', superChatType = 'superChatType', + fullScreenSCWidth = 'fullScreenSCWidth', keyboardControl = 'keyboardControl', pauseOnMinimize = 'pauseOnMinimize', pgcSkipType = 'pgcSkipType', audioPlayMode = 'audioPlayMode', - showBatteryLevel = 'showBatteryLevel'; + showBatteryLevel = 'showBatteryLevel', + playerVolume = 'playerVolume', + maxVolume = 'maxVolume'; static const String enableVerticalExpand = 'enableVerticalExpand', feedBackEnable = 'feedBackEnable', @@ -52,7 +55,6 @@ abstract final class SettingBoxKey { banWordForRecommend = 'banWordForRecommend', applyFilterToRelatedVideos = 'applyFilterToRelatedVideos', autoUpdate = 'autoUpdate', - autoClearCache = 'autoClearCache', maxCacheSize = 'maxCacheSize', defaultShowComment = 'defaultShowComment', replySortType = 'replySortType', @@ -150,7 +152,9 @@ abstract final class SettingBoxKey { showDynDispute = 'showDynDispute', touchSlopH = 'touchSlopH', floatingNavBar = 'floatingNavBar', - removeSafeArea = 'removeSafeArea'; + removeSafeArea = 'removeSafeArea', + angleDegrees = 'angleDegrees', + liveStream = 'liveStream'; static const String minimizeOnExit = 'minimizeOnExit', windowSize = 'windowSize', diff --git a/lib/utils/storage_pref.dart b/lib/utils/storage_pref.dart index 3328ef7d1d..14cdff96a8 100644 --- a/lib/utils/storage_pref.dart +++ b/lib/utils/storage_pref.dart @@ -1,6 +1,7 @@ import 'dart:io'; -import 'dart:math' show pow, sqrt; +import 'package:PiliPlus/common/widgets/gesture/horizontal_drag_gesture_recognizer.dart' + show deviceTouchSlop; import 'package:PiliPlus/common/widgets/pair.dart'; import 'package:PiliPlus/http/constants.dart'; import 'package:PiliPlus/models/common/bar_hide_type.dart'; @@ -25,12 +26,14 @@ import 'package:PiliPlus/models/common/video/video_decode_type.dart'; import 'package:PiliPlus/models/common/video/video_quality.dart'; import 'package:PiliPlus/models/user/danmaku_rule.dart'; import 'package:PiliPlus/models/user/info.dart'; +import 'package:PiliPlus/pages/setting/pages/fullscreen_sc_size.dart' + show kFullScreenSCWidth; import 'package:PiliPlus/plugin/pl_player/models/audio_output_type.dart'; import 'package:PiliPlus/plugin/pl_player/models/bottom_progress_behavior.dart'; import 'package:PiliPlus/plugin/pl_player/models/fullscreen_mode.dart'; import 'package:PiliPlus/plugin/pl_player/models/hwdec_type.dart'; import 'package:PiliPlus/plugin/pl_player/models/play_repeat.dart'; -import 'package:PiliPlus/utils/extension/context_ext.dart'; +import 'package:PiliPlus/utils/device_utils.dart'; import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/login_utils.dart'; @@ -102,8 +105,10 @@ abstract final class Pref { static List get springDescription => List.from( _setting.get(SettingBoxKey.springDescription) ?? - [0.5, 100.0, 2.2 * sqrt(50)], // [mass, stiffness, damping] + // duration: 0.3, bounce: 0.0 + const [1.0, 438.64908449286037, 41.88790204786391], ); + // [0.5, 100.0, 2.2 * math.sqrt(50)], // [mass, stiffness, damping] static List get speedList => List.from( _video.get( @@ -193,7 +198,9 @@ abstract final class Pref { static FullScreenMode get fullScreenMode { int? index = _setting.get(SettingBoxKey.fullScreenMode); if (index == null) { - final FullScreenMode mode = horizontalScreen && isTablet ? .none : .auto; + final FullScreenMode mode = horizontalScreen && DeviceUtils.isTablet + ? .none + : .auto; _setting.put(SettingBoxKey.fullScreenMode, mode.index); return mode; } @@ -238,20 +245,38 @@ abstract final class Pref { defaultValue: AudioQuality.k192.code, ); - static String get defaultDecode => _setting.get( - SettingBoxKey.defaultDecode, - defaultValue: VideoDecodeFormatType.AVC.codes.first, - ); + static List get preferCodecs { + // TODO: remove next 2 version + if (_setting.get('defaultDecode') case String codecStr) { + String? codecStr2 = _setting.get('secondDecode'); + _setting.deleteAll(const ['defaultDecode', 'secondDecode']); + final codecs = [ + VideoDecodeFormatType.values.firstWhere( + (i) => i.codes.contains(codecStr), + ), + if (codecStr2 != null && codecStr2 != codecStr) + VideoDecodeFormatType.values.firstWhere( + (i) => i.codes.contains(codecStr2), + ), + ]; + _setting.put( + SettingBoxKey.preferCodecs, + codecs.map((i) => i.name).toList(), + ); + return codecs; + } - static String get secondDecode => _setting.get( - SettingBoxKey.secondDecode, - defaultValue: VideoDecodeFormatType.AV1.codes.first, - ); + final codecs = _setting.get(SettingBoxKey.preferCodecs); + if (codecs is List && codecs.isNotEmpty) { + return codecs.map((i) => VideoDecodeFormatType.values.byName(i)).toList(); + } + return const []; + } static String get hardwareDecoding => _setting.get( SettingBoxKey.hardwareDecoding, defaultValue: Platform.isAndroid - ? HwDecType.autoSafe.hwdec + ? HwDecType.androidDefault : HwDecType.auto.hwdec, ); @@ -590,25 +615,19 @@ abstract final class Pref { _setting.get(SettingBoxKey.showPgcTimeline, defaultValue: true); static num get maxCacheSize => - _setting.get(SettingBoxKey.maxCacheSize) ?? pow(1024, 3); + _setting.get(SettingBoxKey.maxCacheSize) ?? 1 << 30; static bool get optTabletNav => _setting.get(SettingBoxKey.optTabletNav, defaultValue: true); - static bool get horizontalScreen => - _setting.get(SettingBoxKey.horizontalScreen) ?? isTablet; - - static bool get isTablet { - bool isTablet; - if (Get.context != null) { - isTablet = Get.context!.isTablet; - } else { - final view = WidgetsBinding.instance.platformDispatcher.views.first; - final screenSize = view.physicalSize / view.devicePixelRatio; - isTablet = screenSize.shortestSide >= 600; + static bool get horizontalScreen { + bool? horizontalScreen = _setting.get(SettingBoxKey.horizontalScreen); + if (horizontalScreen == null) { + final isTablet = DeviceUtils.isTablet; + _setting.put(SettingBoxKey.horizontalScreen, isTablet); + return isTablet; } - _setting.put(SettingBoxKey.horizontalScreen, isTablet); - return isTablet; + return horizontalScreen; } static String get banWordForDyn => @@ -715,9 +734,6 @@ abstract final class Pref { !Platform.isIOS && _setting.get(SettingBoxKey.dynamicColor, defaultValue: true); - static bool get autoClearCache => - _setting.get(SettingBoxKey.autoClearCache, defaultValue: false); - static bool get enableSystemProxy => _setting.get(SettingBoxKey.enableSystemProxy, defaultValue: false); @@ -797,8 +813,31 @@ abstract final class Pref { static bool get enableLongShowControl => _setting.get(SettingBoxKey.enableLongShowControl, defaultValue: false); - static bool get expandBuffer => - _setting.get(SettingBoxKey.expandBuffer, defaultValue: false); + static double get bufferSize => + _setting.get(SettingBoxKey.bufferSize, defaultValue: 4.0); + + static double get bufferSec => + _setting.get(SettingBoxKey.bufferSec, defaultValue: 16.0); + + static Map initBuffer([double playbackSpeed = 1.0]) { + final bufSec = Pref.bufferSec * playbackSpeed; + final bufSiz = (Pref.bufferSize * 0x100000).toStringAsFixed(0); + return { + 'cache': 'yes', + 'cache-secs': bufSec.toStringAsFixed(3), + 'demuxer-hysteresis-secs': (bufSec / 1.5).toStringAsFixed(3), + 'demuxer-max-bytes': bufSiz, + 'demuxer-max-back-bytes': bufSiz, + }; + } + + static Map initLiveBuffer() { + return { + 'cache': 'yes', + 'demuxer-max-bytes': (Pref.bufferSize * 0x200000).toStringAsFixed(0), + 'demuxer-max-back-bytes': '0', + }; + } static String get audioOutput => _setting.get( SettingBoxKey.audioOutput, @@ -896,6 +935,11 @@ abstract final class Pref { defaultValue: SuperChatType.valid.index, )]; + static double get fullScreenSCWidth => _setting.get( + SettingBoxKey.fullScreenSCWidth, + defaultValue: kFullScreenSCWidth, + ); + static bool get minimizeOnExit => _setting.get(SettingBoxKey.minimizeOnExit, defaultValue: true); @@ -964,8 +1008,10 @@ abstract final class Pref { static bool get showDynDispute => _setting.get(SettingBoxKey.showDynDispute, defaultValue: false); - static double get touchSlopH => - _setting.get(SettingBoxKey.touchSlopH, defaultValue: 24.0); + static double get touchSlopH => _setting.get( + SettingBoxKey.touchSlopH, + defaultValue: deviceTouchSlop + 6.0, + ); static bool get saveReply => _setting.get(SettingBoxKey.saveReply, defaultValue: true); @@ -975,4 +1021,15 @@ abstract final class Pref { static bool get removeSafeArea => _setting.get(SettingBoxKey.removeSafeArea, defaultValue: false); + + static int get angleDegrees => + _setting.get(SettingBoxKey.angleDegrees, defaultValue: 30); + + static double get playerVolume => // mobile + _setting.get(SettingBoxKey.playerVolume, defaultValue: 100.0); + + static double get maxVolume => // desktop + _setting.get(SettingBoxKey.maxVolume, defaultValue: 2.0); + + static List? get liveStream => _setting.get(SettingBoxKey.liveStream); } diff --git a/lib/utils/storage_utils.dart b/lib/utils/storage_utils.dart new file mode 100644 index 0000000000..3c5b6353ee --- /dev/null +++ b/lib/utils/storage_utils.dart @@ -0,0 +1,34 @@ +import 'dart:io' show File; +import 'dart:typed_data' show Uint8List; + +import 'package:PiliPlus/utils/platform_utils.dart'; +import 'package:file_picker/file_picker.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; + +abstract final class StorageUtils { + static Future saveBytes2File({ + required String name, + required Uint8List bytes, + required List allowedExtensions, + FileType type = FileType.custom, + }) async { + try { + final path = await FilePicker.saveFile( + allowedExtensions: allowedExtensions, + type: type, + fileName: name, + bytes: PlatformUtils.isDesktop ? Uint8List(0) : bytes, + ); + if (path == null) { + SmartDialog.showToast("取消保存"); + return; + } + if (PlatformUtils.isDesktop) { + await File(path).writeAsBytes(bytes); + } + SmartDialog.showToast("已保存"); + } catch (e) { + SmartDialog.showToast("保存失败: $e"); + } + } +} diff --git a/lib/utils/theme_utils.dart b/lib/utils/theme_utils.dart index f1c76b1be1..d37f75c35c 100644 --- a/lib/utils/theme_utils.dart +++ b/lib/utils/theme_utils.dart @@ -1,11 +1,31 @@ import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/main.dart'; import 'package:PiliPlus/utils/extension/theme_ext.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:flutter/cupertino.dart' show CupertinoThemeData; +import 'package:flutter/foundation.dart' show PlatformDispatcher; import 'package:flutter/material.dart'; abstract final class ThemeUtils { + static late ThemeData lightTheme; + + static late ThemeData darkTheme; + + static late ThemeMode themeMode; + + static ThemeData get theme { + if (themeMode == .dark || + (themeMode == .system && + PlatformDispatcher.instance.platformBrightness == .dark)) { + return darkTheme; + } + return lightTheme; + } + + static bool get isDarkMode => theme.isDark; + + static String themeUrl(bool isDark) => + 'native.theme=${isDark ? 2 : 1}&night=${isDark ? 1 : 0}'; + static ThemeData getThemeData({ required ColorScheme colorScheme, required bool isDynamic, @@ -134,9 +154,6 @@ abstract final class ThemeUtils { if (Pref.isPureBlackTheme) { themeData = darkenTheme(themeData); } - if (Pref.darkVideoPage) { - MyApp.darkThemeData = themeData; - } } return themeData; } diff --git a/lib/utils/update.dart b/lib/utils/update.dart index 71a6450f3d..d4cb26fc3b 100644 --- a/lib/utils/update.dart +++ b/lib/utils/update.dart @@ -45,7 +45,7 @@ abstract final class Update { SmartDialog.show( animationType: SmartAnimationType.centerFade_otherSlide, builder: (context) { - final ThemeData theme = Theme.of(context); + final colorScheme = ColorScheme.of(context); Widget downloadBtn(String text, {String? ext}) => TextButton( onPressed: () => onDownload(data, ext: ext), child: Text(text), @@ -70,9 +70,7 @@ abstract final class Update { ), child: Text( "点此查看完整更新(即commit)内容", - style: TextStyle( - color: theme.colorScheme.primary, - ), + style: TextStyle(color: colorScheme.primary), ), ), ], @@ -88,18 +86,14 @@ abstract final class Update { }, child: Text( '不再提醒', - style: TextStyle( - color: theme.colorScheme.outline, - ), + style: TextStyle(color: colorScheme.outline), ), ), TextButton( onPressed: SmartDialog.dismiss, child: Text( '取消', - style: TextStyle( - color: theme.colorScheme.outline, - ), + style: TextStyle(color: colorScheme.outline), ), ), if (Platform.isWindows) ...[ diff --git a/lib/utils/url_utils.dart b/lib/utils/url_utils.dart index a6f50d42d3..0ab631c664 100644 --- a/lib/utils/url_utils.dart +++ b/lib/utils/url_utils.dart @@ -4,8 +4,7 @@ import 'package:PiliPlus/utils/accounts/account.dart'; import 'package:PiliPlus/utils/id_utils.dart'; import 'package:PiliPlus/utils/page_utils.dart'; import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart' show kDebugMode; -import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart' show kDebugMode, debugPrint; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; abstract final class UrlUtils { diff --git a/lib/utils/utils.dart b/lib/utils/utils.dart index 6e15a5b4a2..c6fcf584e7 100644 --- a/lib/utils/utils.dart +++ b/lib/utils/utils.dart @@ -1,135 +1,15 @@ -import 'dart:async'; -import 'dart:convert'; -import 'dart:io'; +import 'dart:convert' show JsonEncoder, base64; import 'dart:math' show Random; -import 'package:PiliPlus/common/constants.dart'; -import 'package:PiliPlus/utils/platform_utils.dart'; import 'package:catcher_2/catcher_2.dart'; -import 'package:connectivity_plus/connectivity_plus.dart'; -import 'package:device_info_plus/device_info_plus.dart'; -import 'package:file_picker/file_picker.dart'; -import 'package:flutter/services.dart'; +import 'package:flutter/services.dart' show Clipboard, ClipboardData; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:get/get.dart'; -import 'package:share_plus/share_plus.dart'; abstract final class Utils { static final random = Random(); - static const channel = MethodChannel(Constants.appName); - static const jsonEncoder = JsonEncoder.withIndent(' '); - static String levelName( - Object level, { - bool isSeniorMember = false, - }) => 'assets/images/lv/lv${isSeniorMember ? '6_s' : level}.png'; - - static Color index2Color(int index, Color color) => switch (index) { - 0 => const Color(0xFFfdad13), - 1 => const Color(0xFF8aace1), - 2 => const Color(0xFFdfa777), - _ => color, - }; - - static bool getDimensionFromUri(String uri) { - try { - final params = Uri.parse(uri).queryParameters; - final width = int.parse(params['player_width']!); - final height = int.parse(params['player_height']!); - return params['player_rotate'] == '1' ? width > height : height > width; - } catch (_) { - return false; - } - } - - static String themeUrl(bool isDark) => - 'native.theme=${isDark ? 2 : 1}&night=${isDark ? 1 : 0}'; - - static Future saveBytes2File({ - required String name, - required Uint8List bytes, - required List allowedExtensions, - FileType type = FileType.custom, - }) async { - try { - final path = await FilePicker.saveFile( - allowedExtensions: allowedExtensions, - type: type, - fileName: name, - bytes: PlatformUtils.isDesktop ? null : bytes, - ); - if (path == null) { - SmartDialog.showToast("取消保存"); - return; - } - if (PlatformUtils.isDesktop) { - await File(path).writeAsBytes(bytes); - } - SmartDialog.showToast("已保存"); - } catch (e) { - SmartDialog.showToast("保存失败: $e"); - } - } - - static int? safeToInt(dynamic value) => switch (value) { - int e => e, - String e => int.tryParse(e), - num e => e.toInt(), - _ => null, - }; - - static Future get isWiFi async { - try { - return PlatformUtils.isMobile && - (await Connectivity().checkConnectivity()).contains( - ConnectivityResult.wifi, - ); - } catch (_) { - return true; - } - } - - static Color parseColor(String color) => - Color(int.parse('FF${color.substring(1)}', radix: 16)); - - static Color parseMedalColor(String color) => Color( - int.parse('${color.substring(7)}${color.substring(1, 7)}', radix: 16), - ); - - static late int sdkInt; - - static bool? _isIpad; - static Future get isIpad async { - if (!Platform.isIOS) return false; - return _isIpad ??= (await DeviceInfoPlugin().iosInfo).model - .toLowerCase() - .contains('ipad'); - } - - static Future get sharePositionOrigin async { - if (await isIpad) { - final size = Get.size; - return Rect.fromLTRB(0, 0, size.width, size.height / 2); - } - return null; - } - - static Future shareText(String text) async { - if (PlatformUtils.isDesktop) { - copyText(text); - return; - } - try { - await SharePlus.instance.share( - ShareParams(text: text, sharePositionOrigin: await sharePositionOrigin), - ); - } catch (e) { - SmartDialog.showToast(e.toString()); - } - } - static final numericRegex = RegExp(r'^[\d\.]+$'); static bool isStringNumeric(String str) { return numericRegex.hasMatch(str); diff --git a/lib/utils/video_utils.dart b/lib/utils/video_utils.dart index 08d000a80e..5f080810f7 100644 --- a/lib/utils/video_utils.dart +++ b/lib/utils/video_utils.dart @@ -1,7 +1,9 @@ import 'package:PiliPlus/models/common/video/cdn_type.dart'; +import 'package:PiliPlus/models/common/video/video_decode_type.dart'; import 'package:PiliPlus/models_new/live/live_room_play_info/codec.dart'; +import 'package:PiliPlus/utils/extension/iterable_ext.dart'; import 'package:PiliPlus/utils/storage_pref.dart'; -import 'package:flutter/foundation.dart'; +import 'package:flutter/foundation.dart' show kDebugMode, debugPrint; abstract final class VideoUtils { static CDNService cdnService = Pref.defaultCDNService; @@ -88,9 +90,32 @@ abstract final class VideoUtils { .toString(); } - static String getLiveCdnUrl(CodecItem e) { - return (liveCdnUrl ?? e.urlInfo!.first.host!) + - e.baseUrl! + - e.urlInfo!.first.extra!; + static String getLiveCdnUrl(CodecItem e, {int index = 0}) { + final urlInfo = e.urlInfo.getOrFirst(index); + return (liveCdnUrl ?? urlInfo.host) + e.baseUrl + urlInfo.extra; + } + + static VideoDecodeFormatType selectCodec( + Iterable codecs, + List preferCodecs, + ) { + if (preferCodecs.isNotEmpty) { + int bestIndex = preferCodecs.length; + for (final e in codecs) { + for (int i = 0; i < bestIndex; i++) { + if (preferCodecs[i].codes.any(e.startsWith)) { + bestIndex = i; + if (bestIndex == 0) { + return preferCodecs[0]; + } + break; + } + } + } + if (bestIndex < preferCodecs.length) { + return preferCodecs[bestIndex]; + } + } + return VideoDecodeFormatType.fromString(codecs.first); } } diff --git a/lib/utils/waterfall.dart b/lib/utils/waterfall.dart index 55fde4762a..c19fc159bc 100644 --- a/lib/utils/waterfall.dart +++ b/lib/utils/waterfall.dart @@ -1,9 +1,8 @@ import 'package:PiliPlus/common/skeleton/dynamic_card.dart'; import 'package:PiliPlus/common/style.dart'; -import 'package:PiliPlus/common/widgets/flutter/sliver_layout_builder.dart'; import 'package:PiliPlus/utils/global_data.dart'; import 'package:PiliPlus/utils/grid.dart'; -import 'package:flutter/material.dart' hide SliverLayoutBuilder; +import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart' show SliverConstraints; import 'package:waterfall_flow/waterfall_flow.dart' show SliverWaterfallFlowDelegate; diff --git a/pubspec.lock b/pubspec.lock index cd427db4ce..6180794977 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,18 +5,18 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "8d7ff3948166b8ec5da0fbb5962000926b8e02f2ed9b3e51d1738905fbd4c98d" + sha256: cd6add6f846f35fb79f3c315296703c1a24f3cfd7f4739d91a74961c1c7e9f1b url: "https://pub.dev" source: hosted - version: "93.0.0" + version: "100.0.0" analyzer: dependency: transitive description: name: analyzer - sha256: de7148ed2fcec579b19f122c1800933dfa028f6d9fd38a152b04b1516cec120b + sha256: "6ba98576948803398b69e3a444df24eacdbe12ed699c7014e120ea38552debbf" url: "https://pub.dev" source: hosted - version: "10.0.1" + version: "13.0.0" ansicolor: dependency: transitive description: @@ -29,10 +29,10 @@ packages: dependency: "direct main" description: name: app_links - sha256: "3462d9defc61565fde4944858b59bec5be2b9d5b05f20aed190adb3ad08a7abc" + sha256: "9d3c82f634c7f5b5c752f7ee46b67724246043f5e1d5fc1b433dd5b38d780dbe" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.2.0" app_links_linux: dependency: transitive description: @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: app_links_platform_interface - sha256: "05f5379577c513b534a29ddea68176a4d4802c46180ee8e2e966257158772a3f" + sha256: "78a18580eecac98108d1eef52a7db668bc317714f5205e616973363326efe333" url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" app_links_web: dependency: transitive description: @@ -92,10 +92,11 @@ packages: audio_service: dependency: "direct main" description: - name: audio_service - sha256: cb122c7c2639d2a992421ef96b67948ad88c5221da3365ccef1031393a76e044 - url: "https://pub.dev" - source: hosted + path: audio_service + ref: main + resolved-ref: "81cf56d5a8b8e9c8aa5a4c9396aebe094e09622f" + url: "https://github.com/bggRGjQaUbCoE/audio_service.git" + source: git version: "0.18.18" audio_service_platform_interface: dependency: transitive @@ -125,10 +126,10 @@ packages: dependency: "direct main" description: name: battery_plus - sha256: ad16fcb55b7384be6b4bbc763d5e2031ac7ea62b2d9b6b661490c7b9741155bf + sha256: f14567a9548ac57c010df641acbd35869429b3dc234fec0bb61b01f429285e61 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.1.0" battery_plus_platform_interface: dependency: transitive description: @@ -157,10 +158,10 @@ packages: dependency: transitive description: name: build - sha256: aadd943f4f8cc946882c954c187e6115a84c98c81ad1d9c6cbf0895a8c85da9c + sha256: a156715e7cd728130c592f30552575908aae5b100005fbc1f0fb16b3c03a3d10 url: "https://pub.dev" source: hosted - version: "4.0.5" + version: "4.0.6" build_config: dependency: transitive description: @@ -181,10 +182,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "22fdcc3cfeb9d974d7408718c4be15ec5e9b1b350088f3a6c88f154e74dd700d" + sha256: "1523ce62448ebac2c15a8ba5fbad8acac169788658a7dd2a1c2d9c2a9318b9a6" url: "https://pub.dev" source: hosted - version: "2.14.1" + version: "2.15.0" built_collection: dependency: transitive description: @@ -197,50 +198,45 @@ packages: dependency: transitive description: name: built_value - sha256: "0730c18c770d05636a8f945c32a4d7d81cb6e0f0148c8db4ad12e7748f7e49af" + sha256: "34e4067d30ce212937df995f03b69992eea683539ceeac7f679a1f1eba055b56" url: "https://pub.dev" source: hosted - version: "8.12.5" - cached_network_image: + version: "8.12.6" + cached_network_image_ce: dependency: "direct main" description: - name: cached_network_image - sha256: "7c1183e361e5c8b0a0f21a28401eecdbde252441106a9816400dd4c2b2424916" - url: "https://pub.dev" - source: hosted - version: "3.4.1" - cached_network_image_platform_interface: - dependency: transitive - description: - name: cached_network_image_platform_interface - sha256: "35814b016e37fbdc91f7ae18c8caf49ba5c88501813f73ce8a07027a395e2829" - url: "https://pub.dev" - source: hosted - version: "4.1.1" - cached_network_image_web: + path: cached_network_image + ref: develop + resolved-ref: "6ad91e9bf71254803e222b7d254751576f2f6d7b" + url: "https://github.com/My-Responsitories/flutter_cached_network_image_ce.git" + source: git + version: "4.6.4" + cached_network_image_platform_interface_ce: dependency: transitive description: - name: cached_network_image_web - sha256: "980842f4e8e2535b8dbd3d5ca0b1f0ba66bf61d14cc3a17a9b4788a3685ba062" - url: "https://pub.dev" - source: hosted - version: "1.3.1" + path: cached_network_image_platform_interface + ref: "6ad91e9bf71254803e222b7d254751576f2f6d7b" + resolved-ref: "6ad91e9bf71254803e222b7d254751576f2f6d7b" + url: "https://github.com/My-Responsitories/flutter_cached_network_image_ce.git" + source: git + version: "5.2.0" canvas_danmaku: dependency: "direct main" description: path: "." ref: main - resolved-ref: af10fe3cd4d1f7071988f4d9d13cc17616f4de30 + resolved-ref: "697d4516df2fc3ba7417c7ce9aba079d34ba13e5" url: "https://github.com/bggRGjQaUbCoE/canvas_danmaku.git" source: git version: "0.2.6" catcher_2: dependency: "direct main" description: - name: catcher_2 - sha256: ac9dd03230fa4058d14d46450335ca3c40564f734d793e52f81053f3cbf95009 - url: "https://pub.dev" - source: hosted + path: "." + ref: dev + resolved-ref: "9138d8d2aeefc564efd2f35fde3f54ad1b7e89a7" + url: "https://github.com/My-Responsitories/catcher_2.git" + source: git version: "2.1.9" characters: dependency: "direct main" @@ -254,11 +250,11 @@ packages: dependency: "direct main" description: path: "packages/chat_bottom_container" - ref: main - resolved-ref: dba2bf10db4a6f89564d9be63ce17b18f0f7e3e5 + ref: dev + resolved-ref: "227fe87aeedcd53573b17d6a6e5c08834fbc4e45" url: "https://github.com/bggRGjQaUbCoE/flutter_chat_packages.git" source: git - version: "0.3.2" + version: "0.5.0" checked_yaml: dependency: transitive description: @@ -267,6 +263,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.4" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" cli_util: dependency: transitive description: @@ -287,10 +291,10 @@ packages: dependency: transitive description: name: code_assets - sha256: "83ccdaa064c980b5596c35dd64a8d3ecc68620174ab9b90b6343b753aa721687" + sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8 url: "https://pub.dev" source: hosted - version: "1.0.0" + version: "1.2.1" collection: dependency: "direct main" description: @@ -303,10 +307,10 @@ packages: dependency: "direct main" description: name: connectivity_plus - sha256: "62ffa266d9a23b79fb3fcbc206afc00bb979417ba57b1324c546b5aab95ba057" + sha256: cad0e811a289ea2a941119dc483c204ec1684cbb9a8fc7351fe4a230b8313160 url: "https://pub.dev" source: hosted - version: "7.1.1" + version: "7.2.0" connectivity_plus_platform_interface: dependency: transitive description: @@ -355,30 +359,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.2" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd" - url: "https://pub.dev" - source: hosted - version: "1.0.9" dart_style: dependency: transitive description: name: dart_style - sha256: "29f7ecc274a86d32920b1d9cfc7502fa87220da41ec60b55f329559d5732e2b2" + sha256: "59d53ef8eaed9d288ed9767618e2b31c4fa0383a127db59d5eb2e737a7638a60" url: "https://pub.dev" source: hosted - version: "3.1.7" + version: "3.1.9" dbus: dependency: transitive description: name: dbus - sha256: d0c98dcd4f5169878b6cf8f6e0a52403a9dff371a3e2f019697accbf6f44a270 + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" url: "https://pub.dev" source: hosted - version: "0.7.12" + version: "0.7.14" desktop_webview_window: dependency: "direct main" description: @@ -392,18 +388,18 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: b4fed1b2835da9d670d7bed7db79ae2a94b0f5ad6312268158a9b5479abbacdd + sha256: "0891702f96b2e465fe567b7ec448380e6b1c14f60af552a8536d9f583b6b8442" url: "https://pub.dev" source: hosted - version: "12.4.0" + version: "13.2.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: e1ea89119e34903dca74b883d0dd78eb762814f97fb6c76f35e9ff74d261a18f + sha256: "04b173a92e2d9161dfead145667037c8d834db725ce2e7b942bfe18fd2f45a46" url: "https://pub.dev" source: hosted - version: "7.0.3" + version: "8.1.0" dio: dependency: "direct main" description: @@ -416,10 +412,10 @@ packages: dependency: "direct main" description: name: dio_http2_adapter - sha256: "79f3d69b155b92a786c8734bd11860390b986210d4e07cbb6a5c8c806a7187b2" + sha256: c56522c50bb3bab7e3ca77c153b0f03ae672a87d8c32e46be8f2868693ec9f0a url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.7.1" dio_web_adapter: dependency: transitive description: @@ -432,10 +428,10 @@ packages: dependency: "direct main" description: name: dlna_dart - sha256: "8a4f0e4f378615c99f2af679dc9f0c72fe4a0fb2f3eea96b637fe691dfcf0649" + sha256: da607931548720331b7503f72c426fa7fbd19e9144a4778d293c16a5bc186407 url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.1.1" dynamic_color: dependency: "direct main" description: @@ -489,7 +485,7 @@ packages: description: path: "." ref: mod - resolved-ref: e55d90cf2a1666e6ccf63e3f5779c8c4b12c5204 + resolved-ref: "161cd202c20bf0ba2394e3a86381d58864dc9e4e" url: "https://github.com/bggRGjQaUbCoE/extended_nested_scroll_view.git" source: git version: "6.2.1" @@ -509,6 +505,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.0" + ffi_leak_tracker: + dependency: transitive + description: + name: ffi_leak_tracker + sha256: "4093d4ef9ca06ffe2786e73bfb25e22aa92112b9bb4ec941f11e3e6b61489a97" + url: "https://pub.dev" + source: hosted + version: "0.1.2" file: dependency: transitive description: @@ -521,11 +525,11 @@ packages: dependency: "direct main" description: path: "." - ref: mod - resolved-ref: d1dde80df07a6ec46d9291cfeba6c01de53b5305 + ref: dev + resolved-ref: "02eb0aede6ca2278bea54eb5cc9ec520bf8165fc" url: "https://github.com/bggRGjQaUbCoE/flutter_file_picker.git" source: git - version: "11.0.2" + version: "12.0.0-beta.7" file_selector_linux: dependency: transitive description: @@ -582,28 +586,11 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.1" - floating: - dependency: "direct main" - description: - path: "." - ref: version-3 - resolved-ref: a66ec0b9dabde6b5880a3f4f3425e57842a38e51 - url: "https://github.com/bggRGjQaUbCoE/floating.git" - source: git - version: "3.0.0" flutter: dependency: "direct main" description: flutter source: sdk version: "0.0.0" - flutter_cache_manager: - dependency: "direct main" - description: - name: flutter_cache_manager - sha256: "400b6592f16a4409a7f2bb929a9a7e38c72cceb8ffb99ee57bbf2cb2cecf8386" - url: "https://pub.dev" - source: hosted - version: "3.4.1" flutter_displaymode: dependency: "direct main" description: @@ -629,12 +616,13 @@ packages: source: hosted version: "6.1.5" flutter_inappwebview_android: - dependency: transitive + dependency: "direct overridden" description: - name: flutter_inappwebview_android - sha256: "62557c15a5c2db5d195cb3892aab74fcaec266d7b86d59a6f0027abd672cddba" - url: "https://pub.dev" - source: hosted + path: flutter_inappwebview_android + ref: "v6.1.5" + resolved-ref: "0bfa46dfff87f0d9e9d5e13cbd5c4a7c7310f8c9" + url: "https://github.com/bggRGjQaUbCoE/flutter_inappwebview.git" + source: git version: "1.1.3" flutter_inappwebview_internal_annotations: dependency: transitive @@ -677,12 +665,13 @@ packages: source: hosted version: "1.1.2" flutter_inappwebview_windows: - dependency: transitive + dependency: "direct overridden" description: - name: flutter_inappwebview_windows - sha256: "8b4d3a46078a2cdc636c4a3d10d10f2a16882f6be607962dbfff8874d1642055" - url: "https://pub.dev" - source: hosted + path: flutter_inappwebview_windows + ref: "v6.1.5" + resolved-ref: "0bfa46dfff87f0d9e9d5e13cbd5c4a7c7310f8c9" + url: "https://github.com/bggRGjQaUbCoE/flutter_inappwebview.git" + source: git version: "0.6.0" flutter_launcher_icons: dependency: "direct dev" @@ -705,36 +694,28 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_mailer: - dependency: transitive - description: - name: flutter_mailer - sha256: "149e51d4e3ba12f8b61e1923ff4304f308acb856b92e9d09326bec6a1ad943d6" - url: "https://pub.dev" - source: hosted - version: "3.0.1" flutter_native_splash: dependency: "direct dev" description: name: flutter_native_splash - sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002" + sha256: "9db4b80b044e9af17cc4b1272137fc7ace0054d879ef8210a76adc34aaf4cdff" url: "https://pub.dev" source: hosted - version: "2.4.7" + version: "2.4.8" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "38d1c268de9097ff59cf0e844ac38759fc78f76836d37edad06fa21e182055a0" + sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785" url: "https://pub.dev" source: hosted - version: "2.0.34" + version: "2.0.35" flutter_smart_dialog: dependency: "direct main" description: path: "." ref: main - resolved-ref: b87bda5672e1c8494853bb44bbf08515ef748bca + resolved-ref: "1dbede0ee22f161ec2c928e1e82f0ae9339a097d" url: "https://github.com/bggRGjQaUbCoE/flutter_smart_dialog.git" source: git version: "5.1.0" @@ -751,10 +732,10 @@ packages: dependency: "direct main" description: name: flutter_svg - sha256: "1ded017b39c8e15c8948ea855070a5ff8ff8b3d5e83f3446e02d6bb12add7ad9" + sha256: "35882981abcbfb8c15b286f0cd690ff25bac12d95eff3e25ee207f37d4c42e7f" url: "https://pub.dev" source: hosted - version: "2.2.4" + version: "2.3.0" flutter_test: dependency: "direct dev" description: flutter @@ -764,37 +745,30 @@ packages: dependency: "direct main" description: name: flutter_volume_controller - sha256: "22edb0993ad03ecbc8d1164daeb5b39d798d409625db692675a86889403b1532" + sha256: "78297fd48ce6330d39a11a4c1ae219a3256249f99dec34cfbe9323d2cbb5ebb9" url: "https://pub.dev" source: hosted - version: "1.3.4" + version: "2.0.1" flutter_web_plugins: dependency: transitive description: flutter source: sdk version: "0.0.0" - fluttertoast: - dependency: transitive - description: - name: fluttertoast - sha256: "144ddd74d49c865eba47abe31cbc746c7b311c82d6c32e571fd73c4264b740e2" - url: "https://pub.dev" - source: hosted - version: "9.0.0" font_awesome_flutter: dependency: "direct main" description: - name: font_awesome_flutter - sha256: f50ce90dbe26d977415b9540400d6778bef00894aced6358ae578abd92b14b10 - url: "https://pub.dev" - source: hosted + path: "." + ref: "v10.9.0" + resolved-ref: cf4a19ece5cb296d73d90f0d73106f779c85fa26 + url: "https://github.com/bggRGjQaUbCoE/font_awesome_flutter.git" + source: git version: "10.9.0" get: dependency: "direct main" description: path: "." ref: "version_4.7.2" - resolved-ref: "81b8a71982f89b46fa868b315cd71ff6a6ddf895" + resolved-ref: "388fcb22ef24ac0a693949148d29fa6b4922159f" url: "https://github.com/bggRGjQaUbCoE/getx.git" source: git version: "4.7.2" @@ -814,22 +788,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" - gt3_flutter_plugin: - dependency: "direct main" - description: - name: gt3_flutter_plugin - sha256: "9cf4070f3f1811b7aa7a1daebdec5886dbda8e64f932a713dab21d8128d7dad2" - url: "https://pub.dev" - source: hosted - version: "0.1.1" gtk: dependency: transitive description: name: gtk - sha256: e8ce9ca4b1df106e4d72dad201d345ea1a036cc12c360f1a7d5a758f78ffa42c + sha256: "4ff85b2a16724029dd9e5bbb5a94b6918f9973f74ba571c949d2002801879cf5" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.0" hive_ce: dependency: "direct main" description: @@ -842,10 +808,10 @@ packages: dependency: transitive description: name: hooks - sha256: "025f060e86d2d4c3c47b56e33caf7f93bf9283340f26d23424ebcfccf34f621e" + sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "2.0.2" html: dependency: "direct main" description: @@ -922,18 +888,18 @@ packages: dependency: "direct main" description: name: image_picker - sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320" + sha256: "91c025426c2881c551100bce834e201c835a170151545f58d17da5180ca7d9ac" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "66810af8e99b2657ee98e5c6f02064f69bb63f7a70e343937f70946c5f8c6622" + sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a" url: "https://pub.dev" source: hosted - version: "0.8.13+16" + version: "0.8.13+19" image_picker_for_web: dependency: transitive description: @@ -1007,7 +973,7 @@ packages: source: hosted version: "0.6.1" jni: - dependency: transitive + dependency: "direct main" description: name: jni sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f @@ -1022,6 +988,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.1" + jnigen: + dependency: "direct dev" + description: + path: "pkgs/jnigen" + ref: HEAD + resolved-ref: "170d0550b6d906aa09bb10910774c43a0ddf46c0" + url: "https://github.com/dart-lang/native.git" + source: git + version: "0.17.0-wip" js: dependency: transitive description: @@ -1034,10 +1009,10 @@ packages: dependency: "direct main" description: name: json_annotation - sha256: cb09e7dac6210041fad964ed7fbee004f14258b4eca4040f72d1234062ace4c8 + sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80" url: "https://pub.dev" source: hosted - version: "4.11.0" + version: "4.12.0" leak_tracker: dependency: transitive description: @@ -1102,14 +1077,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" - mailer: - dependency: transitive - description: - name: mailer - sha256: "7b8691b080809ea1b2fa2f1b0d49c7c089fb328bd23e68aa5818b9cf5f4b420d" - url: "https://pub.dev" - source: hosted - version: "7.1.0" matcher: dependency: transitive description: @@ -1131,7 +1098,7 @@ packages: description: path: "." ref: const - resolved-ref: c11f18c031f1045900dc3e817f7519eb863c2550 + resolved-ref: "2bb68d1de3c7bb8c6117fce54d43da65a2387163" url: "https://github.com/bggRGjQaUbCoE/material_design_icons_flutter.git" source: git version: "7.0.7447" @@ -1148,7 +1115,7 @@ packages: description: path: media_kit ref: "version_1.2.5" - resolved-ref: "14c3ee41df4f51292b89eaf8f8c97420220fe136" + resolved-ref: deac6b62569584b6a5e28e6c60c187a0a7281b3a url: "https://github.com/My-Responsitories/media-kit.git" source: git version: "1.1.11" @@ -1157,7 +1124,7 @@ packages: description: path: "libs/android/media_kit_libs_android_video" ref: "version_1.2.5" - resolved-ref: "14c3ee41df4f51292b89eaf8f8c97420220fe136" + resolved-ref: deac6b62569584b6a5e28e6c60c187a0a7281b3a url: "https://github.com/My-Responsitories/media-kit.git" source: git version: "1.3.7" @@ -1165,9 +1132,9 @@ packages: dependency: "direct overridden" description: path: "libs/ios/media_kit_libs_ios_video" - ref: dev - resolved-ref: "547999bfb8b5cae9f9aca6125f46fd7cb500e994" - url: "https://github.com/bggRGjQaUbCoE/media-kit.git" + ref: "version_1.2.5" + resolved-ref: deac6b62569584b6a5e28e6c60c187a0a7281b3a + url: "https://github.com/My-Responsitories/media-kit.git" source: git version: "1.1.4" media_kit_libs_linux: @@ -1191,7 +1158,7 @@ packages: description: path: "libs/universal/media_kit_libs_video" ref: "version_1.2.5" - resolved-ref: "14c3ee41df4f51292b89eaf8f8c97420220fe136" + resolved-ref: deac6b62569584b6a5e28e6c60c187a0a7281b3a url: "https://github.com/My-Responsitories/media-kit.git" source: git version: "1.0.5" @@ -1200,7 +1167,7 @@ packages: description: path: "libs/windows/media_kit_libs_windows_video" ref: "version_1.2.5" - resolved-ref: "14c3ee41df4f51292b89eaf8f8c97420220fe136" + resolved-ref: deac6b62569584b6a5e28e6c60c187a0a7281b3a url: "https://github.com/My-Responsitories/media-kit.git" source: git version: "1.0.10" @@ -1209,7 +1176,7 @@ packages: description: path: media_kit_native_event_loop ref: "version_1.2.5" - resolved-ref: "14c3ee41df4f51292b89eaf8f8c97420220fe136" + resolved-ref: deac6b62569584b6a5e28e6c60c187a0a7281b3a url: "https://github.com/My-Responsitories/media-kit.git" source: git version: "1.0.9" @@ -1218,7 +1185,7 @@ packages: description: path: media_kit_video ref: "version_1.2.5" - resolved-ref: "14c3ee41df4f51292b89eaf8f8c97420220fe136" + resolved-ref: deac6b62569584b6a5e28e6c60c187a0a7281b3a url: "https://github.com/My-Responsitories/media-kit.git" source: git version: "1.2.5" @@ -1234,10 +1201,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: "direct main" description: @@ -1251,18 +1218,10 @@ packages: description: path: "." ref: master - resolved-ref: "731bb77e39cca77065c86ece38902d35f5d653d2" + resolved-ref: "7be186c79adca7d3ff5abc34bc75d171c68c2694" url: "https://github.com/bggRGjQaUbCoE/flutter_native_device_orientation.git" source: git version: "2.0.5" - native_toolchain_c: - dependency: transitive - description: - name: native_toolchain_c - sha256: "6ba77bb18063eebe9de401f5e6437e95e1438af0a87a3a39084fbd37c90df572" - url: "https://pub.dev" - source: hosted - version: "0.17.6" nm: dependency: transitive description: @@ -1275,10 +1234,10 @@ packages: dependency: transitive description: name: objective_c - sha256: "100a1c87616ab6ed41ec263b083c0ef3261ee6cd1dc3b0f35f8ddfa4f996fe52" + sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed" url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.4.1" octo_image: dependency: transitive description: @@ -1299,18 +1258,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "468c26b4254ab01979fa5e4a98cb343ea3631b9acee6f21028997419a80e1a20" + sha256: f5c435dc0e0d461e5b32471a870f769b6a1cc46930637efe24fbc535314e78ad url: "https://pub.dev" source: hosted - version: "9.0.1" + version: "10.2.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "202a487f08836a592a6bd4f901ac69b3a8f146af552bbd14407b6b41e1c3f086" + sha256: db762cb2f4f25ee60fb6359773861b0f199e00b90d237bd85a76a1e806b46ef4 url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "4.1.0" path: dependency: "direct main" description: @@ -1331,10 +1290,10 @@ packages: dependency: "direct main" description: name: path_provider - sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825 url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.6" path_provider_android: dependency: transitive description: @@ -1355,18 +1314,18 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.2.2" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.3" path_provider_windows: dependency: transitive description: @@ -1387,10 +1346,10 @@ packages: dependency: "direct main" description: name: permission_handler_apple - sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + sha256: "79dfa1df734798aa3cfdad166d3a3698c206d8813de13516ea1071b5d7e2f420" url: "https://pub.dev" source: hosted - version: "9.4.7" + version: "9.4.10" permission_handler_platform_interface: dependency: "direct main" description: @@ -1479,14 +1438,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.0" - punycoder: - dependency: transitive - description: - name: punycoder - sha256: aed79c05986a18782caa9bad649a4a786e840e1baaf6a2e1aa3a25d143d28e6e - url: "https://pub.dev" - source: hosted - version: "0.2.2" qr: dependency: transitive description: @@ -1512,7 +1463,7 @@ packages: source: hosted version: "0.6.0" rxdart: - dependency: "direct overridden" + dependency: transitive description: name: rxdart sha256: "5c3004a4a8dbb94bd4bf5412a4def4acdaa12e12f269737a5751369e12d1a962" @@ -1523,98 +1474,90 @@ packages: dependency: "direct main" description: name: saver_gallery - sha256: "3f983d4be63aff52523c3e097a9b00ce9ab8444f9a982c878cde9d0359f4681d" + sha256: dcecd87113ffcb0eb467bb0ef5ed8f6dd894e26ff3dfd5822624281e7d2cb894 url: "https://pub.dev" source: hosted - version: "4.1.1" + version: "5.1.0" screen_brightness_android: - dependency: "direct overridden" + dependency: transitive description: name: screen_brightness_android - sha256: d34f5321abd03bc3474f4c381f53d189117eba0b039eac1916aa92cca5fd0a96 + sha256: "2008ad8e9527cc968f7a4de1ec58b476d495b3c612a149dbd6550c4f046da147" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.6" screen_brightness_ios: - dependency: "direct overridden" + dependency: transitive description: name: screen_brightness_ios - sha256: "2493953340ecfe8f4f13f61db50ce72533a55b0bbd58ba1402893feecf3727f5" + sha256: "352d355e8523a186ba1e494b74218e5ca96e51975a0630b8ed89fbb7ff609762" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" screen_brightness_platform_interface: dependency: "direct main" description: name: screen_brightness_platform_interface - sha256: "737bd47b57746bc4291cab1b8a5843ee881af499514881b0247ec77447ee769c" + sha256: "2de60c0ba569b898950029cc1f7e9dd72bda44a22beb5054aac331cb6fce2ff2" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" screen_retriever: dependency: "direct main" description: name: screen_retriever - sha256: "570dbc8e4f70bac451e0efc9c9bb19fa2d6799a11e6ef04f946d7886d2e23d0c" + sha256: "42cc3b402a0f67d2455a0d067553d0f13453f6a008d98eababf8b63958d506bd" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.1" screen_retriever_linux: dependency: transitive description: name: screen_retriever_linux - sha256: f7f8120c92ef0784e58491ab664d01efda79a922b025ff286e29aa123ea3dd18 + sha256: "2a476f1a5538065bc5badf376cfdc83d6ecf07d77eb2391b9c2bff5a76970048" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.1" screen_retriever_macos: dependency: transitive description: name: screen_retriever_macos - sha256: "71f956e65c97315dd661d71f828708bd97b6d358e776f1a30d5aa7d22d78a149" + sha256: b5abb900fcb86614ff10b738b34e37b9e1d03b0447280668e2bc8a98bdc7bd59 url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.1" screen_retriever_platform_interface: dependency: transitive description: name: screen_retriever_platform_interface - sha256: ee197f4581ff0d5608587819af40490748e1e39e648d7680ecf95c05197240c0 + sha256: "3af22d926bedf20c2caa308eea376776451a3af125919ce072e56525fded8901" url: "https://pub.dev" source: hosted - version: "0.2.0" + version: "0.2.1" screen_retriever_windows: dependency: transitive description: name: screen_retriever_windows - sha256: "449ee257f03ca98a57288ee526a301a430a344a161f9202b4fcc38576716fe13" - url: "https://pub.dev" - source: hosted - version: "0.2.0" - sentry: - dependency: transitive - description: - name: sentry - sha256: "1f78300740739ff4b4920802687879231554350eab73eb229778f463aabda440" + sha256: c44b38a4c4bab34af259180a70a4eee1e29384e7b82e627c9faa68afcdab2e73 url: "https://pub.dev" source: hosted - version: "9.19.0" + version: "0.2.1" share_plus: dependency: "direct main" description: name: share_plus - sha256: "223873d106614442ea6f20db5a038685cc5b32a2fba81cdecaefbbae0523f7fa" + sha256: "9eee8283462d91a7a1c8bdb67d08874abd75a2f8fae3bc0ca033035e375fb3d8" url: "https://pub.dev" source: hosted - version: "12.0.2" + version: "13.2.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: "88023e53a13429bd65d8e85e11a9b484f49d4c190abbd96c7932b74d6927cc9a" + sha256: "7f7ae28cf400d13f811e297ff37742dba83b79e0a6f5dce14eec0248274e6ce9" url: "https://pub.dev" source: hosted - version: "6.1.0" + version: "7.1.0" shared_preferences: dependency: transitive description: @@ -1627,10 +1570,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 + sha256: "93ae5884a9df5d3bb696825bceb3a17590754548b5d740eba51500afc8d088f5" url: "https://pub.dev" source: hosted - version: "2.4.23" + version: "2.4.26" shared_preferences_foundation: dependency: transitive description: @@ -1708,46 +1651,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.2" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: "564cfed0746fe53140c23b70b308e045c3b31f17778f2f326ccb7d804ea0250a" - url: "https://pub.dev" - source: hosted - version: "2.4.2+1" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: "881e28efdcc9950fd8e9bb42713dcf1103e62a2e7168f23c9338d82db13dec40" - url: "https://pub.dev" - source: hosted - version: "2.4.2+3" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "5e8377564d95166761a968ed96104e0569b6b6cc611faac92a36ab8a169112c3" - url: "https://pub.dev" - source: hosted - version: "2.5.6+1" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - 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: transitive description: @@ -1793,10 +1696,10 @@ packages: dependency: "direct main" description: name: synchronized - sha256: "63896c27e81b28f8cb4e69ead0d3e8f03f1d1e5fc531a3e579cabed6a2c7c9e5" + sha256: "93b153dcb6a26dcddee6ca087dd634b53e38c10b5aa163e8e49501a776456153" url: "https://pub.dev" source: hosted - version: "3.4.0+1" + version: "3.4.1" term_glyph: dependency: transitive description: @@ -1809,18 +1712,18 @@ packages: dependency: transitive description: name: test_api - sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.10" + version: "0.7.11" tray_manager: dependency: "direct main" description: name: tray_manager - sha256: c5fd83b0ae4d80be6eaedfad87aaefab8787b333b8ebd064b0e442a81006035b + sha256: "1a659b08baa6e9b91ef8ce16eda37740de398be1c4cf322b8a1ddfef25c68c5a" url: "https://pub.dev" source: hosted - version: "0.5.2" + version: "0.5.3" typed_data: dependency: transitive description: @@ -1837,14 +1740,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.1" - unorm_dart: - dependency: transitive - description: - name: unorm_dart - sha256: "0c69186b03ca6addab0774bcc0f4f17b88d4ce78d9d4d8f0619e30a99ead58e7" - url: "https://pub.dev" - source: hosted - version: "0.3.2" upower: dependency: transitive description: @@ -1865,10 +1760,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "3bb000251e55d4a209aa0e2e563309dc9bb2befea2295fd0cec1f51760aac572" + sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 url: "https://pub.dev" source: hosted - version: "6.3.29" + version: "6.3.32" url_launcher_ios: dependency: transitive description: @@ -1905,10 +1800,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: d0412fcf4c6b31ecfdb7762359b7206ffba3bbffd396c6d9f9c4616ece476c1f + sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.4.3" url_launcher_windows: dependency: transitive description: @@ -1929,10 +1824,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "81da85e9ca8885ade47f9685b953cb098970d11be4821ac765580a6607ea4373" + sha256: "2306c03da2ba81724afeb589c351ebbc0aa7d86005925be8f8735856dbe5e42d" url: "https://pub.dev" source: hosted - version: "1.1.21" + version: "1.2.2" vector_graphics_codec: dependency: transitive description: @@ -1945,10 +1840,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: "5a88dd14c0954a5398af544651c7fb51b457a2a556949bfb25369b210ef73a74" + sha256: "142a9146f447d15b10bdc00e21d5f4d83e5b32bb5f8f8f5a04c75311344923a3" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.2.6" vector_math: dependency: "direct main" description: @@ -1969,26 +1864,26 @@ packages: dependency: transitive description: name: vm_service - sha256: "046d3928e16fa4dc46e8350415661755ab759d9fc97fc21b5ab295f71e4f0499" + sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360" url: "https://pub.dev" source: hosted - version: "15.1.0" + version: "15.2.0" wakelock_plus: dependency: "direct main" description: name: wakelock_plus - sha256: ddf3db70eaa10c37558ff817519b85d527dbd21034fd5d8e1c2e85f31588f1c1 + sha256: "824c5bba0f800e86d32e57d3d1843c531f090005cc89d9a837933e6601093d53" url: "https://pub.dev" source: hosted - version: "1.5.2" + version: "1.6.1" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "14b2e5b9e35c2631e656913c47adecdd71633ae92896a27a64c8f1fcfabc21cc" + sha256: b13f99e992e7ae6a152e16c5559d3c07ff445b13330192662494e614ca3e7d7b url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.5.1" watcher: dependency: transitive description: @@ -2039,21 +1934,21 @@ packages: source: git version: "1.2.2" win32: - dependency: transitive + dependency: "direct main" description: name: win32 - sha256: d7cb55e04cd34096cd3a79b3330245f54cb96a370a1c27adb3c84b917de8b08e + sha256: ba6f4bba816c8d7e3c1580e170f3786d216951cc6b94babc3b814c08d2cb2738 url: "https://pub.dev" source: hosted - version: "5.15.0" + version: "6.3.0" win32_registry: dependency: transitive description: name: win32_registry - sha256: "6f1b564492d0147b330dd794fee8f512cec4977957f310f9951b5f9d83618dae" + sha256: "73b1d78920a9d6e03f8b4e43e612b87bf3152a0e5c5e5150267762b7c4116904" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "3.0.3" window_manager: dependency: "direct main" description: @@ -2088,5 +1983,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.11.0 <4.0.0" - flutter: "3.41.6" + dart: ">=3.12.0 <4.0.0" + flutter: "3.44.4" diff --git a/pubspec.yaml b/pubspec.yaml index f766e072d9..d00e142564 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -17,11 +17,11 @@ publish_to: "none" # Remove this line if you wish to publish to pub.dev # In Windows, build-name is used as the major, minor, and patch parts # of the product and file versions while build-number is used as the build suffix. # update when release -version: 2.0.5+1 +version: 2.0.9+1 environment: - sdk: ">=3.10.0" - flutter: 3.41.6 # update `.fvmrc` config + sdk: ">=3.12.0" + flutter: 3.44.4 # Dependencies specify other packages that your package needs in order to work. # To automatically upgrade your package dependencies to the latest versions @@ -34,282 +34,208 @@ dependencies: sdk: flutter flutter_localizations: sdk: flutter - # The following adds the Cupertino Icons font to your application. - # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.8 - # 动态取色 - dynamic_color: ^1.8.1 - # get: ^4.7.2 - get: + app_links: ^7.1.1 + archive: ^4.0.0 + audio_service: git: - url: https://github.com/bggRGjQaUbCoE/getx.git - ref: version_4.7.2 - - # 网络 - dio: ^5.9.1 + url: https://github.com/bggRGjQaUbCoE/audio_service.git + path: audio_service + ref: main + audio_session: ^0.2.2 + battery_plus: ^7.0.0 + brotli: ^0.6.0 + cached_network_image_ce: + git: + url: https://github.com/My-Responsitories/flutter_cached_network_image_ce.git + path: cached_network_image + ref: develop + canvas_danmaku: + git: + url: https://github.com/bggRGjQaUbCoE/canvas_danmaku.git + ref: main + catcher_2: + git: + url: https://github.com/My-Responsitories/catcher_2.git + ref: dev + characters: ^1.4.1 + chat_bottom_container: + git: + url: https://github.com/bggRGjQaUbCoE/flutter_chat_packages.git + path: packages/chat_bottom_container + ref: dev + collection: ^1.19.1 + connectivity_plus: ^7.1.1 cookie_jar: ^4.0.8 - connectivity_plus: ^7.0.0 - dio_http2_adapter: ^2.5.3 - - # 图片 - cached_network_image: ^3.4.1 - # extended_image: ^9.0.7 - saver_gallery: ^4.1.0 - - # QRCode - # qr_flutter: ^4.1.0 - pretty_qr_code: ^3.3.0 - - # 存储 - path_provider: ^2.1.5 - hive_ce: ^2.19.3 - - # 设备信息 - device_info_plus: ^12.1.0 - # 权限 - # permission_handler: ^12.0.0+1 - permission_handler_apple: ^9.4.7 - permission_handler_android: ^13.0.1 - permission_handler_platform_interface: ^4.3.0 - # 分享 - share_plus: ^12.0.0 - # cookie 管理 - # webview_cookie_manager: ^2.0.6 - # 浏览器 - # webview_flutter: ^4.10.0 - flutter_inappwebview: ^6.1.5 + crypto: ^3.0.6 desktop_webview_window: git: url: https://github.com/Predidit/linux_webview_window ref: main - # 解决sliver滑动不同步 - # extended_nested_scroll_view: ^6.2.1 + device_info_plus: ^13.1.0 + dio: ^5.9.1 + dio_http2_adapter: ^2.7.1 + dlna_dart: ^0.1.0 + dynamic_color: ^1.8.1 + easy_debounce: ^2.0.3 + encrypt: ^5.0.3 + expandable: ^5.0.1 extended_nested_scroll_view: git: url: https://github.com/bggRGjQaUbCoE/extended_nested_scroll_view.git ref: mod - # 上拉加载 - # loading_more_list: ^7.1.0 - # 下拉刷新 - # pull_to_refresh_notification: ^3.1.0 - # 图标 - font_awesome_flutter: 10.9.0 - # material_design_icons_flutter: ^7.0.7296 - material_design_icons_flutter: + file_picker: git: - url: https://github.com/bggRGjQaUbCoE/material_design_icons_flutter.git - ref: const - # toast + url: https://github.com/bggRGjQaUbCoE/flutter_file_picker.git + ref: dev + fixnum: ^1.1.1 + fl_chart: ^1.0.0 + flex_seed_scheme: ^4.0.1 + flutter_displaymode: ^0.7.0 + flutter_html: ^3.0.0-beta.2 + flutter_inappwebview: ^6.1.5 flutter_smart_dialog: git: url: https://github.com/bggRGjQaUbCoE/flutter_smart_dialog.git ref: main - # 下滑关闭 - # dismissible_page: ^1.0.2 - # custom_sliding_segmented_control: ^1.8.4 - # 加密 - crypto: ^3.0.6 - encrypt: ^5.0.3 - - # 视频播放器 - media_kit: 1.1.11 # Primary package. - # media_kit_video: ^1.2.5 # For video rendering. - media_kit_video: + flutter_sortable_wrap: git: - url: https://github.com/bggRGjQaUbCoE/media-kit.git - path: media_kit_video - ref: version_1.2.5 + url: https://github.com/bggRGjQaUbCoE/flutter_sortable_wrap.git + ref: master + flutter_svg: ^2.0.14 + flutter_volume_controller: ^2.0.0 + font_awesome_flutter: + git: + url: https://github.com/bggRGjQaUbCoE/font_awesome_flutter.git + ref: v10.9.0 + get: + git: + url: https://github.com/bggRGjQaUbCoE/getx.git + ref: version_4.7.2 + hive_ce: ^2.19.3 + html: ^0.15.4 + http2: ^2.3.1 + image_cropper: ^12.0.0 + image_picker: ^1.1.2 + intl: ^0.20.2 + jni: ^1.0.0 + json_annotation: ^4.11.0 + live_photo_maker: ^0.0.6 + logger: ^2.5.0 + material_color_utilities: ^0.13.0 + material_design_icons_flutter: + git: + url: https://github.com/bggRGjQaUbCoE/material_design_icons_flutter.git + ref: const + material_new_shapes: ^1.0.0 + media_kit: 1.1.11 media_kit_libs_video: 1.0.5 - - # 媒体通知 - audio_service: ^0.18.15 - audio_session: ^0.2.2 - - # 音量、亮度、屏幕控制 - flutter_volume_controller: ^1.3.3 - wakelock_plus: ^1.2.8 - # universal_platform: ^1.1.0 - # auto_orientation: - # git: - # url: https://github.com/bggRGjQaUbCoE/auto_orientation.git - # ref: master + media_kit_video: 1.2.5 + mime: ^2.0.0 native_device_orientation: git: url: https://github.com/bggRGjQaUbCoE/flutter_native_device_orientation.git ref: master + package_info_plus: ^10.1.0 + path: ^1.9.1 + path_provider: ^2.1.5 + permission_handler_android: ^13.0.1 + permission_handler_apple: ^9.4.7 + permission_handler_platform_interface: ^4.3.0 + pretty_qr_code: ^3.3.0 protobuf: ^6.0.0 - # animations: ^2.0.11 - - # 获取appx信息 - package_info_plus: ^9.0.0 - url_launcher: ^6.3.1 - # 防抖节流 - easy_debounce: ^2.0.3 - # 高帧率 - flutter_displaymode: ^0.7.0 - # scheme跳转 - app_links: ^7.0.0 - # 弹幕 - # ns_danmaku: - # git: - # url: https://github.com/bggRGjQaUbCoE/flutter_ns_danmaku.git - # ref: master - canvas_danmaku: - git: - url: https://github.com/bggRGjQaUbCoE/canvas_danmaku.git - ref: main - # 状态栏图标控制 - # status_bar_control: ^3.2.1 - # 代理 - # system_proxy: ^0.1.0 - # pip - # floating: ^3.0.0 - floating: + re_highlight: ^0.0.3 + saver_gallery: ^5.0.2 + screen_brightness_platform_interface: ^2.1.0 + screen_retriever: ^0.2.0 + share_plus: ^13.1.0 + stream_transform: ^2.1.1 + super_sliver_list: git: - url: https://github.com/bggRGjQaUbCoE/floating.git - ref: version-3 - # html解析 - html: ^0.15.4 - # html渲染 - flutter_html: ^3.0.0-beta.2 - # 极验 - gt3_flutter_plugin: ^0.1.0 + url: https://github.com/bggRGjQaUbCoE/super_sliver_list.git + ref: mod + synchronized: ^3.3.0 + tray_manager: ^0.5.1 + url_launcher: ^6.3.1 uuid: ^4.5.1 - # scrollable_positioned_list: ^0.3.8 - # nil: ^1.1.1 - catcher_2: ^2.1.0 - logger: ^2.5.0 - #瀑布流 + vector_math: ^2.2.0 + wakelock_plus: ^1.2.8 waterfall_flow: ^3.1.0 - #跑马灯 - # marquee: ^2.3.0 - #富文本 - # extended_text: ^14.1.0 - # chat_bottom_container: ^0.2.0 - chat_bottom_container: - git: - url: https://github.com/bggRGjQaUbCoE/flutter_chat_packages.git - ref: main - path: packages/chat_bottom_container - image_picker: ^1.1.2 - intl: ^0.20.2 - archive: ^4.0.0 - flutter_svg: ^2.0.14 - image_cropper: ^12.0.0 - #解压直播消息 - brotli: ^0.6.0 - expandable: ^5.0.1 - flex_seed_scheme: ^4.0.1 - live_photo_maker: ^0.0.6 - fl_chart: ^1.0.0 - synchronized: ^3.3.0 - # document_file_save_plus: ^2.0.0 - # webdav_client: ^1.2.2 + web_socket_channel: ^3.0.3 webdav_client: git: url: https://github.com/wgh136/webdav_client.git ref: main - re_highlight: ^0.0.3 - flutter_sortable_wrap: - git: - url: https://github.com/bggRGjQaUbCoE/flutter_sortable_wrap.git - ref: master - web_socket_channel: ^3.0.3 - # image: ^4.7.1 - # window_manager: ^0.5.1 + win32: ^6.3.0 window_manager: git: url: https://github.com/bggRGjQaUbCoE/window_manager.git path: packages/window_manager ref: main - tray_manager: ^0.5.1 - # file_picker: ^10.3.3 - file_picker: - git: - url: https://github.com/bggRGjQaUbCoE/flutter_file_picker.git - ref: mod - super_sliver_list: - git: - url: https://github.com/bggRGjQaUbCoE/super_sliver_list.git - ref: mod - dlna_dart: ^0.1.0 - battery_plus: ^7.0.0 - material_new_shapes: ^1.0.0 - - vector_math: any - fixnum: any - json_annotation: any - stream_transform: any - screen_brightness_platform_interface: any - mime: any - path: any - collection: any - material_color_utilities: any - flutter_cache_manager: any - http2: any - screen_retriever: any - characters: any dependency_overrides: - # screen_brightness: ^2.1. - screen_brightness_ios: ^2.1.2 - screen_brightness_android: ^2.1.3 - screen_brightness_platform_interface: ^2.1.0 - path: ^1.9.1 - mime: ^2.0.0 - rxdart: ^0.28.0 + flutter_inappwebview_android: + git: + url: https://github.com/bggRGjQaUbCoE/flutter_inappwebview.git + path: flutter_inappwebview_android + ref: v6.1.5 + flutter_inappwebview_windows: + git: + url: https://github.com/bggRGjQaUbCoE/flutter_inappwebview.git + path: flutter_inappwebview_windows + ref: v6.1.5 media_kit: git: url: https://github.com/My-Responsitories/media-kit.git path: media_kit ref: version_1.2.5 - media_kit_video: + media_kit_libs_android_video: git: url: https://github.com/My-Responsitories/media-kit.git - path: media_kit_video + path: libs/android/media_kit_libs_android_video + ref: version_1.2.5 + media_kit_libs_ios_video: + git: + url: https://github.com/My-Responsitories/media-kit.git + path: libs/ios/media_kit_libs_ios_video ref: version_1.2.5 media_kit_libs_video: git: url: https://github.com/My-Responsitories/media-kit.git path: libs/universal/media_kit_libs_video ref: version_1.2.5 - media_kit_native_event_loop: + media_kit_libs_windows_video: git: url: https://github.com/My-Responsitories/media-kit.git - path: media_kit_native_event_loop + path: libs/windows/media_kit_libs_windows_video ref: version_1.2.5 - media_kit_libs_android_video: + media_kit_native_event_loop: git: url: https://github.com/My-Responsitories/media-kit.git - path: libs/android/media_kit_libs_android_video + path: media_kit_native_event_loop ref: version_1.2.5 - media_kit_libs_windows_video: + media_kit_video: git: url: https://github.com/My-Responsitories/media-kit.git - path: libs/windows/media_kit_libs_windows_video + path: media_kit_video ref: version_1.2.5 - media_kit_libs_ios_video: + cached_network_image_ce: git: - url: https://github.com/bggRGjQaUbCoE/media-kit.git - path: libs/ios/media_kit_libs_ios_video - ref: dev - font_awesome_flutter: 10.9.0 + url: https://github.com/My-Responsitories/flutter_cached_network_image_ce.git + path: cached_network_image + ref: develop dev_dependencies: flutter_test: sdk: flutter - - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^6.0.0 - flutter_launcher_icons: ^0.14.4 - # hive_generator: ^2.0.1 build_runner: ^2.10.3 + flutter_launcher_icons: ^0.14.4 + flutter_lints: ^6.0.0 flutter_native_splash: ^2.4.6 + jnigen: + git: + url: https://github.com/dart-lang/native.git + path: pkgs/jnigen flutter_launcher_icons: android: true @@ -334,40 +260,20 @@ flutter_native_splash: image: assets/images/logo/logo_2.png image_dark: assets/images/logo/logo_2.png -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - # The following line ensures that the Material Icons font is - # included with your application, so that you can use the icons in - # the material Icons class. uses-material-design: true - # To add assets to your application, add an assets section, like this: assets: - path: assets/images/ - - path: assets/images/lv/ - path: assets/images/logo/ - path: assets/images/logo/ico/ platforms: [windows] - path: assets/images/logo/desktop/ platforms: [linux, macos] - path: assets/images/live/ - - path: assets/images/video/ - path: assets/images/paycoins/ - path: assets/shaders/ - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - # For details regarding adding assets from package dependencies, see - # https://flutter.dev/assets-and-images/#from-packages - - # To add custom fonts to your application, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: fonts: - family: digital_id_num fonts: @@ -375,12 +281,3 @@ flutter: - family: custom_icon fonts: - asset: assets/fonts/custom_icon.ttf - # - family: Jura-Bold - # fonts: - # - asset: assets/fonts/Jura-Bold.ttf - # - family: HarmonyOS - # fonts: - # - asset: assets/fonts/HarmonyOS_Sans_SC_Regular.ttf - - # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages diff --git a/tool/README.md b/tool/README.md new file mode 100644 index 0000000000..c6fb71ecb7 --- /dev/null +++ b/tool/README.md @@ -0,0 +1 @@ +run `dart run tool/jnigen.dart` \ No newline at end of file diff --git a/tool/jnigen.dart b/tool/jnigen.dart new file mode 100644 index 0000000000..3d41a69253 --- /dev/null +++ b/tool/jnigen.dart @@ -0,0 +1,23 @@ +import 'dart:io' show Platform; + +import 'package:jnigen/jnigen.dart'; + +void main(List args) { + final packageRoot = Platform.script.resolve('../'); + generateJniBindings( + Config( + outputConfig: OutputConfig( + dartConfig: DartCodeOutputConfig( + path: packageRoot.resolve('lib/utils/android/bindings.g.dart'), + structure: .singleFile, + ), + ), + androidSdkConfig: AndroidSdkConfig(addGradleDeps: true), + sourcePath: [packageRoot.resolve('android/app/src/main/java')], + classes: [ + 'com.example.piliplus.AndroidHelper', + 'java.lang.Runnable', + ], + ), + ); +} diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index 860ccf8303..7630619904 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -4,9 +4,6 @@ #include "flutter/generated_plugin_registrant.h" -#include -#include - FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} @@ -29,24 +26,6 @@ bool FlutterWindow::OnCreate() { } RegisterPlugins(flutter_controller_->engine()); - // flutter_inappwebview - // 6.2.0-beta.2+ https://github.com/pichillilorenzo/flutter_inappwebview/issues/2482 - // 6.1.5 https://github.com/pichillilorenzo/flutter_inappwebview/issues/2512#issuecomment-3031039587 - flutter::MethodChannel<> channel( - flutter_controller_->engine()->messenger(), "window_control", - &flutter::StandardMethodCodec::GetInstance()); - channel.SetMethodCallHandler( - [](const flutter::MethodCall<>& call, - std::unique_ptr> result) { - if (call.method_name().compare("closeWindow") == 0) { - HANDLE hProcess = GetCurrentProcess(); - TerminateProcess(hProcess, 0); - result->Success(); - } else { - result->NotImplemented(); - } - }); - SetChildContent(flutter_controller_->view()->GetNativeWindow()); // flutter_controller_->engine()->SetNextFrameCallback([&]() {