fix(perf): memory optimization, prevent OOM on mobile, and completely disable mouse wheel zoom
This commit is contained in:
@@ -4,32 +4,33 @@
|
|||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="Prisma"
|
android:label="Prisma"
|
||||||
android:name="${applicationName}"
|
android:name="${applicationName}"
|
||||||
android:icon="@mipmap/ic_launcher">
|
android:icon="@mipmap/ic_launcher"
|
||||||
<activity
|
android:largeHeap="true">
|
||||||
android:name=".MainActivity"
|
<activity
|
||||||
android:exported="true"
|
android:name=".MainActivity"
|
||||||
android:launchMode="singleTop"
|
android:exported="true"
|
||||||
android:taskAffinity=""
|
android:launchMode="singleTop"
|
||||||
android:theme="@style/LaunchTheme"
|
android:taskAffinity=""
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:theme="@style/LaunchTheme"
|
||||||
android:hardwareAccelerated="true"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:hardwareAccelerated="true"
|
||||||
<!-- Specifies an Android theme to apply to this Activity as soon as
|
android:windowSoftInputMode="adjustResize">
|
||||||
the Android process has started. This theme is visible to the user
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
while the Flutter UI initializes. After that, this theme continues
|
the Android process has started. This theme is visible to the user
|
||||||
to determine the Window background behind the Flutter UI. -->
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
<meta-data
|
to determine the Window background behind the Flutter UI. -->
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
<meta-data
|
||||||
android:resource="@style/NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
/>
|
android:resource="@style/NormalTheme"
|
||||||
<intent-filter>
|
/>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<intent-filter>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
</intent-filter>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</activity>
|
</intent-filter>
|
||||||
<!-- Don't delete the meta-data below.
|
</activity>
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
<!-- Don't delete the meta-data below.
|
||||||
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
@@ -43,15 +44,15 @@
|
|||||||
android:resource="@xml/file_paths" />
|
android:resource="@xml/file_paths" />
|
||||||
</provider>
|
</provider>
|
||||||
</application>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain"/>
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@@ -1,4 +1,15 @@
|
|||||||
const prismaChangelog = [
|
const prismaChangelog = [
|
||||||
|
PrismaChange(
|
||||||
|
version: '3.6.2',
|
||||||
|
title: 'Комплексная оптимизация памяти на телефоне и ПК, полное отключение зума колесиком',
|
||||||
|
bullets: [
|
||||||
|
'Полное отключение масштабирования колесиком мыши: перехвачен внутренний обработчик PointerScrollEvent в интерактивном вьюере, что навсегда исключило нежелательный зум при прокрутке.',
|
||||||
|
'Предотвращение вылетов на смартфонах (OOM): включен флаг android:largeHeap="true", расширяющий доступный лимит оперативной памяти процесса до 512–1024 МБ.',
|
||||||
|
'Ограничение аппетита кэша изображений: глобальный ImageCache в Flutter ограничен безопасным лимитом в 180 МБ и 120 изображений (вместо неограниченного роста до 1 ГБ+), неактивные растры своевременно выгружаются сборщиком мусора.',
|
||||||
|
'Оптимизация декодирования тяжелых артов: полноразмерные изображения с экстремальным разрешением (до 10K+) теперь безопасно декодируются с ограничением memCacheWidth под экран устройства, снижая расход RAM на 80–90%.',
|
||||||
|
'Оптимизация сетки и миниатюр: карточки ленты изолированы в RepaintBoundary для гладкого 120 FPS скролла без паразитных перерисовок, а всем каруселям, аватаркам и мозаикам заданы строгие границы кэширования.',
|
||||||
|
],
|
||||||
|
),
|
||||||
PrismaChange(
|
PrismaChange(
|
||||||
version: '3.6.1',
|
version: '3.6.1',
|
||||||
title: 'Плавная боковая панель без лагов и отключение зума колесиком',
|
title: 'Плавная боковая панель без лагов и отключение зума колесиком',
|
||||||
|
|||||||
@@ -1474,6 +1474,8 @@ class _FavoriteArtistsSection extends ConsumerWidget {
|
|||||||
fav.avatarUrl!.isNotEmpty
|
fav.avatarUrl!.isNotEmpty
|
||||||
? CachedNetworkImage(
|
? CachedNetworkImage(
|
||||||
imageUrl: fav.avatarUrl!,
|
imageUrl: fav.avatarUrl!,
|
||||||
|
memCacheWidth: 120,
|
||||||
|
memCacheHeight: 120,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: (_, __) => const Icon(
|
placeholder: (_, __) => const Icon(
|
||||||
Icons.person_rounded,
|
Icons.person_rounded,
|
||||||
@@ -1599,6 +1601,8 @@ class _FavoriteArtistMediaStrip extends ConsumerWidget {
|
|||||||
imageUrl: post.previewUrl.isNotEmpty
|
imageUrl: post.previewUrl.isNotEmpty
|
||||||
? post.previewUrl
|
? post.previewUrl
|
||||||
: post.sampleUrl,
|
: post.sampleUrl,
|
||||||
|
memCacheWidth: 200,
|
||||||
|
memCacheHeight: 200,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: (_, __) => ColoredBox(
|
placeholder: (_, __) => ColoredBox(
|
||||||
color: Theme.of(context)
|
color: Theme.of(context)
|
||||||
@@ -1706,6 +1710,8 @@ class _ArtistAvatar extends StatelessWidget {
|
|||||||
? fallback
|
? fallback
|
||||||
: CachedNetworkImage(
|
: CachedNetworkImage(
|
||||||
imageUrl: artist.avatarUrl!,
|
imageUrl: artist.avatarUrl!,
|
||||||
|
memCacheWidth: 140,
|
||||||
|
memCacheHeight: 140,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: (_, __) => fallback,
|
placeholder: (_, __) => fallback,
|
||||||
errorWidget: (_, __, ___) => fallback,
|
errorWidget: (_, __, ___) => fallback,
|
||||||
|
|||||||
@@ -314,6 +314,8 @@ class _CollectionCard extends ConsumerWidget {
|
|||||||
if (urls.length == 1) {
|
if (urls.length == 1) {
|
||||||
return CachedNetworkImage(
|
return CachedNetworkImage(
|
||||||
imageUrl: urls[0],
|
imageUrl: urls[0],
|
||||||
|
memCacheWidth: 260,
|
||||||
|
memCacheHeight: 260,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: (_, __) => Container(color: scheme.surfaceContainerHighest),
|
placeholder: (_, __) => Container(color: scheme.surfaceContainerHighest),
|
||||||
errorWidget: (_, __, ___) => Container(
|
errorWidget: (_, __, ___) => Container(
|
||||||
@@ -333,6 +335,8 @@ class _CollectionCard extends ConsumerWidget {
|
|||||||
if (i < urls.length)
|
if (i < urls.length)
|
||||||
CachedNetworkImage(
|
CachedNetworkImage(
|
||||||
imageUrl: urls[i],
|
imageUrl: urls[i],
|
||||||
|
memCacheWidth: 200,
|
||||||
|
memCacheHeight: 200,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: (_, __) =>
|
placeholder: (_, __) =>
|
||||||
Container(color: scheme.surfaceContainerHighest),
|
Container(color: scheme.surfaceContainerHighest),
|
||||||
|
|||||||
@@ -1225,6 +1225,8 @@ class _ArtistCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: CachedNetworkImage(
|
child: CachedNetworkImage(
|
||||||
imageUrl: url,
|
imageUrl: url,
|
||||||
|
memCacheWidth: 128,
|
||||||
|
memCacheHeight: 128,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: (_, __) => Container(
|
placeholder: (_, __) => Container(
|
||||||
color: scheme.surfaceContainerHighest,
|
color: scheme.surfaceContainerHighest,
|
||||||
|
|||||||
@@ -1155,6 +1155,8 @@ class _NeighborStrip extends StatelessWidget {
|
|||||||
imageUrl: post.previewUrl.isNotEmpty
|
imageUrl: post.previewUrl.isNotEmpty
|
||||||
? post.previewUrl
|
? post.previewUrl
|
||||||
: post.sampleUrl,
|
: post.sampleUrl,
|
||||||
|
memCacheWidth: 160,
|
||||||
|
memCacheHeight: 160,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
errorWidget: (_, __, ___) =>
|
errorWidget: (_, __, ___) =>
|
||||||
const Center(child: Icon(Icons.broken_image_rounded)),
|
const Center(child: Icon(Icons.broken_image_rounded)),
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'dart:io';
|
|||||||
import 'package:cached_network_image/cached_network_image.dart';
|
import 'package:cached_network_image/cached_network_image.dart';
|
||||||
import 'package:dio/dio.dart';
|
import 'package:dio/dio.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/gestures.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:media_kit/media_kit.dart';
|
import 'package:media_kit/media_kit.dart';
|
||||||
@@ -316,10 +317,16 @@ class _PostMediaViewerState extends State<PostMediaViewer>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
|
final mq = MediaQuery.maybeOf(context);
|
||||||
|
final dpr = mq?.devicePixelRatio ?? 1.5;
|
||||||
|
final screenWidth = mq?.size.width ?? 1280;
|
||||||
|
final maxCacheWidth =
|
||||||
|
(screenWidth * dpr * 1.5).round().clamp(1080, 2560);
|
||||||
image = CachedNetworkImage(
|
image = CachedNetworkImage(
|
||||||
key: ValueKey(url),
|
key: ValueKey(url),
|
||||||
imageUrl: url,
|
imageUrl: url,
|
||||||
httpHeaders: headers,
|
httpHeaders: headers,
|
||||||
|
memCacheWidth: maxCacheWidth,
|
||||||
fit: BoxFit.contain,
|
fit: BoxFit.contain,
|
||||||
placeholder: (context, url) =>
|
placeholder: (context, url) =>
|
||||||
const Center(child: CircularProgressIndicator()),
|
const Center(child: CircularProgressIndicator()),
|
||||||
@@ -2205,6 +2212,7 @@ class _ZoomableImageState extends State<_ZoomableImage> {
|
|||||||
transformationController: _controller,
|
transformationController: _controller,
|
||||||
minScale: 1,
|
minScale: 1,
|
||||||
maxScale: 6,
|
maxScale: 6,
|
||||||
|
scaleFactor: 1000000.0,
|
||||||
boundaryMargin: const EdgeInsets.all(160),
|
boundaryMargin: const EdgeInsets.all(160),
|
||||||
panEnabled: _zoomed,
|
panEnabled: _zoomed,
|
||||||
scaleEnabled: true,
|
scaleEnabled: true,
|
||||||
@@ -2221,7 +2229,15 @@ class _ZoomableImageState extends State<_ZoomableImage> {
|
|||||||
if (mounted) setState(() => _zoomed = nextZoomed);
|
if (mounted) setState(() => _zoomed = nextZoomed);
|
||||||
_setLocked(nextZoomed || _pointerCount >= 2);
|
_setLocked(nextZoomed || _pointerCount >= 2);
|
||||||
},
|
},
|
||||||
child: child,
|
child: Listener(
|
||||||
|
onPointerSignal: (event) {
|
||||||
|
if (event is PointerScrollEvent) {
|
||||||
|
GestureBinding.instance.pointerSignalResolver
|
||||||
|
.register(event, (_) {});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: child,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -427,6 +427,8 @@ class _ViewedPostTile extends StatelessWidget {
|
|||||||
)
|
)
|
||||||
: CachedNetworkImage(
|
: CachedNetworkImage(
|
||||||
imageUrl: image,
|
imageUrl: image,
|
||||||
|
memCacheWidth: 320,
|
||||||
|
memCacheHeight: 320,
|
||||||
fit: BoxFit.cover,
|
fit: BoxFit.cover,
|
||||||
placeholder: (_, __) => Container(
|
placeholder: (_, __) => Container(
|
||||||
color: scheme.surfaceContainerHighest,
|
color: scheme.surfaceContainerHighest,
|
||||||
|
|||||||
@@ -5,5 +5,8 @@ import 'app/app.dart';
|
|||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
WidgetsFlutterBinding.ensureInitialized();
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
// Optimize image cache to prevent memory bloat and OOM crashes
|
||||||
|
PaintingBinding.instance.imageCache.maximumSizeBytes = 180 * 1024 * 1024; // 180 MB
|
||||||
|
PaintingBinding.instance.imageCache.maximumSize = 120; // 120 images
|
||||||
runApp(const ProviderScope(child: GelRuleApp()));
|
runApp(const ProviderScope(child: GelRuleApp()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -580,7 +580,7 @@ class _DesktopShellState extends State<_DesktopShell>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
'v3.6.1',
|
'v3.6.2',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 10,
|
fontSize: 10,
|
||||||
fontWeight:
|
fontWeight:
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ class _PostCardState extends ConsumerState<PostCard>
|
|||||||
}
|
}
|
||||||
final dpr = MediaQuery.devicePixelRatioOf(context);
|
final dpr = MediaQuery.devicePixelRatioOf(context);
|
||||||
final cacheWidth =
|
final cacheWidth =
|
||||||
(constraints.maxWidth * dpr).round().clamp(320, 1800);
|
(constraints.maxWidth * dpr).round().clamp(280, 960);
|
||||||
final headers = _headersFor(post);
|
final headers = _headersFor(post);
|
||||||
final videoUrls = MediaUrlSelector.video(post);
|
final videoUrls = MediaUrlSelector.video(post);
|
||||||
if (!mobile &&
|
if (!mobile &&
|
||||||
|
|||||||
@@ -68,24 +68,26 @@ class PostMasonryGrid extends StatelessWidget {
|
|||||||
final spacing = mobile ? 8.0 : 12.0;
|
final spacing = mobile ? 8.0 : 12.0;
|
||||||
|
|
||||||
Widget buildCard(Post post) {
|
Widget buildCard(Post post) {
|
||||||
return PostCard(
|
return RepaintBoundary(
|
||||||
post: post,
|
child: PostCard(
|
||||||
blurExplicit: blurExplicit && !nsfwEnabled,
|
post: post,
|
||||||
showBadges: showBadges,
|
blurExplicit: blurExplicit && !nsfwEnabled,
|
||||||
isFavorite: favoriteKeys.contains(post.cacheKey),
|
showBadges: showBadges,
|
||||||
isViewed: viewedKeys.contains(post.cacheKey),
|
isFavorite: favoriteKeys.contains(post.cacheKey),
|
||||||
isDownloaded: downloadedKeys.contains(post.cacheKey),
|
isViewed: viewedKeys.contains(post.cacheKey),
|
||||||
mediaQualityMode: mediaQualityMode,
|
isDownloaded: downloadedKeys.contains(post.cacheKey),
|
||||||
onOpen: () => onOpen(post),
|
mediaQualityMode: mediaQualityMode,
|
||||||
onFavorite: () => onFavorite(post),
|
onOpen: () => onOpen(post),
|
||||||
onPreview: onPreview == null ? null : () => onPreview!(post),
|
onFavorite: () => onFavorite(post),
|
||||||
onHide: onHide == null ? null : () => onHide!(post),
|
onPreview: onPreview == null ? null : () => onPreview!(post),
|
||||||
selectionMode: selectionMode,
|
onHide: onHide == null ? null : () => onHide!(post),
|
||||||
selected: selectedKeys.contains(post.cacheKey),
|
selectionMode: selectionMode,
|
||||||
onToggleSelected:
|
selected: selectedKeys.contains(post.cacheKey),
|
||||||
onToggleSelected == null ? null : () => onToggleSelected!(post),
|
onToggleSelected:
|
||||||
onAddToCollection:
|
onToggleSelected == null ? null : () => onToggleSelected!(post),
|
||||||
onAddToCollection == null ? null : () => onAddToCollection!(post),
|
onAddToCollection:
|
||||||
|
onAddToCollection == null ? null : () => onAddToCollection!(post),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,7 +1,7 @@
|
|||||||
name: gel_rule_app
|
name: gel_rule_app
|
||||||
description: Local backend/core layer for a Flutter booru browser app.
|
description: Local backend/core layer for a Flutter booru browser app.
|
||||||
publish_to: "none"
|
publish_to: "none"
|
||||||
version: 3.6.1+83
|
version: 3.6.2+84
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ">=3.3.0 <4.0.0"
|
sdk: ">=3.3.0 <4.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user