Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Sign in
Toggle navigation
F
Front-End
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
abdullh.alsoleman
Front-End
Commits
a04ab712
Unverified
Commit
a04ab712
authored
Jan 26, 2023
by
Christopher Fujino
Committed by
GitHub
Jan 26, 2023
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Revert "Add API for discovering assets (#118410)" (#119273)
This reverts commit
2b8f2d05
.
parent
2b8f2d05
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
14 additions
and
358 deletions
+14
-358
decode_and_parse_asset_manifest.dart
...marks/lib/foundation/decode_and_parse_asset_manifest.dart
+10
-3
services.dart
packages/flutter/lib/services.dart
+0
-1
asset_bundle.dart
packages/flutter/lib/src/services/asset_bundle.dart
+2
-73
asset_manifest.dart
packages/flutter/lib/src/services/asset_manifest.dart
+0
-134
asset_bundle_test.dart
packages/flutter/test/services/asset_bundle_test.dart
+2
-79
asset_manifest_test.dart
packages/flutter/test/services/asset_manifest_test.dart
+0
-68
No files found.
dev/benchmarks/microbenchmarks/lib/foundation/decode_and_parse_asset_manifest.dart
View file @
a04ab712
...
...
@@ -2,7 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/services.dart'
show
AssetManifest
,
PlatformAssetBundle
,
rootBundle
;
import
'dart:convert'
;
import
'package:flutter/foundation.dart'
;
import
'package:flutter/services.dart'
show
PlatformAssetBundle
;
import
'package:flutter/widgets.dart'
;
import
'../common.dart'
;
...
...
@@ -15,12 +18,16 @@ void main() async {
final
BenchmarkResultPrinter
printer
=
BenchmarkResultPrinter
();
WidgetsFlutterBinding
.
ensureInitialized
();
final
Stopwatch
watch
=
Stopwatch
();
final
PlatformAssetBundle
bundle
=
rootBundle
as
PlatformAssetBundle
;
final
PlatformAssetBundle
bundle
=
PlatformAssetBundle
()
;
final
ByteData
assetManifestBytes
=
await
bundle
.
load
(
'money_asset_manifest.json'
);
watch
.
start
();
for
(
int
i
=
0
;
i
<
_kNumIterations
;
i
++)
{
await
AssetManifest
.
loadFromAssetBundle
(
bundle
);
bundle
.
clear
();
final
String
json
=
utf8
.
decode
(
assetManifestBytes
.
buffer
.
asUint8List
());
// This is a test, so we don't need to worry about this rule.
// ignore: invalid_use_of_visible_for_testing_member
await
AssetImage
.
manifestParser
(
json
);
}
watch
.
stop
();
...
...
packages/flutter/lib/services.dart
View file @
a04ab712
...
...
@@ -11,7 +11,6 @@
library
services
;
export
'src/services/asset_bundle.dart'
;
export
'src/services/asset_manifest.dart'
;
export
'src/services/autofill.dart'
;
export
'src/services/binary_messenger.dart'
;
export
'src/services/binding.dart'
;
...
...
packages/flutter/lib/src/services/asset_bundle.dart
View file @
a04ab712
...
...
@@ -96,22 +96,12 @@ abstract class AssetBundle {
}
/// Retrieve a string from the asset bundle, parse it with the given function,
/// and return th
at
function's result.
/// and return th
e
function's result.
///
/// Implementations may cache the result, so a particular key should only be
/// used with one parser for the lifetime of the asset bundle.
Future
<
T
>
loadStructuredData
<
T
>(
String
key
,
Future
<
T
>
Function
(
String
value
)
parser
);
/// Retrieve [ByteData] from the asset bundle, parse it with the given function,
/// and return that function's result.
///
/// Implementations may cache the result, so a particular key should only be
/// used with one parser for the lifetime of the asset bundle.
Future
<
T
>
loadStructuredBinaryData
<
T
>(
String
key
,
FutureOr
<
T
>
Function
(
ByteData
data
)
parser
)
async
{
final
ByteData
data
=
await
load
(
key
);
return
parser
(
data
);
}
/// If this is a caching asset bundle, and the given key describes a cached
/// asset, then evict the asset from the cache so that the next time it is
/// loaded, the cache will be reread from the asset bundle.
...
...
@@ -164,16 +154,6 @@ class NetworkAssetBundle extends AssetBundle {
return
parser
(
await
loadString
(
key
));
}
/// Retrieve [ByteData] from the asset bundle, parse it with the given function,
/// and return the function's result.
///
/// The result is not cached. The parser is run each time the resource is
/// fetched.
@override
Future
<
T
>
loadStructuredBinaryData
<
T
>(
String
key
,
FutureOr
<
T
>
Function
(
ByteData
data
)
parser
)
async
{
return
parser
(
await
load
(
key
));
}
// TODO(ianh): Once the underlying network logic learns about caching, we
// should implement evict().
...
...
@@ -193,7 +173,6 @@ abstract class CachingAssetBundle extends AssetBundle {
// TODO(ianh): Replace this with an intelligent cache, see https://github.com/flutter/flutter/issues/3568
final
Map
<
String
,
Future
<
String
>>
_stringCache
=
<
String
,
Future
<
String
>>{};
final
Map
<
String
,
Future
<
dynamic
>>
_structuredDataCache
=
<
String
,
Future
<
dynamic
>>{};
final
Map
<
String
,
Future
<
dynamic
>>
_structuredBinaryDataCache
=
<
String
,
Future
<
dynamic
>>{};
@override
Future
<
String
>
loadString
(
String
key
,
{
bool
cache
=
true
})
{
...
...
@@ -242,66 +221,16 @@ abstract class CachingAssetBundle extends AssetBundle {
return
completer
.
future
;
}
/// Retrieve bytedata from the asset bundle, parse it with the given function,
/// and return the function's result.
///
/// The result of parsing the bytedata is cached (the bytedata itself is not).
/// For any given `key`, the `parser` is only run the first time.
///
/// Once the value has been parsed, the future returned by this function for
/// subsequent calls will be a [SynchronousFuture], which resolves its
/// callback synchronously.
@override
Future
<
T
>
loadStructuredBinaryData
<
T
>(
String
key
,
FutureOr
<
T
>
Function
(
ByteData
data
)
parser
)
{
if
(
_structuredBinaryDataCache
.
containsKey
(
key
))
{
return
_structuredBinaryDataCache
[
key
]!
as
Future
<
T
>;
}
// load can return a SynchronousFuture in certain cases, like in the
// flutter_test framework. So, we need to support both async and sync flows.
Completer
<
T
>?
completer
;
// For async flow.
SynchronousFuture
<
T
>?
result
;
// For sync flow.
load
(
key
)
.
then
<
T
>(
parser
)
.
then
<
void
>((
T
value
)
{
result
=
SynchronousFuture
<
T
>(
value
);
if
(
completer
!=
null
)
{
// The load and parse operation ran asynchronously. We already returned
// from the loadStructuredBinaryData function and therefore the caller
// was given the future of the completer.
completer
.
complete
(
value
);
}
},
onError:
(
Object
error
,
StackTrace
stack
)
{
completer
!.
completeError
(
error
,
stack
);
});
if
(
result
!=
null
)
{
// The above code ran synchronously. We can synchronously return the result.
_structuredBinaryDataCache
[
key
]
=
result
!;
return
result
!;
}
// Since the above code is being run asynchronously and thus hasn't run its
// `then` handler yet, we'll return a completer that will be completed
// when the handler does run.
completer
=
Completer
<
T
>();
_structuredBinaryDataCache
[
key
]
=
completer
.
future
;
return
completer
.
future
;
}
@override
void
evict
(
String
key
)
{
_stringCache
.
remove
(
key
);
_structuredDataCache
.
remove
(
key
);
_structuredBinaryDataCache
.
remove
(
key
);
}
@override
void
clear
()
{
_stringCache
.
clear
();
_structuredDataCache
.
clear
();
_structuredBinaryDataCache
.
clear
();
}
@override
...
...
@@ -343,7 +272,7 @@ class PlatformAssetBundle extends CachingAssetBundle {
bool
debugUsePlatformChannel
=
false
;
assert
(()
{
// dart:io is safe to use here since we early return for web
// above. If that code is changed, this needs to be g
ua
rded on
// above. If that code is changed, this needs to be g
au
rded on
// web presence. Override how assets are loaded in tests so that
// the old loader behavior that allows tests to load assets from
// the current package using the package prefix.
...
...
packages/flutter/lib/src/services/asset_manifest.dart
deleted
100644 → 0
View file @
2b8f2d05
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/foundation.dart'
;
import
'asset_bundle.dart'
;
import
'message_codecs.dart'
;
const
String
_kAssetManifestFilename
=
'AssetManifest.bin'
;
/// Contains details about available assets and their variants.
/// See [Asset variants](https://docs.flutter.dev/development/ui/assets-and-images#asset-variants)
/// to learn about asset variants and how to declare them.
abstract
class
AssetManifest
{
/// Loads asset manifest data from an [AssetBundle] object and creates an
/// [AssetManifest] object from that data.
static
Future
<
AssetManifest
>
loadFromAssetBundle
(
AssetBundle
bundle
)
{
return
bundle
.
loadStructuredBinaryData
(
_kAssetManifestFilename
,
_AssetManifestBin
.
fromStandardMessageCodecMessage
);
}
/// Lists the keys of all main assets. This does not include assets
/// that are variants of other assets.
///
/// The logical key maps to the path of an asset specified in the pubspec.yaml
/// file at build time.
///
/// See [Specifying assets](https://docs.flutter.dev/development/ui/assets-and-images#specifying-assets)
/// and [Loading assets](https://docs.flutter.dev/development/ui/assets-and-images#loading-assets) for more
/// information.
List
<
String
>
listAssets
();
/// Retrieves metadata about an asset and its variants.
///
/// Note that this method considers a main asset to be a variant of itself and
/// includes it in the returned list.
///
/// Throws an [ArgumentError] if [key] cannot be found within the manifest. To
/// avoid this, use a key obtained from the [listAssets] method.
List
<
AssetMetadata
>
getAssetVariants
(
String
key
);
}
// Lazily parses the binary asset manifest into a data structure that's easier to work
// with.
//
// The binary asset manifest is a map of asset keys to a list of objects
// representing the asset's variants.
//
// The entries with each variant object are:
// - "asset": the location of this variant to load it from.
// - "dpr": The device-pixel-ratio that the asset is best-suited for.
//
// New fields could be added to this object schema to support new asset variation
// features, such as themes, locale/region support, reading directions, and so on.
class
_AssetManifestBin
implements
AssetManifest
{
_AssetManifestBin
(
Map
<
Object
?,
Object
?>
standardMessageData
):
_data
=
standardMessageData
;
factory
_AssetManifestBin
.
fromStandardMessageCodecMessage
(
ByteData
message
)
{
final
dynamic
data
=
const
StandardMessageCodec
().
decodeMessage
(
message
);
return
_AssetManifestBin
(
data
as
Map
<
Object
?,
Object
?>);
}
final
Map
<
Object
?,
Object
?>
_data
;
final
Map
<
String
,
List
<
AssetMetadata
>>
_typeCastedData
=
<
String
,
List
<
AssetMetadata
>>{};
@override
List
<
AssetMetadata
>
getAssetVariants
(
String
key
)
{
// We lazily delay typecasting to prevent a performance hiccup when parsing
// large asset manifests. This is important to keep an app's first asset
// load fast.
if
(!
_typeCastedData
.
containsKey
(
key
))
{
final
Object
?
variantData
=
_data
[
key
];
if
(
variantData
==
null
)
{
throw
ArgumentError
(
'Asset key
$key
was not found within the asset manifest.'
);
}
_typeCastedData
[
key
]
=
((
_data
[
key
]
??
<
Object
?>[])
as
Iterable
<
Object
?>)
.
cast
<
Map
<
Object
?,
Object
?>>()
.
map
((
Map
<
Object
?,
Object
?>
data
)
=>
AssetMetadata
(
key:
data
[
'asset'
]!
as
String
,
targetDevicePixelRatio:
data
[
'dpr'
]!
as
double
,
main:
false
,
))
.
toList
();
_data
.
remove
(
key
);
}
final
AssetMetadata
mainAsset
=
AssetMetadata
(
key:
key
,
targetDevicePixelRatio:
null
,
main:
true
);
return
<
AssetMetadata
>[
mainAsset
,
...
_typeCastedData
[
key
]!];
}
@override
List
<
String
>
listAssets
()
{
return
<
String
>[...
_data
.
keys
.
cast
<
String
>(),
...
_typeCastedData
.
keys
];
}
}
/// Contains information about an asset.
@immutable
class
AssetMetadata
{
/// Creates an object containing information about an asset.
const
AssetMetadata
({
required
this
.
key
,
required
this
.
targetDevicePixelRatio
,
required
this
.
main
,
});
/// The device pixel ratio that this asset is most ideal for. This is determined
/// by the name of the parent folder of the asset file. For example, if the
/// parent folder is named "3.0x", the target device pixel ratio of that
/// asset will be interpreted as 3.
///
/// This will be null if the parent folder name is not a ratio value followed
/// by an "x".
///
/// See [Declaring resolution-aware image assets](https://docs.flutter.dev/development/ui/assets-and-images#resolution-aware)
/// for more information.
final
double
?
targetDevicePixelRatio
;
/// The asset's key, which is the path to the asset specified in the pubspec.yaml
/// file at build time.
final
String
key
;
/// Whether or not this is a main asset. In other words, this is true if
/// this asset is not a variant of another asset.
///
/// See [Asset variants](https://docs.flutter.dev/development/ui/assets-and-images#asset-variants)
/// for more about asset variants.
final
bool
main
;
}
packages/flutter/test/services/asset_bundle_test.dart
View file @
a04ab712
...
...
@@ -14,28 +14,16 @@ class TestAssetBundle extends CachingAssetBundle {
@override
Future
<
ByteData
>
load
(
String
key
)
async
{
loadCallCount
[
key
]
=
(
loadCallCount
[
key
]
??
0
)
+
1
;
loadCallCount
[
key
]
=
loadCallCount
[
key
]
??
0
+
1
;
if
(
key
==
'AssetManifest.json'
)
{
return
ByteData
.
view
(
Uint8List
.
fromList
(
const
Utf8Encoder
().
convert
(
'{"one": ["one"]}'
)).
buffer
);
}
if
(
key
==
'AssetManifest.bin'
)
{
return
const
StandardMessageCodec
().
encodeMessage
(<
String
,
Object
>{
'one'
:
<
Object
>[]
})!;
}
if
(
key
==
'counter'
)
{
return
ByteData
.
view
(
Uint8List
.
fromList
(
const
Utf8Encoder
().
convert
(
loadCallCount
[
key
]!.
toString
())).
buffer
);
}
if
(
key
==
'one'
)
{
return
ByteData
(
1
)..
setInt8
(
0
,
49
);
}
throw
FlutterError
(
'key not found'
);
}
}
void
main
(
)
{
...
...
@@ -52,7 +40,7 @@ void main() {
final
String
assetString
=
await
bundle
.
loadString
(
'one'
);
expect
(
assetString
,
equals
(
'1'
));
expect
(
bundle
.
loadCallCount
[
'one'
],
2
);
expect
(
bundle
.
loadCallCount
[
'one'
],
1
);
late
Object
loadException
;
try
{
...
...
@@ -113,69 +101,4 @@ void main() {
),
);
},
skip:
isBrowser
);
// https://github.com/flutter/flutter/issues/56314
test
(
'CachingAssetBundle caches results for loadString, loadStructuredData, and loadBinaryStructuredData'
,
()
async
{
final
TestAssetBundle
bundle
=
TestAssetBundle
();
final
String
firstLoadStringResult
=
await
bundle
.
loadString
(
'counter'
);
final
String
secondLoadStringResult
=
await
bundle
.
loadString
(
'counter'
);
expect
(
firstLoadStringResult
,
'1'
);
expect
(
secondLoadStringResult
,
'1'
);
final
String
firstLoadStructuredDataResult
=
await
bundle
.
loadStructuredData
(
'AssetManifest.json'
,
(
String
value
)
=>
Future
<
String
>.
value
(
'one'
));
final
String
secondLoadStructuredDataResult
=
await
bundle
.
loadStructuredData
(
'AssetManifest.json'
,
(
String
value
)
=>
Future
<
String
>.
value
(
'two'
));
expect
(
firstLoadStructuredDataResult
,
'one'
);
expect
(
secondLoadStructuredDataResult
,
'one'
);
final
String
firstLoadStructuredBinaryDataResult
=
await
bundle
.
loadStructuredBinaryData
(
'AssetManifest.bin'
,
(
ByteData
value
)
=>
Future
<
String
>.
value
(
'one'
));
final
String
secondLoadStructuredBinaryDataResult
=
await
bundle
.
loadStructuredBinaryData
(
'AssetManifest.bin'
,
(
ByteData
value
)
=>
Future
<
String
>.
value
(
'two'
));
expect
(
firstLoadStructuredBinaryDataResult
,
'one'
);
expect
(
secondLoadStructuredBinaryDataResult
,
'one'
);
});
test
(
"CachingAssetBundle.clear clears all cached values'"
,
()
async
{
final
TestAssetBundle
bundle
=
TestAssetBundle
();
await
bundle
.
loadString
(
'counter'
);
bundle
.
clear
();
final
String
secondLoadStringResult
=
await
bundle
.
loadString
(
'counter'
);
expect
(
secondLoadStringResult
,
'2'
);
await
bundle
.
loadStructuredData
(
'AssetManifest.json'
,
(
String
value
)
=>
Future
<
String
>.
value
(
'one'
));
bundle
.
clear
();
final
String
secondLoadStructuredDataResult
=
await
bundle
.
loadStructuredData
(
'AssetManifest.json'
,
(
String
value
)
=>
Future
<
String
>.
value
(
'two'
));
expect
(
secondLoadStructuredDataResult
,
'two'
);
await
bundle
.
loadStructuredBinaryData
(
'AssetManifest.bin'
,
(
ByteData
value
)
=>
Future
<
String
>.
value
(
'one'
));
bundle
.
clear
();
final
String
secondLoadStructuredBinaryDataResult
=
await
bundle
.
loadStructuredBinaryData
(
'AssetManifest.bin'
,
(
ByteData
value
)
=>
Future
<
String
>.
value
(
'two'
));
expect
(
secondLoadStructuredBinaryDataResult
,
'two'
);
});
test
(
'CachingAssetBundle.evict evicts a particular key from the cache'
,
()
async
{
final
TestAssetBundle
bundle
=
TestAssetBundle
();
await
bundle
.
loadString
(
'counter'
);
bundle
.
evict
(
'counter'
);
final
String
secondLoadStringResult
=
await
bundle
.
loadString
(
'counter'
);
expect
(
secondLoadStringResult
,
'2'
);
await
bundle
.
loadStructuredData
(
'AssetManifest.json'
,
(
String
value
)
=>
Future
<
String
>.
value
(
'one'
));
bundle
.
evict
(
'AssetManifest.json'
);
final
String
secondLoadStructuredDataResult
=
await
bundle
.
loadStructuredData
(
'AssetManifest.json'
,
(
String
value
)
=>
Future
<
String
>.
value
(
'two'
));
expect
(
secondLoadStructuredDataResult
,
'two'
);
await
bundle
.
loadStructuredBinaryData
(
'AssetManifest.bin'
,
(
ByteData
value
)
=>
Future
<
String
>.
value
(
'one'
));
bundle
.
evict
(
'AssetManifest.bin'
);
final
String
secondLoadStructuredBinaryDataResult
=
await
bundle
.
loadStructuredBinaryData
(
'AssetManifest.bin'
,
(
ByteData
value
)
=>
Future
<
String
>.
value
(
'two'
));
expect
(
secondLoadStructuredBinaryDataResult
,
'two'
);
});
test
(
'loadStructuredBinaryData correctly loads ByteData'
,
()
async
{
final
TestAssetBundle
bundle
=
TestAssetBundle
();
final
Map
<
Object
?,
Object
?>
assetManifest
=
await
bundle
.
loadStructuredBinaryData
(
'AssetManifest.bin'
,
(
ByteData
data
)
=>
const
StandardMessageCodec
().
decodeMessage
(
data
)
as
Map
<
Object
?,
Object
?>);
expect
(
assetManifest
.
keys
.
toList
(),
equals
(<
String
>[
'one'
]));
expect
(
assetManifest
[
'one'
],
<
Object
>[]);
});
}
packages/flutter/test/services/asset_manifest_test.dart
deleted
100644 → 0
View file @
2b8f2d05
// Copyright 2014 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import
'package:flutter/services.dart'
;
import
'package:flutter_test/flutter_test.dart'
;
class
TestAssetBundle
extends
AssetBundle
{
@override
Future
<
ByteData
>
load
(
String
key
)
async
{
if
(
key
==
'AssetManifest.bin'
)
{
final
Map
<
String
,
List
<
Object
>>
binManifestData
=
<
String
,
List
<
Object
>>{
'assets/foo.png'
:
<
Object
>[
<
String
,
Object
>{
'asset'
:
'assets/2x/foo.png'
,
'dpr'
:
2.0
}
],
'assets/bar.png'
:
<
Object
>[],
};
final
ByteData
data
=
const
StandardMessageCodec
().
encodeMessage
(
binManifestData
)!;
return
data
;
}
throw
ArgumentError
(
'Unexpected key'
);
}
@override
Future
<
T
>
loadStructuredData
<
T
>(
String
key
,
Future
<
T
>
Function
(
String
value
)
parser
)
async
{
return
parser
(
await
loadString
(
key
));
}
}
void
main
(
)
{
TestWidgetsFlutterBinding
.
ensureInitialized
();
test
(
'loadFromBundle correctly parses a binary asset manifest'
,
()
async
{
final
AssetManifest
manifest
=
await
AssetManifest
.
loadFromAssetBundle
(
TestAssetBundle
());
expect
(
manifest
.
listAssets
(),
unorderedEquals
(<
String
>[
'assets/foo.png'
,
'assets/bar.png'
]));
final
List
<
AssetMetadata
>
fooVariants
=
manifest
.
getAssetVariants
(
'assets/foo.png'
);
expect
(
fooVariants
.
length
,
2
);
final
AssetMetadata
firstFooVariant
=
fooVariants
[
0
];
expect
(
firstFooVariant
.
key
,
'assets/foo.png'
);
expect
(
firstFooVariant
.
targetDevicePixelRatio
,
null
);
expect
(
firstFooVariant
.
main
,
true
);
final
AssetMetadata
secondFooVariant
=
fooVariants
[
1
];
expect
(
secondFooVariant
.
key
,
'assets/2x/foo.png'
);
expect
(
secondFooVariant
.
targetDevicePixelRatio
,
2.0
);
expect
(
secondFooVariant
.
main
,
false
);
final
List
<
AssetMetadata
>
barVariants
=
manifest
.
getAssetVariants
(
'assets/bar.png'
);
expect
(
barVariants
.
length
,
1
);
final
AssetMetadata
firstBarVariant
=
barVariants
[
0
];
expect
(
firstBarVariant
.
key
,
'assets/bar.png'
);
expect
(
firstBarVariant
.
targetDevicePixelRatio
,
null
);
expect
(
firstBarVariant
.
main
,
true
);
});
test
(
'getAssetVariants throws if given a key not contained in the asset manifest'
,
()
async
{
final
AssetManifest
manifest
=
await
AssetManifest
.
loadFromAssetBundle
(
TestAssetBundle
());
expect
(()
=>
manifest
.
getAssetVariants
(
'invalid asset key'
),
throwsArgumentError
);
});
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment