avm2: Implement FileReference browse
This commit is contained in:
parent
e7b8b75d07
commit
fff841a22e
|
@ -325,6 +325,34 @@ pub fn make_error_2008<'gc>(activation: &mut Activation<'_, 'gc>, param_name: &s
|
|||
}
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
#[cold]
|
||||
pub fn make_error_2037<'gc>(activation: &mut Activation<'_, 'gc>) -> Error<'gc> {
|
||||
let err = error(
|
||||
activation,
|
||||
"Error #2037: Functions called in incorrect sequence, or earlier call was unsuccessful.",
|
||||
2037,
|
||||
);
|
||||
match err {
|
||||
Ok(err) => Error::AvmError(err),
|
||||
Err(err) => err,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
#[cold]
|
||||
pub fn make_error_2097<'gc>(activation: &mut Activation<'_, 'gc>) -> Error<'gc> {
|
||||
let err = argument_error(
|
||||
activation,
|
||||
"Error #2097: The FileFilter Array is not in the correct format.",
|
||||
2097,
|
||||
);
|
||||
match err {
|
||||
Ok(err) => Error::AvmError(err),
|
||||
Err(err) => err,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline(never)]
|
||||
#[cold]
|
||||
pub fn make_error_2126<'gc>(activation: &mut Activation<'_, 'gc>) -> Error<'gc> {
|
||||
|
|
|
@ -168,6 +168,7 @@ pub struct SystemClasses<'gc> {
|
|||
pub statusevent: ClassObject<'gc>,
|
||||
pub contextmenuevent: ClassObject<'gc>,
|
||||
pub filereference: ClassObject<'gc>,
|
||||
pub filefilter: ClassObject<'gc>,
|
||||
pub font: ClassObject<'gc>,
|
||||
pub textline: ClassObject<'gc>,
|
||||
pub sampledataevent: ClassObject<'gc>,
|
||||
|
@ -298,6 +299,7 @@ impl<'gc> SystemClasses<'gc> {
|
|||
statusevent: object,
|
||||
contextmenuevent: object,
|
||||
filereference: object,
|
||||
filefilter: object,
|
||||
font: object,
|
||||
textline: object,
|
||||
sampledataevent: object,
|
||||
|
@ -809,6 +811,7 @@ fn load_playerglobal<'gc>(
|
|||
("flash.media", "Video", video),
|
||||
("flash.net", "URLVariables", urlvariables),
|
||||
("flash.net", "FileReference", filereference),
|
||||
("flash.net", "FileFilter", filefilter),
|
||||
("flash.utils", "ByteArray", bytearray),
|
||||
("flash.system", "ApplicationDomain", application_domain),
|
||||
("flash.text", "Font", font),
|
||||
|
|
|
@ -4,8 +4,8 @@ use crate::avm2::error::type_error;
|
|||
use crate::avm2::object::TObject;
|
||||
use crate::avm2::{Activation, Error, Object, Value};
|
||||
|
||||
pub mod local_connection;
|
||||
pub mod file_reference;
|
||||
pub mod local_connection;
|
||||
pub mod net_connection;
|
||||
pub mod net_stream;
|
||||
pub mod object_encoding;
|
||||
|
|
|
@ -9,7 +9,6 @@ package flash.net
|
|||
{
|
||||
private var _creationDate: Date;
|
||||
private var _creator: String;
|
||||
private var _data: ByteArray;
|
||||
private var _extension: String;
|
||||
private var _modificationDate: Date;
|
||||
private var _name: String;
|
||||
|
@ -29,9 +28,7 @@ package flash.net
|
|||
return this._creator;
|
||||
}
|
||||
|
||||
public function get data(): ByteArray {
|
||||
return this._data;
|
||||
}
|
||||
public native function get data(): ByteArray;
|
||||
|
||||
public function get extension(): String {
|
||||
return this._extension;
|
||||
|
@ -57,9 +54,7 @@ package flash.net
|
|||
return this._type;
|
||||
}
|
||||
|
||||
public function browse(typeFilter:Array = null):Boolean {
|
||||
return false;
|
||||
}
|
||||
public native function browse(typeFilter:Array = null): Boolean;
|
||||
|
||||
public function cancel():void {
|
||||
stub_method("flash.net.FileReference", "cancel");
|
||||
|
@ -69,9 +64,7 @@ package flash.net
|
|||
stub_method("flash.net.FileReference", "download");
|
||||
}
|
||||
|
||||
public function load():void {
|
||||
stub_method("flash.net.FileReference", "load");
|
||||
}
|
||||
public native function load():void;
|
||||
|
||||
public function requestPermission():void {
|
||||
stub_method("flash.net.FileReference", "requestPermission");
|
||||
|
|
|
@ -1 +1,132 @@
|
|||
pub use crate::avm2::object::file_reference_allocator;
|
||||
use crate::avm2::bytearray::ByteArrayStorage;
|
||||
use crate::avm2::error::{make_error_2037, make_error_2097};
|
||||
pub use crate::avm2::object::file_reference_allocator;
|
||||
use crate::avm2::object::{ByteArrayObject, FileReference};
|
||||
use crate::avm2::{Activation, Avm2, Error, EventObject, Object, TObject, Value};
|
||||
use crate::backend::ui::FileFilter;
|
||||
|
||||
pub fn get_data<'gc>(
|
||||
activation: &mut Activation<'_, 'gc>,
|
||||
this: Object<'gc>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<Value<'gc>, Error<'gc>> {
|
||||
let this = this.as_file_reference().unwrap();
|
||||
|
||||
let bytearray = match *this.file_reference() {
|
||||
FileReference::FileDialogResult(ref dialog_result) if this.loaded() => {
|
||||
let bytes = dialog_result.contents();
|
||||
let storage = ByteArrayStorage::from_vec(bytes.to_vec());
|
||||
ByteArrayObject::from_storage(activation, storage)?
|
||||
}
|
||||
// Contrary to other getters `data` will return null instead of throwing.
|
||||
_ => return Ok(Value::Null),
|
||||
};
|
||||
|
||||
Ok(bytearray.into())
|
||||
}
|
||||
|
||||
pub fn browse<'gc>(
|
||||
activation: &mut Activation<'_, 'gc>,
|
||||
this: Object<'gc>,
|
||||
args: &[Value<'gc>],
|
||||
) -> Result<Value<'gc>, Error<'gc>> {
|
||||
let this = this.as_file_reference().unwrap();
|
||||
|
||||
let mut filters = Vec::new();
|
||||
if let Value::Object(obj) = args[0] {
|
||||
if let Some(array_storage) = obj.as_array_storage() {
|
||||
for filter in array_storage.iter() {
|
||||
if let Some(Value::Object(obj)) = filter {
|
||||
let filefilter = activation
|
||||
.avm2()
|
||||
.classes()
|
||||
.filefilter
|
||||
.inner_class_definition();
|
||||
if !obj.is_of_type(filefilter, &mut activation.context) {
|
||||
return Err(make_error_2097(activation));
|
||||
}
|
||||
|
||||
let description = obj.get_public_property("description", activation)?;
|
||||
let extension = obj.get_public_property("extension", activation)?;
|
||||
let mac_type = obj.get_public_property("macType", activation)?;
|
||||
|
||||
// The description and extension must be non-empty strings.
|
||||
match (description, extension) {
|
||||
(Value::String(description), Value::String(extension))
|
||||
if !description.is_empty() && !extension.is_empty() =>
|
||||
{
|
||||
let mac_type = match mac_type {
|
||||
Value::String(mac_type) if !mac_type.is_empty() => {
|
||||
Some(mac_type.to_string())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
|
||||
filters.push(FileFilter {
|
||||
description: description.to_string(),
|
||||
extensions: extension.to_string(),
|
||||
mac_type,
|
||||
});
|
||||
}
|
||||
_ => return Err(make_error_2097(activation)),
|
||||
}
|
||||
} else {
|
||||
return Err(make_error_2097(activation));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let dialog = activation.context.ui.display_file_open_dialog(filters);
|
||||
let result = match dialog {
|
||||
Some(dialog) => {
|
||||
let process = activation.context.load_manager.select_file_dialog_avm2(
|
||||
activation.context.player.clone(),
|
||||
this,
|
||||
dialog,
|
||||
);
|
||||
|
||||
activation.context.navigator.spawn_future(process);
|
||||
true
|
||||
}
|
||||
None => false,
|
||||
};
|
||||
|
||||
Ok(result.into())
|
||||
}
|
||||
|
||||
pub fn load<'gc>(
|
||||
activation: &mut Activation<'_, 'gc>,
|
||||
this: Object<'gc>,
|
||||
_args: &[Value<'gc>],
|
||||
) -> Result<Value<'gc>, Error<'gc>> {
|
||||
let this = this.as_file_reference().unwrap();
|
||||
|
||||
// Somewhat unexpectedly, we don't need to load anything here, because
|
||||
// that already happened during browse() or save().
|
||||
|
||||
let size = match *this.file_reference() {
|
||||
FileReference::None => return Err(make_error_2037(activation)),
|
||||
FileReference::FileDialogResult(ref dialog_result) => dialog_result.size().unwrap_or(0),
|
||||
};
|
||||
|
||||
let open_evt = EventObject::bare_default_event(&mut activation.context, "open");
|
||||
Avm2::dispatch_event(&mut activation.context, open_evt, this.into());
|
||||
|
||||
let progress_evt = EventObject::progress_event(activation, "progress", 0, size, false, false);
|
||||
Avm2::dispatch_event(&mut activation.context, progress_evt, this.into());
|
||||
|
||||
let open_evt2 = EventObject::bare_default_event(&mut activation.context, "open");
|
||||
Avm2::dispatch_event(&mut activation.context, open_evt2, this.into());
|
||||
|
||||
let progress_evt2 =
|
||||
EventObject::progress_event(activation, "progress", size, size, false, false);
|
||||
Avm2::dispatch_event(&mut activation.context, progress_evt2, this.into());
|
||||
|
||||
this.set_loaded(true);
|
||||
|
||||
let complete_evt = EventObject::bare_default_event(&mut activation.context, "complete");
|
||||
Avm2::dispatch_event(&mut activation.context, complete_evt, this.into());
|
||||
|
||||
Ok(Value::Undefined)
|
||||
}
|
||||
|
|
|
@ -88,7 +88,9 @@ pub use crate::avm2::object::domain_object::{
|
|||
};
|
||||
pub use crate::avm2::object::error_object::{error_allocator, ErrorObject, ErrorObjectWeak};
|
||||
pub use crate::avm2::object::event_object::{event_allocator, EventObject, EventObjectWeak};
|
||||
pub use crate::avm2::object::file_reference_object::{file_reference_allocator, FileReferenceObject, FileReferenceObjectWeak};
|
||||
pub use crate::avm2::object::file_reference_object::{
|
||||
file_reference_allocator, FileReference, FileReferenceObject, FileReferenceObjectWeak,
|
||||
};
|
||||
pub use crate::avm2::object::font_object::{font_allocator, FontObject, FontObjectWeak};
|
||||
pub use crate::avm2::object::function_object::{
|
||||
function_allocator, FunctionObject, FunctionObjectWeak,
|
||||
|
|
|
@ -211,6 +211,38 @@ impl<'gc> EventObject<'gc> {
|
|||
)
|
||||
.unwrap() // we don't expect to break here
|
||||
}
|
||||
|
||||
pub fn progress_event<S>(
|
||||
activation: &mut Activation<'_, 'gc>,
|
||||
event_type: S,
|
||||
bytes_loaded: u64,
|
||||
bytes_total: u64,
|
||||
bubbles: bool,
|
||||
cancelable: bool,
|
||||
) -> Object<'gc>
|
||||
where
|
||||
S: Into<AvmString<'gc>>,
|
||||
{
|
||||
let event_type: AvmString<'gc> = event_type.into();
|
||||
|
||||
let progress_event_cls = activation.avm2().classes().progressevent;
|
||||
progress_event_cls
|
||||
.construct(
|
||||
activation,
|
||||
&[
|
||||
event_type.into(),
|
||||
// bubbles
|
||||
bubbles.into(),
|
||||
// cancelable
|
||||
cancelable.into(),
|
||||
// bytesLoaded
|
||||
(bytes_loaded as f64).into(),
|
||||
// bytesToal
|
||||
(bytes_total as f64).into(),
|
||||
],
|
||||
)
|
||||
.unwrap() // we don't expect to break here
|
||||
}
|
||||
}
|
||||
|
||||
impl<'gc> TObject<'gc> for EventObject<'gc> {
|
||||
|
|
|
@ -2,6 +2,7 @@ use crate::avm2::object::script_object::ScriptObjectData;
|
|||
use crate::avm2::object::{ClassObject, Object, ObjectPtr, TObject};
|
||||
use crate::avm2::value::Value;
|
||||
use crate::avm2::{Activation, Error};
|
||||
use crate::backend::ui::FileDialogResult;
|
||||
use gc_arena::barrier::unlock;
|
||||
use gc_arena::{lock::RefLock, Collect, Gc};
|
||||
use gc_arena::{GcWeak, Mutation};
|
||||
|
@ -18,12 +19,13 @@ pub fn file_reference_allocator<'gc>(
|
|||
activation.context.gc(),
|
||||
FileReferenceObjectData {
|
||||
base,
|
||||
reference: RefCell::new(FileReference::None),
|
||||
loaded: Cell::new(false),
|
||||
},
|
||||
))
|
||||
.into())
|
||||
}
|
||||
|
||||
|
||||
#[derive(Clone, Collect, Copy)]
|
||||
#[collect(no_drop)]
|
||||
pub struct FileReferenceObject<'gc>(pub Gc<'gc, FileReferenceObjectData<'gc>>);
|
||||
|
@ -54,11 +56,40 @@ impl<'gc> TObject<'gc> for FileReferenceObject<'gc> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<'gc> FileReferenceObject<'gc> {
|
||||
pub fn init_from_dialog_result(&self, result: Box<dyn FileDialogResult>) -> FileReference {
|
||||
self.0
|
||||
.reference
|
||||
.replace(FileReference::FileDialogResult(result))
|
||||
}
|
||||
|
||||
pub fn file_reference(&self) -> Ref<'_, FileReference> {
|
||||
self.0.reference.borrow()
|
||||
}
|
||||
|
||||
pub fn set_loaded(&self, value: bool) {
|
||||
self.0.loaded.set(value)
|
||||
}
|
||||
|
||||
pub fn loaded(&self) -> bool {
|
||||
self.0.loaded.get()
|
||||
}
|
||||
}
|
||||
|
||||
pub enum FileReference {
|
||||
None,
|
||||
FileDialogResult(Box<dyn FileDialogResult>),
|
||||
}
|
||||
|
||||
#[derive(Collect)]
|
||||
#[collect(no_drop)]
|
||||
pub struct FileReferenceObjectData<'gc> {
|
||||
/// Base script object
|
||||
base: RefLock<ScriptObjectData<'gc>>,
|
||||
|
||||
reference: RefCell<FileReference>,
|
||||
|
||||
loaded: Cell<bool>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for FileReferenceObject<'_> {
|
||||
|
|
|
@ -6,7 +6,8 @@ use crate::avm1::{ExecutionReason, NativeObject};
|
|||
use crate::avm1::{Object, SoundObject, TObject, Value};
|
||||
use crate::avm2::bytearray::ByteArrayStorage;
|
||||
use crate::avm2::object::{
|
||||
ByteArrayObject, EventObject as Avm2EventObject, LoaderStream, TObject as _,
|
||||
ByteArrayObject, EventObject as Avm2EventObject, FileReferenceObject, LoaderStream,
|
||||
TObject as _,
|
||||
};
|
||||
use crate::avm2::{
|
||||
Activation as Avm2Activation, Avm2, Domain as Avm2Domain, Object as Avm2Object,
|
||||
|
@ -248,6 +249,7 @@ impl<'gc> LoadManager<'gc> {
|
|||
| Loader::SoundAvm2 { self_handle, .. }
|
||||
| Loader::NetStream { self_handle, .. }
|
||||
| Loader::FileDialog { self_handle, .. }
|
||||
| Loader::FileDialogAvm2 { self_handle, .. }
|
||||
| Loader::DownloadFileDialog { self_handle, .. }
|
||||
| Loader::UploadFile { self_handle, .. }
|
||||
| Loader::MovieUnloader { self_handle, .. } => *self_handle = Some(handle),
|
||||
|
@ -504,6 +506,22 @@ impl<'gc> LoadManager<'gc> {
|
|||
loader.file_dialog_loader(player, dialog)
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn select_file_dialog_avm2(
|
||||
&mut self,
|
||||
player: Weak<Mutex<Player>>,
|
||||
target_object: FileReferenceObject<'gc>,
|
||||
dialog: DialogResultFuture,
|
||||
) -> OwnedFuture<(), Error> {
|
||||
let loader = Loader::FileDialogAvm2 {
|
||||
self_handle: None,
|
||||
target_object,
|
||||
};
|
||||
let handle = self.add_loader(loader);
|
||||
let loader = self.get_loader_mut(handle).unwrap();
|
||||
loader.file_dialog_loader(player, dialog)
|
||||
}
|
||||
|
||||
/// Display a dialog allowing a user to download a file
|
||||
///
|
||||
/// Returns a future that will be resolved when a file is selected and the download has completed
|
||||
|
@ -708,6 +726,16 @@ pub enum Loader<'gc> {
|
|||
target_object: Object<'gc>,
|
||||
},
|
||||
|
||||
/// Loader that is choosing a file from an AVM2 scope.
|
||||
FileDialogAvm2 {
|
||||
/// The handle to refer to this loader instance.
|
||||
#[collect(require_static)]
|
||||
self_handle: Option<Handle>,
|
||||
|
||||
/// The target AVM2 object to set to the selected file path.
|
||||
target_object: FileReferenceObject<'gc>,
|
||||
},
|
||||
|
||||
/// Loader that is downloading a file from an AVM1 object scope.
|
||||
DownloadFileDialog {
|
||||
/// The handle to refer to this loader instance.
|
||||
|
@ -2399,7 +2427,7 @@ impl<'gc> Loader<'gc> {
|
|||
dialog: DialogResultFuture,
|
||||
) -> OwnedFuture<(), Error> {
|
||||
let handle = match self {
|
||||
Loader::FileDialog { self_handle, .. } => {
|
||||
Loader::FileDialog { self_handle, .. } | Loader::FileDialogAvm2 { self_handle, .. } => {
|
||||
self_handle.expect("Loader not self-introduced")
|
||||
}
|
||||
_ => return Box::pin(async { Err(Error::NotFileDialogLoader) }),
|
||||
|
@ -2418,50 +2446,89 @@ impl<'gc> Loader<'gc> {
|
|||
// Fire the load handler.
|
||||
player.lock().unwrap().update(|uc| -> Result<(), Error> {
|
||||
let loader = uc.load_manager.get_loader(handle);
|
||||
let target_object = match loader {
|
||||
Some(&Loader::FileDialog { target_object, .. }) => target_object,
|
||||
None => return Err(Error::Cancelled),
|
||||
_ => return Err(Error::NotFileDialogLoader),
|
||||
};
|
||||
match loader {
|
||||
Some(&Loader::FileDialog { target_object, .. }) => {
|
||||
let file_ref = match target_object.native() {
|
||||
NativeObject::FileReference(fr) => fr,
|
||||
_ => panic!("NativeObject must be FileReference"),
|
||||
};
|
||||
|
||||
let file_ref = match target_object.native() {
|
||||
NativeObject::FileReference(fr) => fr,
|
||||
_ => panic!("NativeObject must be FileReference"),
|
||||
};
|
||||
let mut activation = Activation::from_stub(
|
||||
uc.reborrow(),
|
||||
ActivationIdentifier::root("[File Dialog]"),
|
||||
);
|
||||
|
||||
let mut activation = Activation::from_stub(
|
||||
uc.reborrow(),
|
||||
ActivationIdentifier::root("[File Dialog]"),
|
||||
);
|
||||
match dialog_result {
|
||||
Ok(dialog_result) => {
|
||||
use crate::avm1::globals::as_broadcaster;
|
||||
|
||||
match dialog_result {
|
||||
Ok(dialog_result) => {
|
||||
use crate::avm1::globals::as_broadcaster;
|
||||
|
||||
if !dialog_result.is_cancelled() {
|
||||
file_ref
|
||||
.init_from_dialog_result(&mut activation, dialog_result.borrow());
|
||||
as_broadcaster::broadcast_internal(
|
||||
&mut activation,
|
||||
target_object,
|
||||
&[target_object.into()],
|
||||
"onSelect".into(),
|
||||
)?;
|
||||
} else {
|
||||
as_broadcaster::broadcast_internal(
|
||||
&mut activation,
|
||||
target_object,
|
||||
&[target_object.into()],
|
||||
"onCancel".into(),
|
||||
)?;
|
||||
if !dialog_result.is_cancelled() {
|
||||
file_ref.init_from_dialog_result(
|
||||
&mut activation,
|
||||
dialog_result.borrow(),
|
||||
);
|
||||
as_broadcaster::broadcast_internal(
|
||||
&mut activation,
|
||||
target_object,
|
||||
&[target_object.into()],
|
||||
"onSelect".into(),
|
||||
)?;
|
||||
} else {
|
||||
as_broadcaster::broadcast_internal(
|
||||
&mut activation,
|
||||
target_object,
|
||||
&[target_object.into()],
|
||||
"onCancel".into(),
|
||||
)?;
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Error on file dialog: {:?}", err);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Error on file dialog: {:?}", err);
|
||||
}
|
||||
}
|
||||
Some(&Loader::FileDialogAvm2 { target_object, .. }) => {
|
||||
match dialog_result {
|
||||
Ok(dialog_result) => {
|
||||
if !dialog_result.is_cancelled() {
|
||||
target_object.init_from_dialog_result(dialog_result);
|
||||
|
||||
Ok(())
|
||||
let mut activation =
|
||||
Avm2Activation::from_nothing(uc.reborrow());
|
||||
let select_event = Avm2EventObject::bare_default_event(
|
||||
&mut activation.context,
|
||||
"select",
|
||||
);
|
||||
Avm2::dispatch_event(
|
||||
&mut activation.context,
|
||||
select_event,
|
||||
target_object.into(),
|
||||
);
|
||||
} else {
|
||||
let mut activation =
|
||||
Avm2Activation::from_nothing(uc.reborrow());
|
||||
let cancel_event = Avm2EventObject::bare_default_event(
|
||||
&mut activation.context,
|
||||
"cancel",
|
||||
);
|
||||
Avm2::dispatch_event(
|
||||
&mut activation.context,
|
||||
cancel_event,
|
||||
target_object.into(),
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
tracing::warn!("Error on file dialog: {:?}", err);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
None => Err(Error::Cancelled),
|
||||
_ => Err(Error::NotFileDialogLoader),
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue