Flutter 3.7.0 (#1556)
* Update `simplistic_editor` for Flutter 3.4 beta * Re-enable beta and master CI * Disable on master * Added sample code for using plugins and channels from background isolates. * goderbauer feedback 1 * goderbauer feedback2 * goderbauer feedback 3 * Add `background_isolate_channels` to CI * Enable beta CI * Enable all `stable` CI projects * `dart fix --apply` * `print` -> `denugPrint` * Make deps min version not pinned * Drop `_isDebug` * Remove unused import * `dart format` * Fixup `linting_tool` * Fixup `form_app` * Enable all `master` CI * Basic fixes * Patch `simplistic_editor` * Fix nl at eol * Comment out `simplistic_editor` * Incorporating @bleroux's latest changes * Clean up CI scripts * Copy `experimental/material_3_demo` to top level * Update `game_template` * Update `animations` * Update `desktop_photo_search` * Update `flutter_maps_firestore` * Update `form_app` * Update `infinite_list` * Update `isolate_example` * Update `jsonexample` * Update `navigation_and_routing` * Update `place_tracker` * Update `platform_channels` * Update `platform_design` * Update `provider_shopper` * Fixup `context_menus` * `dart format` * Update the main `material_3_demo` * Make `tool/flutter_ci_script_stable.sh` executable again Co-authored-by: Bruno Leroux <bruno.leroux@gmail.com> Co-authored-by: Aaron Clarke <aaclarke@google.com>pull/1591/head
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 295 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 450 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 462 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 704 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 586 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 762 B |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1 @@
|
|||||||
|
include: ../analysis_options.yaml
|
@ -0,0 +1,155 @@
|
|||||||
|
// Copyright 2022 The Flutter team. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:io' show Directory;
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:path/path.dart' as path;
|
||||||
|
import 'package:path_provider/path_provider.dart' as path_provider;
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:uuid/uuid.dart' as uuid;
|
||||||
|
|
||||||
|
import 'simple_database.dart';
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// This is the UI which will present the contents of the [SimpleDatabase]. To
|
||||||
|
// see where Background Isolate Channels are used see simple_database.dart.
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'Background Isolate Channels',
|
||||||
|
theme: ThemeData(
|
||||||
|
primarySwatch: Colors.blue,
|
||||||
|
),
|
||||||
|
home: const MyHomePage(title: 'Background Isolate Channels'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyHomePage extends StatefulWidget {
|
||||||
|
const MyHomePage({super.key, required this.title});
|
||||||
|
|
||||||
|
final String title;
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyHomePage> createState() {
|
||||||
|
return _MyHomePageState();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyHomePageState extends State<MyHomePage> {
|
||||||
|
/// The database that is running on a background [Isolate]. This is nullable
|
||||||
|
/// because acquiring a [SimpleDatabase] is an asynchronous operation. This
|
||||||
|
/// value is `null` until the database is initialized.
|
||||||
|
SimpleDatabase? _database;
|
||||||
|
|
||||||
|
/// Local cache of the query results returned by the [SimpleDatabase] for the
|
||||||
|
/// UI to render from. It is nullable since querying the results is
|
||||||
|
/// asynchronous. The value is `null` before any result has been received.
|
||||||
|
List<String>? _entries;
|
||||||
|
|
||||||
|
/// What is searched for in the [SimpleDatabase].
|
||||||
|
String _query = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
// Write the value to [SharedPreferences] which will get read on the
|
||||||
|
// [SimpleDatabase]'s isolate. For this example the value is always true
|
||||||
|
// just for demonstration purposes.
|
||||||
|
final Future<void> sharedPreferencesSet = SharedPreferences.getInstance()
|
||||||
|
.then(
|
||||||
|
(sharedPreferences) => sharedPreferences.setBool('isDebug', true));
|
||||||
|
final Future<Directory> tempDirFuture =
|
||||||
|
path_provider.getTemporaryDirectory();
|
||||||
|
|
||||||
|
// Wait until the [SharedPreferences] value is set and the temporary
|
||||||
|
// directory is received before opening the database. If
|
||||||
|
// [sharedPreferencesSet] does not happen before opening the
|
||||||
|
// [SimpleDatabase] there has to be a way to refresh
|
||||||
|
// [_SimpleDatabaseServer]'s [SharedPreferences] cached values.
|
||||||
|
Future.wait([sharedPreferencesSet, tempDirFuture]).then((values) {
|
||||||
|
final Directory? tempDir = values[1] as Directory?;
|
||||||
|
final String dbPath = path.join(tempDir!.path, 'database.db');
|
||||||
|
SimpleDatabase.open(dbPath).then((database) {
|
||||||
|
setState(() {
|
||||||
|
_database = database;
|
||||||
|
});
|
||||||
|
_refresh();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_database?.stop();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs a find on [SimpleDatabase] with [query] and updates the listed
|
||||||
|
/// contents.
|
||||||
|
void _refresh({String? query}) {
|
||||||
|
if (query != null) {
|
||||||
|
_query = query;
|
||||||
|
}
|
||||||
|
_database!.find(_query).toList().then((entries) {
|
||||||
|
setState(() {
|
||||||
|
_entries = entries;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a UUID and a timestamp to the [SimpleDatabase].
|
||||||
|
void _addDate() {
|
||||||
|
final DateTime now = DateTime.now();
|
||||||
|
final DateFormat formatter =
|
||||||
|
DateFormat('EEEE MMMM d, HH:mm:ss\n${const uuid.Uuid().v4()}');
|
||||||
|
final String formatted = formatter.format(now);
|
||||||
|
_database!.addEntry(formatted).then((_) => _refresh());
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text(widget.title),
|
||||||
|
),
|
||||||
|
body: Column(
|
||||||
|
children: [
|
||||||
|
TextField(
|
||||||
|
onChanged:
|
||||||
|
_database == null ? null : (query) => _refresh(query: query),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: 'Search',
|
||||||
|
suffixIcon: Icon(Icons.search),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: ListView.builder(
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
return ListTile(title: Text(_entries![index]));
|
||||||
|
},
|
||||||
|
itemCount: _entries?.length ?? 0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
floatingActionButton: FloatingActionButton(
|
||||||
|
onPressed: _database == null ? null : _addDate,
|
||||||
|
tooltip: 'Add',
|
||||||
|
child: const Icon(Icons.add),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,255 @@
|
|||||||
|
// Copyright 2022 The Flutter team. All rights reserved.
|
||||||
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
import 'dart:collection';
|
||||||
|
import 'dart:convert';
|
||||||
|
import 'dart:io';
|
||||||
|
import 'dart:isolate';
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
// **WARNING:** This is not production code and is only intended to be used for
|
||||||
|
// demonstration purposes.
|
||||||
|
//
|
||||||
|
// The following database works by spawning a background isolate and
|
||||||
|
// communicating with it over Dart's SendPort API. It is presented below as a
|
||||||
|
// demonstration of the feature "Background Isolate Channels" and shows using
|
||||||
|
// plugins from a background isolate. The [SimpleDatabase] operates on the root
|
||||||
|
// isolate and the [_SimpleDatabaseServer] operates on a background isolate.
|
||||||
|
//
|
||||||
|
// Here is an example of the protocol they use to communicate:
|
||||||
|
//
|
||||||
|
// _________________ ________________________
|
||||||
|
// [:SimpleDatabase] [:_SimpleDatabaseServer]
|
||||||
|
// ----------------- ------------------------
|
||||||
|
// | |
|
||||||
|
// |<---------------(init)------------------------|
|
||||||
|
// |----------------(init)----------------------->|
|
||||||
|
// |<---------------(ack)------------------------>|
|
||||||
|
// | |
|
||||||
|
// |----------------(add)------------------------>|
|
||||||
|
// |<---------------(ack)-------------------------|
|
||||||
|
// | |
|
||||||
|
// |----------------(query)---------------------->|
|
||||||
|
// |<---------------(result)----------------------|
|
||||||
|
// |<---------------(result)----------------------|
|
||||||
|
// |<---------------(done)------------------------|
|
||||||
|
//
|
||||||
|
///////////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
/// The size of the database entries in bytes.
|
||||||
|
const int _entrySize = 256;
|
||||||
|
|
||||||
|
/// All the command codes that can be sent and received between [SimpleDatabase] and
|
||||||
|
/// [_SimpleDatabaseServer].
|
||||||
|
enum _Codes {
|
||||||
|
init,
|
||||||
|
add,
|
||||||
|
query,
|
||||||
|
ack,
|
||||||
|
result,
|
||||||
|
done,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A command sent between [SimpleDatabase] and [_SimpleDatabaseServer].
|
||||||
|
class _Command {
|
||||||
|
const _Command(this.code, {this.arg0, this.arg1});
|
||||||
|
|
||||||
|
final _Codes code;
|
||||||
|
final Object? arg0;
|
||||||
|
final Object? arg1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A SimpleDatabase that stores entries of strings to disk where they can be
|
||||||
|
/// queried.
|
||||||
|
///
|
||||||
|
/// All the disk operations and queries are executed in a background isolate
|
||||||
|
/// operating. This class just sends and receives messages to the isolate.
|
||||||
|
class SimpleDatabase {
|
||||||
|
SimpleDatabase._(this._isolate, this._path);
|
||||||
|
|
||||||
|
final Isolate _isolate;
|
||||||
|
final String _path;
|
||||||
|
late final SendPort _sendPort;
|
||||||
|
// Completers are stored in a queue so multiple commands can be queued up and
|
||||||
|
// handled serially.
|
||||||
|
final Queue<Completer<void>> _completers = Queue<Completer<void>>();
|
||||||
|
// Similarly, StreamControllers are stored in a queue so they can be handled
|
||||||
|
// asynchronously and serially.
|
||||||
|
final Queue<StreamController<String>> _resultsStream =
|
||||||
|
Queue<StreamController<String>>();
|
||||||
|
|
||||||
|
/// Open the database at [path] and launch the server on a background isolate..
|
||||||
|
static Future<SimpleDatabase> open(String path) async {
|
||||||
|
final ReceivePort receivePort = ReceivePort();
|
||||||
|
final Isolate isolate =
|
||||||
|
await Isolate.spawn(_SimpleDatabaseServer._run, receivePort.sendPort);
|
||||||
|
final SimpleDatabase result = SimpleDatabase._(isolate, path);
|
||||||
|
Completer<void> completer = Completer<void>();
|
||||||
|
result._completers.addFirst(completer);
|
||||||
|
receivePort.listen((message) {
|
||||||
|
result._handleCommand(message as _Command);
|
||||||
|
});
|
||||||
|
await completer.future;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Writes [value] to the database.
|
||||||
|
Future<void> addEntry(String value) {
|
||||||
|
// No processing happens on the calling isolate, it gets delegated to the
|
||||||
|
// background isolate, see [__SimpleDatabaseServer._doAddEntry].
|
||||||
|
Completer<void> completer = Completer<void>();
|
||||||
|
_completers.addFirst(completer);
|
||||||
|
_sendPort.send(_Command(_Codes.add, arg0: value));
|
||||||
|
return completer.future;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns all the strings in the database that contain [query].
|
||||||
|
Stream<String> find(String query) {
|
||||||
|
// No processing happens on the calling isolate, it gets delegated to the
|
||||||
|
// background isolate, see [__SimpleDatabaseServer._doFind].
|
||||||
|
StreamController<String> resultsStream = StreamController<String>();
|
||||||
|
_resultsStream.addFirst(resultsStream);
|
||||||
|
_sendPort.send(_Command(_Codes.query, arg0: query));
|
||||||
|
return resultsStream.stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handler invoked when a message is received from the port communicating
|
||||||
|
/// with the database server.
|
||||||
|
void _handleCommand(_Command command) {
|
||||||
|
switch (command.code) {
|
||||||
|
case _Codes.init:
|
||||||
|
_sendPort = command.arg0 as SendPort;
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// Before using platform channels and plugins from background isolates we
|
||||||
|
// need to register it with its root isolate. This is achieved by
|
||||||
|
// acquiring a [RootIsolateToken] which the background isolate uses to
|
||||||
|
// invoke [BackgroundIsolateBinaryMessenger.ensureInitialized].
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
RootIsolateToken rootIsolateToken = RootIsolateToken.instance!;
|
||||||
|
_sendPort
|
||||||
|
.send(_Command(_Codes.init, arg0: _path, arg1: rootIsolateToken));
|
||||||
|
break;
|
||||||
|
case _Codes.ack:
|
||||||
|
_completers.removeLast().complete();
|
||||||
|
break;
|
||||||
|
case _Codes.result:
|
||||||
|
_resultsStream.last.add(command.arg0 as String);
|
||||||
|
break;
|
||||||
|
case _Codes.done:
|
||||||
|
_resultsStream.removeLast().close();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
debugPrint('SimpleDatabase unrecognized command: ${command.code}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Kills the background isolate and its database server.
|
||||||
|
void stop() {
|
||||||
|
_isolate.kill();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The portion of the [SimpleDatabase] that runs on the background isolate.
|
||||||
|
///
|
||||||
|
/// This is where we use the new feature Background Isolate Channels, which
|
||||||
|
/// allows us to use plugins from background isolates.
|
||||||
|
class _SimpleDatabaseServer {
|
||||||
|
_SimpleDatabaseServer(this._sendPort);
|
||||||
|
|
||||||
|
final SendPort _sendPort;
|
||||||
|
late final String _path;
|
||||||
|
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// Here the plugin is used from the background isolate.
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
|
||||||
|
/// The main entrypoint for the background isolate sent to [Isolate.spawn].
|
||||||
|
static void _run(SendPort sendPort) {
|
||||||
|
ReceivePort receivePort = ReceivePort();
|
||||||
|
sendPort.send(_Command(_Codes.init, arg0: receivePort.sendPort));
|
||||||
|
final _SimpleDatabaseServer server = _SimpleDatabaseServer(sendPort);
|
||||||
|
receivePort.listen((message) async {
|
||||||
|
final _Command command = message as _Command;
|
||||||
|
await server._handleCommand(command);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Handle the [command] received from the [ReceivePort].
|
||||||
|
Future<void> _handleCommand(_Command command) async {
|
||||||
|
switch (command.code) {
|
||||||
|
case _Codes.init:
|
||||||
|
_path = command.arg0 as String;
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// The [RootIsolateToken] is required for
|
||||||
|
// [BackgroundIsolateBinaryMessenger.ensureInitialized] and must be
|
||||||
|
// obtained on the root isolate and passed into the background isolate via
|
||||||
|
// a [SendPort].
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
RootIsolateToken rootIsolateToken = command.arg1 as RootIsolateToken;
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
// [BackgroundIsolateBinaryMessenger.ensureInitialized] for each
|
||||||
|
// background isolate that will use plugins. This sets up the
|
||||||
|
// [BinaryMessenger] that the Platform Channels will communicate with on
|
||||||
|
// the background isolate.
|
||||||
|
// ----------------------------------------------------------------------
|
||||||
|
BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken);
|
||||||
|
_sendPort.send(const _Command(_Codes.ack, arg0: null));
|
||||||
|
break;
|
||||||
|
case _Codes.add:
|
||||||
|
_doAddEntry(command.arg0 as String);
|
||||||
|
break;
|
||||||
|
case _Codes.query:
|
||||||
|
_doFind(command.arg0 as String);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
debugPrint(
|
||||||
|
'_SimpleDatabaseServer unrecognized command ${command.code}');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform the add entry operation.
|
||||||
|
void _doAddEntry(String value) {
|
||||||
|
debugPrint('Performing add: $value');
|
||||||
|
File file = File(_path);
|
||||||
|
if (!file.existsSync()) {
|
||||||
|
file.createSync();
|
||||||
|
}
|
||||||
|
RandomAccessFile writer = file.openSync(mode: FileMode.append);
|
||||||
|
List<int> bytes = utf8.encode(value);
|
||||||
|
if (bytes.length > _entrySize) {
|
||||||
|
bytes = bytes.sublist(0, _entrySize);
|
||||||
|
} else if (bytes.length < _entrySize) {
|
||||||
|
List<int> newBytes = List.filled(_entrySize, 0);
|
||||||
|
for (int i = 0; i < bytes.length; ++i) {
|
||||||
|
newBytes[i] = bytes[i];
|
||||||
|
}
|
||||||
|
bytes = newBytes;
|
||||||
|
}
|
||||||
|
writer.writeFromSync(bytes);
|
||||||
|
writer.closeSync();
|
||||||
|
_sendPort.send(const _Command(_Codes.ack, arg0: null));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Perform the find entry operation.
|
||||||
|
void _doFind(String query) {
|
||||||
|
debugPrint('Performing find: $query');
|
||||||
|
File file = File(_path);
|
||||||
|
if (file.existsSync()) {
|
||||||
|
RandomAccessFile reader = file.openSync();
|
||||||
|
List<int> buffer = List.filled(_entrySize, 0);
|
||||||
|
while (reader.readIntoSync(buffer) == _entrySize) {
|
||||||
|
List<int> foo = buffer.takeWhile((value) => value != 0).toList();
|
||||||
|
String string = utf8.decode(foo);
|
||||||
|
if (string.contains(query)) {
|
||||||
|
_sendPort.send(_Command(_Codes.result, arg0: string));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.closeSync();
|
||||||
|
}
|
||||||
|
_sendPort.send(const _Command(_Codes.done, arg0: null));
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
name: background_isolate_channels
|
||||||
|
description: A new Flutter project.
|
||||||
|
|
||||||
|
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
||||||
|
|
||||||
|
version: 1.0.0+1
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: '>=2.19.0-224.0.dev <3.0.0'
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
cupertino_icons: ^1.0.2
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
intl: ^0.18.0
|
||||||
|
path: ^1.8.2
|
||||||
|
path_provider: ^2.0.11
|
||||||
|
shared_preferences: ^2.0.15
|
||||||
|
uuid: ^3.0.6
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_lints: ^2.0.1
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 295 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 450 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 462 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 704 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 586 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 762 B |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 295 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 450 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 462 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 704 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 586 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 762 B |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.4 KiB |