Merge pull request 'Paginate sticker pack access' (#298) from feat/sticker-pagination into master
Reviewed-on: https://codeberg.org/moxxy/moxxy/pulls/298
This commit is contained in:
commit
1e94910ebd
@ -376,6 +376,9 @@
|
||||
"importFailure": "Failed to import sticker pack",
|
||||
"stickerPackSize": "(${size})"
|
||||
},
|
||||
"stickerPacks": {
|
||||
"title": "Sticker Packs"
|
||||
},
|
||||
"storage": {
|
||||
"title": "Storage",
|
||||
"storageUsed": "Storage used: ${size}",
|
||||
|
@ -36,9 +36,6 @@ files:
|
||||
roster:
|
||||
type: List<RosterItem>?
|
||||
deserialise: true
|
||||
stickers:
|
||||
type: List<StickerPack>?
|
||||
deserialise: true
|
||||
# Triggered if a conversation has been added.
|
||||
# Also returned by [AddConversationCommand]
|
||||
- name: ConversationAddedEvent
|
||||
@ -322,6 +319,22 @@ files:
|
||||
conversations:
|
||||
type: List<Conversation>
|
||||
deserialize: true
|
||||
- name: PagedStickerPackResult
|
||||
extends: BackgroundEvent
|
||||
implements:
|
||||
- JsonImplementation
|
||||
attributes:
|
||||
stickerPacks:
|
||||
type: List<StickerPack>
|
||||
deserialise: true
|
||||
- name: GetStickerPackByIdResult
|
||||
extends: BackgroundEvent
|
||||
implements:
|
||||
- JsonImplementation
|
||||
attributes:
|
||||
stickerPack:
|
||||
type: StickerPack?
|
||||
deserialise: true
|
||||
generate_builder: true
|
||||
builder_name: "Event"
|
||||
builder_baseclass: "BackgroundEvent"
|
||||
@ -643,6 +656,20 @@ files:
|
||||
# Milliseconds from now in the past; The maximum age of a file to not
|
||||
# get deleted.
|
||||
timeOffset: int
|
||||
- name: GetPagedStickerPackCommand
|
||||
extends: BackgroundCommand
|
||||
implements:
|
||||
- JsonImplementation
|
||||
attributes:
|
||||
olderThan: bool
|
||||
timestamp: int?
|
||||
includeStickers: bool
|
||||
- name: GetStickerPackByIdCommand
|
||||
extends: BackgroundCommand
|
||||
implements:
|
||||
- JsonImplementation
|
||||
attributes:
|
||||
id: String
|
||||
- name: DebugCommand
|
||||
extends: BackgroundCommand
|
||||
implements:
|
||||
|
@ -56,6 +56,7 @@ import 'package:moxxyv2/ui/pages/settings/licenses.dart';
|
||||
import 'package:moxxyv2/ui/pages/settings/network.dart';
|
||||
import 'package:moxxyv2/ui/pages/settings/privacy/privacy.dart';
|
||||
import 'package:moxxyv2/ui/pages/settings/settings.dart';
|
||||
import 'package:moxxyv2/ui/pages/settings/sticker_packs.dart';
|
||||
import 'package:moxxyv2/ui/pages/settings/stickers.dart';
|
||||
import 'package:moxxyv2/ui/pages/settings/storage/shared_media.dart';
|
||||
import 'package:moxxyv2/ui/pages/settings/storage/storage.dart';
|
||||
@ -334,6 +335,8 @@ class MyAppState extends State<MyApp> with WidgetsBindingObserver {
|
||||
);
|
||||
case stickersRoute:
|
||||
return StickersSettingsPage.route;
|
||||
case stickerPacksRoute:
|
||||
return StickerPacksSettingsPage.route;
|
||||
case stickerPackRoute:
|
||||
return StickerPackPage.route;
|
||||
case storageSettingsRoute:
|
||||
|
@ -183,7 +183,8 @@ Future<void> createDatabase(Database db, int version) async {
|
||||
description TEXT NOT NULL,
|
||||
hashAlgorithm TEXT NOT NULL,
|
||||
hashValue TEXT NOT NULL,
|
||||
restricted INTEGER NOT NULL
|
||||
restricted INTEGER NOT NULL,
|
||||
addedTimestamp INTEGER NOT NULL
|
||||
)''',
|
||||
);
|
||||
|
||||
|
@ -45,6 +45,7 @@ import 'package:moxxyv2/service/database/migrations/0003_avatar_hashes.dart';
|
||||
import 'package:moxxyv2/service/database/migrations/0003_new_omemo.dart';
|
||||
import 'package:moxxyv2/service/database/migrations/0003_new_omemo_pseudo_messages.dart';
|
||||
import 'package:moxxyv2/service/database/migrations/0003_remove_subscriptions.dart';
|
||||
import 'package:moxxyv2/service/database/migrations/0003_sticker_pack_timestamp.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:random_string/random_string.dart';
|
||||
// ignore: implementation_imports
|
||||
@ -152,6 +153,7 @@ const List<DatabaseMigration<Database>> migrations = [
|
||||
DatabaseMigration(39, upgradeFromV38ToV39),
|
||||
DatabaseMigration(40, upgradeFromV39ToV40),
|
||||
DatabaseMigration(41, upgradeFromV40ToV41),
|
||||
DatabaseMigration(42, upgradeFromV41ToV42),
|
||||
];
|
||||
|
||||
class DatabaseService {
|
||||
|
@ -0,0 +1,30 @@
|
||||
import 'package:moxxyv2/service/database/constants.dart';
|
||||
import 'package:sqflite_sqlcipher/sqflite.dart';
|
||||
|
||||
Future<void> upgradeFromV41ToV42(Database db) async {
|
||||
/// Add the new column
|
||||
await db.execute(
|
||||
'''
|
||||
ALTER TABLE $stickerPacksTable ADD COLUMN addedTimestamp INTEGER NOT NULL DEFAULT 0;
|
||||
''',
|
||||
);
|
||||
|
||||
/// Ensure that the sticker packs are sorted (albeit randomly)
|
||||
final stickerPackIds = await db.query(
|
||||
stickerPacksTable,
|
||||
columns: ['id'],
|
||||
);
|
||||
|
||||
var counter = 0;
|
||||
for (final id in stickerPackIds) {
|
||||
await db.update(
|
||||
stickerPacksTable,
|
||||
{
|
||||
'addedTimestamp': counter,
|
||||
},
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
counter++;
|
||||
}
|
||||
}
|
@ -108,6 +108,8 @@ void setupBackgroundEventHandler() {
|
||||
EventTypeMatcher<RequestAvatarForJidCommand>(performRequestAvatarForJid),
|
||||
EventTypeMatcher<GetStorageUsageCommand>(performGetStorageUsage),
|
||||
EventTypeMatcher<DeleteOldMediaFilesCommand>(performOldMediaFileDeletion),
|
||||
EventTypeMatcher<GetPagedStickerPackCommand>(performGetPagedStickerPacks),
|
||||
EventTypeMatcher<GetStickerPackByIdCommand>(performGetStickerPackById),
|
||||
EventTypeMatcher<DebugCommand>(performDebugCommand),
|
||||
]);
|
||||
|
||||
@ -189,7 +191,6 @@ Future<PreStartDoneEvent> _buildPreStartDoneEvent(
|
||||
.where((c) => c.open)
|
||||
.toList(),
|
||||
roster: await GetIt.I.get<RosterService>().loadRosterFromDatabase(),
|
||||
stickers: await GetIt.I.get<StickersService>().getStickerPacks(),
|
||||
);
|
||||
}
|
||||
|
||||
@ -1205,6 +1206,7 @@ Future<void> performFetchStickerPack(
|
||||
stickerPack.restricted,
|
||||
false,
|
||||
0,
|
||||
0,
|
||||
),
|
||||
),
|
||||
id: id,
|
||||
@ -1375,6 +1377,38 @@ Future<void> performOldMediaFileDeletion(
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> performGetPagedStickerPacks(
|
||||
GetPagedStickerPackCommand command, {
|
||||
dynamic extra,
|
||||
}) async {
|
||||
final result = await GetIt.I.get<StickersService>().getPaginatedStickerPacks(
|
||||
command.olderThan,
|
||||
command.timestamp,
|
||||
command.includeStickers,
|
||||
);
|
||||
|
||||
sendEvent(
|
||||
PagedStickerPackResult(
|
||||
stickerPacks: result,
|
||||
),
|
||||
id: extra as String,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> performGetStickerPackById(
|
||||
GetStickerPackByIdCommand command, {
|
||||
dynamic extra,
|
||||
}) async {
|
||||
sendEvent(
|
||||
GetStickerPackByIdResult(
|
||||
stickerPack: await GetIt.I.get<StickersService>().getStickerPackById(
|
||||
command.id,
|
||||
),
|
||||
),
|
||||
id: extra as String,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> performDebugCommand(
|
||||
DebugCommand command, {
|
||||
dynamic extra,
|
||||
|
@ -68,7 +68,8 @@ Future<String> computeCachedPathForFile(
|
||||
return path.join(
|
||||
basePath,
|
||||
hash != null
|
||||
? '$hash.$ext'
|
||||
// NOTE: [ext] already includes a leading "."
|
||||
? '$hash$ext'
|
||||
: '$filename.${DateTime.now().millisecondsSinceEpoch}.$ext',
|
||||
);
|
||||
}
|
||||
|
@ -16,6 +16,7 @@ import 'package:moxxyv2/service/httpfiletransfer/location.dart';
|
||||
import 'package:moxxyv2/service/preferences.dart';
|
||||
import 'package:moxxyv2/service/service.dart';
|
||||
import 'package:moxxyv2/service/xmpp_state.dart';
|
||||
import 'package:moxxyv2/shared/constants.dart';
|
||||
import 'package:moxxyv2/shared/events.dart';
|
||||
import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/shared/models/file_metadata.dart';
|
||||
@ -24,7 +25,6 @@ import 'package:moxxyv2/shared/models/sticker_pack.dart';
|
||||
import 'package:path/path.dart' as p;
|
||||
|
||||
class StickersService {
|
||||
final Map<String, StickerPack> _stickerPacks = {};
|
||||
final Logger _log = Logger('StickersService');
|
||||
|
||||
/// Computes the total amount of storage occupied by the stickers in the sticker
|
||||
@ -59,8 +59,6 @@ class StickersService {
|
||||
}
|
||||
|
||||
Future<StickerPack?> getStickerPackById(String id) async {
|
||||
if (_stickerPacks.containsKey(id)) return _stickerPacks[id];
|
||||
|
||||
final db = GetIt.I.get<DatabaseService>().database;
|
||||
final rawPack = await db.query(
|
||||
stickerPacksTable,
|
||||
@ -106,7 +104,7 @@ JOIN
|
||||
[id],
|
||||
);
|
||||
|
||||
_stickerPacks[id] = StickerPack.fromDatabaseJson(
|
||||
final stickerPack = StickerPack.fromDatabaseJson(
|
||||
rawPack.first,
|
||||
rawStickers.map((sticker) {
|
||||
return Sticker.fromDatabaseJson(
|
||||
@ -120,26 +118,11 @@ JOIN
|
||||
size: await getStickerPackSizeById(id),
|
||||
);
|
||||
|
||||
return _stickerPacks[id]!;
|
||||
}
|
||||
|
||||
Future<List<StickerPack>> getStickerPacks() async {
|
||||
if (_stickerPacks.isEmpty) {
|
||||
final rawPackIds = await GetIt.I.get<DatabaseService>().database.query(
|
||||
stickerPacksTable,
|
||||
columns: ['id'],
|
||||
);
|
||||
for (final rawPack in rawPackIds) {
|
||||
final id = rawPack['id']! as String;
|
||||
await getStickerPackById(id);
|
||||
}
|
||||
}
|
||||
|
||||
_log.finest('Got ${_stickerPacks.length} sticker packs');
|
||||
return _stickerPacks.values.toList();
|
||||
return stickerPack;
|
||||
}
|
||||
|
||||
Future<void> removeStickerPack(String id) async {
|
||||
final db = GetIt.I.get<DatabaseService>().database;
|
||||
final pack = await getStickerPackById(id);
|
||||
assert(pack != null, 'The sticker pack must exist');
|
||||
|
||||
@ -160,15 +143,17 @@ JOIN
|
||||
}
|
||||
|
||||
// Remove from the database
|
||||
await GetIt.I.get<DatabaseService>().database.delete(
|
||||
await db.delete(
|
||||
stickersTable,
|
||||
where: 'stickerPackId = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
await db.delete(
|
||||
stickerPacksTable,
|
||||
where: 'id = ?',
|
||||
whereArgs: [id],
|
||||
);
|
||||
|
||||
// Remove from the cache
|
||||
_stickerPacks.remove(id);
|
||||
|
||||
// Retract from PubSub
|
||||
final state = await GetIt.I.get<XmppStateService>().getXmppState();
|
||||
final result = await GetIt.I
|
||||
@ -440,6 +425,7 @@ JOIN
|
||||
pack.hashValue,
|
||||
pack.restricted,
|
||||
true,
|
||||
DateTime.now().millisecondsSinceEpoch,
|
||||
0,
|
||||
);
|
||||
await _addStickerPackFromData(stickerPack);
|
||||
@ -487,8 +473,11 @@ JOIN
|
||||
|
||||
// Only copy the sticker to storage if we don't already have it
|
||||
var fm = fileMetadataRaw.fileMetadata;
|
||||
if (!fileMetadataRaw.retrieved &&
|
||||
if (!fileMetadataRaw.retrieved ||
|
||||
fileMetadataRaw.fileMetadata.path == null) {
|
||||
_log.finest(
|
||||
'Copying sticker ${sticker.metadata.name!} to media storage',
|
||||
);
|
||||
final stickerFile = archive.findFile(sticker.metadata.name!)!;
|
||||
final file = File(stickerPath);
|
||||
await file.writeAsBytes(
|
||||
@ -499,12 +488,21 @@ JOIN
|
||||
fm = await fs.updateFileMetadata(
|
||||
fm.id,
|
||||
size: file.lengthSync(),
|
||||
path: stickerPath,
|
||||
);
|
||||
size += file.lengthSync();
|
||||
} else {
|
||||
_log.finest(
|
||||
'Not copying sticker ${sticker.metadata.name!} as we already have it',
|
||||
);
|
||||
}
|
||||
|
||||
// Check if the sticker has size
|
||||
if (fm.size == null) {
|
||||
_log.finest(
|
||||
'Sticker ${sticker.metadata.name!} has no size. Calculating it',
|
||||
);
|
||||
|
||||
// Update the File Metadata entry
|
||||
fm = await fs.updateFileMetadata(
|
||||
fm.id,
|
||||
@ -520,7 +518,7 @@ JOIN
|
||||
pack.hashValue,
|
||||
sticker.metadata.desc!,
|
||||
sticker.suggests,
|
||||
fileMetadataRaw.fileMetadata,
|
||||
fm,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -530,9 +528,6 @@ JOIN
|
||||
size: size,
|
||||
);
|
||||
|
||||
// Add it to the cache
|
||||
_stickerPacks[pack.hashValue] = stickerPackWithStickers;
|
||||
|
||||
_log.info(
|
||||
'Sticker pack ${stickerPack.id} successfully added to the database',
|
||||
);
|
||||
@ -541,4 +536,110 @@ JOIN
|
||||
unawaited(_publishStickerPack(pack));
|
||||
return stickerPackWithStickers;
|
||||
}
|
||||
|
||||
/// Returns a paginated list of sticker packs.
|
||||
/// [includeStickers] controls whether the stickers for a given sticker pack are
|
||||
/// fetched from the database. Setting this to false, i.e. not loading the stickers,
|
||||
/// can be useful, for example, when we're only interested in listing the sticker
|
||||
/// packs without the stickers being visible.
|
||||
Future<List<StickerPack>> getPaginatedStickerPacks(
|
||||
bool olderThan,
|
||||
int? timestamp,
|
||||
bool includeStickers,
|
||||
) async {
|
||||
final db = GetIt.I.get<DatabaseService>().database;
|
||||
final comparator = olderThan ? '<' : '>';
|
||||
final query = timestamp != null ? 'addedTimestamp $comparator ?' : null;
|
||||
|
||||
final stickerPacksRaw = await db.query(
|
||||
stickerPacksTable,
|
||||
where: query,
|
||||
orderBy: 'addedTimestamp DESC',
|
||||
limit: stickerPackPaginationSize,
|
||||
);
|
||||
|
||||
final stickerPacks = List<StickerPack>.empty(growable: true);
|
||||
for (final pack in stickerPacksRaw) {
|
||||
// Query the stickers
|
||||
List<Map<String, Object?>> stickersRaw;
|
||||
if (includeStickers) {
|
||||
stickersRaw = await db.rawQuery(
|
||||
'''
|
||||
SELECT
|
||||
st.*,
|
||||
fm.id AS fm_id,
|
||||
fm.path AS fm_path,
|
||||
fm.sourceUrls AS fm_sourceUrls,
|
||||
fm.mimeType AS fm_mimeType,
|
||||
fm.thumbnailType AS fm_thumbnailType,
|
||||
fm.thumbnailData AS fm_thumbnailData,
|
||||
fm.width AS fm_width,
|
||||
fm.height AS fm_height,
|
||||
fm.plaintextHashes AS fm_plaintextHashes,
|
||||
fm.encryptionKey AS fm_encryptionKey,
|
||||
fm.encryptionIv AS fm_encryptionIv,
|
||||
fm.encryptionScheme AS fm_encryptionScheme,
|
||||
fm.cipherTextHashes AS fm_cipherTextHashes,
|
||||
fm.filename AS fm_filename,
|
||||
fm.size AS fm_size
|
||||
FROM
|
||||
$stickersTable AS st,
|
||||
$fileMetadataTable AS fm
|
||||
WHERE
|
||||
st.stickerPackId = ? AND
|
||||
st.file_metadata_id = fm.id
|
||||
''',
|
||||
[
|
||||
pack['id']! as String,
|
||||
],
|
||||
);
|
||||
} else {
|
||||
stickersRaw = List<Map<String, Object?>>.empty();
|
||||
}
|
||||
|
||||
final stickerPack = StickerPack.fromDatabaseJson(
|
||||
pack,
|
||||
stickersRaw.map((sticker) {
|
||||
return Sticker.fromDatabaseJson(
|
||||
sticker,
|
||||
FileMetadata.fromDatabaseJson(
|
||||
getPrefixedSubMap(sticker, 'fm_'),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
);
|
||||
|
||||
/// If stickers were not requested, we still have to get the size of the
|
||||
/// sticker pack anyway.
|
||||
int size;
|
||||
if (includeStickers && stickerPack.stickers.isNotEmpty) {
|
||||
size = stickerPack.stickers
|
||||
.map((sticker) => sticker.fileMetadata.size ?? 0)
|
||||
.reduce((value, element) => value + element);
|
||||
} else {
|
||||
final sizeResult = await db.rawQuery(
|
||||
'''
|
||||
SELECT
|
||||
SUM(fm.size) as size
|
||||
FROM
|
||||
$fileMetadataTable as fm,
|
||||
$stickersTable as st
|
||||
WHERE
|
||||
st.stickerPackId = ? AND
|
||||
st.file_metadata_id = fm.id
|
||||
''',
|
||||
[pack['id']! as String],
|
||||
);
|
||||
size = sizeResult.first['size'] as int? ?? 0;
|
||||
}
|
||||
|
||||
stickerPacks.add(
|
||||
stickerPack.copyWith(
|
||||
size: size,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return stickerPacks;
|
||||
}
|
||||
}
|
||||
|
@ -14,3 +14,9 @@ const int maxSharedMediaPages = 3;
|
||||
|
||||
/// The amount of conversations for which we cache the first page.
|
||||
const int conversationMessagePageCacheSize = 4;
|
||||
|
||||
/// The amount of sticker packs we fetch per paginated request
|
||||
const stickerPackPaginationSize = 10;
|
||||
|
||||
/// The amount of sticker packs we can cache in memory.
|
||||
const maxStickerPackPages = 2;
|
||||
|
@ -18,6 +18,9 @@ class StickerPack with _$StickerPack {
|
||||
bool restricted,
|
||||
bool local,
|
||||
|
||||
/// The timestamp (milliseconds since epoch) when the sticker pack was added
|
||||
int addedTimestamp,
|
||||
|
||||
/// The size in bytes
|
||||
int size,
|
||||
) = _StickerPack;
|
||||
@ -38,6 +41,7 @@ class StickerPack with _$StickerPack {
|
||||
pack.restricted,
|
||||
local,
|
||||
0,
|
||||
0,
|
||||
);
|
||||
|
||||
/// JSON
|
||||
|
@ -1,5 +1,4 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
@ -44,18 +43,22 @@ class StickerPackBloc extends Bloc<StickerPackEvent, StickerPackState> {
|
||||
);
|
||||
|
||||
// Apply
|
||||
final stickerPack = GetIt.I
|
||||
.get<stickers.StickersBloc>()
|
||||
.state
|
||||
.stickerPacks
|
||||
.firstWhereOrNull(
|
||||
(StickerPack pack) => pack.id == event.stickerPackId,
|
||||
);
|
||||
assert(stickerPack != null, 'The sticker pack must be found');
|
||||
final stickerPackResult =
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
GetStickerPackByIdCommand(
|
||||
id: event.stickerPackId,
|
||||
),
|
||||
) as GetStickerPackByIdResult;
|
||||
assert(
|
||||
stickerPackResult.stickerPack != null,
|
||||
'The sticker pack must be found',
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isWorking: false,
|
||||
stickerPack: stickerPack,
|
||||
stickerPack: stickerPackResult.stickerPack,
|
||||
),
|
||||
);
|
||||
}
|
||||
@ -152,21 +155,13 @@ class StickerPackBloc extends Bloc<StickerPackEvent, StickerPackState> {
|
||||
),
|
||||
);
|
||||
|
||||
if (result is StickerPackInstallSuccessEvent) {
|
||||
GetIt.I.get<stickers.StickersBloc>().add(
|
||||
stickers.StickerPackAddedEvent(result.stickerPack),
|
||||
);
|
||||
|
||||
// Leave the page
|
||||
GetIt.I.get<NavigationBloc>().add(
|
||||
PoppedRouteEvent(),
|
||||
);
|
||||
} else {
|
||||
// Leave the page
|
||||
GetIt.I.get<NavigationBloc>().add(
|
||||
PoppedRouteEvent(),
|
||||
);
|
||||
// Leave the page
|
||||
GetIt.I.get<NavigationBloc>().add(
|
||||
PoppedRouteEvent(),
|
||||
);
|
||||
|
||||
// Notify on failure
|
||||
if (result is! StickerPackInstallSuccessEvent) {
|
||||
await Fluttertoast.showToast(
|
||||
msg: t.pages.stickerPack.fetchingFailure,
|
||||
gravity: ToastGravity.SNACKBAR,
|
||||
@ -179,16 +174,22 @@ class StickerPackBloc extends Bloc<StickerPackEvent, StickerPackState> {
|
||||
StickerPackRequested event,
|
||||
Emitter<StickerPackState> emit,
|
||||
) async {
|
||||
// Find out if the sticker pack is locally available or not
|
||||
final stickerPack = GetIt.I
|
||||
.get<stickers.StickersBloc>()
|
||||
.state
|
||||
.stickerPacks
|
||||
.firstWhereOrNull(
|
||||
(StickerPack pack) => pack.id == event.stickerPackId,
|
||||
);
|
||||
emit(
|
||||
state.copyWith(
|
||||
isWorking: true,
|
||||
),
|
||||
);
|
||||
|
||||
if (stickerPack == null) {
|
||||
final stickerPackResult =
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
GetStickerPackByIdCommand(
|
||||
id: event.stickerPackId,
|
||||
),
|
||||
) as GetStickerPackByIdResult;
|
||||
|
||||
// Find out if the sticker pack is locally available or not
|
||||
if (stickerPackResult.stickerPack == null) {
|
||||
await _onRemoteStickerPackRequested(
|
||||
RemoteStickerPackRequested(
|
||||
event.stickerPackId,
|
||||
@ -197,9 +198,11 @@ class StickerPackBloc extends Bloc<StickerPackEvent, StickerPackState> {
|
||||
emit,
|
||||
);
|
||||
} else {
|
||||
await _onLocalStickerPackRequested(
|
||||
LocallyAvailableStickerPackRequested(event.stickerPackId),
|
||||
emit,
|
||||
emit(
|
||||
state.copyWith(
|
||||
isWorking: false,
|
||||
stickerPack: stickerPackResult.stickerPack,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,13 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:collection/collection.dart';
|
||||
import 'package:file_picker/file_picker.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||
import 'package:moxplatform/moxplatform.dart';
|
||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||
import 'package:moxxyv2/shared/commands.dart';
|
||||
import 'package:moxxyv2/shared/events.dart';
|
||||
import 'package:moxxyv2/shared/models/sticker.dart';
|
||||
import 'package:moxxyv2/shared/models/sticker_pack.dart';
|
||||
import 'package:moxxyv2/ui/controller/sticker_pack_controller.dart';
|
||||
import 'package:moxxyv2/ui/helpers.dart';
|
||||
|
||||
part 'stickers_bloc.freezed.dart';
|
||||
@ -20,59 +16,20 @@ part 'stickers_state.dart';
|
||||
|
||||
class StickersBloc extends Bloc<StickersEvent, StickersState> {
|
||||
StickersBloc() : super(StickersState()) {
|
||||
on<StickersSetEvent>(_onStickersSet);
|
||||
on<StickerPackRemovedEvent>(_onStickerPackRemoved);
|
||||
on<StickerPackImportedEvent>(_onStickerPackImported);
|
||||
on<StickerPackAddedEvent>(_onStickerPackAdded);
|
||||
}
|
||||
|
||||
Future<void> _onStickersSet(
|
||||
StickersSetEvent event,
|
||||
Emitter<StickersState> emit,
|
||||
) async {
|
||||
// Also store a mapping of (pack Id, sticker Id) -> Sticker to allow fast lookup
|
||||
// of the sticker in the UI.
|
||||
final map = <StickerKey, Sticker>{};
|
||||
for (final pack in event.stickerPacks) {
|
||||
for (final sticker in pack.stickers) {
|
||||
if (!sticker.isImage) continue;
|
||||
|
||||
map[StickerKey(pack.id, sticker.id)] = sticker;
|
||||
}
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
stickerPacks: event.stickerPacks,
|
||||
stickerMap: map,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onStickerPackRemoved(
|
||||
StickerPackRemovedEvent event,
|
||||
Emitter<StickersState> emit,
|
||||
) async {
|
||||
final stickerPack = state.stickerPacks.firstWhereOrNull(
|
||||
(StickerPack sp) => sp.id == event.stickerPackId,
|
||||
)!;
|
||||
final sm = Map<StickerKey, Sticker>.from(state.stickerMap);
|
||||
for (final sticker in stickerPack.stickers) {
|
||||
sm.remove(StickerKey(stickerPack.id, sticker.id));
|
||||
|
||||
// Evict stickers from the cache
|
||||
unawaited(FileImage(File(sticker.fileMetadata.path!)).evict());
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
stickerPacks: List.from(
|
||||
state.stickerPacks.where((sp) => sp.id != event.stickerPackId),
|
||||
),
|
||||
stickerMap: sm,
|
||||
),
|
||||
// Remove from the UI
|
||||
BidirectionalStickerPackController.instance?.removeItem(
|
||||
(stickerPack) => stickerPack.id == event.stickerPackId,
|
||||
);
|
||||
|
||||
// Notify the backend
|
||||
await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
RemoveStickerPackCommand(
|
||||
stickerPackId: event.stickerPackId,
|
||||
@ -103,36 +60,19 @@ class StickersBloc extends Bloc<StickersEvent, StickersState> {
|
||||
),
|
||||
);
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
isImportRunning: false,
|
||||
),
|
||||
);
|
||||
|
||||
if (result is StickerPackImportSuccessEvent) {
|
||||
final sm = Map<StickerKey, Sticker>.from(state.stickerMap);
|
||||
for (final sticker in result.stickerPack.stickers) {
|
||||
if (!sticker.isImage) continue;
|
||||
|
||||
sm[StickerKey(result.stickerPack.id, sticker.id)] = sticker;
|
||||
}
|
||||
emit(
|
||||
state.copyWith(
|
||||
stickerPacks: List<StickerPack>.from([
|
||||
...state.stickerPacks,
|
||||
result.stickerPack,
|
||||
]),
|
||||
stickerMap: sm,
|
||||
isImportRunning: false,
|
||||
),
|
||||
);
|
||||
|
||||
await Fluttertoast.showToast(
|
||||
msg: t.pages.settings.stickers.importSuccess,
|
||||
gravity: ToastGravity.SNACKBAR,
|
||||
toastLength: Toast.LENGTH_SHORT,
|
||||
);
|
||||
} else {
|
||||
emit(
|
||||
state.copyWith(
|
||||
isImportRunning: false,
|
||||
),
|
||||
);
|
||||
|
||||
await Fluttertoast.showToast(
|
||||
msg: t.pages.settings.stickers.importFailure,
|
||||
gravity: ToastGravity.SNACKBAR,
|
||||
@ -140,26 +80,4 @@ class StickersBloc extends Bloc<StickersEvent, StickersState> {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onStickerPackAdded(
|
||||
StickerPackAddedEvent event,
|
||||
Emitter<StickersState> emit,
|
||||
) async {
|
||||
final sm = Map<StickerKey, Sticker>.from(state.stickerMap);
|
||||
for (final sticker in event.stickerPack.stickers) {
|
||||
if (!sticker.isImage) continue;
|
||||
|
||||
sm[StickerKey(event.stickerPack.id, sticker.id)] = sticker;
|
||||
}
|
||||
|
||||
emit(
|
||||
state.copyWith(
|
||||
stickerPacks: List<StickerPack>.from([
|
||||
...state.stickerPacks,
|
||||
event.stickerPack,
|
||||
]),
|
||||
stickerMap: sm,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,6 @@ part of 'stickers_bloc.dart';
|
||||
|
||||
abstract class StickersEvent {}
|
||||
|
||||
class StickersSetEvent extends StickersEvent {
|
||||
StickersSetEvent(
|
||||
this.stickerPacks,
|
||||
);
|
||||
final List<StickerPack> stickerPacks;
|
||||
}
|
||||
|
||||
/// Triggered by the UI when a sticker pack has been removed
|
||||
class StickerPackRemovedEvent extends StickersEvent {
|
||||
StickerPackRemovedEvent(this.stickerPackId);
|
||||
@ -17,9 +10,3 @@ class StickerPackRemovedEvent extends StickersEvent {
|
||||
|
||||
/// Triggered by the UI when a sticker pack has been imported
|
||||
class StickerPackImportedEvent extends StickersEvent {}
|
||||
|
||||
/// Triggered by the UI when a sticker pack has been imported
|
||||
class StickerPackAddedEvent extends StickersEvent {
|
||||
StickerPackAddedEvent(this.stickerPack);
|
||||
final StickerPack stickerPack;
|
||||
}
|
||||
|
@ -20,8 +20,6 @@ class StickerKey {
|
||||
@freezed
|
||||
class StickersState with _$StickersState {
|
||||
factory StickersState({
|
||||
@Default([]) List<StickerPack> stickerPacks,
|
||||
@Default({}) Map<StickerKey, Sticker> stickerMap,
|
||||
@Default(false) bool isImportRunning,
|
||||
}) = _StickersState;
|
||||
}
|
||||
|
@ -160,6 +160,7 @@ const String backgroundCroppingRoute = '$settingsRoute/appearance/background';
|
||||
const String conversationSettingsRoute = '$settingsRoute/conversation';
|
||||
const String appearanceRoute = '$settingsRoute/appearance';
|
||||
const String stickersRoute = '$settingsRoute/stickers';
|
||||
const String stickerPacksRoute = '$settingsRoute/stickers/sticker_packs';
|
||||
const String storageSettingsRoute = '$settingsRoute/storage';
|
||||
const String storageSharedMediaSettingsRoute = '$settingsRoute/storage/media';
|
||||
const String blocklistRoute = '/blocklist';
|
||||
|
@ -213,6 +213,22 @@ class BidirectionalController<T> {
|
||||
return found;
|
||||
}
|
||||
|
||||
/// Removes the first item for which [test] returns true.
|
||||
void removeItem(bool Function(T) test) {
|
||||
var found = false;
|
||||
for (var i = 0; i < _cache.length; i++) {
|
||||
if (test(_cache[i])) {
|
||||
_cache.removeAt(i);
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (found) {
|
||||
_dataStreamController.add(_cache);
|
||||
}
|
||||
}
|
||||
|
||||
/// Animate to the bottom of the view.
|
||||
void animateToBottom() {
|
||||
_controller.animateTo(
|
||||
|
66
lib/ui/controller/sticker_pack_controller.dart
Normal file
66
lib/ui/controller/sticker_pack_controller.dart
Normal file
@ -0,0 +1,66 @@
|
||||
import 'package:moxplatform/moxplatform.dart';
|
||||
import 'package:moxxyv2/shared/commands.dart';
|
||||
import 'package:moxxyv2/shared/constants.dart';
|
||||
import 'package:moxxyv2/shared/events.dart';
|
||||
import 'package:moxxyv2/shared/models/sticker_pack.dart';
|
||||
import 'package:moxxyv2/ui/controller/bidirectional_controller.dart';
|
||||
|
||||
class BidirectionalStickerPackController
|
||||
extends BidirectionalController<StickerPack> {
|
||||
BidirectionalStickerPackController(this.includeStickers)
|
||||
: assert(
|
||||
instance == null,
|
||||
'There can only be one BidirectionalStickerPackController',
|
||||
),
|
||||
super(
|
||||
pageSize: stickerPackPaginationSize,
|
||||
maxPageAmount: maxStickerPackPages,
|
||||
) {
|
||||
instance = this;
|
||||
}
|
||||
|
||||
/// A flag telling the UI to also include stickers in the sticker pack requests.
|
||||
final bool includeStickers;
|
||||
|
||||
/// Singleton instance access.
|
||||
static BidirectionalStickerPackController? instance;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
|
||||
instance = null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<StickerPack>> fetchOlderDataImpl(
|
||||
StickerPack? oldestElement,
|
||||
) async {
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
final result = await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
GetPagedStickerPackCommand(
|
||||
olderThan: true,
|
||||
timestamp: oldestElement?.addedTimestamp,
|
||||
includeStickers: includeStickers,
|
||||
),
|
||||
) as PagedStickerPackResult;
|
||||
|
||||
return result.stickerPacks;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<StickerPack>> fetchNewerDataImpl(
|
||||
StickerPack? newestElement,
|
||||
) async {
|
||||
// ignore: cast_nullable_to_non_nullable
|
||||
final result = await MoxplatformPlugin.handler.getDataSender().sendData(
|
||||
GetPagedStickerPackCommand(
|
||||
olderThan: false,
|
||||
timestamp: newestElement?.addedTimestamp,
|
||||
includeStickers: includeStickers,
|
||||
),
|
||||
) as PagedStickerPackResult;
|
||||
|
||||
return result.stickerPacks;
|
||||
}
|
||||
}
|
@ -14,7 +14,6 @@ import 'package:moxxyv2/ui/bloc/conversation_bloc.dart' as conversation;
|
||||
import 'package:moxxyv2/ui/bloc/conversations_bloc.dart' as conversations;
|
||||
import 'package:moxxyv2/ui/bloc/newconversation_bloc.dart' as new_conversation;
|
||||
import 'package:moxxyv2/ui/bloc/profile_bloc.dart' as profile;
|
||||
import 'package:moxxyv2/ui/bloc/stickers_bloc.dart' as stickers;
|
||||
import 'package:moxxyv2/ui/controller/conversation_controller.dart';
|
||||
import 'package:moxxyv2/ui/prestart.dart';
|
||||
import 'package:moxxyv2/ui/service/avatars.dart';
|
||||
@ -34,7 +33,6 @@ void setupEventHandler() {
|
||||
EventTypeMatcher<PreStartDoneEvent>(preStartDone),
|
||||
EventTypeMatcher<ServiceReadyEvent>(onServiceReady),
|
||||
EventTypeMatcher<MessageNotificationTappedEvent>(onNotificationTappend),
|
||||
EventTypeMatcher<StickerPackAddedEvent>(onStickerPackAdded),
|
||||
EventTypeMatcher<StreamNegotiationsCompletedEvent>(
|
||||
onStreamNegotiationsDone,
|
||||
),
|
||||
@ -183,15 +181,6 @@ Future<void> onNotificationTappend(
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onStickerPackAdded(
|
||||
StickerPackAddedEvent event, {
|
||||
dynamic extra,
|
||||
}) async {
|
||||
GetIt.I.get<stickers.StickersBloc>().add(
|
||||
stickers.StickerPackAddedEvent(event.stickerPack),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onStreamNegotiationsDone(
|
||||
StreamNegotiationsCompletedEvent event, {
|
||||
dynamic extra,
|
||||
|
114
lib/ui/pages/settings/sticker_packs.dart
Normal file
114
lib/ui/pages/settings/sticker_packs.dart
Normal file
@ -0,0 +1,114 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||
import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/shared/models/sticker_pack.dart';
|
||||
import 'package:moxxyv2/ui/bloc/sticker_pack_bloc.dart';
|
||||
import 'package:moxxyv2/ui/constants.dart';
|
||||
import 'package:moxxyv2/ui/controller/sticker_pack_controller.dart';
|
||||
import 'package:moxxyv2/ui/widgets/settings/row.dart';
|
||||
import 'package:moxxyv2/ui/widgets/topbar.dart';
|
||||
import 'package:phosphor_flutter/phosphor_flutter.dart';
|
||||
|
||||
class StickerPacksSettingsPage extends StatefulWidget {
|
||||
const StickerPacksSettingsPage({super.key});
|
||||
|
||||
static MaterialPageRoute<dynamic> get route => MaterialPageRoute<dynamic>(
|
||||
builder: (_) => const StickerPacksSettingsPage(),
|
||||
settings: const RouteSettings(
|
||||
name: stickerPacksRoute,
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
StickerPacksSettingsState createState() => StickerPacksSettingsState();
|
||||
}
|
||||
|
||||
class StickerPacksSettingsState extends State<StickerPacksSettingsPage> {
|
||||
final BidirectionalStickerPackController _controller =
|
||||
BidirectionalStickerPackController(false);
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_controller.fetchOlderData();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: BorderlessTopbar.title(t.pages.settings.stickerPacks.title),
|
||||
body: StreamBuilder<List<StickerPack>>(
|
||||
stream: _controller.dataStream,
|
||||
initialData: const [],
|
||||
builder: (context, snapshot) {
|
||||
return ListView.builder(
|
||||
itemCount: snapshot.data!.length,
|
||||
itemBuilder: (context, index) {
|
||||
final sizeString = fileSizeToString(
|
||||
snapshot.data![index].size,
|
||||
);
|
||||
return SettingsRow(
|
||||
titleWidget: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
snapshot.data![index].name,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
t.pages.settings.stickers
|
||||
.stickerPackSize(size: sizeString),
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
description: snapshot.data![index].description,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
prefix: const Padding(
|
||||
padding: EdgeInsets.only(right: 16),
|
||||
child: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
// TODO(PapaTutuWawa): Sticker pack thumbnails would be nice
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(radiusLarge),
|
||||
child: ColoredBox(
|
||||
color: Colors.white60,
|
||||
child: Icon(
|
||||
PhosphorIcons.stickerBold,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
onTap: () {
|
||||
GetIt.I.get<StickerPackBloc>().add(
|
||||
LocallyAvailableStickerPackRequested(
|
||||
snapshot.data![index].id,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -2,16 +2,13 @@ import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||
import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/shared/models/preferences.dart';
|
||||
import 'package:moxxyv2/ui/bloc/preferences_bloc.dart';
|
||||
import 'package:moxxyv2/ui/bloc/sticker_pack_bloc.dart';
|
||||
import 'package:moxxyv2/ui/bloc/stickers_bloc.dart';
|
||||
import 'package:moxxyv2/ui/constants.dart';
|
||||
import 'package:moxxyv2/ui/widgets/settings/row.dart';
|
||||
import 'package:moxxyv2/ui/widgets/settings/title.dart';
|
||||
import 'package:moxxyv2/ui/widgets/topbar.dart';
|
||||
import 'package:phosphor_flutter/phosphor_flutter.dart';
|
||||
|
||||
class StickersSettingsPage extends StatelessWidget {
|
||||
const StickersSettingsPage({super.key});
|
||||
@ -26,9 +23,9 @@ class StickersSettingsPage extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return BlocBuilder<StickersBloc, StickersState>(
|
||||
builder: (_, stickers) => WillPopScope(
|
||||
builder: (_, stickersState) => WillPopScope(
|
||||
onWillPop: () async {
|
||||
return !stickers.isImportRunning;
|
||||
return !stickersState.isImportRunning;
|
||||
},
|
||||
child: Stack(
|
||||
children: [
|
||||
@ -42,126 +39,60 @@ class StickersSettingsPage extends StatelessWidget {
|
||||
body: BlocBuilder<PreferencesBloc, PreferencesState>(
|
||||
builder: (_, prefs) => Padding(
|
||||
padding: EdgeInsets.zero,
|
||||
child: ListView.builder(
|
||||
itemCount: stickers.stickerPacks.length + 1,
|
||||
itemBuilder: (___, index) {
|
||||
if (index == 0) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SectionTitle(
|
||||
t.pages.settings.stickers.displayStickers,
|
||||
),
|
||||
SettingsRow(
|
||||
title:
|
||||
t.pages.settings.stickers.displayStickers,
|
||||
suffix: Switch(
|
||||
value: prefs.enableStickers,
|
||||
onChanged: (value) {
|
||||
context.read<PreferencesBloc>().add(
|
||||
PreferencesChangedEvent(
|
||||
prefs.copyWith(
|
||||
enableStickers: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SettingsRow(
|
||||
title: t.pages.settings.stickers.autoDownload,
|
||||
description:
|
||||
t.pages.settings.stickers.autoDownloadBody,
|
||||
suffix: Switch(
|
||||
value: prefs.autoDownloadStickersFromContacts,
|
||||
onChanged: (value) {
|
||||
context.read<PreferencesBloc>().add(
|
||||
PreferencesChangedEvent(
|
||||
prefs.copyWith(
|
||||
autoDownloadStickersFromContacts:
|
||||
value,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SettingsRow(
|
||||
onTap: () {
|
||||
GetIt.I.get<StickersBloc>().add(
|
||||
StickerPackImportedEvent(),
|
||||
);
|
||||
},
|
||||
title:
|
||||
t.pages.settings.stickers.importStickerPack,
|
||||
),
|
||||
SectionTitle(
|
||||
t.pages.settings.stickers.stickerPacksSection,
|
||||
),
|
||||
if (stickers.stickerPacks.isEmpty)
|
||||
SettingsRow(
|
||||
title: t.pages.conversation
|
||||
.stickerPickerNoStickersLine1,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
final sizeString = fileSizeToString(
|
||||
stickers.stickerPacks[index - 1].size,
|
||||
);
|
||||
return SettingsRow(
|
||||
titleWidget: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
stickers.stickerPacks[index - 1].name,
|
||||
style: const TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w400,
|
||||
),
|
||||
),
|
||||
),
|
||||
Text(
|
||||
t.pages.settings.stickers
|
||||
.stickerPackSize(size: sizeString),
|
||||
style: const TextStyle(
|
||||
color: Colors.grey,
|
||||
),
|
||||
),
|
||||
],
|
||||
child: ListView(
|
||||
children: [
|
||||
SectionTitle(
|
||||
t.pages.settings.stickers.displayStickers,
|
||||
),
|
||||
SettingsRow(
|
||||
title: t.pages.settings.stickers.displayStickers,
|
||||
suffix: Switch(
|
||||
value: prefs.enableStickers,
|
||||
onChanged: (value) {
|
||||
context.read<PreferencesBloc>().add(
|
||||
PreferencesChangedEvent(
|
||||
prefs.copyWith(
|
||||
enableStickers: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SettingsRow(
|
||||
title: t.pages.settings.stickers.autoDownload,
|
||||
description:
|
||||
stickers.stickerPacks[index - 1].description,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
prefix: const Padding(
|
||||
padding: EdgeInsets.only(right: 16),
|
||||
child: SizedBox(
|
||||
width: 48,
|
||||
height: 48,
|
||||
// TODO(PapaTutuWawa): Sticker pack thumbnails would be nice
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.all(radiusLarge),
|
||||
child: ColoredBox(
|
||||
color: Colors.white60,
|
||||
child: Icon(
|
||||
PhosphorIcons.stickerBold,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
t.pages.settings.stickers.autoDownloadBody,
|
||||
suffix: Switch(
|
||||
value: prefs.autoDownloadStickersFromContacts,
|
||||
onChanged: (value) {
|
||||
context.read<PreferencesBloc>().add(
|
||||
PreferencesChangedEvent(
|
||||
prefs.copyWith(
|
||||
autoDownloadStickersFromContacts: value,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
SettingsRow(
|
||||
onTap: () {
|
||||
GetIt.I.get<StickerPackBloc>().add(
|
||||
LocallyAvailableStickerPackRequested(
|
||||
stickers.stickerPacks[index - 1].id,
|
||||
),
|
||||
GetIt.I.get<StickersBloc>().add(
|
||||
StickerPackImportedEvent(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
title: t.pages.settings.stickers.importStickerPack,
|
||||
),
|
||||
SettingsRow(
|
||||
title: t.pages.settings.storage.manageStickers,
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
stickerPacksRoute,
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -175,9 +106,9 @@ class StickersSettingsPage extends StatelessWidget {
|
||||
child: AnimatedOpacity(
|
||||
duration: const Duration(milliseconds: 100),
|
||||
curve: Curves.decelerate,
|
||||
opacity: stickers.isImportRunning ? 1 : 0,
|
||||
opacity: stickersState.isImportRunning ? 1 : 0,
|
||||
child: IgnorePointer(
|
||||
ignoring: !stickers.isImportRunning,
|
||||
ignoring: !stickersState.isImportRunning,
|
||||
child: const ColoredBox(
|
||||
color: Colors.black54,
|
||||
child: Align(
|
||||
|
@ -254,7 +254,7 @@ class StorageSettingsPageState extends State<StorageSettingsPage> {
|
||||
SettingsRow(
|
||||
title: t.pages.settings.storage.manageStickers,
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(stickersRoute);
|
||||
Navigator.of(context).pushNamed(stickerPacksRoute);
|
||||
},
|
||||
),
|
||||
],
|
||||
|
@ -24,9 +24,23 @@ class StickerWrapper extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (sticker.fileMetadata.path != null) {
|
||||
return Image.file(
|
||||
File(sticker.fileMetadata.path!),
|
||||
return Image(
|
||||
image: ResizeImage.resizeIfNeeded(
|
||||
null,
|
||||
null,
|
||||
FileImage(
|
||||
File(sticker.fileMetadata.path!),
|
||||
),
|
||||
),
|
||||
fit: cover ? BoxFit.contain : null,
|
||||
loadingBuilder: (_, child, event) {
|
||||
if (event == null) return child;
|
||||
|
||||
return const ClipRRect(
|
||||
borderRadius: BorderRadius.all(radiusLarge),
|
||||
child: ShimmerWidget(),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
return Image.network(
|
||||
@ -132,85 +146,87 @@ class StickerPackPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildBody(BuildContext context, StickerPackState state) {
|
||||
return Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
state.stickerPack?.description ?? '',
|
||||
),
|
||||
if (state.stickerPack?.restricted ?? false)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Text(
|
||||
t.pages.stickerPack.restricted,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: MediaQuery.of(context).size.width * 0.7,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 8,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
state.stickerPack?.description ?? '',
|
||||
),
|
||||
if (state.stickerPack?.restricted ?? false)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: Text(
|
||||
t.pages.stickerPack.restricted,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: _buildButton(context, state),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 8,
|
||||
right: 8,
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: _buildButton(context, state),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 16,
|
||||
left: 8,
|
||||
right: 8,
|
||||
),
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 8,
|
||||
crossAxisSpacing: 8,
|
||||
),
|
||||
itemCount: state.stickerPack!.stickers.length,
|
||||
itemBuilder: (_, index) {
|
||||
final sticker = state.stickerPack!.stickers[index];
|
||||
return InkWell(
|
||||
child: StickerWrapper(
|
||||
sticker,
|
||||
cover: false,
|
||||
),
|
||||
onTap: () {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return IgnorePointer(
|
||||
child: StickerWrapper(
|
||||
sticker,
|
||||
cover: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
itemCount: state.stickerPack!.stickers.length,
|
||||
itemBuilder: (_, index) {
|
||||
final sticker = state.stickerPack!.stickers[index];
|
||||
return InkWell(
|
||||
child: StickerWrapper(
|
||||
sticker,
|
||||
cover: false,
|
||||
),
|
||||
onTap: () {
|
||||
showDialog<void>(
|
||||
context: context,
|
||||
builder: (context) {
|
||||
return IgnorePointer(
|
||||
child: StickerWrapper(
|
||||
sticker,
|
||||
cover: false,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,6 @@ import 'package:moxxyv2/ui/bloc/navigation_bloc.dart';
|
||||
import 'package:moxxyv2/ui/bloc/newconversation_bloc.dart';
|
||||
import 'package:moxxyv2/ui/bloc/preferences_bloc.dart';
|
||||
import 'package:moxxyv2/ui/bloc/share_selection_bloc.dart';
|
||||
import 'package:moxxyv2/ui/bloc/stickers_bloc.dart';
|
||||
import 'package:moxxyv2/ui/constants.dart';
|
||||
import 'package:moxxyv2/ui/service/data.dart';
|
||||
import 'package:moxxyv2/ui/service/sharing.dart';
|
||||
@ -48,11 +47,6 @@ Future<void> preStartDone(PreStartDoneEvent result, {dynamic extra}) async {
|
||||
result.roster!,
|
||||
),
|
||||
);
|
||||
GetIt.I.get<StickersBloc>().add(
|
||||
StickersSetEvent(
|
||||
result.stickers!,
|
||||
),
|
||||
);
|
||||
GetIt.I.get<ShareSelectionBloc>().add(
|
||||
ShareSelectionInitEvent(
|
||||
result.conversations!,
|
||||
|
@ -2,13 +2,11 @@ import 'dart:async';
|
||||
import 'dart:io';
|
||||
import 'package:badges/badges.dart' as badges;
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:get_it/get_it.dart';
|
||||
import 'package:moxxyv2/i18n/strings.g.dart';
|
||||
import 'package:moxxyv2/shared/constants.dart';
|
||||
import 'package:moxxyv2/shared/helpers.dart';
|
||||
import 'package:moxxyv2/shared/models/conversation.dart';
|
||||
import 'package:moxxyv2/ui/bloc/stickers_bloc.dart';
|
||||
import 'package:moxxyv2/ui/constants.dart';
|
||||
import 'package:moxxyv2/ui/service/data.dart';
|
||||
import 'package:moxxyv2/ui/widgets/avatar.dart';
|
||||
@ -215,7 +213,7 @@ class ConversationsListRowState extends State<ConversationsListRow> {
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLastMessagePreview(StickersState state) {
|
||||
Widget _buildLastMessagePreview() {
|
||||
Widget? preview;
|
||||
if (widget.conversation.lastMessage!.stickerPackId != null) {
|
||||
if (widget.conversation.lastMessage!.fileMetadata!.path != null) {
|
||||
@ -417,17 +415,7 @@ class ConversationsListRowState extends State<ConversationsListRow> {
|
||||
!widget.conversation.isTyping)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 5),
|
||||
child:
|
||||
BlocBuilder<StickersBloc, StickersState>(
|
||||
buildWhen: (prev, next) =>
|
||||
prev.stickerPacks.length !=
|
||||
next.stickerPacks.length &&
|
||||
widget.conversation.lastMessage
|
||||
?.stickerPackId !=
|
||||
null,
|
||||
builder: (_, state) =>
|
||||
_buildLastMessagePreview(state),
|
||||
),
|
||||
child: _buildLastMessagePreview(),
|
||||
)
|
||||
else
|
||||
const SizedBox(height: 30),
|
||||
|
@ -10,6 +10,7 @@ import 'package:moxxyv2/ui/bloc/navigation_bloc.dart' as nav;
|
||||
import 'package:moxxyv2/ui/bloc/sticker_pack_bloc.dart';
|
||||
import 'package:moxxyv2/ui/bloc/stickers_bloc.dart';
|
||||
import 'package:moxxyv2/ui/constants.dart';
|
||||
import 'package:moxxyv2/ui/controller/sticker_pack_controller.dart';
|
||||
|
||||
/// A wrapper data class to group by a sticker pack's id, but display its title.
|
||||
@immutable
|
||||
@ -36,110 +37,42 @@ class _StickerPackSeparator {
|
||||
int get hashCode => name.hashCode ^ id.hashCode;
|
||||
}
|
||||
|
||||
class StickerPicker extends StatelessWidget {
|
||||
class StickerPicker extends StatefulWidget {
|
||||
StickerPicker({
|
||||
required this.width,
|
||||
required this.onStickerTapped,
|
||||
super.key,
|
||||
}) {
|
||||
_itemSize = (width - 2 * 15 - 3 * 30) / 4;
|
||||
itemSize = (width - 2 * 15 - 3 * 30) / 4;
|
||||
}
|
||||
|
||||
final double width;
|
||||
late final double _itemSize;
|
||||
|
||||
final void Function(Sticker) onStickerTapped;
|
||||
|
||||
Widget _buildList(BuildContext context, StickersState state) {
|
||||
// TODO(PapaTutuWawa): Solve this somewhere else
|
||||
final stickerPacks =
|
||||
state.stickerPacks.where((pack) => !pack.restricted).toList();
|
||||
late final double itemSize;
|
||||
|
||||
if (stickerPacks.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Align(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${t.pages.conversation.stickerPickerNoStickersLine1}\n${t.pages.conversation.stickerPickerNoStickersLine2}',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<nav.NavigationBloc>().add(
|
||||
nav.PushedNamedEvent(
|
||||
const nav.NavigationDestination(stickersRoute),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(t.pages.conversation.stickerSettings),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@override
|
||||
StickerPickerState createState() => StickerPickerState();
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: GroupedListView<StickerPack, _StickerPackSeparator>(
|
||||
elements: stickerPacks,
|
||||
groupBy: (stickerPack) => _StickerPackSeparator(
|
||||
stickerPack.name,
|
||||
stickerPack.id,
|
||||
),
|
||||
groupSeparatorBuilder: (separator) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Text(
|
||||
separator.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
sort: false,
|
||||
indexedItemBuilder: (context, stickerPack, index) => GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 16,
|
||||
),
|
||||
itemCount: stickerPack.stickers.length,
|
||||
itemBuilder: (_, index) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
onStickerTapped(
|
||||
stickerPack.stickers[index],
|
||||
);
|
||||
},
|
||||
onLongPress: () {
|
||||
Vibrate.feedback(FeedbackType.medium);
|
||||
class StickerPickerState extends State<StickerPicker> {
|
||||
final BidirectionalStickerPackController _controller =
|
||||
BidirectionalStickerPackController(true);
|
||||
|
||||
context.read<StickerPackBloc>().add(
|
||||
LocallyAvailableStickerPackRequested(
|
||||
stickerPack.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Image.file(
|
||||
File(
|
||||
stickerPack.stickers[index].fileMetadata.path!,
|
||||
),
|
||||
key: ValueKey('${stickerPack.id}_$index'),
|
||||
fit: BoxFit.contain,
|
||||
width: _itemSize,
|
||||
height: _itemSize,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
// Fetch the initial state
|
||||
_controller.fetchOlderData();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_controller.dispose();
|
||||
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
@ -148,7 +81,105 @@ class StickerPicker extends StatelessWidget {
|
||||
builder: (context, state) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: _buildList(context, state),
|
||||
child: StreamBuilder<List<StickerPack>>(
|
||||
stream: _controller.dataStream,
|
||||
initialData: const [],
|
||||
builder: (context, snapshot) {
|
||||
if (snapshot.connectionState != ConnectionState.none &&
|
||||
snapshot.connectionState != ConnectionState.waiting &&
|
||||
snapshot.data!.isEmpty) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Align(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
'${t.pages.conversation.stickerPickerNoStickersLine1}\n${t.pages.conversation.stickerPickerNoStickersLine2}',
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () {
|
||||
context.read<nav.NavigationBloc>().add(
|
||||
nav.PushedNamedEvent(
|
||||
const nav.NavigationDestination(
|
||||
stickersRoute,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Text(t.pages.conversation.stickerSettings),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: GroupedListView<StickerPack, _StickerPackSeparator>(
|
||||
controller: _controller.scrollController,
|
||||
elements: snapshot.data!,
|
||||
groupBy: (stickerPack) => _StickerPackSeparator(
|
||||
stickerPack.name,
|
||||
stickerPack.id,
|
||||
),
|
||||
groupSeparatorBuilder: (separator) => Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
child: Text(
|
||||
separator.name,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
sort: false,
|
||||
indexedItemBuilder: (context, stickerPack, index) =>
|
||||
GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: 4,
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 16,
|
||||
),
|
||||
itemCount: stickerPack.stickers.length,
|
||||
itemBuilder: (_, index) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
widget.onStickerTapped(
|
||||
stickerPack.stickers[index],
|
||||
);
|
||||
},
|
||||
onLongPress: () {
|
||||
Vibrate.feedback(FeedbackType.medium);
|
||||
|
||||
context.read<StickerPackBloc>().add(
|
||||
LocallyAvailableStickerPackRequested(
|
||||
stickerPack.id,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Image.file(
|
||||
File(
|
||||
stickerPack.stickers[index].fileMetadata.path!,
|
||||
),
|
||||
key: ValueKey('${stickerPack.id}_$index'),
|
||||
fit: BoxFit.contain,
|
||||
width: widget.itemSize,
|
||||
height: widget.itemSize,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user