
This change provides the ability to upload save states. When a state is downloaded, the state (savestate.state), screenshot (screenshot.jpg), and a json file (rominfo.json) describing the save state are zipped and downloaded. The json file description is defined as follows: ```json { "Name": "Super Mario Bros. (1985-09-13)(Nintendo)(JP-US).zip", "StateDateTime": "2024-06-24T05:34:38", "StateName": "", "MD5": "7d158dcd242e77ba249ac8342474aa77", "SHA1": "3d4b04dc78f9d998f17d9fe9ad982a83b5ed72df", "Type": "ROM" } ``` | Attribute | Value | | -------- | ------| | Name | The name of the ROM that the state belongs to. This is merely a convenience attribute. | | StateDateTime | The date and time (in UTC) when the state was initially saved. | | StateName | The name of the state | | MD5 | The MD5 hash of the ROM that the state belongs to. | | SHA1 | The SHA1 hash of the ROM that the state belongs to. | | Type | Whether the state belongs to a ROM or ROM Group | If the zip is re-uploaded, the above json file will be used to automatically match the saved state to the ROM that created it. If a zip is uploaded without the above three files, the upload will fail. If a file is uploaded that is not a zip, it will be stored against the currently running ROM and a warning will be displayed that Gaseous was unable to verify that the state belongs to the ROM, and may not function as expected. Closes #336
532 lines
24 KiB
C#
532 lines
24 KiB
C#
using Microsoft.AspNetCore.Mvc;
|
|
using Microsoft.AspNetCore.Authorization;
|
|
using gaseous_server.Models;
|
|
using gaseous_server.Classes;
|
|
using Authentication;
|
|
using Microsoft.AspNetCore.Identity;
|
|
using System.Data;
|
|
using Asp.Versioning;
|
|
using System.IO.Compression;
|
|
|
|
namespace gaseous_server.Controllers.v1_1
|
|
{
|
|
[Route("api/v{version:apiVersion}/[controller]")]
|
|
[ApiVersion("1.0")]
|
|
[ApiVersion("1.1")]
|
|
[ApiController]
|
|
public class StateManagerController : ControllerBase
|
|
{
|
|
private readonly UserManager<ApplicationUser> _userManager;
|
|
private readonly SignInManager<ApplicationUser> _signInManager;
|
|
|
|
public StateManagerController(
|
|
UserManager<ApplicationUser> userManager,
|
|
SignInManager<ApplicationUser> signInManager
|
|
)
|
|
{
|
|
_userManager = userManager;
|
|
_signInManager = signInManager;
|
|
}
|
|
|
|
[MapToApiVersion("1.0")]
|
|
[MapToApiVersion("1.1")]
|
|
[HttpPost]
|
|
[Authorize]
|
|
[ProducesResponseType(typeof(Models.GameStateItem), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
[Route("{RomId}")]
|
|
public async Task<ActionResult> SaveStateAsync(long RomId, UploadStateModel uploadState, bool IsMediaGroup = false)
|
|
{
|
|
var user = await _userManager.GetUserAsync(User);
|
|
|
|
byte[] CompressedState = Common.Compress(uploadState.StateByteArray);
|
|
|
|
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
|
|
string sql = "INSERT INTO GameState (UserId, RomId, IsMediaGroup, StateDateTime, Name, Screenshot, State, Zipped) VALUES (@userid, @romid, @ismediagroup, @statedatetime, @name, @screenshot, @state, @zipped); SELECT LAST_INSERT_ID();";
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>
|
|
{
|
|
{ "userid", user.Id },
|
|
{ "romid", RomId },
|
|
{ "ismediagroup", IsMediaGroup },
|
|
{ "statedatetime", DateTime.UtcNow },
|
|
{ "name", "" },
|
|
{ "screenshot", uploadState.ScreenshotByteArray },
|
|
{ "state", CompressedState },
|
|
{ "zipped", true }
|
|
};
|
|
DataTable data = db.ExecuteCMD(sql, dbDict);
|
|
|
|
if (IsMediaGroup == false)
|
|
{
|
|
Logging.Log(Logging.LogType.Information, "Save State", "Saved state for rom id " + RomId + ". State size: " + uploadState.StateByteArrayBase64.Length + " Compressed size: " + CompressedState.Length);
|
|
}
|
|
else
|
|
{
|
|
Logging.Log(Logging.LogType.Information, "Save State", "Saved state for media group id " + RomId + ". State size: " + uploadState.StateByteArrayBase64.Length + " Compressed size: " + CompressedState.Length);
|
|
}
|
|
|
|
return Ok(await GetStateAsync(RomId, (long)(ulong)data.Rows[0][0], IsMediaGroup));
|
|
}
|
|
|
|
[MapToApiVersion("1.0")]
|
|
[MapToApiVersion("1.1")]
|
|
[HttpGet]
|
|
[Authorize]
|
|
[ProducesResponseType(typeof(List<Models.GameStateItem>), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
[Route("{RomId}")]
|
|
public async Task<ActionResult> GetAllStateAsync(long RomId, bool IsMediaGroup = false)
|
|
{
|
|
var user = await _userManager.GetUserAsync(User);
|
|
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
|
|
string sql = "SELECT Id, StateDateTime, `Name`, Screenshot FROM GameState WHERE RomId = @romid AND IsMediaGroup = @ismediagroup AND UserId = @userid ORDER BY StateDateTime DESC;";
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>
|
|
{
|
|
{ "romid", RomId },
|
|
{ "userid", user.Id },
|
|
{ "ismediagroup", IsMediaGroup }
|
|
};
|
|
DataTable data = db.ExecuteCMD(sql, dbDict);
|
|
|
|
List<Models.GameStateItem> gameStates = new List<GameStateItem>();
|
|
foreach (DataRow row in data.Rows)
|
|
{
|
|
gameStates.Add(BuildGameStateItem(row));
|
|
}
|
|
|
|
return Ok(gameStates);
|
|
}
|
|
|
|
[MapToApiVersion("1.0")]
|
|
[MapToApiVersion("1.1")]
|
|
[HttpGet]
|
|
[Authorize]
|
|
[ProducesResponseType(typeof(Models.GameStateItem), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
[Route("{RomId}/{StateId}")]
|
|
public async Task<ActionResult> GetStateAsync(long RomId, long StateId, bool IsMediaGroup = false)
|
|
{
|
|
var user = await _userManager.GetUserAsync(User);
|
|
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
|
|
string sql = "SELECT Id, StateDateTime, `Name`, Screenshot FROM GameState WHERE Id = @id AND RomId = @romid AND IsMediaGroup = @ismediagroup AND UserId = @userid;";
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>
|
|
{
|
|
{ "id", StateId },
|
|
{ "romid", RomId },
|
|
{ "userid", user.Id },
|
|
{ "ismediagroup", IsMediaGroup }
|
|
};
|
|
DataTable data = db.ExecuteCMD(sql, dbDict);
|
|
|
|
if (data.Rows.Count == 0)
|
|
{
|
|
// invalid match - return not found
|
|
return NotFound();
|
|
}
|
|
else
|
|
{
|
|
GameStateItem stateItem = BuildGameStateItem(data.Rows[0]);
|
|
|
|
return Ok(stateItem);
|
|
}
|
|
}
|
|
|
|
[MapToApiVersion("1.0")]
|
|
[MapToApiVersion("1.1")]
|
|
[HttpDelete]
|
|
[Authorize]
|
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
[Route("{RomId}/{StateId}")]
|
|
public async Task<ActionResult> DeleteStateAsync(long RomId, long StateId, bool IsMediaGroup = false)
|
|
{
|
|
var user = await _userManager.GetUserAsync(User);
|
|
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
|
|
string sql = "DELETE FROM GameState WHERE Id = @id AND RomId = @romid AND IsMediaGroup = @ismediagroup AND UserId = @userid;";
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>
|
|
{
|
|
{ "id", StateId },
|
|
{ "romid", RomId },
|
|
{ "userid", user.Id },
|
|
{ "ismediagroup", IsMediaGroup }
|
|
};
|
|
db.ExecuteNonQuery(sql, dbDict);
|
|
|
|
return Ok();
|
|
}
|
|
|
|
[MapToApiVersion("1.0")]
|
|
[MapToApiVersion("1.1")]
|
|
[HttpPut]
|
|
[Authorize]
|
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
[Route("{RomId}/{StateId}")]
|
|
public async Task<ActionResult> EditStateAsync(long RomId, long StateId, GameStateItemUpdateModel model, bool IsMediaGroup = false)
|
|
{
|
|
var user = await _userManager.GetUserAsync(User);
|
|
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
|
|
string sql = "UPDATE GameState SET `Name` = @name WHERE Id = @id AND RomId = @romid AND IsMediaGroup = @ismediagroup AND UserId = @userid;";
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>
|
|
{
|
|
{ "id", StateId },
|
|
{ "romid", RomId },
|
|
{ "userid", user.Id },
|
|
{ "ismediagroup", IsMediaGroup },
|
|
{ "name", model.Name }
|
|
};
|
|
db.ExecuteNonQuery(sql, dbDict);
|
|
|
|
return Ok();
|
|
}
|
|
|
|
[MapToApiVersion("1.0")]
|
|
[MapToApiVersion("1.1")]
|
|
[HttpGet]
|
|
[Authorize]
|
|
[ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
[Route("{RomId}/{StateId}/Screenshot/")]
|
|
[Route("{RomId}/{StateId}/Screenshot/image.png")]
|
|
public async Task<ActionResult> GetStateScreenshotAsync(long RomId, long StateId, bool IsMediaGroup = false)
|
|
{
|
|
var user = await _userManager.GetUserAsync(User);
|
|
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
|
|
string sql = "SELECT Screenshot FROM GameState WHERE Id = @id AND RomId = @romid AND IsMediaGroup = @ismediagroup AND UserId = @userid;";
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>
|
|
{
|
|
{ "id", StateId },
|
|
{ "romid", RomId },
|
|
{ "userid", user.Id },
|
|
{ "ismediagroup", IsMediaGroup }
|
|
};
|
|
DataTable data = db.ExecuteCMD(sql, dbDict);
|
|
|
|
if (data.Rows.Count == 0)
|
|
{
|
|
// invalid match - return not found
|
|
return NotFound();
|
|
}
|
|
else
|
|
{
|
|
string filename = "image.jpg";
|
|
byte[] bytes = (byte[])data.Rows[0][0];
|
|
string contentType = "image/png";
|
|
|
|
var cd = new System.Net.Mime.ContentDisposition
|
|
{
|
|
FileName = filename,
|
|
Inline = true,
|
|
};
|
|
|
|
Response.Headers.Add("Content-Disposition", cd.ToString());
|
|
Response.Headers.Add("Cache-Control", "public, max-age=604800");
|
|
|
|
return File(bytes, contentType);
|
|
}
|
|
}
|
|
|
|
[MapToApiVersion("1.0")]
|
|
[MapToApiVersion("1.1")]
|
|
[HttpGet]
|
|
[Authorize]
|
|
[ProducesResponseType(typeof(FileStreamResult), StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
[Route("{RomId}/{StateId}/State/")]
|
|
[Route("{RomId}/{StateId}/State/savestate.state")]
|
|
public async Task<ActionResult> GetStateDataAsync(long RomId, long StateId, bool IsMediaGroup = false, bool StateOnly = false)
|
|
{
|
|
var user = await _userManager.GetUserAsync(User);
|
|
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
|
|
string sql = "SELECT * FROM GameState WHERE Id = @id AND RomId = @romid AND IsMediaGroup = @ismediagroup AND UserId = @userid;";
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>
|
|
{
|
|
{ "id", StateId },
|
|
{ "romid", RomId },
|
|
{ "userid", user.Id },
|
|
{ "ismediagroup", IsMediaGroup }
|
|
};
|
|
DataTable data = db.ExecuteCMD(sql, dbDict);
|
|
|
|
if (data.Rows.Count == 0)
|
|
{
|
|
// invalid match - return not found
|
|
return NotFound();
|
|
}
|
|
else
|
|
{
|
|
// get rom data
|
|
Roms.GameRomItem romItem = Roms.GetRom(RomId);
|
|
|
|
byte[] bytes;
|
|
if ((bool)data.Rows[0]["Zipped"] == false)
|
|
{
|
|
bytes = (byte[])data.Rows[0]["State"];
|
|
}
|
|
else
|
|
{
|
|
bytes = Common.Decompress((byte[])data.Rows[0]["State"]);
|
|
}
|
|
|
|
string contentType = "";
|
|
string filename = ((DateTime)data.Rows[0]["StateDateTime"]).ToString("yyyy-MM-ddTHH-mm-ss") + "-" + Path.GetFileNameWithoutExtension(romItem.Name);
|
|
|
|
|
|
if (StateOnly == true)
|
|
{
|
|
contentType = "application/octet-stream";
|
|
filename = filename + ".state";
|
|
}
|
|
else
|
|
{
|
|
contentType = "application/zip";
|
|
filename = filename + ".zip";
|
|
|
|
Dictionary<string, object> RomInfo = new Dictionary<string, object>
|
|
{
|
|
{ "Name", romItem.Name },
|
|
{ "StateDateTime", data.Rows[0]["StateDateTime"] },
|
|
{ "StateName", data.Rows[0]["Name"] }
|
|
};
|
|
if ((int)data.Rows[0]["IsMediaGroup"] == 0)
|
|
{
|
|
RomInfo.Add("MD5", romItem.Md5);
|
|
RomInfo.Add("SHA1", romItem.Sha1);
|
|
RomInfo.Add("Type", "ROM");
|
|
}
|
|
else
|
|
{
|
|
RomInfo.Add("Type", "Media Group");
|
|
RomInfo.Add("MediaGroupId", (long)data.Rows[0]["RomId"]);
|
|
}
|
|
string RomInfoString = Newtonsoft.Json.JsonConvert.SerializeObject(RomInfo, Newtonsoft.Json.Formatting.Indented, new Newtonsoft.Json.JsonSerializerSettings { NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore });
|
|
|
|
// compile zip file
|
|
using (var compressedFileStream = new MemoryStream())
|
|
{
|
|
List<Dictionary<string, object>> Attachments = new List<Dictionary<string, object>>();
|
|
Attachments.Add(new Dictionary<string, object>
|
|
{
|
|
{ "Name", "savestate.state" },
|
|
{ "Body", bytes }
|
|
});
|
|
// check if value is dbnull
|
|
if (data.Rows[0]["Screenshot"] != DBNull.Value)
|
|
{
|
|
Attachments.Add(new Dictionary<string, object>
|
|
{
|
|
{ "Name", "screenshot.jpg" },
|
|
{ "Body", (byte[])data.Rows[0]["Screenshot"] }
|
|
});
|
|
}
|
|
Attachments.Add(new Dictionary<string, object>
|
|
{
|
|
{ "Name", "rominfo.json" },
|
|
{ "Body", System.Text.Encoding.UTF8.GetBytes(RomInfoString) }
|
|
});
|
|
|
|
//Create an archive and store the stream in memory.
|
|
using (var zipArchive = new ZipArchive(compressedFileStream, ZipArchiveMode.Create, false))
|
|
{
|
|
foreach (var Attachment in Attachments)
|
|
{
|
|
//Create a zip entry for each attachment
|
|
var zipEntry = zipArchive.CreateEntry(Attachment["Name"].ToString());
|
|
|
|
//Get the stream of the attachment
|
|
using (var originalFileStream = new MemoryStream((byte[])Attachment["Body"]))
|
|
using (var zipEntryStream = zipEntry.Open())
|
|
{
|
|
//Copy the attachment stream to the zip entry stream
|
|
originalFileStream.CopyTo(zipEntryStream);
|
|
}
|
|
}
|
|
}
|
|
|
|
//return new FileContentResult(compressedFileStream.ToArray(), "application/zip") { FileDownloadName = filename };
|
|
bytes = compressedFileStream.ToArray();
|
|
}
|
|
}
|
|
|
|
var cd = new System.Net.Mime.ContentDisposition
|
|
{
|
|
FileName = filename,
|
|
Inline = true,
|
|
};
|
|
|
|
Response.Headers.Add("Content-Disposition", cd.ToString());
|
|
Response.Headers.Add("Cache-Control", "public, max-age=604800");
|
|
|
|
return File(bytes, contentType);
|
|
}
|
|
}
|
|
|
|
[MapToApiVersion("1.0")]
|
|
[MapToApiVersion("1.1")]
|
|
[HttpPost]
|
|
[Authorize]
|
|
[ProducesResponseType(StatusCodes.Status200OK)]
|
|
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
|
[RequestSizeLimit(long.MaxValue)]
|
|
[Consumes("multipart/form-data")]
|
|
[DisableRequestSizeLimit, RequestFormLimits(MultipartBodyLengthLimit = long.MaxValue, ValueLengthLimit = int.MaxValue)]
|
|
[Route("Upload")]
|
|
public async Task<ActionResult> UploadStateDataAsync(IFormFile file, long RomId = 0, bool IsMediaGroup = false)
|
|
{
|
|
// get user
|
|
var user = await _userManager.GetUserAsync(User);
|
|
|
|
if (file.Length > 0)
|
|
{
|
|
MemoryStream fileContent = new MemoryStream();
|
|
file.CopyTo(fileContent);
|
|
|
|
// test if file is a zip file
|
|
try
|
|
{
|
|
using (var zipArchive = new ZipArchive(fileContent, ZipArchiveMode.Read, false))
|
|
{
|
|
foreach (var entry in zipArchive.Entries)
|
|
{
|
|
if (entry.FullName == "rominfo.json")
|
|
{
|
|
using (var stream = entry.Open())
|
|
using (var reader = new StreamReader(stream))
|
|
{
|
|
string RomInfoString = reader.ReadToEnd();
|
|
Dictionary<string, object> RomInfo = Newtonsoft.Json.JsonConvert.DeserializeObject<Dictionary<string, object>>(RomInfoString);
|
|
|
|
// get rom data
|
|
Roms.GameRomItem romItem;
|
|
|
|
try
|
|
{
|
|
romItem = Roms.GetRom((string)RomInfo["MD5"]);
|
|
}
|
|
catch (Roms.InvalidRomHash)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
// get state data
|
|
byte[] StateData = null;
|
|
byte[] ScreenshotData = null;
|
|
string StateName = RomInfo["StateName"].ToString();
|
|
DateTime StateDateTime = DateTime.Parse(RomInfo["StateDateTime"].ToString());
|
|
IsMediaGroup = RomInfo["Type"].ToString() == "Media Group" ? true : false;
|
|
|
|
if (zipArchive.GetEntry("savestate.state") != null)
|
|
{
|
|
using (var stateStream = zipArchive.GetEntry("savestate.state").Open())
|
|
using (var stateReader = new MemoryStream())
|
|
{
|
|
stateStream.CopyTo(stateReader);
|
|
StateData = stateReader.ToArray();
|
|
}
|
|
}
|
|
if (zipArchive.GetEntry("screenshot.jpg") != null)
|
|
{
|
|
using (var screenshotStream = zipArchive.GetEntry("screenshot.jpg").Open())
|
|
using (var screenshotReader = new MemoryStream())
|
|
{
|
|
screenshotStream.CopyTo(screenshotReader);
|
|
ScreenshotData = screenshotReader.ToArray();
|
|
}
|
|
}
|
|
|
|
// save state
|
|
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
|
|
string sql = "INSERT INTO GameState (UserId, RomId, IsMediaGroup, StateDateTime, Name, Screenshot, State, Zipped) VALUES (@userid, @romid, @ismediagroup, @statedatetime, @name, @screenshot, @state, @zipped); SELECT LAST_INSERT_ID();";
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>
|
|
{
|
|
{ "userid", user.Id },
|
|
{ "romid", romItem.Id },
|
|
{ "ismediagroup", IsMediaGroup },
|
|
{ "statedatetime", StateDateTime },
|
|
{ "name", StateName },
|
|
{ "screenshot", ScreenshotData },
|
|
{ "state", Common.Compress(StateData) },
|
|
{ "zipped", true }
|
|
};
|
|
DataTable data = db.ExecuteCMD(sql, dbDict);
|
|
|
|
RomInfo.Add("RomId", romItem.Id);
|
|
RomInfo.Add("Management", "Managed");
|
|
return Ok(RomInfo);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return BadRequest("File is not a valid Gaseous state file.");
|
|
}
|
|
catch
|
|
{
|
|
// not a zip file
|
|
if (RomId != 0)
|
|
{
|
|
// get rom data
|
|
Roms.GameRomItem romItem;
|
|
|
|
try
|
|
{
|
|
romItem = Roms.GetRom(RomId);
|
|
}
|
|
catch (Roms.InvalidRomHash)
|
|
{
|
|
return NotFound();
|
|
}
|
|
|
|
// save state
|
|
Database db = new Database(Database.databaseType.MySql, Config.DatabaseConfiguration.ConnectionString);
|
|
string sql = "INSERT INTO GameState (UserId, RomId, IsMediaGroup, StateDateTime, Name, Screenshot, State, Zipped) VALUES (@userid, @romid, @ismediagroup, @statedatetime, @name, @screenshot, @state, @zipped); SELECT LAST_INSERT_ID();";
|
|
Dictionary<string, object> dbDict = new Dictionary<string, object>
|
|
{
|
|
{ "userid", user.Id },
|
|
{ "romid", RomId },
|
|
{ "ismediagroup", IsMediaGroup },
|
|
{ "statedatetime", DateTime.UtcNow },
|
|
{ "name", "" },
|
|
{ "screenshot", null },
|
|
{ "state", Common.Compress(fileContent.ToArray()) },
|
|
{ "zipped", true }
|
|
};
|
|
DataTable data = db.ExecuteCMD(sql, dbDict);
|
|
|
|
return Ok(new Dictionary<string, object>
|
|
{
|
|
{ "RomId", RomId },
|
|
{ "Management", "Unmanaged" }
|
|
});
|
|
}
|
|
else
|
|
{
|
|
return BadRequest("No rom id provided.");
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
return BadRequest("File is empty.");
|
|
}
|
|
}
|
|
|
|
private Models.GameStateItem BuildGameStateItem(DataRow dr)
|
|
{
|
|
bool HasScreenshot = true;
|
|
if (dr["Screenshot"] == DBNull.Value)
|
|
{
|
|
HasScreenshot = false;
|
|
}
|
|
GameStateItem stateItem = new GameStateItem
|
|
{
|
|
Id = (long)dr["Id"],
|
|
Name = (string)dr["Name"],
|
|
SaveTime = DateTime.Parse(((DateTime)dr["StateDateTime"]).ToString("yyyy-MM-ddThh:mm:ss") + 'Z'),
|
|
HasScreenshot = HasScreenshot
|
|
};
|
|
|
|
return stateItem;
|
|
}
|
|
}
|
|
} |