diff --git a/data/com.github.tkashkin.gamehub.gschema.xml.in b/data/com.github.tkashkin.gamehub.gschema.xml.in index 134b2287..0a4b3ce2 100644 --- a/data/com.github.tkashkin.gamehub.gschema.xml.in +++ b/data/com.github.tkashkin.gamehub.gschema.xml.in @@ -146,6 +146,21 @@ + + + true + Is EpicGames enabled + + + false + Is user authenticated + + + '' + EpicGames access sid + + + true @@ -221,6 +236,14 @@ '~/Games/itch' itch.io games directory + + 'legendary' + Legendary client command + + + '~/legendary' + EpicGames games directory + diff --git a/src/data/Runnable.vala b/src/data/Runnable.vala index 20e3e694..cbaaa295 100644 --- a/src/data/Runnable.vala +++ b/src/data/Runnable.vala @@ -341,10 +341,10 @@ namespace GameHub.Data public abstract class Installer { - public string id { get; protected set; } - public Platform platform { get; protected set; default = Platform.CURRENT; } - public int64 full_size { get; protected set; default = 0; } - public string? version { get; protected set; } + public string id { get; protected set; } + public Platform platform { get; protected set; default = Platform.CURRENT; } + public virtual int64 full_size { get; protected set; default = 0; } + public string? version { get; protected set; } public virtual string name { owned get { return id; } } diff --git a/src/data/sources/epicgames/EpicGames.vala b/src/data/sources/epicgames/EpicGames.vala index 622b3fc8..a011b1a1 100644 --- a/src/data/sources/epicgames/EpicGames.vala +++ b/src/data/sources/epicgames/EpicGames.vala @@ -26,18 +26,21 @@ namespace GameHub.Data.Sources.EpicGames public class EpicGames: GameSource { public static EpicGames instance; + + private bool? installed = null; + public File? legendary_executable = null; public override string id { get { return "epicgames"; } } public override string name { get { return "EpicGames"; } } public override string icon { get { return "source-epicgames-symbolic"; } } - private Regex regex = /\*\s*([^(]*)\s\(App\sname:\s([a-zA-Z0-9]+),\sversion:\s([^)]*)\)/; + private Settings.Auth.EpicGames settings; + private FSUtils.Paths.Settings paths = FSUtils.Paths.Settings.instance; - private bool enable = true; public override bool enabled { - get { return enable; } - set { enable = value; } + get { return settings.enabled; } + set { settings.enabled = value; } } @@ -50,42 +53,72 @@ namespace GameHub.Data.Sources.EpicGames { instance = this; legendary_wrapper = new LegendaryWrapper(); + settings = Settings.Auth.EpicGames.instance; } public override bool is_installed(bool refresh) { - debug("[EpicGames] is_installed: NOT IMPLEMENTED"); - return true; + /* + Epic games depends on + */ + if(installed != null && !refresh) + { + return (!) installed; + } + + //check if legendary exists + var legendary = Utils.find_executable(paths.legendary_command); + + if(legendary == null || !legendary.query_exists()) + { + debug("[EpicGames] is_installed: Legendary not found"); + + } + else + { + debug("[EpicGames] is_installed: LegendaryYES"); + } + + legendary_executable = legendary; + installed = legendary_executable != null && legendary_executable.query_exists(); + + return (!) installed; } public override async bool install() { - debug("[EpicGames] install: NOT IMPLEMENTED"); return true; } public override async bool authenticate() { - debug("[EpicGames] authenticate: NOT IMPLEMENTED"); - return true; + debug("[EpicGames] Performing auth"); + var username = yield legendary_wrapper.auth(); + settings.authenticated = username != null; + if(username != null) { + user_name = username; + return true; + }else return false; } public override bool is_authenticated() { - debug("[EpicGames] is_authenticated: NOT IMPLEMENTED"); - return true; + var result = legendary_wrapper.is_authenticated(); + settings.authenticated = result; + + if (result) { + legendary_wrapper.get_username.begin ((obj, res) => { + user_name = legendary_wrapper.get_username.end (res); + }); + } + + return result; } public override bool can_authenticate_automatically() { debug("[EpicGames] can_authenticate_automatically: NOT IMPLEMENTED"); - return true; - } - - public async bool refresh_token() - { - debug("[EpicGames] refresh_token: NOT IMPLEMENTED"); - return true; + return false; } private ArrayList _games = new ArrayList(Game.is_equal); @@ -133,6 +166,15 @@ namespace GameHub.Data.Sources.EpicGames { var g = new EpicGamesGame(this, game.name, game.id); bool is_new_game = !_games.contains(g); + if(is_new_game && (!Settings.UI.Behavior.instance.merge_games || !Tables.Merges.is_game_merged(g))) + { + _games.add(g); + if(game_loaded != null) + { + game_loaded(g, false); + } + } + if(is_new_game) { g.save(); if(game_loaded != null) diff --git a/src/data/sources/epicgames/EpicGamesGame.vala b/src/data/sources/epicgames/EpicGamesGame.vala index b204128b..7aaac468 100644 --- a/src/data/sources/epicgames/EpicGamesGame.vala +++ b/src/data/sources/epicgames/EpicGamesGame.vala @@ -16,6 +16,7 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GameHub. If not, see . */ +using Gtk; using Gee; using GameHub.Data.DB; @@ -50,11 +51,10 @@ namespace GameHub.Data.Sources.EpicGames public override void update_status() { var state = Game.State.UNINSTALLED; - if (((EpicGames)source).legendary_wrapper.is_installed(id)) { + if (((EpicGames) source).legendary_wrapper.is_installed(id)) { state = Game.State.INSTALLED; debug ("New installed game: \tname = %s\t", name); } else { - debug ("New not installed game: \tname = %s\t", name); } @@ -116,52 +116,99 @@ namespace GameHub.Data.Sources.EpicGames } public override async void uninstall() { - ((EpicGames)source).legendary_wrapper.uninstall(id); + ((EpicGames) source).legendary_wrapper.uninstall(id); update_status(); } public override async void run() { - ((EpicGames)source).legendary_wrapper.run(id); + ((EpicGames) source).legendary_wrapper.run(id); } + public override void import(bool update=true) + { + var chooser = new FileChooserDialog(_("Select directory"), GameHub.UI.Windows.MainWindow.instance, FileChooserAction.SELECT_FOLDER); + + chooser.add_button(_("Cancel"), ResponseType.CANCEL); + var select_btn = chooser.add_button(_("Select"), ResponseType.ACCEPT); + + select_btn.get_style_context().add_class(Gtk.STYLE_CLASS_SUGGESTED_ACTION); + select_btn.grab_default(); + + if(chooser.run() == ResponseType.ACCEPT) + { + install_dir = chooser.get_file(); + ((EpicGames) source).legendary_wrapper.import_game(id, install_dir.get_path()); + + if(update) { + update_status(); + save(); + } + } + + chooser.destroy(); + } + public class EpicGamesInstaller: Runnable.Installer { + private FSUtils.Paths.Settings paths = FSUtils.Paths.Settings.instance; public EpicGamesGame game; - public override string name { owned get { return "TEST"; } } + private EpicGames epic; + public override string name { owned get { return game.name; } } + + private int64 _full_size = 0; + public override int64 full_size { + get { + if(_full_size != 0) return _full_size; + else { + var size = epic.legendary_wrapper.get_install_size (game.id); + _full_size = size; + return _full_size; + } + + } + set {} + } public EpicGamesInstaller(EpicGamesGame game, string id) { this.game = game; id = id; platform = Platform.CURRENT; + epic = (EpicGames)(game.source); } public override async void install(Runnable runnable, CompatTool? tool=null) { + EpicGamesGame? game = null; if(runnable is EpicGamesGame) { game = runnable as EpicGamesGame; } - EpicGames epic = (EpicGames)(game.source); - Utils.thread("EpicGamesGame.Installer", () => { - game.status = new Game.Status(Game.State.DOWNLOADING, game, null); + EpicDownload ed = new EpicDownload(game.id); + game.status = new Game.Status(Game.State.DOWNLOADING, game, ed); + + ed.cancelled.connect(() => { + epic.legendary_wrapper.cancel_installation(); + }); + + var game_folder = (paths.epic_games == null || paths.epic_games == "") ? null : paths.epic_games; + epic.legendary_wrapper.install(game.id, game_folder, progress => { + ed.status = new EpicDownload.EpicStatus(progress / 100); + game.status = new Game.Status(Game.State.DOWNLOADING, game, ed); + }); - epic.legendary_wrapper.install(game.id); Idle.add(install.callback); }); yield; if(game != null) game.status = new Game.Status(Game.State.INSTALLED, game, null); - - runnable.update_status(); - - debug("install"); + game.update_status(); } } } diff --git a/src/data/sources/epicgames/LegendaryWrapper.vala b/src/data/sources/epicgames/LegendaryWrapper.vala index 82db64c2..418f24be 100644 --- a/src/data/sources/epicgames/LegendaryWrapper.vala +++ b/src/data/sources/epicgames/LegendaryWrapper.vala @@ -1,4 +1,5 @@ using Gee; +using GameHub.Utils; namespace GameHub.Data.Sources.EpicGames { @@ -10,7 +11,18 @@ namespace GameHub.Data.Sources.EpicGames public class LegendaryWrapper { - private Regex regex = /\*\s*([^(]*)\s\(App\sname:\s([a-zA-Z0-9]+),\sversion:\s([^)]*)\)/; + private Regex regex = /\s?\*\s?(.+)\(App\sname:\s([a-zA-Z0-9]+)\s.\s[Vv]ersion:\s([^)]*)\)/; + private Regex authValidationRegex = /credentials are still valid/; + private Regex authSuccessRegex = /Successfully logged in as "([^"]+)/; + private Regex sidExtractionRegex = /sid":"([^"]+)/; + private Regex fileUserRegex = /displayName":\s"([^"]+)/; + private Regex progressRegex = /Progress:\s([0-9\.]+)/; + private Regex installSizeRegex = /Install size:\s([0-9\.]+)/; + + private FSUtils.Paths.Settings paths = FSUtils.Paths.Settings.instance; + private Settings.Auth.EpicGames settings = Settings.Auth.EpicGames.instance; + + private Subprocess? downloadProcess = null; public LegendaryWrapper() { @@ -21,7 +33,7 @@ namespace GameHub.Data.Sources.EpicGames string? line = null; MatchInfo info; - var output = new DataInputStream(new Subprocess.newv ({"legendary", "list-games"}, STDOUT_PIPE).get_stdout_pipe ()); + var output = new DataInputStream(new Subprocess.newv ({paths.legendary_command, "list-games"}, STDOUT_PIPE).get_stdout_pipe ()); while ((line = output.read_line()) != null) { if (regex.match (line, 0, out info)) { @@ -56,24 +68,134 @@ namespace GameHub.Data.Sources.EpicGames return ""; } - public void install(string id) - { - // FIXME: It can be done much better - var process = new Subprocess.newv ({"legendary", "download", id}, STDOUT_PIPE | STDIN_PIPE); - var input = new DataOutputStream(process.get_stdin_pipe ()); + public void cancel_installation() { + if(downloadProcess != null) { + downloadProcess.send_signal (2); + } + } + + public int64 get_install_size(string id) { + var process = new Subprocess.newv ( + {paths.legendary_command, "download", id}, STDOUT_PIPE | STDERR_PIPE); var output = new DataInputStream(process.get_stdout_pipe ()); + var errOutput = new DataInputStream(process.get_stderr_pipe ()); + + string line; + MatchInfo info; + while ((line = errOutput.read_line ()) != null) { + if(installSizeRegex.match (line, 0, out info)) { + int64 size = ((int64) double.parse (info.fetch (1))) * 1000000; + process.send_signal (2); + return size; + } + } + + process.send_signal (2); + return 0; + } + + public void install(string id, string? game_folder = null, Utils.FutureResult? progress=null) + { + string[] command = {paths.legendary_command, "install", id}; + if (game_folder != null) { + command = {paths.legendary_command, "install", id, "--base-path", game_folder}; + } + + downloadProcess = new Subprocess.newv (command, STDOUT_PIPE | STDIN_PIPE | STDERR_PIPE); + + var input = new DataOutputStream(downloadProcess.get_stdin_pipe ()); + var output = new DataInputStream(downloadProcess.get_stdout_pipe ()); + var outputError = new DataInputStream(downloadProcess.get_stderr_pipe ()); + string? line = null; input.put_string("y\n"); - while ((line = output.read_line()) != null) { - debug("[EpicGames] %s", line); + + MatchInfo info; + while ((line = outputError.read_line()) != null) { + if(progressRegex.match (line, 0, out info)) { + progress(double.parse (info.fetch(1))); + } } + refresh_installed = true; } + public bool is_authenticated() { + var savedSid = settings.sid; + if (savedSid == "") savedSid = "test"; + + string? output = null; + string? error = null; + new Subprocess.newv ({paths.legendary_command, "auth", "--sid", savedSid}, STDOUT_PIPE | STDERR_PIPE).communicate_utf8(null, null, out output, out error); + + if (output.contains("ERROR")) return false; + else if(output.contains("Stored credentials are still valid")) return true; + else if (error.contains("ERROR")) return false; + else if(error.contains("Stored credentials are still valid")) return true; + + return false; + } + + public async string? get_username() { + File userfile = File.new_for_path(GLib.Environment.get_home_dir () + "/.config/legendary/user.json"); + if (userfile.query_exists ()) { + var dis = new DataInputStream (userfile.read ()); + string line; + + MatchInfo match; + while ((line = dis.read_line (null)) != null) + if(fileUserRegex.match (line, 0, out match)) return match.fetch (1); + + return null; + }else return null; + } + + public async string? auth() + { + if(is_authenticated()) { + var username = yield get_username(); + if (username != null) return username; + } + + //Do auth process + string url = "https://www.epicgames.com/id/login?redirectUrl=https://www.epicgames.com/id/api/redirect"; + var wnd = new GameHub.UI.Windows.WebAuthWindow(EpicGames.instance.name, url, "https://www.epicgames.com/id/api/redirect"); + + string? username = null; + wnd.pageLoaded.connect(page => { + MatchInfo info; + sidExtractionRegex.match(page, 0, out info); + var sid = info.fetch(1); + + //Legendary auth with sid + var output = new DataInputStream(new Subprocess.newv ({paths.legendary_command, "auth", "--sid", sid}, STDOUT_PIPE).get_stdout_pipe ()); + string? line = null; + while ((line = output.read_line()) != null) { + if(authSuccessRegex.match(line, 0, out info)) { + username = info.fetch(1); + settings.sid = sid; + debug("[EpicGames] Successfully logged in as %s", username); + + Idle.add(auth.callback); + break; + } + } + }); + + wnd.canceled.connect(() => Idle.add(auth.callback)); + + wnd.set_size_request(550, 680); + wnd.show_all(); + wnd.present(); + + yield; + return username; + } + public void uninstall(string id) { // FIXME: It can be done much better - var process = new Subprocess.newv ({"legendary", "uninstall", id}, STDOUT_PIPE | STDIN_PIPE); + var process = new Subprocess.newv ({paths.legendary_command, "uninstall", id}, STDOUT_PIPE | STDIN_PIPE); var input = new DataOutputStream(process.get_stdin_pipe ()); var output = new DataInputStream(process.get_stdout_pipe ()); string? line = null; @@ -84,9 +206,13 @@ namespace GameHub.Data.Sources.EpicGames refresh_installed = true; } + public void import_game(string id, string path) { + var process = new Subprocess.newv ({paths.legendary_command, "import-game", id, path}, STDOUT_PIPE); + } + public void run(string id) { // FIXME: not good idea - new Subprocess.newv ({"legendary", "launch", id}, STDOUT_PIPE); + new Subprocess.newv ({paths.legendary_command, "launch", id}, STDOUT_PIPE); } private bool refresh_installed = true; @@ -104,7 +230,7 @@ namespace GameHub.Data.Sources.EpicGames private void build_installed_list() { - var installed_output = new DataInputStream(new Subprocess.newv ({"legendary", "list-installed"}, STDOUT_PIPE).get_stdout_pipe ()); + var installed_output = new DataInputStream(new Subprocess.newv ({paths.legendary_command, "list-installed"}, STDOUT_PIPE).get_stdout_pipe ()); _installed.clear(); string? line = null; MatchInfo info; diff --git a/src/meson.build b/src/meson.build index 23bc6db2..e53e0f8b 100644 --- a/src/meson.build +++ b/src/meson.build @@ -108,7 +108,7 @@ sources = [ 'ui/dialogs/SettingsDialog/pages/general/Collection.vala', 'ui/dialogs/SettingsDialog/pages/general/Tweaks.vala', 'ui/dialogs/SettingsDialog/pages/sources/Steam.vala', - 'ui/dialogs/SettingsDialog/pages/sources/EpicGames.vala', + 'ui/dialogs/SettingsDialog/pages/sources/EpicGames.vala', 'ui/dialogs/SettingsDialog/pages/sources/GOG.vala', 'ui/dialogs/SettingsDialog/pages/sources/Humble.vala', 'ui/dialogs/SettingsDialog/pages/sources/Itch.vala', @@ -191,7 +191,8 @@ sources = [ 'settings/Tweaks.vala', 'utils/downloader/Downloader.vala', - 'utils/downloader/SoupDownloader.vala' + 'utils/downloader/SoupDownloader.vala', + 'utils/downloader/EpicDownloader.vala' ] if get_option('os') == 'linux' diff --git a/src/settings/Auth.vala b/src/settings/Auth.vala index fa0b91cf..3d2c7b76 100644 --- a/src/settings/Auth.vala +++ b/src/settings/Auth.vala @@ -56,6 +56,31 @@ namespace GameHub.Settings.Auth } } + public class EpicGames: SettingsSchema + { + public bool enabled { get; set; } + public bool authenticated { get; set; } + public string sid { get; set; } + + public EpicGames() + { + base(ProjectConfig.PROJECT_NAME + ".auth.epicgames"); + } + + private static EpicGames? _instance; + public static unowned EpicGames instance + { + get + { + if(_instance == null) + { + _instance = new EpicGames(); + } + return _instance; + } + } + } + public class GOG: SettingsSchema { public bool enabled { get; set; } diff --git a/src/ui/dialogs/InstallDialog.vala b/src/ui/dialogs/InstallDialog.vala index 03cc0f7a..f2835417 100644 --- a/src/ui/dialogs/InstallDialog.vala +++ b/src/ui/dialogs/InstallDialog.vala @@ -213,7 +213,11 @@ namespace GameHub.UI.Dialogs } else { - subtitle_label.label = _("Installer size: %s").printf(fsize(installers[0].full_size)); + subtitle_label.label = _("Installer size: calculating..."); + Utils.thread("CalculatingFileSize", () => { + subtitle_label.label = _("Installer size: %s").printf(fsize(installers[0].full_size)); + }); + } Revealer? compat_tool_revealer = new Revealer(); @@ -374,7 +378,11 @@ namespace GameHub.UI.Dialogs name.hexpand = true; name.halign = Align.START; - var size = new Label(fsize(installer.full_size)); + var size = new Label("calculating..."); + Utils.thread("CalculatingFileSize", () => { + size.label = fsize(installer.full_size); + }); + size.halign = Align.END; box.add(icon); diff --git a/src/ui/dialogs/SettingsDialog/pages/sources/EpicGames.vala b/src/ui/dialogs/SettingsDialog/pages/sources/EpicGames.vala index d7539d66..980baf5f 100644 --- a/src/ui/dialogs/SettingsDialog/pages/sources/EpicGames.vala +++ b/src/ui/dialogs/SettingsDialog/pages/sources/EpicGames.vala @@ -20,11 +20,19 @@ using Gtk; using GameHub.Utils; using GameHub.UI.Widgets; +using GameHub.Data.Sources.EpicGames; namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Sources { public class EpicGames: SettingsDialogPage { + + private LegendaryWrapper? legendary_wrapper { get; private set; } + private Settings.Auth.EpicGames epicgames_auth; + private GameHub.Data.Sources.EpicGames.EpicGames epic = GameHub.Data.Sources.EpicGames.EpicGames.instance; + + private Button logout_btn; + public EpicGames(SettingsDialog dlg) { Object( @@ -40,12 +48,56 @@ namespace GameHub.UI.Dialogs.SettingsDialog.Pages.Sources construct { var paths = FSUtils.Paths.Settings.instance; + epicgames_auth = Settings.Auth.EpicGames.instance; + + status_switch.active = epicgames_auth.enabled; + status_switch.notify["active"].connect(() => { + epicgames_auth.enabled = status_switch.active; + request_restart(); + update(); + }); + logout_btn = new Button.with_label(_("Logout")); + action_area.add(logout_btn); + + logout_btn.clicked.connect(() => { + epicgames_auth.authenticated = false; + epicgames_auth.sid = ""; + request_restart(); + update(); + }); + + add_entry("Legendary client command", paths.legendary_command, (command) => { + paths.legendary_command = command; + }); + + var game_default_folder = (paths.epic_games == null || paths.epic_games == "") ? _("Default") : paths.epic_games; + add_file_chooser(_("Games directory"), FileChooserAction.SELECT_FOLDER, game_default_folder, v => { paths.epic_games = v; request_restart(); }, false); update(); } private void update() { + content_area.sensitive = epicgames_auth.enabled; + logout_btn.sensitive = epicgames_auth.authenticated; + + if(!epicgames_auth.enabled) + { + status = description = _("Disabled"); + } + else if(!epic.is_installed()) + { + status = description = _("Missing Legendary"); + } + else if(!epicgames_auth.authenticated) + { + status = description = _("Not authenticated"); + } + else + { + var user_name = GameHub.Data.Sources.EpicGames.EpicGames.instance.user_name; + status = description = user_name != null ? _("Authenticated as %s").printf(user_name) : _("Authenticated"); + } } } diff --git a/src/ui/windows/WebAuthWindow.vala b/src/ui/windows/WebAuthWindow.vala index 9eec4673..1d024ee5 100644 --- a/src/ui/windows/WebAuthWindow.vala +++ b/src/ui/windows/WebAuthWindow.vala @@ -36,6 +36,7 @@ namespace GameHub.UI.Windows private bool is_finished = false; public signal void finished(string url); + public signal void pageLoaded(string page); public signal void canceled(); private const string GOG_CSS = "body { background-color: #d2d2d2 !important; } ._modal__box { box-shadow: none !important; vertical-align: top !important; margin-top: 0 !important; } ._modal__control, .form__buttons-container, .form__separator { display: none !important; }"; @@ -129,16 +130,30 @@ namespace GameHub.UI.Windows debug("[WebAuth/%s] Finished with result `%s`", source, token); } finished(token); - destroy(); + + //Execute javascript to extract page source + runJavascript.begin ("document.body.childNodes[0].textContent", (obj, res) => { + pageLoaded(runJavascript.end (res).get_js_value().to_string()); + destroy(); + }); } + + }); webview.load_uri(url); add(webview); + #endif destroy.connect(() => { if(!is_finished) canceled(); }); } + + private async WebKit.JavascriptResult runJavascript(string script) { + return yield webview.run_javascript(script); + } + + } } diff --git a/src/utils/FSUtils.vala b/src/utils/FSUtils.vala index c90b72da..86057b6f 100644 --- a/src/utils/FSUtils.vala +++ b/src/utils/FSUtils.vala @@ -41,6 +41,8 @@ namespace GameHub.Utils public string humble_games { get; set; } public string itch_home { get; set; } public string itch_games { get; set; } + public string legendary_command { get; set; } + public string epic_games { get; set; } public Settings() { diff --git a/src/utils/downloader/EpicDownloader.vala b/src/utils/downloader/EpicDownloader.vala new file mode 100644 index 00000000..a23a380b --- /dev/null +++ b/src/utils/downloader/EpicDownloader.vala @@ -0,0 +1,49 @@ +/* +This file is part of GameHub. +Copyright (C) 2018-2019 Anatoliy Kashkin +Copyright (C) 2020 Adam Jordanek + +GameHub is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +GameHub is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GameHub. If not, see . +*/ + +using GLib; +using Gee; + +namespace GameHub.Utils.Downloader +{ + + public class EpicDownload : Download { + + public EpicDownload(string id) + { + base(id); + } + + public signal void cancelled(); + public override void cancel() { + cancelled(); + } + + public class EpicStatus : Download.Status { + private double _progress = -1; + public override double progress { get { return _progress; } } + + public EpicStatus(double progress) { + this._progress = progress; + } + + } + } + +} \ No newline at end of file