avm2: Implement FileReference save

This commit is contained in:
Tom Schuster 2023-11-25 18:56:57 +01:00
parent 9101ab322e
commit 3ddf0aec28
3 changed files with 197 additions and 4 deletions

View File

@ -64,9 +64,7 @@ package flash.net
stub_method("flash.net.FileReference", "requestPermission");
}
public function save(data:*, defaultFileName:String = null):void {
stub_method("flash.net.FileReference", "save");
}
public native function save(data:*, defaultFileName:String = null):void;
public function upload(request:URLRequest, uploadDataFieldName:String = "Filedata", testUpload:Boolean = false):void {
stub_method("flash.net.FileReference", "upload");

View File

@ -1,5 +1,5 @@
use crate::avm2::bytearray::ByteArrayStorage;
use crate::avm2::error::{make_error_2037, make_error_2097};
use crate::avm2::error::{argument_error, 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};
@ -164,3 +164,57 @@ pub fn load<'gc>(
Ok(Value::Undefined)
}
pub fn save<'gc>(
activation: &mut Activation<'_, 'gc>,
this: Object<'gc>,
args: &[Value<'gc>],
) -> Result<Value<'gc>, Error<'gc>> {
let this = this.as_file_reference().unwrap();
let data = args[0];
let data = match data {
Value::Null | Value::Undefined => {
// For some reason this isn't a proper error.
return Err(Error::AvmError(argument_error(activation, "data", 0)?));
}
Value::Object(obj) => {
if let Some(bytearray) = obj.as_bytearray() {
bytearray.bytes().to_vec()
} else if let Some(xml) = obj.as_xml_object() {
xml.as_xml_string(activation).to_string().into_bytes()
} else {
data.coerce_to_string(activation)?.to_string().into_bytes()
}
}
_ => data.coerce_to_string(activation)?.to_string().into_bytes(),
};
let file_name = if let Value::String(name) = args[1] {
name.to_string()
} else {
"".into()
};
// Create and spawn dialog
let dialog = activation.context.ui.display_file_save_dialog(
file_name.to_owned(),
format!("Select location to save the file {}", file_name),
);
match dialog {
Some(dialog) => {
let process = activation.context.load_manager.save_file_dialog(
activation.context.player.clone(),
this,
dialog,
data,
);
activation.context.navigator.spawn_future(process);
}
None => return Err(Error::AvmError(error(activation, "Error #2174: Only one download, upload, load or save operation can be active at a time on each FileReference.", 2174)?)),
}
Ok(Value::Undefined)
}

View File

@ -185,6 +185,9 @@ pub enum Error {
#[error("Non-file dialog loader spawned as file dialog loader")]
NotFileDialogLoader,
#[error("Non-file save dialog loader spawned as file save dialog loader")]
NotFileSaveDialogLoader,
#[error("Non-file download dialog loader spawned as file download dialog loader")]
NotFileDownloadDialogLoader,
@ -250,6 +253,7 @@ impl<'gc> LoadManager<'gc> {
| Loader::NetStream { self_handle, .. }
| Loader::FileDialog { self_handle, .. }
| Loader::FileDialogAvm2 { self_handle, .. }
| Loader::SaveFileDialog { self_handle, .. }
| Loader::DownloadFileDialog { self_handle, .. }
| Loader::UploadFile { self_handle, .. }
| Loader::MovieUnloader { self_handle, .. } => *self_handle = Some(handle),
@ -522,6 +526,24 @@ impl<'gc> LoadManager<'gc> {
loader.file_dialog_loader(player, dialog)
}
/// Display a dialog allowing a user to save a file
#[must_use]
pub fn save_file_dialog(
&mut self,
player: Weak<Mutex<Player>>,
target_object: FileReferenceObject<'gc>,
dialog: DialogResultFuture,
data: Vec<u8>,
) -> OwnedFuture<(), Error> {
let loader = Loader::SaveFileDialog {
self_handle: None,
target_object,
};
let handle = self.add_loader(loader);
let loader = self.get_loader_mut(handle).unwrap();
loader.file_save_dialog_loader(player, dialog, data)
}
/// 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
@ -736,6 +758,16 @@ pub enum Loader<'gc> {
target_object: FileReferenceObject<'gc>,
},
/// Loader that is saving a file to disk from an AVM2 scope.
SaveFileDialog {
/// The handle to refer to this loader instance.
#[collect(require_static)]
self_handle: Option<Handle>,
/// The target AVM2 object to select a save location for.
target_object: FileReferenceObject<'gc>,
},
/// Loader that is downloading a file from an AVM1 object scope.
DownloadFileDialog {
/// The handle to refer to this loader instance.
@ -2533,6 +2565,115 @@ impl<'gc> Loader<'gc> {
})
}
/// Loader to handle saving a file to disk.
pub fn file_save_dialog_loader(
&mut self,
player: Weak<Mutex<Player>>,
dialog: DialogResultFuture,
data: Vec<u8>,
) -> OwnedFuture<(), Error> {
let handle = match self {
Loader::SaveFileDialog { self_handle, .. } => {
self_handle.expect("Loader not self-introduced")
}
_ => return Box::pin(async { Err(Error::NotFileSaveDialogLoader) }),
};
let player = player
.upgrade()
.expect("Could not upgrade weak reference to player");
Box::pin(async move {
let dialog_result = dialog.await;
// Dialog is done, allow opening new dialogs
player.lock().unwrap().ui_mut().close_file_dialog();
// 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::SaveFileDialog { target_object, .. }) => target_object,
None => return Err(Error::Cancelled),
_ => return Err(Error::NotFileSaveDialogLoader),
};
match dialog_result {
Ok(mut dialog_result) => {
if !dialog_result.is_cancelled() {
dialog_result.write(&data);
dialog_result.refresh();
target_object.init_from_dialog_result(dialog_result);
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(),
);
let open_event = Avm2EventObject::bare_default_event(
&mut activation.context,
"open",
);
Avm2::dispatch_event(
&mut activation.context,
open_event,
target_object.into(),
);
let size = data.len() as u64;
let progress_evt = Avm2EventObject::progress_event(
&mut activation,
"progress",
size,
size,
false,
false,
);
Avm2::dispatch_event(
&mut activation.context,
progress_evt,
target_object.into(),
);
let complete_event = Avm2EventObject::bare_default_event(
&mut activation.context,
"complete",
);
Avm2::dispatch_event(
&mut activation.context,
complete_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!("Save dialog had an error {:?}", err);
}
}
Ok(())
})
})
}
/// Loader to handle a file download dialog
///
/// Fetches the data from `url`, saves the data to the selected destination and processes callbacks