From b46493c8fbbd87910e53181eda294cfda4c5fba3 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sun, 11 Dec 2022 23:37:22 -0500 Subject: [PATCH 001/227] Create the initial functionality for a Web Services module that will handle all remote connections --- Lib/Shared/Includes.ahk | 14 ++ .../WebServices/Entity/WebServiceEntity.ahk | 135 +++++++++++++ .../Entity/WebServiceProviderEntity.ahk | 90 +++++++++ .../Event/WebServiceRequestEvent.ahk | 17 ++ .../Event/WebServiceResponseEvent.ahk | 14 ++ .../WebServices/Events/WebServicesEvents.ahk | 7 + .../LaunchpadLoginWindow.ahk | 29 +++ .../ManageWindow/ManageWebServicesWindow.ahk | 30 +++ .../JwtWebServiceAuthenticator.ahk | 150 ++++++++++++++ .../WebServiceAuthenticatorBase.ahk | 71 +++++++ .../BasicWebServiceRequest.ahk | 3 + .../WebServiceRequestBase.ahk | 191 ++++++++++++++++++ .../CachedWebServiceResponse.ahk | 32 +++ .../HttpReqWebServiceResponse.ahk | 17 ++ .../WebServiceResponseBase.ahk | 57 ++++++ .../WebServices/WebServices.module.json | 76 +++++++ 16 files changed, 933 insertions(+) create mode 100644 Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk create mode 100644 Lib/Shared/Modules/WebServices/Entity/WebServiceProviderEntity.ahk create mode 100644 Lib/Shared/Modules/WebServices/Event/WebServiceRequestEvent.ahk create mode 100644 Lib/Shared/Modules/WebServices/Event/WebServiceResponseEvent.ahk create mode 100644 Lib/Shared/Modules/WebServices/Events/WebServicesEvents.ahk create mode 100644 Lib/Shared/Modules/WebServices/Gui/AuthenticationGui/LaunchpadLoginWindow.ahk create mode 100644 Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk create mode 100644 Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk create mode 100644 Lib/Shared/Modules/WebServices/WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk create mode 100644 Lib/Shared/Modules/WebServices/WebServiceRequest/BasicWebServiceRequest.ahk create mode 100644 Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk create mode 100644 Lib/Shared/Modules/WebServices/WebServiceResponse/CachedWebServiceResponse.ahk create mode 100644 Lib/Shared/Modules/WebServices/WebServiceResponse/HttpReqWebServiceResponse.ahk create mode 100644 Lib/Shared/Modules/WebServices/WebServiceResponse/WebServiceResponseBase.ahk create mode 100644 Lib/Shared/Modules/WebServices/WebServices.module.json diff --git a/Lib/Shared/Includes.ahk b/Lib/Shared/Includes.ahk index 9cfe560e..c55e0691 100644 --- a/Lib/Shared/Includes.ahk +++ b/Lib/Shared/Includes.ahk @@ -5,6 +5,20 @@ #Include Modules\Auth\AuthProvider\JwtAuthProvider.ahk #Include Modules\LaunchpadApi\AuthProvider\LaunchpadApiAuthProvider.ahk #Include Modules\LaunchpadApi\DataSource\ApiDataSource.ahk +#Include Modules\WebServices\Entity\WebServiceEntity.ahk +#Include Modules\WebServices\Entity\WebServiceProviderEntity.ahk +#Include Modules\WebServices\Event\WebServiceRequestEvent.ahk +#Include Modules\WebServices\Event\WebServiceResponseEvent.ahk +#Include Modules\WebServices\Events\WebServicesEvents.ahk +#Include Modules\WebServices\Gui\AuthenticationGui\LaunchpadLoginWindow.ahk +#Include Modules\WebServices\Gui\ManageWindow\ManageWebServicesWindow.ahk +#Include Modules\WebServices\WebServiceAuthenticator\JwtWebServiceAuthenticator.ahk +#Include Modules\WebServices\WebServiceAuthenticator\WebServiceAuthenticatorBase.ahk +#Include Modules\WebServices\WebServiceRequest\BasicWebServiceRequest.ahk +#Include Modules\WebServices\WebServiceRequest\WebServiceRequestBase.ahk +#Include Modules\WebServices\WebServiceResponse\CachedWebServiceResponse.ahk +#Include Modules\WebServices\WebServiceResponse\HttpReqWebServiceResponse.ahk +#Include Modules\WebServices\WebServiceResponse\WebServiceResponseBase.ahk #Include Vendor\Gdip_All.ahk #Include Vendor\LV_Constants.ahk #Include Volantis.App\App\AppBase.ahk diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk new file mode 100644 index 00000000..6688f04c --- /dev/null +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -0,0 +1,135 @@ +class WebServiceEntity extends AppEntityBase { + cacheObj := "" + stateObj := "" + persistentStateObj := "" + + AuthData[key] { + get => this.GetAuthData(key) + set => this.SetAuthData(key, value) + } + + PersistentAuthData[key] { + get => this.GetPersistentAuthData(key) + set => this.SetPersistentAuthData(key, value) + } + + __New(app, id, entityTypeId, container, cacheObj, stateObj, persistentStateObj, eventMgr, storageObj, idSanitizer, parentEntity := "") { + this.cacheObj := cacheObj + this.stateObj := stateObj + this.persistentStateObj := persistentStateObj + + super.__New(app, id, entityTypeId, container, eventMgr, storageObj, idSanitizer, parentEntity) + } + + static Create(container, eventMgr, id, entityTypeId, storageObj, idSanitizer, parentEntity := "") { + className := this.Prototype.__Class + + return %className%( + container.GetApp(), + id, + entityTypeId, + container, + container.Get("cache.web_services"), + container.Get("state.web_services"), + container.Get("state.web_service_persistent"), + eventMgr, + storageObj, + idSanitizer, + parentEntity + ) + } + + BaseFieldDefinitions() { + definitions := super.BaseFieldDefinitions() + + definitions["Provider"] := Map( + "type", "entity_reference", + "entityType", "web_service_provider" + "required", true + ) + + return definitions + } + + Request(path, method := "", data := "", useAuthentication := -1, cacheResponse := true) { + if (!method) { + method := this["Provider"]["DefaultMethod"] + } + + if (useAuthentication == -1) { + useAuthentication := this["Provider"]["AuthenticateRequestsByDefault"] + } + + return BasicWebServiceRequest(this.eventMgr, this, this.cacheObj, method, path, data, useAuthentication, cacheResponse) + } + + GetAuthData(key := "") { + return this._getStateData(this.stateObj, key) + } + + SetAuthData(keyOrMap, value) { + return this._setStateData(this.stateObj, keyOrMap, value) + } + + ResetAuthData(newData := "") { + if (!newData) { + newData := Map() + } + + this._createStateParents(this.stateObj) + this.stateObj["WebServices"][this.Id]["AuthData"] := newData + this.stateObj.SaveState() + } + + GetPersistentAuthData(key := "") { + return this._getStateData(this.persistentStateObj, key) + } + + SetPersistentAuthData(key, value) { + return this._setStateData(this.persistentStateObj, key, value) + } + + _getStateData(stateObj, key) { + save := this._createStateParents(stateObj) + + if (save) { + stateObj.SaveState() + } + + authData := stateObj["WebServices"][this.Id]["AuthData"] + + if (key) { + authData := (authData.Has(key) ? authData[key] : "") + } + + return authData + } + + _setStateData(stateObj, key, value) { + this._createStateParents(stateObj) + stateObj["WebServices"][this.Id]["AuthData"][key] := value + stateObj.SaveState() + return this + } + + _createStateParents(stateObj) { + modified := false + + if (!stateObj.Has("WebServices")) { + stateObj["WebServices"] := Map() + modified := true + } + + if (!stateObj["WebServices"].Has(this.Id)) { + stateObj["WebServices"][this.Id] := Map() + modified := true + } + + if (!stateObj["WebServices"][this.Id].Has("AuthData")) { + stateObj["WebServices"][this.Id]["AuthData"] := Map() + modified := true + } + + return modified + } +} diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceProviderEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceProviderEntity.ahk new file mode 100644 index 00000000..8ee45343 --- /dev/null +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceProviderEntity.ahk @@ -0,0 +1,90 @@ +class WebServiceProviderEntity extends AppEntityBase { + BaseFieldDefinitions() { + definitions := super.BaseFieldDefinitions() + + definitions["EndpointUrl"] := Map( + "default", "", + "required", true + ) + + definitions["AuthenticationEndpointUrl"] := Map( + "default", "", + "required", false + ) + + definitions["AuthenticationRefreshPath"] := Map( + "default", "", + "required", false + ) + + definitions["IconSrc"] := Map( + "type", "icon_file", + "default", "webhook", + "required", true + ) + + definitions["SupportsAuthentication"] := Map( + "type", "boolean", + "required", false, + "default", false + ) + + definitions["Authenticator"] := Map( + "type", "service_reference", + "servicePrefix", "web_services_authenticator.", + "default", "", + "required", false + ) + + definitions["DefaultMethod"] := Map( + "default", "GET", + "required", false + ) + + definitions["AuthenticateRequestsByDefault"] := Map( + "type", "boolean", + "default", false, + "required", false + ) + + definitions["LoginWindow"] := Map( + "default", "", + "required", false + ) + + definitions["AppKey"] := Map( + "default", "", + "required", false + ) + + return definitions + } + + Url(path, queryParams := "") { + if (InStr(path, "/") != 1) { + path := "/" . path + } + + return UrlObj(this["EndpointUrl"] . path) + .AddQueryParams(queryParams) + } + + FullPath(path) { + url := this.Url(path) + return url.Path + } + + GetAuthenticationRefreshUrl(queryParams := "") { + endpointUrl := this["AuthenticationEndpointUrl"] + ? this["AuthenticationEndpointUrl"] + : this["EndpointUrl"] + refreshPath := this["AuthenticationRefreshPath"] + + if (refreshPath && InStr(refreshPath, "/") != 1) { + refreshPath := "/" . refreshPath + } + + return UrlObj(endpointUrl . refreshPath) + .AddQueryParams(queryParams) + } +} diff --git a/Lib/Shared/Modules/WebServices/Event/WebServiceRequestEvent.ahk b/Lib/Shared/Modules/WebServices/Event/WebServiceRequestEvent.ahk new file mode 100644 index 00000000..1b8ba941 --- /dev/null +++ b/Lib/Shared/Modules/WebServices/Event/WebServiceRequestEvent.ahk @@ -0,0 +1,17 @@ +class WebServiceRequestEvent extends EventBase { + _requestObj := "" + + __New(eventName, requestObj) { + this._requestObj := requestObj + + super.__New(eventName) + } + + Request { + get => this.requestObj + } + + HttpReq { + get => this.Request.GetHttpReq() + } +} diff --git a/Lib/Shared/Modules/WebServices/Event/WebServiceResponseEvent.ahk b/Lib/Shared/Modules/WebServices/Event/WebServiceResponseEvent.ahk new file mode 100644 index 00000000..8db376bb --- /dev/null +++ b/Lib/Shared/Modules/WebServices/Event/WebServiceResponseEvent.ahk @@ -0,0 +1,14 @@ +class WebServiceResponseEvent extends EventBase { + _responseObj := "" + + __New(eventName, responseObj) { + this._responseObj := responseObj + + super.__New(eventName) + } + + Response { + get => this._responseObj + set => this._responseObj := value + } +} diff --git a/Lib/Shared/Modules/WebServices/Events/WebServicesEvents.ahk b/Lib/Shared/Modules/WebServices/Events/WebServicesEvents.ahk new file mode 100644 index 00000000..787d5a1a --- /dev/null +++ b/Lib/Shared/Modules/WebServices/Events/WebServicesEvents.ahk @@ -0,0 +1,7 @@ +class WebServicesEvents { + static WEB_SERVICES_HTTP_REQ_ALTER := 0x4200 + static WEB_SERVICES_REQUEST_PRESEND := 0x4210 + static WEB_SERVICES_CACHED_RESPONSE_CREATED := 0x4215 + static WEB_SERVICES_HTTP_RESPONSE_CREATED := 0x4217 + static WEB_SERVICES_RESPONSE_ALTER := 0x4220 +} \ No newline at end of file diff --git a/Lib/Shared/Modules/WebServices/Gui/AuthenticationGui/LaunchpadLoginWindow.ahk b/Lib/Shared/Modules/WebServices/Gui/AuthenticationGui/LaunchpadLoginWindow.ahk new file mode 100644 index 00000000..53e76866 --- /dev/null +++ b/Lib/Shared/Modules/WebServices/Gui/AuthenticationGui/LaunchpadLoginWindow.ahk @@ -0,0 +1,29 @@ +class LaunchpadLoginWindow extends FormGuiBase { + entityObj := "" + entityManager := "" + missingFields := Map() + dataSource := "" + + GetDefaultConfig(container, config) { + defaults := super.GetDefaultConfig(container, config) + defaults["title"] := "Login" + defaults["text"] := "Logging in allows enhanced features such as online backup, restore, personalization, and sharing with the community.`n`nIf you'd like to log in, click the `"Get token`" button to go to the launchpad.games site to retrieve a valid login token and then paste it below." + defaults["buttons"] := "*&Login|&Cancel" + return defaults + } + + Controls() { + super.Controls() + this.Add("ButtonControl", "xs y+m vGetAuthToken w150 h30", "Get Token") + this.AddHeading("Login Token") + this.guiObj.AddEdit("vAuthToken xs y+m r1 w" . this.windowSettings["contentWidth"] . " c" . this.themeObj.GetColor("editText")) + } + + OnGetAuthToken(btn, info) { + Run("https://launchpad.games/profile") + } + + ProcessResult(result, submittedData := "") { + return (result == "Login") ? this.guiObj["AuthToken"].Text : "" + } +} diff --git a/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk b/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk new file mode 100644 index 00000000..bc1a3c1f --- /dev/null +++ b/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk @@ -0,0 +1,30 @@ +class ManageWebServicesWindow extends ManageEntitiesWindow { + listViewColumns := Array("ID", "PROVIDER", "NAME", "AUTHENTICATED") + + GetListViewData(lv) { + data := Map() + + for key, webService in this.entityMgr { + data[key] := [ + webService["id"], + webService["Provider"]["name"], + webService["name"], + webService.AuthData["authenticated"] ? "Yes" : "No" + ] + } + + return data + } + + ViewEntity(key) { + ; @todo generic view operation for double-clicking non-editable entities + } + + AddEntity() { + ; @todo Implement generic add dialog and operation + } + + DeleteEntity(key) { + ; @todo Implement generic delete dialog and operation + } +} diff --git a/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk new file mode 100644 index 00000000..e5031f7d --- /dev/null +++ b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk @@ -0,0 +1,150 @@ +class JwtWebServiceAuthenticator extends WebServiceAuthenticatorBase { + Login(webServiceEnt, retryCount := 0) { + if (retryCount > this.maxRetries) { + throw OperationFailedException("Login failed after " . retryCount . " tries.") + } + + if (!this._hasRefreshToken(webServiceEnt)) { + this._reauthenticate(webServiceEnt) + } + + success := this._hasRefreshToken(webServiceEnt) + ? this._refreshAuthentication(webServiceEnt) + : false + + if (!success) { + success := this.Login(webServiceEnt, retryCount + 1) + } + + return success + } + + Logout(webServiceEnt) { + webServiceEnt.PersistentAuthData["auth_token"] := "" + webServiceEnt.PersistentAuthData["refresh_token"] := "" + webServiceEnt.ResetAuthData(Map("authenticated", false)) + + return true + } + + RefreshAuthentication(webServiceEnt) { + if (this.NeedsRefresh(webServiceEnt)) { + this.Login(webServiceEnt) + } + } + + AlterRequest(webServiceEnt, request, httpReqObj) { + bearerToken := webServiceEnt.AuthData["auth_token"] + + if (bearerToken) { + httpReqObj.requestHeaders["Authorization"] := "Bearer " . bearerToken + } + } + + _hasRefreshToken(webServiceEnt) { + return !!(webServiceEnt.PersistentAuthData["refresh_token"]) + } + + _reauthenticate(webServiceEnt) { + refreshToken := this._authenticationGui(webServiceEnt) + + if (refreshToken) { + this._setRefreshToken(webServiceEnt, refreshToken) + } + + return refreshToken + } + + _getRefreshToken(webServiceEnt) { + return webServiceEnt.PersistentAuthData["refresh_token"] + } + + _setRefreshToken(webServiceEnt, refreshToken) { + webServiceEnt.PersistentAuthData["refresh_token"] := refreshToken + } + + _extractAuthData(webServiceEnt, response) { + loginData := response.GetJsonData() + authData := Map( + "authenticated", (loginData.Has("user_id") && !!(loginData["user_id"])) + ) + persistentData := Map() + authDataMap := Map() + persistentDataMap := Map( + "user_id", "user_id", + "refresh_token", "refresh_token", + "id_token", "auth_token", + "access_token", "access_token" + ) + skipKeys := [ + "expires_in" + ] + + if (loginData.Has("expires_in")) { + persistentData["expires"] := DateAdd(A_Now, loginData["expires_in"], "S") + } + + for key, val in loginData { + if (persistentDataMap.Has(key)) { + persistentData[persistentDataMap[key]] := loginData[key] + } else if (authDataMap.Has(key)) { + authData[authDataMap[key]] := loginData[key] + } else if (!authData.Has(key) && !persistentData.Has(key)) { + skip := false + + for index, skipKey in skipKeys { + if (key == skipKey) { + skip := true + break + } + } + + if (!skip) { + authData[key] := val + } + } + } + + for key, val in authData { + webServiceEnt.AuthData[key] := val + } + + for key, val in persistentData { + webServiceEnt.PersistentAuthData[key] := val + } + } + + _refreshAuthentication(webServiceEnt) { + apiKey := webServiceEnt["Provider"]["AppKey"] + refreshToken := webServiceEnt.PersistentAuthData["refresh_token"] + refreshUrl := webServiceEnt["Provider"].GetAuthenticationRefreshUrl(Map("token", apiKey)) + response := "" + + if (!apiKey) { + throw OperationFailedException("Missing API key for auth refresh.") + } + + if (!refreshToken) { + throw OperationFailedException("Missing refresh token for auth refresh.") + } + + if (refreshUrl) { + payload := Map( + "grant_type", "refresh_token", + "refresh_token", refreshToken + ) + + response := webServiceEnt.Request(refreshUrl, "POST", payload, false, false).Send() + } + + success := response.IsSuccessful() + + if (response && success) { + this._extractAuthData(webServiceEnt, response) + } else { + webServiceEnt.PersistentAuthData["refresh_token"] := "" + } + + return success + } +} diff --git a/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk new file mode 100644 index 00000000..41b7ab38 --- /dev/null +++ b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk @@ -0,0 +1,71 @@ +class WebServiceAuthenticatorBase { + refreshThresholdSeconds := 600 + authenticatedStateKey := "authenticated" + expiresStateKey := "expires" + maxRetries := 3 + guiMgr := "" + + __New(guiMgr) { + this.guiMgr := guiMgr + } + + Login(webServiceEnt, retryCount := 0) { + + } + + Logout(webServiceEnt) { + + } + + RefreshAuthentication(webServiceEnt) { + + } + + AlterRequest(webServiceEnt, request, httpReqObj) { + + } + + IsAuthenticated(webServiceEnt) { + return (webServiceEnt.AuthData[this.authenticatedStateKey] && !this.AuthenticationIsExpired(webServiceEnt)) + } + + NeedsRefresh(webServiceEnt) { + needsRefresh := false + + if (this.IsAuthenticated(webServiceEnt)) { + expires := webServiceEnt.AuthData[this.expiresStateKey] + + if (expires) { + diff := DateDiff(A_Now, expires, "S") + needsRefresh := (diff >= (0 - this.refreshThresholdSeconds)) + } else { + needsRefresh := true + } + } else { + needsRefresh := true + } + + return needsRefresh + } + + AuthenticationIsExpired(webServiceEnt) { + expired := true + + if (webServiceEnt.AuthData[this.authenticatedStateKey] && webServiceEnt.AuthData[this.expiresStateKey]) { + expired := (DateDiff(A_Now, webServiceEnt.AuthData["expires"], "S") >= 0) + } + + return expired + } + + _authenticationGui(webServiceEnt) { + loginWindowGui := webServiceEnt["Provider"]["LoginWindow"] + result := "" + + if (loginWindowGui) { + result := this.guiMgr.Dialog(Map("type", loginWindowGui)) + } + + return result + } +} diff --git a/Lib/Shared/Modules/WebServices/WebServiceRequest/BasicWebServiceRequest.ahk b/Lib/Shared/Modules/WebServices/WebServiceRequest/BasicWebServiceRequest.ahk new file mode 100644 index 00000000..a723ff4c --- /dev/null +++ b/Lib/Shared/Modules/WebServices/WebServiceRequest/BasicWebServiceRequest.ahk @@ -0,0 +1,3 @@ +class BasicWebServiceRequest extends WebServiceRequestBase { + +} \ No newline at end of file diff --git a/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk b/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk new file mode 100644 index 00000000..dd5f400d --- /dev/null +++ b/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk @@ -0,0 +1,191 @@ +class WebServiceRequestBase { + eventMgr := "" + webServiceEnt := "" + cacheObj := "" + path := "" + method := "" + data := "" + useAuthentication := false + httpReqObj := "" + responseObj := "" + cacheResponse := true + _url := "" + + Url { + get => this._getUrl() + } + + Response { + get => this.responseObj + } + + __New(eventMgr, webServiceEnt, cacheObj, method := "", path := "", data := "", useAuthentication := false, cacheResponse := true) { + this.eventMgr := eventMgr + this.webServiceEnt := webServiceEnt + + if (!method) { + method := "GET" + } + + this.method := method + + if (path) { + this.path := path + } + + if (data) { + this.data := data + } + + this.useAuthentication := useAuthentication + this.cacheResponse := cacheResponse + } + + GetPath() { + return this.path + } + + SetPath(path := "") { + this.path := path + + if (this._url) { + this._url.Path := this.webServiceEnt["Provider"].Path(path) + } + + return this + } + + GetMethod() { + return this.method + } + + SetMethod(method := "GET") { + this.method := method + + return this + } + + GetData() { + return this.data + } + + SetData(data := "", clearCache := false) { + this.data := data + + if (clearCache) { + this.cacheObj.RemoveItem(this.GetPath()) + } + + return this + } + + GetUseAuthentication() { + return this.useAuthentication + } + + SetUseAuthentication(useAuthentication := false) { + this.useAuthentication := useAuthentication + + return this + } + + GetHttpReq() { + if (!this.httpReqObj) { + this.httpReqObj := WinHttpReq(this.Url) + } + + return this.httpReqObj + } + + SetHttpReq(httpReqObj := "") { + this.httpReqObj := httpReqObj + } + + _getUrl() { + if (!this._url) { + this._url := this.webServiceEnt["Provider"].Url(this.path) + } + + return this._url + } + + Send(resend := false) { + if (resend || !this.responseObj) { + if (this.RequestIsCached()) { + this.responseObj := this._createCachedResponse() + } else { + httpReqObj := this.GetHttpReq() + + if (this.GetUseAuthentication() && this.webServiceEnt["Provider"].Has("Authenticator", false)) { + authenticator := this.webServiceEnt["Provider"]["Authenticator"] + + if (authenticator.NeedsRefresh(this.webServiceEnt)) { + authenticator.RefreshAuthentication(this.webServiceEnt) + } + + authenticator.AlterRequest(this.webServiceEnt, this, httpReqObj) + } + + event := WebServiceRequestEvent(WebServicesEvents.WEB_SERVICES_HTTP_REQ_ALTER, this) + this.eventMgr.DispatchEvent(event) + + event := WebServiceRequestEvent(WebServicesEvents.WEB_SERVICES_REQUEST_PRESEND, this) + this.eventMgr.DispatchEvent(event) + + httpReqObj.Send(this.GetMethod(), this.GetData()) + this.responseObj := this._createHttpReqResponse() + this._cacheResponse() + } + + event := WebServiceResponseEvent(WebServicesEvents.WEB_SERVICES_RESPONSE_ALTER, this, this.responseObj) + this.eventMgr.DispatchEvent(event) + + this.responseObj := event.Response + } + + return this.responseObj + } + + RequestIsCached() { + path := this.GetPath() + + return this.cacheObj.ItemExists(path && !this.cacheObj.ItemNeedsUpdate(path)) + } + + _createCachedResponse() { + response := CachedWebServiceResponse(this.webServiceEnt, this) + + event := WebServiceResponseEvent(WebServicesEvents.WEB_SERVICES_CACHED_RESPONSE_CREATED, this, response) + this.eventMgr.DispatchEvent(event) + + return event.Response + } + + _createHttpReqResponse() { + response := HttpReqWebServiceResponse(this.webServiceEnt, this) + + event := WebServiceResponseEvent(WebServicesEvents.WEB_SERVICES_HTTP_RESPONSE_CREATED, this, response) + this.eventMgr.DispatchEvent(event) + + return event.Response + } + + _cacheResponse() { + if (this.responseObj && this.cacheResponse) { + path := this.GetPath() + + if (this.responseObj.IsSuccessful()) { + body := this.responseObj.GetResponseBody() + + if (body) { + this.cacheObj.WriteItem(path, body, this.responseObj.GetHttpStatusCode()) + } else { + ; Response is empty, delete any existing cache for this item + this.cacheObj.RemoveItem(path) + } + } else if (this.responseObj.IsNotFound()) { + this.cacheObj.SetNotFound(path) + } + } + } +} diff --git a/Lib/Shared/Modules/WebServices/WebServiceResponse/CachedWebServiceResponse.ahk b/Lib/Shared/Modules/WebServices/WebServiceResponse/CachedWebServiceResponse.ahk new file mode 100644 index 00000000..a237cfb8 --- /dev/null +++ b/Lib/Shared/Modules/WebServices/WebServiceResponse/CachedWebServiceResponse.ahk @@ -0,0 +1,32 @@ +class CachedWebServiceResponse extends WebServiceResponseBase { + cacheObj := "" + + __New(webServiceEnt, webServiceReq) { + if (webServiceEnt.Has("Cache", false)) { + this.cacheObj := webServiceEnt.cacheObj + } + + super.__New(webServiceEnt, webServiceReq) + } + + GetHttpStatusCode() { + responseCode := 0 + + if (this.cacheObj) { + responseCode := this.cacheObj.GetResponseCode(this.webServiceReq.GetPath()) + } + + return responseCode + } + + GetResponseBody() { + body := "" + path := this.webServiceReq.GetPath() + + if (this.cacheObj.ItemExists(path)) { + body := this.cacheObj.ReadItem(path) + } + + return body + } +} diff --git a/Lib/Shared/Modules/WebServices/WebServiceResponse/HttpReqWebServiceResponse.ahk b/Lib/Shared/Modules/WebServices/WebServiceResponse/HttpReqWebServiceResponse.ahk new file mode 100644 index 00000000..d7857400 --- /dev/null +++ b/Lib/Shared/Modules/WebServices/WebServiceResponse/HttpReqWebServiceResponse.ahk @@ -0,0 +1,17 @@ +class HttpReqWebServiceResponse extends WebServiceResponseBase { + httpReqObj := "" + + __New(webServiceEnt, webServiceReq) { + this.httpReqObj := webServiceReq.GetHttpReq() + + super.__New(webServiceEnt, webServiceReq) + } + + GetHttpStatusCode() { + return this.httpReqObj.GetStatusCode() + } + + GetResponseBody() { + return Trim(this.httpReqObj.GetResponseData()) + } +} diff --git a/Lib/Shared/Modules/WebServices/WebServiceResponse/WebServiceResponseBase.ahk b/Lib/Shared/Modules/WebServices/WebServiceResponse/WebServiceResponseBase.ahk new file mode 100644 index 00000000..e7413bfa --- /dev/null +++ b/Lib/Shared/Modules/WebServices/WebServiceResponse/WebServiceResponseBase.ahk @@ -0,0 +1,57 @@ +class WebServiceResponseBase { + webServiceEnt := "" + webServiceReq := "" + successCodes := [200] + notFoundCodes := [404] + + __New(webServiceEnt, webServiceReq) { + this.webServiceEnt := webServiceEnt + this.webServiceReq := webServiceReq + } + + GetHttpStatusCode() { + return "" + } + + GetResponseBody() { + return "" + } + + GetJsonData() { + body := this.GetResponseBody() + + if (!body) { + body := "{}" + } + + return JsonData().FromString(&body) + } + + IsSuccessful() { + httpCode := this.GetHttpStatusCode() + success := false + + for , successCode in this.successCodes { + if (httpCode == successCode) { + success := true + break + } + } + + return success + } + + IsNotFound() { + httpCode := this.GetHttpStatusCode() + notFound := false + + for , notFoundCode in this.notFoundCodes { + if (httpCode == notFoundCode) { + notFound := true + break + } + } + + return notFound + } +} diff --git a/Lib/Shared/Modules/WebServices/WebServices.module.json b/Lib/Shared/Modules/WebServices/WebServices.module.json new file mode 100644 index 00000000..835a2290 --- /dev/null +++ b/Lib/Shared/Modules/WebServices/WebServices.module.json @@ -0,0 +1,76 @@ +{ + "module": { + "name": "Web Services", + "type": "AppModule", + "icon": "", + "category": "Web Services", + "tags": ["Launchpad", "LaunchpadBuilder"], + "description": "Enables Launchpad to connect to and authenticate with remove services on the Internet for additional functionality.", + "author": { + "name": "Ben McClure, Volantis Dev", + "url": "https://volantisdev.com" + }, + "website": "https://launchpad.games", + "version": "{{VERSION}}", + "appVersion": "", + "dependencies": [] + }, + "parameters": { + "config.web_services_file": "@@{data_dir}\\WebServices.json", + "config.web_service_providers_file": "@@{data_dir}\\WebServiceProviders.json", + "config.web_services_state_path": "@@{data_dir}\\WebServicesState.json", + "config.web_services_view_mode": "Report", + "entity_type.web_service": { + "name_singular": "Web Service", + "name_plural": "Web Services", + "entity_class": "WebServiceEntity", + "storage_config_storage_parent_key": "WebServices", + "storage_config_path_parameter": "config.web_services_file", + "manager_view_mode_parameter": "config.web_services_view_mode", + "default_icon": "webhook", + "allow_add": true, + "allow_edit": true, + "allow_delete": true, + "manager_gui": "ManageWebServicesWindow", + "manager_link_in_tools_menu": true + }, + "entity_type.web_service_provider": { + "name_singular": "Service Provider", + "name_plural": "Service Providers", + "entity_class": "WebServiceProviderEntity", + "definition_loader_class": "ParameterEntityDefinitionLoader", + "definition_loader_parameter_key": "web_services.providers", + "storage_config_path_parameter": "config.web_service_providers_file" + }, + "web_services.providers.launchpad_api": { + "EndpointUrl": "https://api.launchpad.games/v1", + "AuthenticationEndpointUrl": "https://securetoken.googleapis.com/v1", + "IconSrc": "Logo.ico", + "SupportsAuthentication": true, + "Authenticator": "jwt", + "AppKey": "AIzaSyCbwzOWJjTft77P96dV5VB3dAx9TjdDowQ", + "LoginWindow": "LaunchpadLoginWindow" + }, + "web_services.services.api": { + "Provider": "launchpad_api" + } + }, + "services": { + "cache_state.web_services": { + "class": "CacheState", + "arguments": ["@{App}", "@@config.cache_dir", "WebServices.json"] + }, + "cache.web_services": { + "class": "FileCache", + "arguments": ["@{App}", "@cache_state.web_services", "@@config.cache_dir", "WebServices"] + }, + "state.web_services": { + "class": "JsonState", + "arguments": ["@{App}", "@@config.web_services_state_path"] + }, + "web_services_authenticator.jwt": { + "class": "JwtWebServiceAuthenticator", + "arguments": ["@manager.gui"] + } + } +} From 506950e4cf5b890425583a3f797b90db072128de Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 00:24:37 -0500 Subject: [PATCH 002/227] Ignore task output on build-includes task in vscode --- .vscode/tasks.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 3002c722..fb00b155 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -10,7 +10,8 @@ ], "options": { "cwd": "${workspaceFolder}" - } + }, + "problemMatcher": [] }, { "label": "build-launchpad", From 50c60384835588aef9245aa11ceb5456e3ce28b6 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 00:25:11 -0500 Subject: [PATCH 003/227] Add a ParameterState state class that loads and saves from temporary parameter storage --- Lib/Shared/Includes.ahk | 1 + .../Volantis.App/State/ParameterState.ahk | 39 +++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 Lib/Shared/Volantis.App/State/ParameterState.ahk diff --git a/Lib/Shared/Includes.ahk b/Lib/Shared/Includes.ahk index c55e0691..1dcb7297 100644 --- a/Lib/Shared/Includes.ahk +++ b/Lib/Shared/Includes.ahk @@ -110,6 +110,7 @@ #Include Volantis.App\State\AppState.ahk #Include Volantis.App\State\CacheState.ahk #Include Volantis.App\State\JsonState.ahk +#Include Volantis.App\State\ParameterState.ahk #Include Volantis.App\State\StateBase.ahk #Include Volantis.Base\CLR\CLR.ahk #Include Volantis.Base\Event\EventBase.ahk diff --git a/Lib/Shared/Volantis.App/State/ParameterState.ahk b/Lib/Shared/Volantis.App/State/ParameterState.ahk new file mode 100644 index 00000000..7e573004 --- /dev/null +++ b/Lib/Shared/Volantis.App/State/ParameterState.ahk @@ -0,0 +1,39 @@ +class ParameterState extends StateBase { + parameterKey := "" + + __New(app, parameterKey, autoLoad := false) { + this.parameterKey := parameterKey + super.__New(app, "", autoLoad) + } + + SaveState(newState := "") { + if (newState != "") { + this.stateMap := newState + } + + if (this.parameterKey) { + this.container.Parameters[this.parameterKey] := this.stateMap + } + + return this.stateMap + } + + LoadState() { + if (this.parameterKey && !this.stateLoaded) { + newState := super.LoadState() + + if (this.container.HasParameter(this.parameterKey)) { + paramValue := this.container.Parameters[this.parameterKey] + + if (HasBase(paramValue, Map.Prototype)) { + newState := paramValue + } + } + + this.stateMap := newState + this.stateLoaded := true + } + + return this.stateMap + } +} From ad7ff99400186ffe2da6e21e50f4f82ed5db7e86 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 00:25:26 -0500 Subject: [PATCH 004/227] Save the container in StateBase --- Lib/Shared/Volantis.App/State/StateBase.ahk | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/Shared/Volantis.App/State/StateBase.ahk b/Lib/Shared/Volantis.App/State/StateBase.ahk index 4aaf8b0a..65f0c2b4 100644 --- a/Lib/Shared/Volantis.App/State/StateBase.ahk +++ b/Lib/Shared/Volantis.App/State/StateBase.ahk @@ -1,5 +1,6 @@ class StateBase { app := "" + container := "" stateMap := Map() stateLoaded := false @@ -21,6 +22,7 @@ class StateBase { InvalidParameterException.CheckTypes("StateBase", "app", app, "AppBase") this.app := app + this.container := app.Services if (state != "") { InvalidParameterException.CheckTypes("StateBase", "state", state, "Map") From 1fd39680ac7decdcefdb61a9af73dc6dd5bb03f2 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 00:25:46 -0500 Subject: [PATCH 005/227] Move entity icon retrieval to a separate method in ManageEntitiesWindow --- .../Gui/ManageWindow/ManageEntitiesWindow.ahk | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk b/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk index 4d18b352..15be3be8 100644 --- a/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk +++ b/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk @@ -74,10 +74,9 @@ class ManageEntitiesWindow extends ManageWindowBase { defaultIcon := this.themeObj.GetIconPath(defaultIconName) iconNum := 1 - iconField := this.entityType.definition["icon_field"] for key, entityObj in this.entityMgr { - iconSrc := entityObj[iconField] + iconSrc := this.GetEntityIconSrc(entityObj) if (!InStr(iconSrc, ":\")) { iconSrc := this.themeObj.GetIconPath(iconSrc) @@ -94,6 +93,17 @@ class ManageEntitiesWindow extends ManageWindowBase { return IL } + GetEntityIconSrc(entityObj) { + iconSrc := "" + iconField := this.entityType.definition["icon_field"] + + if (iconField && entityObj.Has(iconField)) { + iconSrc := entityObj[iconField] + } + + return iconSrc + } + OnDoubleClick(LV, rowNum) { key := this.listView.GetRowKey(rowNum) From b8521c2995ef5da538232aa2eab8ead0c46518dc Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 00:26:02 -0500 Subject: [PATCH 006/227] Make API integration optional in AppEntityBase --- .../Volantis.App/Entity/AppEntityBase.ahk | 97 ++++++++++--------- 1 file changed, 53 insertions(+), 44 deletions(-) diff --git a/Lib/Shared/Volantis.App/Entity/AppEntityBase.ahk b/Lib/Shared/Volantis.App/Entity/AppEntityBase.ahk index 5b0e84ce..803fd056 100644 --- a/Lib/Shared/Volantis.App/Entity/AppEntityBase.ahk +++ b/Lib/Shared/Volantis.App/Entity/AppEntityBase.ahk @@ -2,6 +2,7 @@ class AppEntityBase extends FieldableEntity { app := "" dataSourcePath := "" existsInDataSource := false + mergeDataFromApi := true __New(app, id, entityTypeId, container, eventMgr, storageObj, idSanitizer, parentEntity := "") { this.app := app @@ -32,10 +33,12 @@ class AppEntityBase extends FieldableEntity { "weight", 100 ) - groups["api"] := Map( - "name", "API", - "weight", 150 - ) + if (this.mergeDataFromApi) { + groups["api"] := Map( + "name", "API", + "weight", 150 + ) + } return groups } @@ -43,23 +46,25 @@ class AppEntityBase extends FieldableEntity { BaseFieldDefinitions() { definitions := super.BaseFieldDefinitions() - definitions["DataSourceKeys"] := Map( - "description", "The data source keys to load defaults from, in order.", - "help", "The default data source is 'api' which connects to the default api endpoint (Which can be any HTTP location compatible with Launchpad's API format)", - "default", [this.app.Config["data_source_key"]], - "multiple", true, - "group", "api", - "processValue", false, - "modes", Map("simple", Map("formField", false)) - ) - - definitions["DataSourceItemKey"] := Map( - "description", "The key that is used to look up the entity's data from configured external data sources.", - "help", "It defaults to the key which is usually sufficient, but it can be overridden by setting this value.`n`nAddtionally, multiple copies of the same data source entity can exist by giving them different keys but using the same DataSourceKey", - "group", "api", - "processValue", false, - "modes", Map("simple", Map("formField", false)) - ) + if (this.mergeDataFromApi) { + definitions["DataSourceKeys"] := Map( + "description", "The data source keys to load defaults from, in order.", + "help", "The default data source is 'api' which connects to the default api endpoint (Which can be any HTTP location compatible with Launchpad's API format)", + "default", [this.app.Config["data_source_key"]], + "multiple", true, + "group", "api", + "processValue", false, + "modes", Map("simple", Map("formField", false)) + ) + + definitions["DataSourceItemKey"] := Map( + "description", "The key that is used to look up the entity's data from configured external data sources.", + "help", "It defaults to the key which is usually sufficient, but it can be overridden by setting this value.`n`nAddtionally, multiple copies of the same data source entity can exist by giving them different keys but using the same DataSourceKey", + "group", "api", + "processValue", false, + "modes", Map("simple", Map("formField", false)) + ) + } definitions["AssetsDir"] := Map( "type", "directory", @@ -109,20 +114,21 @@ class AppEntityBase extends FieldableEntity { } AggregateDataSourceDefaults(includeParentData := true, includeChildData := true) { - defaults := (this.parentEntity != "" && includeParentData) - ? this.parentEntity.AggregateDataSourceDefaults(includeParentData, false) - : Map() + defaults := Map() - ; @todo Uncomment if needed, remove if not - ;this.GetData().SetLayer("ds", defaults) + if (this.mergeDataFromApi) { + defaults := (this.parentEntity != "" && includeParentData) + ? this.parentEntity.AggregateDataSourceDefaults(includeParentData, false) + : defaults - for index, dataSource in this.GetAllDataSources() { - defaults := this.merger.Merge(this.GetDataSourceDefaults(dataSource), defaults) - } + for index, dataSource in this.GetAllDataSources() { + defaults := this.merger.Merge(this.GetDataSourceDefaults(dataSource), defaults) + } - if (includeChildData) { - for key, child in this.GetReferencedEntities(true) { - defaults := this.merger.Merge(child.AggregateDataSourceDefaults(false, includeChildData), defaults) + if (includeChildData) { + for key, child in this.GetReferencedEntities(true) { + defaults := this.merger.Merge(child.AggregateDataSourceDefaults(false, includeChildData), defaults) + } } } @@ -132,7 +138,7 @@ class AppEntityBase extends FieldableEntity { GetAllDataSources() { dataSources := Map() - if (this.Has("DataSourceKeys", false)) { + if (this.mergeDataFromApi && this.Has("DataSourceKeys", false)) { dataSourceKeys := this["DataSourceKeys"] if (!HasBase(dataSourceKeys, Array.Prototype)) { @@ -155,21 +161,24 @@ class AppEntityBase extends FieldableEntity { GetDataSourceDefaults(dataSource) { defaults := Map() - itemKey := this.DiscoverDataSourceItemKey() - if (itemKey) { - dsData := dataSource.ReadJson(itemKey, this.GetDataSourceItemPath()) + if (this.mergeDataFromApi) { + itemKey := this.DiscoverDataSourceItemKey() - if (dsData) { - this.existsInDataSource := true + if (itemKey) { + dsData := dataSource.ReadJson(itemKey, this.GetDataSourceItemPath()) - if (dsData.Has("data")) { - dsData := dsData["data"] - } + if (dsData) { + this.existsInDataSource := true + + if (dsData.Has("data")) { + dsData := dsData["data"] + } - if (dsData.Has("defaults")) { - defaults := this.merger.Merge(dsData["defaults"], defaults) - defaults := this.MergeAdditionalDataSourceDefaults(defaults, dsData) + if (dsData.Has("defaults")) { + defaults := this.merger.Merge(dsData["defaults"], defaults) + defaults := this.MergeAdditionalDataSourceDefaults(defaults, dsData) + } } } } From 73d168f28fd81c7fd22428e3857e219e78632e81 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 00:26:21 -0500 Subject: [PATCH 007/227] Fix icon references in web services manager window --- .../Gui/ManageWindow/ManageWebServicesWindow.ahk | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk b/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk index bc1a3c1f..9e1420da 100644 --- a/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk +++ b/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk @@ -1,14 +1,13 @@ class ManageWebServicesWindow extends ManageEntitiesWindow { - listViewColumns := Array("ID", "PROVIDER", "NAME", "AUTHENTICATED") + listViewColumns := Array("SERVICE", "PROVIDER", "AUTHENTICATED") GetListViewData(lv) { data := Map() for key, webService in this.entityMgr { data[key] := [ - webService["id"], - webService["Provider"]["name"], webService["name"], + webService["Provider"]["name"], webService.AuthData["authenticated"] ? "Yes" : "No" ] } @@ -16,6 +15,10 @@ class ManageWebServicesWindow extends ManageEntitiesWindow { return data } + GetEntityIconSrc(entityObj) { + return entityObj["Provider"]["IconSrc"] + } + ViewEntity(key) { ; @todo generic view operation for double-clicking non-editable entities } From 92ab4ad66c847a2899275f8c79379acc340a53d3 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 00:26:47 -0500 Subject: [PATCH 008/227] Remove old API fields from web services entities --- .../Modules/WebServices/Entity/WebServiceProviderEntity.ahk | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceProviderEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceProviderEntity.ahk index 8ee45343..c8b02d43 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceProviderEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceProviderEntity.ahk @@ -1,4 +1,6 @@ class WebServiceProviderEntity extends AppEntityBase { + mergeDataFromApi := false + BaseFieldDefinitions() { definitions := super.BaseFieldDefinitions() From b84087000ac08ae2aa0d96a3cba1a412824f0761 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 00:27:23 -0500 Subject: [PATCH 009/227] Fix bugs with WebServiceEntity and make the fields not editable if the id is "api" --- .../WebServices/Entity/WebServiceEntity.ahk | 30 +++++++++++-------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index 6688f04c..70642c1b 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -2,6 +2,7 @@ class WebServiceEntity extends AppEntityBase { cacheObj := "" stateObj := "" persistentStateObj := "" + mergeDataFromApi := false AuthData[key] { get => this.GetAuthData(key) @@ -30,8 +31,8 @@ class WebServiceEntity extends AppEntityBase { entityTypeId, container, container.Get("cache.web_services"), + container.Get("state.web_services_tmp"), container.Get("state.web_services"), - container.Get("state.web_service_persistent"), eventMgr, storageObj, idSanitizer, @@ -42,10 +43,15 @@ class WebServiceEntity extends AppEntityBase { BaseFieldDefinitions() { definitions := super.BaseFieldDefinitions() + if (this.idVal == "api" && definitions.Has("name")) { + definitions["name"]["editable"] := false + } + definitions["Provider"] := Map( "type", "entity_reference", - "entityType", "web_service_provider" - "required", true + "entityType", "web_service_provider", + "required", true, + "editable", (this.idVal != "api") ) return definitions @@ -77,7 +83,7 @@ class WebServiceEntity extends AppEntityBase { } this._createStateParents(this.stateObj) - this.stateObj["WebServices"][this.Id]["AuthData"] := newData + this.stateObj.State["WebServices"][this.Id]["AuthData"] := newData this.stateObj.SaveState() } @@ -96,7 +102,7 @@ class WebServiceEntity extends AppEntityBase { stateObj.SaveState() } - authData := stateObj["WebServices"][this.Id]["AuthData"] + authData := stateObj.State["WebServices"][this.Id]["AuthData"] if (key) { authData := (authData.Has(key) ? authData[key] : "") @@ -107,7 +113,7 @@ class WebServiceEntity extends AppEntityBase { _setStateData(stateObj, key, value) { this._createStateParents(stateObj) - stateObj["WebServices"][this.Id]["AuthData"][key] := value + stateObj.State["WebServices"][this.Id]["AuthData"][key] := value stateObj.SaveState() return this } @@ -115,18 +121,18 @@ class WebServiceEntity extends AppEntityBase { _createStateParents(stateObj) { modified := false - if (!stateObj.Has("WebServices")) { - stateObj["WebServices"] := Map() + if (!stateObj.State.Has("WebServices")) { + stateObj.State["WebServices"] := Map() modified := true } - if (!stateObj["WebServices"].Has(this.Id)) { - stateObj["WebServices"][this.Id] := Map() + if (!stateObj.State["WebServices"].Has(this.Id)) { + stateObj.State["WebServices"][this.Id] := Map() modified := true } - if (!stateObj["WebServices"][this.Id].Has("AuthData")) { - stateObj["WebServices"][this.Id]["AuthData"] := Map() + if (!stateObj.State["WebServices"][this.Id].Has("AuthData")) { + stateObj.State["WebServices"][this.Id]["AuthData"] := Map() modified := true } From ffd8b48b4b87d43fd4f0adb6d76970a7099be1ea Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 00:27:41 -0500 Subject: [PATCH 010/227] Fixes to WebServices module file --- Lib/Shared/Modules/WebServices/WebServices.module.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Lib/Shared/Modules/WebServices/WebServices.module.json b/Lib/Shared/Modules/WebServices/WebServices.module.json index 835a2290..df8f02f3 100644 --- a/Lib/Shared/Modules/WebServices/WebServices.module.json +++ b/Lib/Shared/Modules/WebServices/WebServices.module.json @@ -27,6 +27,7 @@ "storage_config_storage_parent_key": "WebServices", "storage_config_path_parameter": "config.web_services_file", "manager_view_mode_parameter": "config.web_services_view_mode", + "definition_loader_parameter_key": "web_services.services", "default_icon": "webhook", "allow_add": true, "allow_edit": true, @@ -43,6 +44,7 @@ "storage_config_path_parameter": "config.web_service_providers_file" }, "web_services.providers.launchpad_api": { + "name": "Launchpad API", "EndpointUrl": "https://api.launchpad.games/v1", "AuthenticationEndpointUrl": "https://securetoken.googleapis.com/v1", "IconSrc": "Logo.ico", @@ -52,6 +54,7 @@ "LoginWindow": "LaunchpadLoginWindow" }, "web_services.services.api": { + "name": "Launchpad API", "Provider": "launchpad_api" } }, @@ -68,6 +71,10 @@ "class": "JsonState", "arguments": ["@{App}", "@@config.web_services_state_path"] }, + "state.web_services_tmp": { + "class": "ParameterState", + "arguments": ["@{App}", "web_services.state.tmp"] + }, "web_services_authenticator.jwt": { "class": "JwtWebServiceAuthenticator", "arguments": ["@manager.gui"] From 7a11449dd3ed24ba2b803e2a56a85d682f09cee1 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 00:43:41 -0500 Subject: [PATCH 011/227] Fix logo icon reference for launchpad web service provider --- Lib/Shared/Modules/WebServices/WebServices.module.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/Shared/Modules/WebServices/WebServices.module.json b/Lib/Shared/Modules/WebServices/WebServices.module.json index df8f02f3..bbf68b85 100644 --- a/Lib/Shared/Modules/WebServices/WebServices.module.json +++ b/Lib/Shared/Modules/WebServices/WebServices.module.json @@ -47,7 +47,7 @@ "name": "Launchpad API", "EndpointUrl": "https://api.launchpad.games/v1", "AuthenticationEndpointUrl": "https://securetoken.googleapis.com/v1", - "IconSrc": "Logo.ico", + "IconSrc": "Logo", "SupportsAuthentication": true, "Authenticator": "jwt", "AppKey": "AIzaSyCbwzOWJjTft77P96dV5VB3dAx9TjdDowQ", From e2461aafcbf2ae168af095d89f8540bd25de35f4 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 00:44:17 -0500 Subject: [PATCH 012/227] Make Provider field never editable after creating a Web Service entity --- Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index 70642c1b..49991050 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -51,7 +51,7 @@ class WebServiceEntity extends AppEntityBase { "type", "entity_reference", "entityType", "web_service_provider", "required", true, - "editable", (this.idVal != "api") + "editable", false ) return definitions From 286624c8320360ec6eea56c84ddfab7b88f707f7 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 00:45:29 -0500 Subject: [PATCH 013/227] Allow runtime config storage entities (entities that don't save data to a file directly) --- .../Volantis.Entity/Factory/EntityTypeFactory.ahk | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/Lib/Shared/Volantis.Entity/Factory/EntityTypeFactory.ahk b/Lib/Shared/Volantis.Entity/Factory/EntityTypeFactory.ahk index 88d866bd..8383c8fa 100644 --- a/Lib/Shared/Volantis.Entity/Factory/EntityTypeFactory.ahk +++ b/Lib/Shared/Volantis.Entity/Factory/EntityTypeFactory.ahk @@ -40,7 +40,8 @@ class EntityTypeFactory { "manager_view_mode_parameter", "", "manager_gui", "ManageEntitiesWindow", "manager_link_in_tools_menu", false, - "manager_menu_link_text", "" + "manager_menu_link_text", "", + "storage_type", "persistent" ) } @@ -97,7 +98,7 @@ class EntityTypeFactory { ), ) - if (definition["storage_class"] == "ConfigEntityStorage" && definition["storage_config_path_parameter"]) { + if (definition["storage_type"] == "persistent" && definition["storage_class"] == "ConfigEntityStorage" && definition["storage_config_path_parameter"]) { services["config_storage." . id] := Map( "class", definition["storage_config_storage_class"], "arguments", ["@@" . definition["storage_config_path_parameter"], definition["storage_config_storage_parent_key"]] @@ -107,6 +108,11 @@ class EntityTypeFactory { "class", "PersistentConfig", "arguments", ["@config_storage." . id, "@{}", "entity_data." . id] ) + } else if (definition["storage_type"] == "runtime") { + services["config." . id] := Map( + "class", "RuntimeConfig", + "arguments", ["@{}", "entity_data." . id] + ) } entityClass := definition["entity_class"] From d94ce496dc1a643a66bd9deb3a90ad6f9ec8a482 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 00:46:02 -0500 Subject: [PATCH 014/227] Make web_service_provider entity type store its data in runtime config only --- Lib/Shared/Modules/WebServices/WebServices.module.json | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/WebServices.module.json b/Lib/Shared/Modules/WebServices/WebServices.module.json index bbf68b85..2b8cff25 100644 --- a/Lib/Shared/Modules/WebServices/WebServices.module.json +++ b/Lib/Shared/Modules/WebServices/WebServices.module.json @@ -17,7 +17,6 @@ }, "parameters": { "config.web_services_file": "@@{data_dir}\\WebServices.json", - "config.web_service_providers_file": "@@{data_dir}\\WebServiceProviders.json", "config.web_services_state_path": "@@{data_dir}\\WebServicesState.json", "config.web_services_view_mode": "Report", "entity_type.web_service": { @@ -41,7 +40,7 @@ "entity_class": "WebServiceProviderEntity", "definition_loader_class": "ParameterEntityDefinitionLoader", "definition_loader_parameter_key": "web_services.providers", - "storage_config_path_parameter": "config.web_service_providers_file" + "storage_type": "runtime" }, "web_services.providers.launchpad_api": { "name": "Launchpad API", From 6977913002730b6e99f8feaa24fb2f241b33025e Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 00:54:59 -0500 Subject: [PATCH 015/227] Add helper methods Login, Logout, and IsAuthenticated to WebServiceEntity, as well as Authenticated property --- .../WebServices/Entity/WebServiceEntity.ahk | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index 49991050..d483cec1 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -4,6 +4,10 @@ class WebServiceEntity extends AppEntityBase { persistentStateObj := "" mergeDataFromApi := false + Authenticated { + get => this.IsAuthenticated() + } + AuthData[key] { get => this.GetAuthData(key) set => this.SetAuthData(key, value) @@ -40,6 +44,28 @@ class WebServiceEntity extends AppEntityBase { ) } + IsAuthenticated() { + isAuthenticated := false + + if (this["Provider"]["SupportsAuthentication"]) { + isAuthenticated := this["Provider"]["Authenticator"].IsAuthenticated(this) + } + + return isAuthenticated + } + + Login() { + if (this["Provider"]["SupportsAuthentication"]) { + this["Provider"]["Authenticator"].Login(this) + } + } + + Logout() { + if (this["Provider"]["SupportsAuthentication"]) { + this["Provider"]["Authenticator"].Logout(this) + } + } + BaseFieldDefinitions() { definitions := super.BaseFieldDefinitions() From 6e296b958ae9a5eb2568e7757d572bf296f4d268 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 01:44:28 -0500 Subject: [PATCH 016/227] Store the request object in WebServiceResponseEvent as well --- .../Modules/WebServices/Event/WebServiceResponseEvent.ahk | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/Shared/Modules/WebServices/Event/WebServiceResponseEvent.ahk b/Lib/Shared/Modules/WebServices/Event/WebServiceResponseEvent.ahk index 8db376bb..c2d5773d 100644 --- a/Lib/Shared/Modules/WebServices/Event/WebServiceResponseEvent.ahk +++ b/Lib/Shared/Modules/WebServices/Event/WebServiceResponseEvent.ahk @@ -1,12 +1,18 @@ class WebServiceResponseEvent extends EventBase { + _requestObj := "" _responseObj := "" - __New(eventName, responseObj) { + __New(eventName, requestObj, responseObj) { + this._requestObj := requestObj this._responseObj := responseObj super.__New(eventName) } + Request { + get => this._requestObj + } + Response { get => this._responseObj set => this._responseObj := value From e5b16fd76fb92782dabb640aaf955aa6cd2b686b Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 01:45:01 -0500 Subject: [PATCH 017/227] Add Login or Logout context menu item to Web Services --- .../ManageWindow/ManageWebServicesWindow.ahk | 38 +++++++++++++++++-- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk b/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk index 9e1420da..9509d307 100644 --- a/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk +++ b/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk @@ -19,15 +19,47 @@ class ManageWebServicesWindow extends ManageEntitiesWindow { return entityObj["Provider"]["IconSrc"] } + GetContextMenuItems(entityObj) { + menuItems := super.GetContextMenuItems(entityObj) + + if (entityObj["Provider"]["SupportsAuthentication"]) { + if (entityObj.Authenticated) { + menuItems.InsertAt(1, Map("label", "&Logout", "name", "WebServiceLogout")) + } else { + menuItems.InsertAt(1, Map("label", "&Login", "name", "WebServiceLogin")) + } + } + + return menuItems + } + + ProcessContextMenuResult(result, key) { + if (result == "WebServiceLogout") { + this.Logout(key) + } else if (result == "WebServiceLogin") { + this.Login(key) + } else { + super.ProcessContextMenuResult(result, key) + } + } + + Logout(key) { + return this.entityMgr[key].Logout() + } + + Login(key) { + return this.entityMgr[key].Login() + } + ViewEntity(key) { - ; @todo generic view operation for double-clicking non-editable entities + entityObj := this.entityMgr[key] } AddEntity() { - ; @todo Implement generic add dialog and operation + } DeleteEntity(key) { - ; @todo Implement generic delete dialog and operation + entityObj := this.entityMgr[key] } } From 6481fd4612185f0497bd28163fab138126108f92 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 01:45:44 -0500 Subject: [PATCH 018/227] Add missing cache assignment --- .../WebServices/WebServiceRequest/WebServiceRequestBase.ahk | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk b/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk index dd5f400d..fff11911 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk @@ -22,6 +22,8 @@ class WebServiceRequestBase { __New(eventMgr, webServiceEnt, cacheObj, method := "", path := "", data := "", useAuthentication := false, cacheResponse := true) { this.eventMgr := eventMgr this.webServiceEnt := webServiceEnt + this.cacheObj := cacheObj + if (!method) { method := "GET" From 35cdf821fc2da472a2577785544b7c8a2a7a4570 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 01:46:16 -0500 Subject: [PATCH 019/227] Support passing UrlObj objects into the "path" parameter of WebServiceRequestBase --- .../WebServices/WebServiceRequest/WebServiceRequestBase.ahk | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk b/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk index fff11911..063dc665 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk @@ -24,6 +24,10 @@ class WebServiceRequestBase { this.webServiceEnt := webServiceEnt this.cacheObj := cacheObj + if (HasBase(path, UrlObj.Prototype)) { + this._url := path + path := this._url.Path + } if (!method) { method := "GET" @@ -151,7 +155,7 @@ class WebServiceRequestBase { RequestIsCached() { path := this.GetPath() - return this.cacheObj.ItemExists(path && !this.cacheObj.ItemNeedsUpdate(path)) + return (this.cacheObj.ItemExists(path) && !this.cacheObj.ItemNeedsUpdate(path)) } _createCachedResponse() { From ceccd66ad7e22f589dd51ac75510f07fc34d9496 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 01:46:46 -0500 Subject: [PATCH 020/227] Add entityObj parameter to GetContextMenuItems method in ManageEntities window --- .../Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk b/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk index 15be3be8..41d57fab 100644 --- a/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk +++ b/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk @@ -159,7 +159,7 @@ class ManageEntitiesWindow extends ManageWindowBase { this.AutoXYWH("y", ["AddButton"]) } - GetContextMenuItems() { + GetContextMenuItems(entityObj) { definition := this.entityType.definition menuItems := [] @@ -182,7 +182,7 @@ class ManageEntitiesWindow extends ManageWindowBase { key := this.listView.GetRowKey(item) entityObj := this.entityMgr[key] - menuItems := this.GetContextMenuItems() + menuItems := this.GetContextMenuItems(entityObj) result := this.app.Service("manager.gui").Menu(menuItems, this) this.ProcessContextMenuResult(result, key) } From 4a50a006655c667ca0574cec35ef0f791260e6e8 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 01:47:01 -0500 Subject: [PATCH 021/227] Fix regex definition in UrlObj --- Lib/Shared/Volantis.Base/UrlObj/UrlObj.ahk | 26 ++++++++++++---------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/Lib/Shared/Volantis.Base/UrlObj/UrlObj.ahk b/Lib/Shared/Volantis.Base/UrlObj/UrlObj.ahk index 18a0e7e4..1b011249 100644 --- a/Lib/Shared/Volantis.Base/UrlObj/UrlObj.ahk +++ b/Lib/Shared/Volantis.Base/UrlObj/UrlObj.ahk @@ -51,19 +51,21 @@ class UrlObj { urlMap := Map() urlParts := "" - regexStr := "^((P[^:/?#]+):)?(//(P[^/?#]*))?(P[^?#]*)(\?(P[^#]*))?(#(P.*))?" - isUrl := RegExMatch(urlStr, regexStr, urlParts) - - loop urlParts.Count { - matchName := urlParts.Name[A_Index] - matchVal := urlParts[A_Index] - - if (matchName) { - if (matchName == "query") { - matchVal := this._splitQueryStr(matchVal) + regexStr := "^((?P[^:/?#]+):)?(//(?P[^/?#]*))?(?P[^?#]*)(\?(?P[^#]*))?(#(?P.*))?" + isUrl := RegExMatch(urlStr, regexStr, &urlParts) + + if (urlParts) { + loop urlParts.Count { + matchName := urlParts.Name[A_Index] + matchVal := urlParts[A_Index] + + if (matchName) { + if (matchName == "query") { + matchVal := this._splitQueryStr(matchVal) + } + + urlMap[matchName] := matchVal } - - urlMap[matchName] := matchVal } } From bbf5517355a1bcd9875cbb44133b460e5a192a61 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 02:09:55 -0500 Subject: [PATCH 022/227] Fix reference to PlatformsWindow --- Launchpad.services.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Launchpad.services.json b/Launchpad.services.json index 6a725f60..0fe6765a 100644 --- a/Launchpad.services.json +++ b/Launchpad.services.json @@ -66,7 +66,7 @@ "storage_config_path_parameter": "config.platforms_file", "manager_view_mode_parameter": "config.platforms_view_mode", "default_icon": "Platform", - "manager_gui": "ManagePlatformsWindow", + "manager_gui": "PlatformsWindow", "manager_link_in_tools_menu": true }, "entity_type.task": { From 6352c4b41499cffff1766be0ba2d342eb3ba5506 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 02:10:07 -0500 Subject: [PATCH 023/227] Hide Basic platform from Platforms window --- .../Gui/ManageWindow/PlatformsWindow.ahk | 27 +++++++++++-------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/Lib/Launchpad/Gui/ManageWindow/PlatformsWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/PlatformsWindow.ahk index 51a526af..5b13061f 100644 --- a/Lib/Launchpad/Gui/ManageWindow/PlatformsWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/PlatformsWindow.ahk @@ -4,7 +4,7 @@ class PlatformsWindow extends ManageWindowBase { __New(container, themeObj, config) { this.platformManager := container.Get("entity_manager.platform") - this.lvCount := this.platformManager.Count(true) + this.lvCount := this.platformManager.Count(true) - 1 super.__New(container, themeObj, config) } @@ -24,10 +24,12 @@ class PlatformsWindow extends ManageWindowBase { data := Map() for key, platform in this.platformManager { - enabledText := platform["IsEnabled"] ? "Yes" : "No" - detectGamesText := platform["DetectGames"] ? "Yes" : "No" - installedText := platform["IsInstalled"] ? "Yes" : "No" - data[key] := [platform.GetName(), enabledText, detectGamesText, installedText, platform["InstalledVersion"]] + if (key != "Basic") { + enabledText := platform["IsEnabled"] ? "Yes" : "No" + detectGamesText := platform["DetectGames"] ? "Yes" : "No" + installedText := platform["IsInstalled"] ? "Yes" : "No" + data[key] := [platform.GetName(), enabledText, detectGamesText, installedText, platform["InstalledVersion"]] + } } return data @@ -47,14 +49,17 @@ class PlatformsWindow extends ManageWindowBase { iconNum := 1 for key, platform in this.platformManager { - iconSrc := platform["IconSrc"] - if (!iconSrc or !FileExist(iconSrc)) { - iconSrc := defaultIcon - } + if (key != "Basic") { + iconSrc := platform["IconSrc"] - IL_Add(IL, iconSrc) - iconNum++ + if (!iconSrc or !FileExist(iconSrc)) { + iconSrc := defaultIcon + } + + IL_Add(IL, iconSrc) + iconNum++ + } } return IL From 8a1279ce9770aeeca5e42deafc48245fb772648f Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 02:31:52 -0500 Subject: [PATCH 024/227] Standardize module versions for display --- Lib/Shared/Volantis.Module/Module/ModuleBase.ahk | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/Lib/Shared/Volantis.Module/Module/ModuleBase.ahk b/Lib/Shared/Volantis.Module/Module/ModuleBase.ahk index af874ef4..2798c9ed 100644 --- a/Lib/Shared/Volantis.Module/Module/ModuleBase.ahk +++ b/Lib/Shared/Volantis.Module/Module/ModuleBase.ahk @@ -28,7 +28,20 @@ class ModuleBase { } GetVersion() { - return this.moduleInfo.Has("version") ? this.moduleInfo["version"] : "" + versionStr := this.moduleInfo.Has("version") ? this.moduleInfo["version"] : "" + + if (versionStr == "{{VERSION}}") { + + if (AppBase.Instance) { + versionStr := AppBase.Instance.Version + } + + if (versionStr == "{{VERSION}}") { + versionStr := "Built-in" + } + } + + return versionStr } GetServiceFiles() { From 9adba30b5caed83619271c23450dbb1cd49b38c0 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 02:32:52 -0500 Subject: [PATCH 025/227] Pass isCore value in to each module constructor for safety --- Lib/Shared/Volantis.Module/Factory/ModuleFactory.ahk | 2 +- Lib/Shared/Volantis.Module/Module/ModuleBase.ahk | 6 ++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/Shared/Volantis.Module/Factory/ModuleFactory.ahk b/Lib/Shared/Volantis.Module/Factory/ModuleFactory.ahk index b873f5a9..e5b73f15 100644 --- a/Lib/Shared/Volantis.Module/Factory/ModuleFactory.ahk +++ b/Lib/Shared/Volantis.Module/Factory/ModuleFactory.ahk @@ -22,7 +22,7 @@ class ModuleFactory { ), "module." . key, Map( "class", this.classMap.Has(key) ? this.classMap[key] : "SimpleModule", - "arguments", [key, "@module_info." . key, "@module_config." . key], + "arguments", [key, "@module_info." . key, "@module_config." . key, isCore], "file", file, "enabled", enabled, "core", isCore, diff --git a/Lib/Shared/Volantis.Module/Module/ModuleBase.ahk b/Lib/Shared/Volantis.Module/Module/ModuleBase.ahk index 2798c9ed..d8f03ca6 100644 --- a/Lib/Shared/Volantis.Module/Module/ModuleBase.ahk +++ b/Lib/Shared/Volantis.Module/Module/ModuleBase.ahk @@ -2,11 +2,13 @@ class ModuleBase { moduleInfo := "" config := "" key := "" + _core := false - __New(key, moduleInfo, config) { + __New(key, moduleInfo, config, isCore) { this.key := key this.moduleInfo := moduleInfo this.config := config + this._core := isCore } IsEnabled() { @@ -14,7 +16,7 @@ class ModuleBase { } IsCore() { - return (this.config.Has("core") && this.config["core"]) + return this._core } GetConfigValue(key, defaultValue := "") { From ab8d1a5c2638ec4ff35dfbb7a91ac1d4395553a6 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 02:36:08 -0500 Subject: [PATCH 026/227] Replace "Build-in" version string with "Core" --- Lib/Shared/Volantis.Module/Module/ModuleBase.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/Shared/Volantis.Module/Module/ModuleBase.ahk b/Lib/Shared/Volantis.Module/Module/ModuleBase.ahk index d8f03ca6..2712c2b5 100644 --- a/Lib/Shared/Volantis.Module/Module/ModuleBase.ahk +++ b/Lib/Shared/Volantis.Module/Module/ModuleBase.ahk @@ -39,7 +39,7 @@ class ModuleBase { } if (versionStr == "{{VERSION}}") { - versionStr := "Built-in" + versionStr := "Core" } } From 591c10a471c3dbce20a5fd12f52a465a74358a83 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 02:36:36 -0500 Subject: [PATCH 027/227] Add category, source, and better module version strings to module manager --- Lib/Launchpad/Gui/ManageWindow/ManageModulesWindow.ahk | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Lib/Launchpad/Gui/ManageWindow/ManageModulesWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/ManageModulesWindow.ahk index bc961de2..7cc12037 100644 --- a/Lib/Launchpad/Gui/ManageWindow/ManageModulesWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/ManageModulesWindow.ahk @@ -1,5 +1,5 @@ class ManageModulesWindow extends ManageWindowBase { - listViewColumns := Array("NAME", "ENABLED", "SOURCE", "VERSION") + listViewColumns := Array("NAME", "CATEGORY", "ENABLED", "SOURCE", "VERSION") moduleManager := "" needsRestart := false @@ -26,9 +26,8 @@ class ManageModulesWindow extends ManageWindowBase { for name, module in this.moduleManager.All("", false, true) { enabledText := module.IsEnabled() ? "Yes" : "No" - ; TODO Define source - source := "" - data[name] := [name, enabledText, source, module.GetVersion()] + source := module.IsCore() ? "Built-in" : "Third-party" + data[name] := [name, module.moduleInfo["category"], enabledText, source, module.GetVersion()] } return data From b72c3bafd7c76c0122983b084ab9ab67aaa0aa76 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 10:46:06 -0500 Subject: [PATCH 028/227] Standardize all graphics resources to lower-case --- Launchpad.ahk | 2 +- Launchpad.services.json | 4 ++-- LaunchpadOverlay/LaunchpadOverlay.rc | 2 +- LaunchpadTest.ahk | 2 +- Lib/Launchpad/Entity/LauncherEntity.ahk | 2 +- .../Gui/ManageWindow/DetectedGamesWindow.ahk | 2 +- Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk | 4 ++-- .../Gui/ManageWindow/ManageBackupsWindow.ahk | 2 +- .../Gui/ManageWindow/ManageModulesWindow.ahk | 2 +- Lib/Launchpad/Gui/ManageWindow/PlatformsWindow.ahk | 2 +- Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk | 2 +- .../Modules/WebServices/WebServices.module.json | 2 +- Lib/Shared/Volantis.App/Entity/BackupEntity.ahk | 2 +- Lib/Shared/Volantis.Theme/Theme/ThemeBase.ahk | 2 +- .../Graphics/Icons/Dark/{Backup.ico => backup.ico} | Bin .../Graphics/Icons/Dark/{Config.ico => config.ico} | Bin .../Graphics/Icons/Dark/{Game.ico => game.ico} | Bin .../Graphics/Icons/Dark/{Logo.ico => logo.ico} | Bin .../Graphics/Icons/Dark/{Module.png => module.png} | Bin .../Icons/Dark/{Platform.ico => platform.ico} | Bin .../Icons/Gradient/{Backup.ico => backup.ico} | Bin .../Icons/Gradient/{Config.ico => config.ico} | Bin .../Graphics/Icons/Gradient/{Game.ico => game.ico} | Bin .../Graphics/Icons/Gradient/{Logo.ico => logo.ico} | Bin .../Icons/Gradient/{Platform.ico => platform.ico} | Bin .../Graphics/Icons/Light/{Backup.ico => backup.ico} | Bin .../Graphics/Icons/Light/{Config.ico => config.ico} | Bin .../Graphics/Icons/Light/{Game.ico => game.ico} | Bin .../Graphics/Icons/Light/{Logo.ico => logo.ico} | Bin .../Graphics/Icons/Light/{Module.png => module.png} | Bin .../Icons/Light/{Platform.ico => platform.ico} | Bin .../{Launchpad-256.png => launchpad-256.png} | Bin .../Graphics/{Launchpad-64.png => launchpad-64.png} | Bin Resources/Graphics/{Launchpad.ico => launchpad.ico} | Bin Resources/Graphics/{Logo.png => logo.png} | Bin .../{Spinner-Steam.gif => spinner-steam.gif} | Bin Resources/Graphics/{Spinner.gif => spinner.gif} | Bin Resources/Themes/Lightpad.json | 6 +++--- Resources/Themes/Steampad.json | 2 +- Scripts/Build.ahk | 2 +- 40 files changed, 21 insertions(+), 21 deletions(-) rename Resources/Graphics/Icons/Dark/{Backup.ico => backup.ico} (100%) rename Resources/Graphics/Icons/Dark/{Config.ico => config.ico} (100%) rename Resources/Graphics/Icons/Dark/{Game.ico => game.ico} (100%) rename Resources/Graphics/Icons/Dark/{Logo.ico => logo.ico} (100%) rename Resources/Graphics/Icons/Dark/{Module.png => module.png} (100%) rename Resources/Graphics/Icons/Dark/{Platform.ico => platform.ico} (100%) rename Resources/Graphics/Icons/Gradient/{Backup.ico => backup.ico} (100%) rename Resources/Graphics/Icons/Gradient/{Config.ico => config.ico} (100%) rename Resources/Graphics/Icons/Gradient/{Game.ico => game.ico} (100%) rename Resources/Graphics/Icons/Gradient/{Logo.ico => logo.ico} (100%) rename Resources/Graphics/Icons/Gradient/{Platform.ico => platform.ico} (100%) rename Resources/Graphics/Icons/Light/{Backup.ico => backup.ico} (100%) rename Resources/Graphics/Icons/Light/{Config.ico => config.ico} (100%) rename Resources/Graphics/Icons/Light/{Game.ico => game.ico} (100%) rename Resources/Graphics/Icons/Light/{Logo.ico => logo.ico} (100%) rename Resources/Graphics/Icons/Light/{Module.png => module.png} (100%) rename Resources/Graphics/Icons/Light/{Platform.ico => platform.ico} (100%) rename Resources/Graphics/{Launchpad-256.png => launchpad-256.png} (100%) rename Resources/Graphics/{Launchpad-64.png => launchpad-64.png} (100%) rename Resources/Graphics/{Launchpad.ico => launchpad.ico} (100%) rename Resources/Graphics/{Logo.png => logo.png} (100%) rename Resources/Graphics/{Spinner-Steam.gif => spinner-steam.gif} (100%) rename Resources/Graphics/{Spinner.gif => spinner.gif} (100%) diff --git a/Launchpad.ahk b/Launchpad.ahk index 00552dc8..6e5d905f 100644 --- a/Launchpad.ahk +++ b/Launchpad.ahk @@ -15,6 +15,6 @@ Launchpad(Map( "appName", "Launchpad", "developer", "Volantis Development", "version", appVersion, - "trayIcon", "Resources\Graphics\Launchpad.ico", + "trayIcon", "Resources\Graphics\launchpad.ico", "console", true )) diff --git a/Launchpad.services.json b/Launchpad.services.json index 0fe6765a..957f87ec 100644 --- a/Launchpad.services.json +++ b/Launchpad.services.json @@ -33,7 +33,7 @@ "storage_config_storage_parent_key": "Games", "storage_config_path_parameter": "config.launcher_file", "manager_view_mode_parameter": "config.launcher_view_mode", - "default_icon": "Game", + "default_icon": "game", "allow_add": true, "allow_edit": true, "allow_delete": true, @@ -65,7 +65,7 @@ "storage_config_storage_parent_key": "Platforms", "storage_config_path_parameter": "config.platforms_file", "manager_view_mode_parameter": "config.platforms_view_mode", - "default_icon": "Platform", + "default_icon": "platform", "manager_gui": "PlatformsWindow", "manager_link_in_tools_menu": true }, diff --git a/LaunchpadOverlay/LaunchpadOverlay.rc b/LaunchpadOverlay/LaunchpadOverlay.rc index a2535bad..2e1c9f22 100644 --- a/LaunchpadOverlay/LaunchpadOverlay.rc +++ b/LaunchpadOverlay/LaunchpadOverlay.rc @@ -52,7 +52,7 @@ END // Icon with lowest ID value placed first to ensure application icon // remains consistent on all systems. -IDI_ICON1 ICON "E:\\Tools\\Launchpad\\Resources\\Graphics\\Launchpad.ico" +IDI_ICON1 ICON "E:\\Tools\\Launchpad\\Resources\\Graphics\\launchpad.ico" #endif // English (United States) resources ///////////////////////////////////////////////////////////////////////////// diff --git a/LaunchpadTest.ahk b/LaunchpadTest.ahk index 304cb70f..c9720435 100644 --- a/LaunchpadTest.ahk +++ b/LaunchpadTest.ahk @@ -13,7 +13,7 @@ appVersion := "1.0.0" -TraySetIcon("Resources\Graphics\Launchpad.ico") +TraySetIcon("Resources\Graphics\launchpad.ico") HtmlResultViewer("Launchpad Test") .ViewResults(SimpleTestRunner(FilesystemTestLoader().GetTests()).RunTests()) diff --git a/Lib/Launchpad/Entity/LauncherEntity.ahk b/Lib/Launchpad/Entity/LauncherEntity.ahk index edaec377..71f3f1a1 100644 --- a/Lib/Launchpad/Entity/LauncherEntity.ahk +++ b/Lib/Launchpad/Entity/LauncherEntity.ahk @@ -471,7 +471,7 @@ class LauncherEntity extends AppEntityBase { detectedValues["IconSrc"] := this["ManagedGame"].LocateExe() } else { theme := this.container.Get("manager.theme").GetComponent() - detectedValues["IconSrc"] := theme.GetIconPath("Game") + detectedValues["IconSrc"] := theme.GetIconPath("game") } } diff --git a/Lib/Launchpad/Gui/ManageWindow/DetectedGamesWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/DetectedGamesWindow.ahk index e78ab041..5d9aed4f 100644 --- a/Lib/Launchpad/Gui/ManageWindow/DetectedGamesWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/DetectedGamesWindow.ahk @@ -83,7 +83,7 @@ GetListViewImgList(lv, large := false) { IL := IL_Create(this.detectedGames.Count, 1, large) - defaultIcon := this.themeObj.GetIconPath("Game") + defaultIcon := this.themeObj.GetIconPath("game") iconNum := 1 for key, detectedGameObj in this.detectedGames { diff --git a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk index 25dfa92e..6e84d702 100644 --- a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk @@ -513,7 +513,7 @@ GetListViewImgList(lv, large := false) { IL := IL_Create(this.launcherManager.Count(true), 1, large) - defaultIcon := this.themeObj.GetIconPath("Game") + defaultIcon := this.themeObj.GetIconPath("game") iconNum := 1 for key, launcher in this.launcherManager { @@ -533,7 +533,7 @@ GetItemImage(launcher) { iconSrc := launcher["IconSrc"] assetIcon := launcher["AssetsDir"] . "\" . launcher.Id . ".ico" - defaultIcon := this.themeObj.GetIconPath("Game") + defaultIcon := this.themeObj.GetIconPath("game") if ((!iconSrc || !FileExist(iconSrc)) && FileExist(assetIcon)) { iconSrc := assetIcon diff --git a/Lib/Launchpad/Gui/ManageWindow/ManageBackupsWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/ManageBackupsWindow.ahk index a73baba9..3ceb36f4 100644 --- a/Lib/Launchpad/Gui/ManageWindow/ManageBackupsWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/ManageBackupsWindow.ahk @@ -40,7 +40,7 @@ class ManageBackupsWindow extends ManageWindowBase { GetListViewImgList(lv, large := false) { IL := IL_Create(this.backupManager.Count(true), 1, large) - defaultIcon := this.themeObj.GetIconPath("Backup") + defaultIcon := this.themeObj.GetIconPath("backup") iconNum := 1 for key, backup in this.backupManager { diff --git a/Lib/Launchpad/Gui/ManageWindow/ManageModulesWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/ManageModulesWindow.ahk index 7cc12037..ec0e10b3 100644 --- a/Lib/Launchpad/Gui/ManageWindow/ManageModulesWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/ManageModulesWindow.ahk @@ -43,7 +43,7 @@ class ManageModulesWindow extends ManageWindowBase { GetListViewImgList(lv, large := false) { IL := IL_Create(this.lvCount, 1, large) - defaultIcon := this.themeObj.GetIconPath("Module") + defaultIcon := this.themeObj.GetIconPath("module") iconNum := 1 for key, module in this.moduleManager.All("", false, true) { diff --git a/Lib/Launchpad/Gui/ManageWindow/PlatformsWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/PlatformsWindow.ahk index 5b13061f..845423b6 100644 --- a/Lib/Launchpad/Gui/ManageWindow/PlatformsWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/PlatformsWindow.ahk @@ -45,7 +45,7 @@ class PlatformsWindow extends ManageWindowBase { GetListViewImgList(lv, large := false) { IL := IL_Create(this.platformManager.Count(true), 1, large) - defaultIcon := this.themeObj.GetIconPath("Platform") + defaultIcon := this.themeObj.GetIconPath("platform") iconNum := 1 for key, platform in this.platformManager { diff --git a/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk b/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk index 7397b273..d787c321 100644 --- a/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk +++ b/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk @@ -7,7 +7,7 @@ class LaunchpadBuilder extends AppBase { parameters["config.api_authentication"] := true parameters["config.dist_dir"] := this.appDir . "\Dist" parameters["config.build_dir"] := this.appDir . "\Build" - parameters["config.icon_file"] := this.appDir . "\Resources\Graphics\Launchpad.ico" + parameters["config.icon_file"] := this.appDir . "\Resources\Graphics\launchpad.ico" parameters["config.github_username"] := "" parameters["config.github_token"] := "" parameters["config.github_repo"] := "VolantisDev/Launchpad" diff --git a/Lib/Shared/Modules/WebServices/WebServices.module.json b/Lib/Shared/Modules/WebServices/WebServices.module.json index 2b8cff25..72d1a473 100644 --- a/Lib/Shared/Modules/WebServices/WebServices.module.json +++ b/Lib/Shared/Modules/WebServices/WebServices.module.json @@ -46,7 +46,7 @@ "name": "Launchpad API", "EndpointUrl": "https://api.launchpad.games/v1", "AuthenticationEndpointUrl": "https://securetoken.googleapis.com/v1", - "IconSrc": "Logo", + "IconSrc": "logo", "SupportsAuthentication": true, "Authenticator": "jwt", "AppKey": "AIzaSyCbwzOWJjTft77P96dV5VB3dAx9TjdDowQ", diff --git a/Lib/Shared/Volantis.App/Entity/BackupEntity.ahk b/Lib/Shared/Volantis.App/Entity/BackupEntity.ahk index 9ff0d0b0..8d25858b 100644 --- a/Lib/Shared/Volantis.App/Entity/BackupEntity.ahk +++ b/Lib/Shared/Volantis.App/Entity/BackupEntity.ahk @@ -25,7 +25,7 @@ class BackupEntity extends AppEntityBase { definitions["IconSrc"] := Map( "type", "icon_file", "description", "The path to this an icon (.ico or .exe).", - "default", this.app.Service("manager.theme")[].GetIconPath("Backup") + "default", this.app.Service("manager.theme")[].GetIconPath("backup") ) definitions["Source"] := Map( diff --git a/Lib/Shared/Volantis.Theme/Theme/ThemeBase.ahk b/Lib/Shared/Volantis.Theme/Theme/ThemeBase.ahk index 61ead15e..a33e9428 100644 --- a/Lib/Shared/Volantis.Theme/Theme/ThemeBase.ahk +++ b/Lib/Shared/Volantis.Theme/Theme/ThemeBase.ahk @@ -8,7 +8,7 @@ class ThemeBase { defaultTheme := "Lightpad" vars := Map() colors := Map("background", "FFFFFF", "text", "000000", "textInactive", "959595", "accent", "9466FC", "accentBright", "EEE6FF", "accentBg", "8A57F0", "transColor", "") - themeAssets := Map("logo", "Resources\Graphics\Logo.png", "icon", "Resources\Graphics\Launchpad.ico", "spinner", "Resources\Graphics\Spinner.gif") + themeAssets := Map("logo", "Resources\Graphics\logo.png", "icon", "Resources\Graphics\launchpad.ico", "spinner", "Resources\Graphics\spinner.gif") symbols := Map() buttons := Map("height", Map("s", 20, "m", 30, "l", 40, "xl", 80), "fixedWidth", Map("s", 80, "m", 100, "l", 120, "xl", 140)) labels := Map("height", "auto", "fixedWidth", 75, "font", "normal") diff --git a/Resources/Graphics/Icons/Dark/Backup.ico b/Resources/Graphics/Icons/Dark/backup.ico similarity index 100% rename from Resources/Graphics/Icons/Dark/Backup.ico rename to Resources/Graphics/Icons/Dark/backup.ico diff --git a/Resources/Graphics/Icons/Dark/Config.ico b/Resources/Graphics/Icons/Dark/config.ico similarity index 100% rename from Resources/Graphics/Icons/Dark/Config.ico rename to Resources/Graphics/Icons/Dark/config.ico diff --git a/Resources/Graphics/Icons/Dark/Game.ico b/Resources/Graphics/Icons/Dark/game.ico similarity index 100% rename from Resources/Graphics/Icons/Dark/Game.ico rename to Resources/Graphics/Icons/Dark/game.ico diff --git a/Resources/Graphics/Icons/Dark/Logo.ico b/Resources/Graphics/Icons/Dark/logo.ico similarity index 100% rename from Resources/Graphics/Icons/Dark/Logo.ico rename to Resources/Graphics/Icons/Dark/logo.ico diff --git a/Resources/Graphics/Icons/Dark/Module.png b/Resources/Graphics/Icons/Dark/module.png similarity index 100% rename from Resources/Graphics/Icons/Dark/Module.png rename to Resources/Graphics/Icons/Dark/module.png diff --git a/Resources/Graphics/Icons/Dark/Platform.ico b/Resources/Graphics/Icons/Dark/platform.ico similarity index 100% rename from Resources/Graphics/Icons/Dark/Platform.ico rename to Resources/Graphics/Icons/Dark/platform.ico diff --git a/Resources/Graphics/Icons/Gradient/Backup.ico b/Resources/Graphics/Icons/Gradient/backup.ico similarity index 100% rename from Resources/Graphics/Icons/Gradient/Backup.ico rename to Resources/Graphics/Icons/Gradient/backup.ico diff --git a/Resources/Graphics/Icons/Gradient/Config.ico b/Resources/Graphics/Icons/Gradient/config.ico similarity index 100% rename from Resources/Graphics/Icons/Gradient/Config.ico rename to Resources/Graphics/Icons/Gradient/config.ico diff --git a/Resources/Graphics/Icons/Gradient/Game.ico b/Resources/Graphics/Icons/Gradient/game.ico similarity index 100% rename from Resources/Graphics/Icons/Gradient/Game.ico rename to Resources/Graphics/Icons/Gradient/game.ico diff --git a/Resources/Graphics/Icons/Gradient/Logo.ico b/Resources/Graphics/Icons/Gradient/logo.ico similarity index 100% rename from Resources/Graphics/Icons/Gradient/Logo.ico rename to Resources/Graphics/Icons/Gradient/logo.ico diff --git a/Resources/Graphics/Icons/Gradient/Platform.ico b/Resources/Graphics/Icons/Gradient/platform.ico similarity index 100% rename from Resources/Graphics/Icons/Gradient/Platform.ico rename to Resources/Graphics/Icons/Gradient/platform.ico diff --git a/Resources/Graphics/Icons/Light/Backup.ico b/Resources/Graphics/Icons/Light/backup.ico similarity index 100% rename from Resources/Graphics/Icons/Light/Backup.ico rename to Resources/Graphics/Icons/Light/backup.ico diff --git a/Resources/Graphics/Icons/Light/Config.ico b/Resources/Graphics/Icons/Light/config.ico similarity index 100% rename from Resources/Graphics/Icons/Light/Config.ico rename to Resources/Graphics/Icons/Light/config.ico diff --git a/Resources/Graphics/Icons/Light/Game.ico b/Resources/Graphics/Icons/Light/game.ico similarity index 100% rename from Resources/Graphics/Icons/Light/Game.ico rename to Resources/Graphics/Icons/Light/game.ico diff --git a/Resources/Graphics/Icons/Light/Logo.ico b/Resources/Graphics/Icons/Light/logo.ico similarity index 100% rename from Resources/Graphics/Icons/Light/Logo.ico rename to Resources/Graphics/Icons/Light/logo.ico diff --git a/Resources/Graphics/Icons/Light/Module.png b/Resources/Graphics/Icons/Light/module.png similarity index 100% rename from Resources/Graphics/Icons/Light/Module.png rename to Resources/Graphics/Icons/Light/module.png diff --git a/Resources/Graphics/Icons/Light/Platform.ico b/Resources/Graphics/Icons/Light/platform.ico similarity index 100% rename from Resources/Graphics/Icons/Light/Platform.ico rename to Resources/Graphics/Icons/Light/platform.ico diff --git a/Resources/Graphics/Launchpad-256.png b/Resources/Graphics/launchpad-256.png similarity index 100% rename from Resources/Graphics/Launchpad-256.png rename to Resources/Graphics/launchpad-256.png diff --git a/Resources/Graphics/Launchpad-64.png b/Resources/Graphics/launchpad-64.png similarity index 100% rename from Resources/Graphics/Launchpad-64.png rename to Resources/Graphics/launchpad-64.png diff --git a/Resources/Graphics/Launchpad.ico b/Resources/Graphics/launchpad.ico similarity index 100% rename from Resources/Graphics/Launchpad.ico rename to Resources/Graphics/launchpad.ico diff --git a/Resources/Graphics/Logo.png b/Resources/Graphics/logo.png similarity index 100% rename from Resources/Graphics/Logo.png rename to Resources/Graphics/logo.png diff --git a/Resources/Graphics/Spinner-Steam.gif b/Resources/Graphics/spinner-steam.gif similarity index 100% rename from Resources/Graphics/Spinner-Steam.gif rename to Resources/Graphics/spinner-steam.gif diff --git a/Resources/Graphics/Spinner.gif b/Resources/Graphics/spinner.gif similarity index 100% rename from Resources/Graphics/Spinner.gif rename to Resources/Graphics/spinner.gif diff --git a/Resources/Themes/Lightpad.json b/Resources/Themes/Lightpad.json index 2049c352..bcabf3a0 100644 --- a/Resources/Themes/Lightpad.json +++ b/Resources/Themes/Lightpad.json @@ -108,9 +108,9 @@ "transColor": "" }, "themeAssets": { - "icon": "Graphics\\Launchpad.ico", - "logo": "Graphics\\Logo.png", - "spinner": "Graphics\\Spinner.gif" + "icon": "Graphics\\launchpad.ico", + "logo": "Graphics\\logo.png", + "spinner": "Graphics\\spinner.gif" }, "symbols": { "arrowDown": "ArrowDownSymbol", diff --git a/Resources/Themes/Steampad.json b/Resources/Themes/Steampad.json index 9b6eae9f..676bf731 100644 --- a/Resources/Themes/Steampad.json +++ b/Resources/Themes/Steampad.json @@ -107,7 +107,7 @@ }, "themeAssets": { "logo": "", - "spinner": "Graphics\\Spinner-Steam.gif" + "spinner": "Graphics\\spinner-Steam.gif" }, "buttons": { "styles": { diff --git a/Scripts/Build.ahk b/Scripts/Build.ahk index 9d166304..998e9099 100644 --- a/Scripts/Build.ahk +++ b/Scripts/Build.ahk @@ -11,6 +11,6 @@ LaunchpadBuilder(Map( "appName", "Launchpad", "developer", "Volantis Development", "version", appVersion, - "trayIcon", appDir . "\Resources\Graphics\Launchpad.ico", + "trayIcon", appDir . "\Resources\Graphics\launchpad.ico", "console", true, )) From 41faf6eaa32c4a091122208f4c484a93bd5e5cf8 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 10:46:16 -0500 Subject: [PATCH 029/227] Fix spinner filename --- Resources/Themes/Steampad.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Resources/Themes/Steampad.json b/Resources/Themes/Steampad.json index 676bf731..c4e36465 100644 --- a/Resources/Themes/Steampad.json +++ b/Resources/Themes/Steampad.json @@ -107,7 +107,7 @@ }, "themeAssets": { "logo": "", - "spinner": "Graphics\\spinner-Steam.gif" + "spinner": "Graphics\\spinner-steam.gif" }, "buttons": { "styles": { From ec9ba3d238bdeaae5046d85a2296bb39910bef96 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 11:09:43 -0500 Subject: [PATCH 030/227] Fix reg lookup key for Epic Games platform --- .../Modules/Epic/GamePlatform/EpicPlatform.ahk | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/Launchpad/Modules/Epic/GamePlatform/EpicPlatform.ahk b/Lib/Launchpad/Modules/Epic/GamePlatform/EpicPlatform.ahk index 06e768d5..9a9d87fe 100644 --- a/Lib/Launchpad/Modules/Epic/GamePlatform/EpicPlatform.ahk +++ b/Lib/Launchpad/Modules/Epic/GamePlatform/EpicPlatform.ahk @@ -3,12 +3,12 @@ class EpicPlatform extends RegistryLookupGamePlatformBase { displayName := "Epic Store" launcherType := "Epic" gameType := "Epic" - installDirRegView := 32 - installDirRegKey := "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{0E63B233-DC24-442C-BD38-0B91D90FEC5B}" - versionRegView := 32 - versionRegKey := "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{0E63B233-DC24-442C-BD38-0B91D90FEC5B}" - uninstallCmdRegView := 32 - uninstallCmdRegKey := "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\{0E63B233-DC24-442C-BD38-0B91D90FEC5B}" + installDirRegView := 64 + installDirRegKey := "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\B4B4F9022FD3528499604D6D8AE00CE9\InstallProperties" + versionRegView := 64 + versionRegKey := "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\B4B4F9022FD3528499604D6D8AE00CE9\InstallProperties" + uninstallCmdRegView := 64 + uninstallCmdRegKey := "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products\B4B4F9022FD3528499604D6D8AE00CE9\InstallProperties" Install() { Run("https://www.epicgames.com/store/en-US/download") From 8946e3290b0101a2544feaff6ba75ea0254dd4fc Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 12:03:02 -0500 Subject: [PATCH 031/227] Filter keys in DetectedGame objects more thoroughly, allow detecting DisplayName for detected games --- Lib/Launchpad/DetectedGame/DetectedGame.ahk | 26 +++++++++++-------- .../Epic/GamePlatform/EpicPlatform.ahk | 18 +++++++++++-- 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/Lib/Launchpad/DetectedGame/DetectedGame.ahk b/Lib/Launchpad/DetectedGame/DetectedGame.ahk index 02851d7c..600a58a7 100644 --- a/Lib/Launchpad/DetectedGame/DetectedGame.ahk +++ b/Lib/Launchpad/DetectedGame/DetectedGame.ahk @@ -17,9 +17,9 @@ class DetectedGame { prioritySuffixes := ["-Win64-Shipping", "-Win32-Shipping"] filterExes := [] - __New(key, platform, launcherType, gameType := "Default", installDir := "", exeName := "", launcherSpecificId := "", possibleExeNames := "") { + __New(key, platform, launcherType, gameType := "Default", installDir := "", exeName := "", launcherSpecificId := "", possibleExeNames := "", displayName := "") { this.key := key - this.displayName := key + this.displayName := displayName ? displayName : key this.platform := platform this.detectedKey := key this.launcherType := launcherType @@ -153,15 +153,19 @@ class DetectedGame { } } - key := StrReplace(key, ": ", " - ") - key := StrReplace(key, ":", "") - key := StrReplace(key, "\", "") - key := StrReplace(key, "/", "") - key := StrReplace(key, "*", "") - key := StrReplace(key, "?", "") - key := StrReplace(key, "`"", "") - key := StrReplace(key, "®", "") - key := StrReplace(key, "â„¢", "") + replacements := [ + [" : ", " - "], + [": ", " - "], + [":", "-"], + ["®", ""], + ["â„¢", ""] + ] + + for , vals in replacements { + key := StrReplace(key, vals[1], vals[2]) + } + + key := RegExReplace(key, "[\\/:*?`"<>|]'") return key } diff --git a/Lib/Launchpad/Modules/Epic/GamePlatform/EpicPlatform.ahk b/Lib/Launchpad/Modules/Epic/GamePlatform/EpicPlatform.ahk index 9a9d87fe..b42c67bc 100644 --- a/Lib/Launchpad/Modules/Epic/GamePlatform/EpicPlatform.ahk +++ b/Lib/Launchpad/Modules/Epic/GamePlatform/EpicPlatform.ahk @@ -41,7 +41,21 @@ class EpicPlatform extends RegistryLookupGamePlatformBase { } if (isGame) { - key := obj["Name"] + key := obj.Has("Name") ? obj["Name"] : "" + + if (!key && obj.Has("DisplayName")) { + key := obj["DisplayName"] + } + + if (!key && obj.Has("MandatoryAppFolderName")) { + key := obj["MandatoryAppFolderName"] + } + + if (!key) { + throw AppException("Could not determine detected game key.") + } + + displayName := obj.Has("DisplayName") ? obj["DisplayName"] : "" installDir := obj["InstallLocation"] launcherSpecificId := obj["AppName"] ;exeName := obj["LaunchExecutable"] @@ -49,7 +63,7 @@ class EpicPlatform extends RegistryLookupGamePlatformBase { locator := GameExeLocator(installDir) possibleExes := locator.Locate("") mainExe := this.DetermineMainExe(key, possibleExes) - games.Push(DetectedGame(key, this, this.launcherType, this.gameType, installDir, mainExe, launcherSpecificId, possibleExes)) + games.Push(DetectedGame(key, this, this.launcherType, this.gameType, installDir, mainExe, launcherSpecificId, possibleExes, displayName)) } } } From b5b650d4014a8fc09ad9e398ac3a8a368105b994 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 12:09:50 -0500 Subject: [PATCH 032/227] Fix saving existing entities during game detection --- Lib/Launchpad/DetectedGame/DetectedGame.ahk | 2 +- Lib/Launchpad/Entity/LauncherEntity.ahk | 4 ++-- Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk | 2 ++ 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/Lib/Launchpad/DetectedGame/DetectedGame.ahk b/Lib/Launchpad/DetectedGame/DetectedGame.ahk index 600a58a7..5707278f 100644 --- a/Lib/Launchpad/DetectedGame/DetectedGame.ahk +++ b/Lib/Launchpad/DetectedGame/DetectedGame.ahk @@ -95,7 +95,7 @@ class DetectedGame { } if (modified) { - launcher.SaveModifiedData() + launcher.SaveEntity(true) } } diff --git a/Lib/Launchpad/Entity/LauncherEntity.ahk b/Lib/Launchpad/Entity/LauncherEntity.ahk index 71f3f1a1..1122a0ac 100644 --- a/Lib/Launchpad/Entity/LauncherEntity.ahk +++ b/Lib/Launchpad/Entity/LauncherEntity.ahk @@ -379,8 +379,8 @@ class LauncherEntity extends AppEntityBase { return ValidateResult } - SaveModifiedData() { - super.SaveModifiedData() + SaveEntity(recurse := true) { + super.SaveEntity(recurse) this.app.State.SetLauncherConfigInfo(this.Id) } diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index a92092c7..826e1d58 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -20,10 +20,12 @@ class EntityBase { EntityTypeId { get => this.entityTypeIdVal + set => this.entityTypeIdVal := value } EntityType { get => this.GetEntityType() + set => this.EntityTypeId := value } FieldData { From 2dcb43d518e716713f7fdc878c9ae7f6246646d5 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 12:43:55 -0500 Subject: [PATCH 033/227] Show username for each auth service if available --- Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk | 4 ++++ .../WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk | 5 +++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index d483cec1..623d7281 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -8,6 +8,10 @@ class WebServiceEntity extends AppEntityBase { get => this.IsAuthenticated() } + UserId { + get => this.PersistentAuthData["user_id"] + } + AuthData[key] { get => this.GetAuthData(key) set => this.SetAuthData(key, value) diff --git a/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk b/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk index 9509d307..7b56457f 100644 --- a/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk +++ b/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk @@ -1,5 +1,5 @@ class ManageWebServicesWindow extends ManageEntitiesWindow { - listViewColumns := Array("SERVICE", "PROVIDER", "AUTHENTICATED") + listViewColumns := Array("SERVICE", "PROVIDER", "USER", "AUTHENTICATED") GetListViewData(lv) { data := Map() @@ -8,7 +8,8 @@ class ManageWebServicesWindow extends ManageEntitiesWindow { data[key] := [ webService["name"], webService["Provider"]["name"], - webService.AuthData["authenticated"] ? "Yes" : "No" + webService.UserId ? webService.UserId : "None", + webService.Authenticated ? "Yes" : "No" ] } From 0caa0c9ae163e79651763b8f7650b404f4121a80 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 15:35:31 -0500 Subject: [PATCH 034/227] Standardize how window and control resizing cascades --- Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk | 1 + Lib/Shared/Volantis.App/Gui/GuiBase.ahk | 19 +++++++++++++++---- .../Gui/ManageWindow/ManageEntitiesWindow.ahk | 1 + .../Gui/ManageWindow/ManageWindowBase.ahk | 5 +++-- .../GuiControl/ListViewControl.ahk | 2 +- .../GuiControl/TitlebarControl.ahk | 4 ++-- 6 files changed, 23 insertions(+), 9 deletions(-) diff --git a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk index 6e84d702..b01f9160 100644 --- a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk @@ -3,6 +3,7 @@ launcherManager := "" platformManager := "" showDetailsPane := true + lvResizeOpts := "h" __New(container, themeObj, config) { this.launcherManager := container.Get("entity_manager.launcher") diff --git a/Lib/Shared/Volantis.App/Gui/GuiBase.ahk b/Lib/Shared/Volantis.App/Gui/GuiBase.ahk index 04c1ab3b..0cb58f9e 100644 --- a/Lib/Shared/Volantis.App/Gui/GuiBase.ahk +++ b/Lib/Shared/Volantis.App/Gui/GuiBase.ahk @@ -30,10 +30,12 @@ class GuiBase { isShown := false config := "" merger := "" + addedControls := [] GetDefaultConfig(container, config) { return Map( "id", Type(this), + "resizable", false, "titlebar", true, "waitForResult", false, "titleIsMenu", false, @@ -81,6 +83,10 @@ class GuiBase { extraOptions["Border"] := true } + if (this.config["resizable"]) { + extraOptions["Resize"] := true + } + if (this.owner != "") { extraOptions["Owner" . this.owner.Hwnd] := true } @@ -106,7 +112,6 @@ class GuiBase { this.margin := this.windowSettings["spacing"]["margin"] this.guiId := this.config["id"] - this.RegisterCallbacks() this.Create() } @@ -169,7 +174,9 @@ class GuiBase { } Add(ctlClass, options := "", params*) { - return %ctlClass%(this, options, params*) + ctlObj := %ctlClass%(this, options, params*) + this.addedControls.Push(ctlObj) + return ctlObj } OnCalcSize(wParam, lParam, msg, hwnd) { @@ -762,8 +769,12 @@ class GuiBase { } OnSize(guiObj, minMax, width, height) { - if (this.config["titlebar"]) { - this.titlebar.OnSize(minMax, width, height) + for index, ctlObj in this.addedControls { + ctlObj.OnSize(guiObj, minMax, width, height) } + + ; if (this.config["titlebar"]) { + ; this.titlebar.OnSize(minMax, width, height) + ; } } } diff --git a/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk b/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk index 41d57fab..0a782c82 100644 --- a/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk +++ b/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk @@ -24,6 +24,7 @@ class ManageEntitiesWindow extends ManageWindowBase { defaults := super.GetDefaultConfig(container, config) defaults["entity_type"] := this.entityTypeId defaults["title"] := this.entityType.definition["name_plural"] + return defaults } diff --git a/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageWindowBase.ahk b/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageWindowBase.ahk index 56c85348..72cb7f01 100644 --- a/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageWindowBase.ahk +++ b/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageWindowBase.ahk @@ -6,11 +6,13 @@ lvWidth := 0 showDetailsPane := false detailsFields := [] + lvResizeOpts := "wh" GetDefaultConfig(container, config) { defaults := super.GetDefaultConfig(container, config) defaults["frameShadow"] := false defaults["saveWindowState"] := true + defaults["resizable"] := true return defaults } @@ -74,8 +76,7 @@ opts.Push("w" . this.lvWidth) } - this.listView := this.Add("ListViewControl", opts, "", this.listViewColumns, "GetListViewData", "GetListViewImgList", "InitListView", "ShouldHighlightRow") - this.listView.resizeOpts := "h" + this.listView := this.Add("ListViewControl", opts, "", this.listViewColumns, "GetListViewData", "GetListViewImgList", "InitListView", "ShouldHighlightRow", this.lvResizeOpts) return this.listView } diff --git a/Lib/Shared/Volantis.App/GuiControl/ListViewControl.ahk b/Lib/Shared/Volantis.App/GuiControl/ListViewControl.ahk index 2d5c06f8..650cad6e 100644 --- a/Lib/Shared/Volantis.App/GuiControl/ListViewControl.ahk +++ b/Lib/Shared/Volantis.App/GuiControl/ListViewControl.ahk @@ -11,7 +11,7 @@ class ListViewControl extends GuiControlBase { imgListL := "" resizeOpts := "wh" - CreateControl(columns, dataCallback, imgListCallback := "", initCallback := "", highlightRowCallback := "", resizeOpts := "") { + CreateControl(columns, dataCallback, imgListCallback := "", initCallback := "", highlightRowCallback := "", resizeOpts := "wh") { global LVM_GETHEADER super.CreateControl(false) columns.InsertAt(this.keyCol, "") diff --git a/Lib/Shared/Volantis.App/GuiControl/TitlebarControl.ahk b/Lib/Shared/Volantis.App/GuiControl/TitlebarControl.ahk index 3e4ed8cb..44cadd03 100644 --- a/Lib/Shared/Volantis.App/GuiControl/TitlebarControl.ahk +++ b/Lib/Shared/Volantis.App/GuiControl/TitlebarControl.ahk @@ -182,7 +182,7 @@ class TitlebarControl extends GuiControlBase { } } - OnSize(minMax, width, height) { + OnSize(guiObj, minMax, width, height) { if (minMax == 1 and this.guiObj.config["showMaximize"]) { this.guiObj.guiObj["WindowUnmaxButton"].Visible := true this.guiObj.guiObj["WindowMaxButton"].Visible := false @@ -209,7 +209,7 @@ class TitlebarControl extends GuiControlBase { this.guiObj.AutoXYWH("x*", ["WindowMaxButton", "WindowUnmaxButton"]) } - if (this.guiObj.config["showMaximize"]) { + if (this.guiObj.config["showMinimize"]) { this.guiObj.AutoXYWH("x*", ["WindowMinButton"]) } } From 9680f3c21e2918533fd61f982676bde6e2da436e Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 15:35:50 -0500 Subject: [PATCH 035/227] Fix missing refresh path for Launchpad API provider --- Lib/Shared/Modules/WebServices/WebServices.module.json | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/Shared/Modules/WebServices/WebServices.module.json b/Lib/Shared/Modules/WebServices/WebServices.module.json index 72d1a473..aa2a1367 100644 --- a/Lib/Shared/Modules/WebServices/WebServices.module.json +++ b/Lib/Shared/Modules/WebServices/WebServices.module.json @@ -46,6 +46,7 @@ "name": "Launchpad API", "EndpointUrl": "https://api.launchpad.games/v1", "AuthenticationEndpointUrl": "https://securetoken.googleapis.com/v1", + "AuthenticationRefreshPath": "token", "IconSrc": "logo", "SupportsAuthentication": true, "Authenticator": "jwt", From 2c3496d3cf02485dd572917d3476c40df9812301 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 15:37:59 -0500 Subject: [PATCH 036/227] Simplify access to AuthData and resolve data-related login issues --- .../WebServices/Entity/WebServiceEntity.ahk | 79 ++++++++++--- .../JwtWebServiceAuthenticator.ahk | 109 +++++++++++------- .../WebServiceAuthenticatorBase.ahk | 6 +- 3 files changed, 131 insertions(+), 63 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index 623d7281..6ccb3202 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -9,19 +9,14 @@ class WebServiceEntity extends AppEntityBase { } UserId { - get => this.PersistentAuthData["user_id"] + get => this.AuthData["user_id"] } AuthData[key] { - get => this.GetAuthData(key) + get => this.GetAuthData(key, true) set => this.SetAuthData(key, value) } - PersistentAuthData[key] { - get => this.GetPersistentAuthData(key) - set => this.SetPersistentAuthData(key, value) - } - __New(app, id, entityTypeId, container, cacheObj, stateObj, persistentStateObj, eventMgr, storageObj, idSanitizer, parentEntity := "") { this.cacheObj := cacheObj this.stateObj := stateObj @@ -99,33 +94,60 @@ class WebServiceEntity extends AppEntityBase { return BasicWebServiceRequest(this.eventMgr, this, this.cacheObj, method, path, data, useAuthentication, cacheResponse) } - GetAuthData(key := "") { - return this._getStateData(this.stateObj, key) + GetAuthData(key := "", includePersistent := true) { + val := this._getStateData(this.stateObj, key) + + if (!val && includePersistent) { + val := this._getStateData(this.persistentStateObj, key) + } + + return val } - SetAuthData(keyOrMap, value) { - return this._setStateData(this.stateObj, keyOrMap, value) + SetAuthData(keyOrMap, value, persist := false) { + result := this._setStateData(this.stateObj, keyOrMap, value) + + if (persist) { + this._setStateData(this.persistentStateObj, keyOrMap, value) + } + + return this } - ResetAuthData(newData := "") { + ResetAuthData(newData := "", persist := false) { if (!newData) { newData := Map() } + if (!newData.Has("authenticated")) { + newData["authenticated"] := false + } + this._createStateParents(this.stateObj) this.stateObj.State["WebServices"][this.Id]["AuthData"] := newData this.stateObj.SaveState() - } - GetPersistentAuthData(key := "") { - return this._getStateData(this.persistentStateObj, key) + if (persist) { + this._createStateParents(this.persistentStateObj) + this.persistentStateObj.State["WebServices"][this.Id]["AuthData"] := Map( + "authenticated", newData["authenticated"] + ) + } + + return this } - SetPersistentAuthData(key, value) { - return this._setStateData(this.persistentStateObj, key, value) + DeleteAuthData(key, persist := false) { + this._deleteStateData(this.stateObj, key) + + if (persist) { + this._deleteStateData(this.persistentStateObj, key) + } + + return this } - _getStateData(stateObj, key) { + _getStateData(stateObj, key := "") { save := this._createStateParents(stateObj) if (save) { @@ -145,6 +167,27 @@ class WebServiceEntity extends AppEntityBase { this._createStateParents(stateObj) stateObj.State["WebServices"][this.Id]["AuthData"][key] := value stateObj.SaveState() + + return this + } + + _deleteStateData(stateObj, key) { + created := this._createStateParents(stateObj) + save := created + + if (!created) { + parent := this._getStateData(stateObj) + + if (HasBase(parent, Map.Prototype) && parent.Has(key)) { + parent.Delete(key) + save := true + } + } + + if (save) { + stateObj.SaveState() + } + return this } diff --git a/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk index e5031f7d..f080cd92 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk @@ -20,9 +20,13 @@ class JwtWebServiceAuthenticator extends WebServiceAuthenticatorBase { } Logout(webServiceEnt) { - webServiceEnt.PersistentAuthData["auth_token"] := "" - webServiceEnt.PersistentAuthData["refresh_token"] := "" - webServiceEnt.ResetAuthData(Map("authenticated", false)) + webServiceEnt + .ResetAuthData() + .DeleteAuthData("auth_token", true) + .DeleteAuthData("refresh_token", true) + .DeleteAuthData("expires", true) + .SetAuthData("authenticated", false, true) + return true } @@ -42,7 +46,7 @@ class JwtWebServiceAuthenticator extends WebServiceAuthenticatorBase { } _hasRefreshToken(webServiceEnt) { - return !!(webServiceEnt.PersistentAuthData["refresh_token"]) + return !!(webServiceEnt.AuthData["refresh_token"]) } _reauthenticate(webServiceEnt) { @@ -56,68 +60,82 @@ class JwtWebServiceAuthenticator extends WebServiceAuthenticatorBase { } _getRefreshToken(webServiceEnt) { - return webServiceEnt.PersistentAuthData["refresh_token"] + return webServiceEnt.AuthData["refresh_token"] } _setRefreshToken(webServiceEnt, refreshToken) { - webServiceEnt.PersistentAuthData["refresh_token"] := refreshToken + webServiceEnt.SetAuthData("refresh_token", refreshToken, true) } _extractAuthData(webServiceEnt, response) { loginData := response.GetJsonData() - authData := Map( - "authenticated", (loginData.Has("user_id") && !!(loginData["user_id"])) - ) - persistentData := Map() - authDataMap := Map() - persistentDataMap := Map( - "user_id", "user_id", - "refresh_token", "refresh_token", - "id_token", "auth_token", - "access_token", "access_token" + + if (!loginData.Has("authenticated")) { + loginData["authenticated"] := !!(loginData.Has("refresh_token") && loginData["refresh_token"]) + } + + keyMap := Map( + "id_token", "auth_token", + "expires_in", "expires" ) - skipKeys := [ - "expires_in" + + persistentKeys := [ + "user_id", + "refresh_token", + "auth_token", + "access_token", + "authenticated", + "expires" ] - if (loginData.Has("expires_in")) { - persistentData["expires"] := DateAdd(A_Now, loginData["expires_in"], "S") - } + expiresInKeys := [ + "expires" + ] + + skipKeys := [] for key, val in loginData { - if (persistentDataMap.Has(key)) { - persistentData[persistentDataMap[key]] := loginData[key] - } else if (authDataMap.Has(key)) { - authData[authDataMap[key]] := loginData[key] - } else if (!authData.Has(key) && !persistentData.Has(key)) { - skip := false - - for index, skipKey in skipKeys { - if (key == skipKey) { - skip := true - break - } + if (keyMap.Has(key)) { + key := keyMap[key] + } + + persist := false + + for , persistKey in persistentKeys { + if (key == persistKey) { + persist := true + break } + } - if (!skip) { - authData[key] := val + expires := false + + for , expiresKey in expiresInKeys { + if (key == expiresKey) { + val := DateAdd(A_Now, val, "S") + break } } - } - for key, val in authData { - webServiceEnt.AuthData[key] := val - } + skip := false + + for , skipKey in skipKeys { + if (key == skipKey) { + skip := true + break + } + } - for key, val in persistentData { - webServiceEnt.PersistentAuthData[key] := val + if (!skip) { + webServiceEnt.SetAuthData(key, val, persist) + } } } _refreshAuthentication(webServiceEnt) { apiKey := webServiceEnt["Provider"]["AppKey"] - refreshToken := webServiceEnt.PersistentAuthData["refresh_token"] - refreshUrl := webServiceEnt["Provider"].GetAuthenticationRefreshUrl(Map("token", apiKey)) + refreshToken := webServiceEnt.AuthData["refresh_token"] + refreshUrl := webServiceEnt["Provider"].GetAuthenticationRefreshUrl(Map("key", apiKey)) response := "" if (!apiKey) { @@ -142,7 +160,10 @@ class JwtWebServiceAuthenticator extends WebServiceAuthenticatorBase { if (response && success) { this._extractAuthData(webServiceEnt, response) } else { - webServiceEnt.PersistentAuthData["refresh_token"] := "" + url := response.httpReqObj.url.ToString(true) + webServiceEnt.SetAuthData("refresh_token", "", true) + + ; @todo handle common http error codes } return success diff --git a/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk index 41b7ab38..87fa435e 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk @@ -26,7 +26,11 @@ class WebServiceAuthenticatorBase { } IsAuthenticated(webServiceEnt) { - return (webServiceEnt.AuthData[this.authenticatedStateKey] && !this.AuthenticationIsExpired(webServiceEnt)) + auth := webServiceEnt.AuthData[this.authenticatedStateKey] + expired := this.AuthenticationIsExpired(webServiceEnt) + + return auth && !expired + } NeedsRefresh(webServiceEnt) { From 7c268aae82b0e4caf1f5f87e52dc54c64e044267 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 15:38:51 -0500 Subject: [PATCH 037/227] Add Auto Login option to web services and show it in the manager window. Additionally, blank out username in manage window for now. --- .../Gui/ManageWindow/ManageWebServicesWindow.ahk | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk b/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk index 7b56457f..d5c68d24 100644 --- a/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk +++ b/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk @@ -1,5 +1,5 @@ class ManageWebServicesWindow extends ManageEntitiesWindow { - listViewColumns := Array("SERVICE", "PROVIDER", "USER", "AUTHENTICATED") + listViewColumns := Array("SERVICE", "PROVIDER", "USER", "AUTHENTICATED", "AUTO-LOGIN") GetListViewData(lv) { data := Map() @@ -8,8 +8,9 @@ class ManageWebServicesWindow extends ManageEntitiesWindow { data[key] := [ webService["name"], webService["Provider"]["name"], - webService.UserId ? webService.UserId : "None", - webService.Authenticated ? "Yes" : "No" + "", + webService.Authenticated ? "Yes" : "No", + webService["AutoLogin"] ? "Yes" : "No" ] } From 176e2cadabbbca0f64761b583dc3cd2bfde851ad Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 15:39:12 -0500 Subject: [PATCH 038/227] Add missing file from last change --- .../WebServices/Entity/WebServiceEntity.ahk | 47 ++++++++++++------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index 6ccb3202..6d9920fb 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -43,6 +43,36 @@ class WebServiceEntity extends AppEntityBase { ) } + BaseFieldDefinitions() { + definitions := super.BaseFieldDefinitions() + + if (this.idVal == "api" && definitions.Has("name")) { + definitions["name"]["editable"] := false + } + + definitions["Provider"] := Map( + "type", "entity_reference", + "entityType", "web_service_provider", + "required", true, + "editable", false + ) + + autoLoginDefault := false + + if (this.Id == "api") { + autoLoginDefault := this.container.GetParameter("config.api_auto_login") + } + + definitions["AutoLogin"] := Map( + "type", "boolean", + "description", "Automatically authenticate with this service when Launchpad starts.", + "required", false, + "default", autoLoginDefault + ) + + return definitions + } + IsAuthenticated() { isAuthenticated := false @@ -65,23 +95,6 @@ class WebServiceEntity extends AppEntityBase { } } - BaseFieldDefinitions() { - definitions := super.BaseFieldDefinitions() - - if (this.idVal == "api" && definitions.Has("name")) { - definitions["name"]["editable"] := false - } - - definitions["Provider"] := Map( - "type", "entity_reference", - "entityType", "web_service_provider", - "required", true, - "editable", false - ) - - return definitions - } - Request(path, method := "", data := "", useAuthentication := -1, cacheResponse := true) { if (!method) { method := this["Provider"]["DefaultMethod"] From 971c3fce8e4c142b1f6e7684c02a38c81f0aa770 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 16:04:32 -0500 Subject: [PATCH 039/227] Remove previous auto-login functionality --- Launchpad.services.json | 1 - Lib/Launchpad/App/Launchpad.ahk | 4 ---- Lib/Launchpad/Gui/Form/SettingsWindow.ahk | 2 -- Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json | 3 +-- .../Modules/WebServices/Entity/WebServiceEntity.ahk | 8 +------- 5 files changed, 2 insertions(+), 16 deletions(-) diff --git a/Launchpad.services.json b/Launchpad.services.json index 957f87ec..233455d7 100644 --- a/Launchpad.services.json +++ b/Launchpad.services.json @@ -2,7 +2,6 @@ "parameters": { "backups_config": {}, "config.api_authentication": false, - "config.api_auto_login": false, "config.api_endpoint": "", "config.assets_dir": "@@{data_dir}\\Launcher Assets", "config.auto_backup_config_files": true, diff --git a/Lib/Launchpad/App/Launchpad.ahk b/Lib/Launchpad/App/Launchpad.ahk index 73eb3320..767330d7 100644 --- a/Lib/Launchpad/App/Launchpad.ahk +++ b/Lib/Launchpad/App/Launchpad.ahk @@ -109,10 +109,6 @@ RunApp(config) { this.MigrateConfiguration() - - if (this.Config["api_auto_login"] && this.Services.Has("Auth")) { - this.Service("Auth").Login() - } super.RunApp(config) diff --git a/Lib/Launchpad/Gui/Form/SettingsWindow.ahk b/Lib/Launchpad/Gui/Form/SettingsWindow.ahk index e6482347..5ffa8e5c 100644 --- a/Lib/Launchpad/Gui/Form/SettingsWindow.ahk +++ b/Lib/Launchpad/Gui/Form/SettingsWindow.ahk @@ -127,8 +127,6 @@ this.AddHeading("API Settings") ctl := this.AddConfigCheckBox("Enable API login for enhanced functionality", "api_authentication") ctl.ctl.NeedsRestart := true - ctl := this.AddConfigCheckBox("Automatically initiate API login when needed", "api_auto_login") - ctl.ctl.NeedsRestart := true tabs.UseTab() diff --git a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json index bd3b74bc..b7b53128 100644 --- a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json +++ b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json @@ -35,7 +35,6 @@ "parameters": { "config.data_source_key": "api", "config.api_endpoint": "https://api.launchpad.games/v1", - "config.api_authentication": true, - "config.api_auto_login": false + "config.api_authentication": true } } diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index 6d9920fb..4bd46cb9 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -57,17 +57,11 @@ class WebServiceEntity extends AppEntityBase { "editable", false ) - autoLoginDefault := false - - if (this.Id == "api") { - autoLoginDefault := this.container.GetParameter("config.api_auto_login") - } - definitions["AutoLogin"] := Map( "type", "boolean", "description", "Automatically authenticate with this service when Launchpad starts.", "required", false, - "default", autoLoginDefault + "default", (this.idVal == "api") ) return definitions From 0d8de85c68e9510d5c41e7c9cf1f1d53c09ae9f8 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 16:04:45 -0500 Subject: [PATCH 040/227] Always pass container into event subscribers --- .../Volantis.Base/EventSubscriber/EventSubscriberBase.ahk | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/Shared/Volantis.Base/EventSubscriber/EventSubscriberBase.ahk b/Lib/Shared/Volantis.Base/EventSubscriber/EventSubscriberBase.ahk index 871cef25..3043d56d 100644 --- a/Lib/Shared/Volantis.Base/EventSubscriber/EventSubscriberBase.ahk +++ b/Lib/Shared/Volantis.Base/EventSubscriber/EventSubscriberBase.ahk @@ -2,6 +2,12 @@ Extending this class is optional as its main purpose is to document the API */ class EventSubscriberBase { + container := "" + + __New(container) { + this.container := container + } + /* Format: Map( @@ -12,6 +18,6 @@ class EventSubscriberBase { ) */ GetEventSubscribers() { - return [] + return Map() } } From a59b4ed4b132821bda09c52296e93bb161880770 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 16:05:06 -0500 Subject: [PATCH 041/227] Move APP_POST_STARTUP event to after the RunApp method has run --- Lib/Shared/Volantis.App/App/AppBase.ahk | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/Shared/Volantis.App/App/AppBase.ahk b/Lib/Shared/Volantis.App/App/AppBase.ahk index 14051eb9..082990c8 100644 --- a/Lib/Shared/Volantis.App/App/AppBase.ahk +++ b/Lib/Shared/Volantis.App/App/AppBase.ahk @@ -425,13 +425,13 @@ class AppBase { event := AppRunEvent(Events.APP_POST_INITIALIZE, this, config) this.Service("manager.event").DispatchEvent(event) - event := AppRunEvent(Events.APP_POST_STARTUP, this, config) - this.Service("manager.event").DispatchEvent(event) - event := AppRunEvent(Events.APP_PRE_RUN, this, config) this.Service("manager.event").DispatchEvent(event) this.RunApp(config) + + event := AppRunEvent(Events.APP_POST_STARTUP, this, config) + this.Service("manager.event").DispatchEvent(event) } LoadServices(config) { From 2c82a37524fccd31c18bb01c72270bbc54444318 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 16:05:24 -0500 Subject: [PATCH 042/227] Add Enabled field to WebService entities --- Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index 4bd46cb9..506bdd28 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -64,6 +64,12 @@ class WebServiceEntity extends AppEntityBase { "default", (this.idVal == "api") ) + definitions["Enabled"] := Map( + "type", "boolean", + "required", false, + "default", true + ) + return definitions } From d8561c65eb66e1f0da43eb82feda475d0b78ba8a Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 16:09:01 -0500 Subject: [PATCH 043/227] Query(Create a WebServicesEventSubscriber that handles auto-login services --- Lib/Shared/Includes.ahk | 1 + .../WebServicesEventSubscriber.ahk | 21 +++++++++++++++++++ .../WebServices/WebServices.module.json | 5 +++++ 3 files changed, 27 insertions(+) create mode 100644 Lib/Shared/Modules/WebServices/EventSubscriber/WebServicesEventSubscriber.ahk diff --git a/Lib/Shared/Includes.ahk b/Lib/Shared/Includes.ahk index 1dcb7297..a426f7cc 100644 --- a/Lib/Shared/Includes.ahk +++ b/Lib/Shared/Includes.ahk @@ -10,6 +10,7 @@ #Include Modules\WebServices\Event\WebServiceRequestEvent.ahk #Include Modules\WebServices\Event\WebServiceResponseEvent.ahk #Include Modules\WebServices\Events\WebServicesEvents.ahk +#Include Modules\WebServices\EventSubscriber\WebServicesEventSubscriber.ahk #Include Modules\WebServices\Gui\AuthenticationGui\LaunchpadLoginWindow.ahk #Include Modules\WebServices\Gui\ManageWindow\ManageWebServicesWindow.ahk #Include Modules\WebServices\WebServiceAuthenticator\JwtWebServiceAuthenticator.ahk diff --git a/Lib/Shared/Modules/WebServices/EventSubscriber/WebServicesEventSubscriber.ahk b/Lib/Shared/Modules/WebServices/EventSubscriber/WebServicesEventSubscriber.ahk new file mode 100644 index 00000000..66ea089b --- /dev/null +++ b/Lib/Shared/Modules/WebServices/EventSubscriber/WebServicesEventSubscriber.ahk @@ -0,0 +1,21 @@ +class WebServicesEventSubscriber extends EventSubscriberBase { + GetEventSubscribers() { + return Map( + Events.APP_POST_STARTUP, [ + ObjBindMethod(this, "OnPostStartup") + ] + ) + } + + OnPostStartup(event, extra, eventName, hwnd) { + webServices := this.container["entity_manager.web_service"] + .EntityQuery(EntityQuery.RESULT_TYPE_ENTITIES) + .Condition(IsTrueCondition(), "Enabled") + .Condition(IsTrueCondition(), "AutoLogin") + .Execute() + + for key, webService in webServices { + webService.Login() + } + } +} diff --git a/Lib/Shared/Modules/WebServices/WebServices.module.json b/Lib/Shared/Modules/WebServices/WebServices.module.json index aa2a1367..74985cb7 100644 --- a/Lib/Shared/Modules/WebServices/WebServices.module.json +++ b/Lib/Shared/Modules/WebServices/WebServices.module.json @@ -78,6 +78,11 @@ "web_services_authenticator.jwt": { "class": "JwtWebServiceAuthenticator", "arguments": ["@manager.gui"] + }, + "event_subscriber.web_services": { + "class": "WebServicesEventSubscriber", + "arguments": ["@{}"], + "tags": ["event_subscriber"] } } } From 358ae4902b607804282050de007368d620b82fd8 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 16:21:10 -0500 Subject: [PATCH 044/227] Allow canceling authentication GUI without retrying --- .../LaunchpadLoginWindow.ahk | 6 +++++- .../JwtWebServiceAuthenticator.ahk | 20 ++++++++++++------- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/Gui/AuthenticationGui/LaunchpadLoginWindow.ahk b/Lib/Shared/Modules/WebServices/Gui/AuthenticationGui/LaunchpadLoginWindow.ahk index 53e76866..d6ef8ff1 100644 --- a/Lib/Shared/Modules/WebServices/Gui/AuthenticationGui/LaunchpadLoginWindow.ahk +++ b/Lib/Shared/Modules/WebServices/Gui/AuthenticationGui/LaunchpadLoginWindow.ahk @@ -24,6 +24,10 @@ } ProcessResult(result, submittedData := "") { - return (result == "Login") ? this.guiObj["AuthToken"].Text : "" + if (result == "Login") { + result := this.guiObj["AuthToken"].Text + } + + return result } } diff --git a/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk index f080cd92..9f61b760 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk @@ -4,16 +4,22 @@ class JwtWebServiceAuthenticator extends WebServiceAuthenticatorBase { throw OperationFailedException("Login failed after " . retryCount . " tries.") } + authResult := "" + if (!this._hasRefreshToken(webServiceEnt)) { - this._reauthenticate(webServiceEnt) + authResult := this._reauthenticate(webServiceEnt) } - success := this._hasRefreshToken(webServiceEnt) - ? this._refreshAuthentication(webServiceEnt) - : false + success := false + + if (authResult != "Cancel") { + if (this._hasRefreshToken(webServiceEnt)) { + success := this._refreshAuthentication(webServiceEnt) + } - if (!success) { - success := this.Login(webServiceEnt, retryCount + 1) + if (!success) { + success := this.Login(webServiceEnt, retryCount + 1) + } } return success @@ -52,7 +58,7 @@ class JwtWebServiceAuthenticator extends WebServiceAuthenticatorBase { _reauthenticate(webServiceEnt) { refreshToken := this._authenticationGui(webServiceEnt) - if (refreshToken) { + if (refreshToken != "Cancel") { this._setRefreshToken(webServiceEnt, refreshToken) } From dba5a62ce55e531a241682d6c0ea966ca31c39b1 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 16:30:32 -0500 Subject: [PATCH 045/227] Show a normal dialog message after exceeding the max login retries instead of throwing an exception --- .../JwtWebServiceAuthenticator.ahk | 3 ++- .../WebServiceAuthenticatorBase.ahk | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk index 9f61b760..ddb016fd 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk @@ -1,7 +1,8 @@ class JwtWebServiceAuthenticator extends WebServiceAuthenticatorBase { Login(webServiceEnt, retryCount := 0) { if (retryCount > this.maxRetries) { - throw OperationFailedException("Login failed after " . retryCount . " tries.") + this._handleLoginFailure("You have used " . retryCount . " of " . this.maxRetries + 1 . " login attempts. Canceling login.") + return false } authResult := "" diff --git a/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk index 87fa435e..e1b8717c 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk @@ -72,4 +72,12 @@ class WebServiceAuthenticatorBase { return result } + + _handleLoginFailure(message) { + this.guiMgr.Dialog(Map( + "title", "Login Failure", + "text", message, + "buttons", "*&OK" + )) + } } From 22386c04f86b8864f532705d78acb37ce0c3f3d7 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 16:40:49 -0500 Subject: [PATCH 046/227] Allow overriding which default context menu buttons show in entity editors --- .../ManageWindow/ManageWebServicesWindow.ahk | 26 ++++++++++++++++--- .../Gui/ManageWindow/ManageEntitiesWindow.ahk | 10 ++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk b/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk index d5c68d24..83a36400 100644 --- a/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk +++ b/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk @@ -35,6 +35,16 @@ class ManageWebServicesWindow extends ManageEntitiesWindow { return menuItems } + _shouldShowButton(entityObj, buttonName) { + shouldShow := super._shouldShowButton(entityObj, buttonName) + + if (shouldShow && buttonName == "DeleteEntity") { + shouldShow := entityObj.Id != "api" + } + + return shouldShow + } + ProcessContextMenuResult(result, key) { if (result == "WebServiceLogout") { this.Logout(key) @@ -46,11 +56,19 @@ class ManageWebServicesWindow extends ManageEntitiesWindow { } Logout(key) { - return this.entityMgr[key].Logout() + result := this.entityMgr[key].Logout() + + this.UpdateListView() + + return result } Login(key) { - return this.entityMgr[key].Login() + result := this.entityMgr[key].Login() + + this.UpdateListView() + + return result } ViewEntity(key) { @@ -58,7 +76,9 @@ class ManageWebServicesWindow extends ManageEntitiesWindow { } AddEntity() { - + ; @todo open add wizard + + this.UpdateListView() } DeleteEntity(key) { diff --git a/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk b/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk index 0a782c82..3c3950d0 100644 --- a/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk +++ b/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk @@ -164,21 +164,25 @@ class ManageEntitiesWindow extends ManageWindowBase { definition := this.entityType.definition menuItems := [] - if (definition["allow_view"]) { + if (definition["allow_view"] && this._shouldShowButton(entityObj, "ViewEntity")) { menuItems.Push(Map("label", "&View", "name", "ViewEntity")) } - if (definition["allow_edit"]) { + if (definition["allow_edit"] && this._shouldShowButton(entityObj, "EditEntity")) { menuItems.Push(Map("label", "Edit", "name", "EditEntity")) } - if (definition["allow_delete"]) { + if (definition["allow_delete"] && this._shouldShowButton(entityObj, "DeleteEntity")) { menuItems.Push(Map("label", "Delete", "name", "DeleteEntity")) } return menuItems } + _shouldShowButton(entityObj, buttonName) { + return true + } + ShowListViewContextMenu(lv, item, isRightClick, X, Y) { key := this.listView.GetRowKey(item) entityObj := this.entityMgr[key] From 650f2cb0c39b7b51e20bba2a93756595f5a697d3 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 17:14:54 -0500 Subject: [PATCH 047/227] Update MainWindow to use new API web service --- Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk | 38 ++++++++++++++----- 1 file changed, 28 insertions(+), 10 deletions(-) diff --git a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk index b01f9160..066e43a4 100644 --- a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk @@ -177,11 +177,26 @@ } } + _getApiWebService() { + webService := "" + + if (this.app.Services.Has("entity_manager.web_service")) { + entityMgr := this.app.Services["entity_manager.web_service"] + + if (entityMgr.Has("api") && entityMgr["api"]["Enabled"]) { + webService := entityMgr["api"] + } + } + + return webService + } + GetStatusInfo() { info := "" + webService := this._getApiWebService() - if (this.container.Has("Auth")) { - info := this.container["Auth"].GetStatusInfo() + if (webService) { + info := webService.GetStatusInfo() } return info @@ -189,9 +204,10 @@ OnStatusIndicatorClick(btn, info) { menuItems := [] + webService := this._getApiWebService() - if (this.container.Has("Auth")) { - if (this.container["Auth"].IsAuthenticated()) { + if (webService) { + if (webService.Authenticated) { menuItems.Push(Map("label", "Account Details", "name", "AccountDetails")) menuItems.Push(Map("label", "Logout", "name", "Logout")) } else { @@ -212,22 +228,24 @@ this.UpdateStatusIndicator() } } else if (result == "Logout") { - if (this.container.Has("Auth")) { - this.container["Auth"].Logout() + if (webService) { + webService.Logout() } } else if (result == "Login") { - if (this.container.Has("Auth")) { - this.container["Auth"].Login() + if (webService) { + webService.Login() } } } StatusWindowIsOnline() { isOnline := false + webService := this._getApiWebService() - if (this.container.Has("Auth")) { - isOnline := this.container["Auth"].IsAuthenticated() + if (webService) { + isOnline := webService.Authenticated } + return isOnline } From 0ec7b0cc152d50b12feaf52a8fe9de13e33035ee Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 17:15:10 -0500 Subject: [PATCH 048/227] Update LaunchpadBuilder to use new API web service --- Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk b/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk index d787c321..0f1b87c7 100644 --- a/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk +++ b/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk @@ -60,8 +60,12 @@ class LaunchpadBuilder extends AppBase { this.ExitApp() } - if (buildInfo.DeployToApi && this.Services.Has("Auth")) { - this.Service("Auth").Login() + if (buildInfo.DeployToApi && this.Services.Has("entity_manager.web_service")) { + entityMgr := this.Services["entity_manager.web_service"] + + if (entityMgr.Has("api") && entityMgr["api"]["Enabled"]) { + entityMgr["api"].Login() + } } version := buildInfo.Version From c8e272a7027066bee775ccdf17634221f4917763 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 17:15:58 -0500 Subject: [PATCH 049/227] Update LaunchpadApi module to depend on WebServices module, and remove old auth provider --- .../AuthProvider/LaunchpadApiAuthProvider.ahk | 30 ------------------- .../LaunchpadApi/LaunchpadApi.module.json | 6 +--- 2 files changed, 1 insertion(+), 35 deletions(-) delete mode 100644 Lib/Shared/Modules/LaunchpadApi/AuthProvider/LaunchpadApiAuthProvider.ahk diff --git a/Lib/Shared/Modules/LaunchpadApi/AuthProvider/LaunchpadApiAuthProvider.ahk b/Lib/Shared/Modules/LaunchpadApi/AuthProvider/LaunchpadApiAuthProvider.ahk deleted file mode 100644 index ff44127d..00000000 --- a/Lib/Shared/Modules/LaunchpadApi/AuthProvider/LaunchpadApiAuthProvider.ahk +++ /dev/null @@ -1,30 +0,0 @@ -class LaunchpadApiAuthProvider extends JwtAuthProvider { - app := "" - - __New(app, stateObj) { - this.app := app - - persistentData := "" - - if (stateObj) { - state := stateObj.GetState() - - if (state.Has("Authentication")) { - persistentData := state["Authentication"] - } - } - - authEndpointUrl := "https://securetoken.googleapis.com/v1" - webApiKey := "AIzaSyCbwzOWJjTft77P96dV5VB3dAx9TjdDowQ" - super.__New(app.Service("manager.data_source").GetDefaultDataSource(), authEndpointUrl, webApiKey, persistentData) - } - - ShowLoginWindow() { - return this.app.Service("manager.gui").Dialog(Map("type", "LoginWindow")) - } - - ExtractAuthInfoFromResponse(httpReqObj) { - authInfoObj := super.ExtractAuthInfoFromResponse(httpReqObj) - return authInfoObj - } -} diff --git a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json index b7b53128..bc9da04e 100644 --- a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json +++ b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json @@ -12,17 +12,13 @@ "website": "https://launchpad.games", "version": "{{VERSION}}", "appVersion": "", - "dependencies": ["Auth"] + "dependencies": ["WebServices"] }, "services": { "data_source.api": { "class": "ApiDataSource", "arguments": ["@{App}", "@manager.cache", "api", "@@config.api_endpoint"] }, - "auth_provider.launchpad_api": { - "class": "LaunchpadApiAuthProvider", - "arguments": ["@{App}", "@state.app"] - }, "cache_state.api": { "class": "CacheState", "arguments": ["@{App}", "@@config.cache_dir", "API.json"] From c39399d0fda4de7bf7663eeb96c32d62fb8c4080 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 17:16:22 -0500 Subject: [PATCH 050/227] Remove unused AuthInfo tests --- Lib/Shared/Includes.test.ahk | 1 - .../Modules/Auth/AuthInfo/AuthInfo.test.ahk | 103 ------------------ 2 files changed, 104 deletions(-) delete mode 100644 Lib/Shared/Modules/Auth/AuthInfo/AuthInfo.test.ahk diff --git a/Lib/Shared/Includes.test.ahk b/Lib/Shared/Includes.test.ahk index 2fe8682e..f157d4f8 100644 --- a/Lib/Shared/Includes.test.ahk +++ b/Lib/Shared/Includes.test.ahk @@ -1,5 +1,4 @@ ; Automatically-generated file. Manual edits will be overwritten. -#Include Modules\Auth\AuthInfo\AuthInfo.test.ahk #Include Volantis.App\App\AppBase.test.ahk #Include Volantis.Base\Event\EventBase.test.ahk #Include Volantis.Utility\Debugger\Debugger.test.ahk diff --git a/Lib/Shared/Modules/Auth/AuthInfo/AuthInfo.test.ahk b/Lib/Shared/Modules/Auth/AuthInfo/AuthInfo.test.ahk deleted file mode 100644 index dfbcde79..00000000 --- a/Lib/Shared/Modules/Auth/AuthInfo/AuthInfo.test.ahk +++ /dev/null @@ -1,103 +0,0 @@ -class AuthInfoTest extends AppTestBase { - TestAuthenticated() { - authInfoObj := AuthInfo() - - authInfoObj.isAuthenticated := false - - this.AssertFalse( - authInfoObj.Authenticated, - "Test setting isAuthenticated to false" - ) - - authInfoObj.Authenticated := true - - this.AssertTrue( - authInfoObj.Authenticated, - "Test changing Authenticated to true" - ) - } - - TestUserId() { - authInfoObj := AuthInfo() - - this.AssertEquals( - "", - authInfoObj.UserId, - "Assert that user ID is blank initially" - ) - - userIds := [ - 123, - "456", - "1234-5678-9012-3456", - "r@nd0m" - ] - - for userId in userIds { - authInfoObj.Set(authInfoObj.userIdField, userId) - - this.AssertEquals( - userId, - authInfoObj.UserId, - "Test changing user ID to " . userId - ) - } - } - - TestGet() { - authInfoObj := AuthInfo() - - this.AssertEmpty( - authInfoObj.Get("nonexistantValue"), - "Getting a non-existent value returns an empty string" - ) - - authInfoObj.Set("testValue", "persistent", true) - - this.AssertEquals( - "persistent", - authInfoObj.Get("testValue"), - "Getting a persistent value is possible" - ) - - authInfoObj.Set("testValue", "overridden", false) - - this.AssertEquals( - "overridden", - authInfoObj.Get("testValue"), - "Getting a secure value overrides a persistent one" - ) - } - - TestSet() { - authInfoObj := AuthInfo() - - authInfoObj.Set("testValue", "persistent", true) - - this.AssertEquals( - "persistent", - authInfoObj.Get("testValue"), - "Setting a persistent value is possible" - ) - - authInfoObj.Set("testValue", "overridden", false) - - this.AssertEquals( - "overridden", - authInfoObj.Get("testValue"), - "Setting a secure value overrides a persistent one" - ) - } - - TestGetPersistentData() { - persistentData := Map("testValue", "persistent") - authInfoObj := AuthInfo() - authInfoObj.persistentData := persistentData - - this.AssertEquals( - persistentData, - authInfoObj.GetPersistentData(), - "GetPersistentData returns all persistent data" - ) - } -} From 156743578c076a81692e7efa1cf5bc400cf6efd0 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 17:17:13 -0500 Subject: [PATCH 051/227] Standardize AlterRequest parameters --- .../WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk | 2 +- .../WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk | 2 +- .../WebServices/WebServiceRequest/WebServiceRequestBase.ahk | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk index ddb016fd..ce99bdce 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/JwtWebServiceAuthenticator.ahk @@ -44,7 +44,7 @@ class JwtWebServiceAuthenticator extends WebServiceAuthenticatorBase { } } - AlterRequest(webServiceEnt, request, httpReqObj) { + AlterRequest(webServiceEnt, httpReqObj) { bearerToken := webServiceEnt.AuthData["auth_token"] if (bearerToken) { diff --git a/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk index e1b8717c..91664f03 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceAuthenticator/WebServiceAuthenticatorBase.ahk @@ -21,7 +21,7 @@ class WebServiceAuthenticatorBase { } - AlterRequest(webServiceEnt, request, httpReqObj) { + AlterRequest(webServiceEnt, httpReqObj) { } diff --git a/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk b/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk index 063dc665..754981b0 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk @@ -129,7 +129,7 @@ class WebServiceRequestBase { authenticator.RefreshAuthentication(this.webServiceEnt) } - authenticator.AlterRequest(this.webServiceEnt, this, httpReqObj) + authenticator.AlterRequest(this.webServiceEnt, httpReqObj) } event := WebServiceRequestEvent(WebServicesEvents.WEB_SERVICES_HTTP_REQ_ALTER, this) From 5b2b4dcba54385f2ba8a8cc5a47d071865dd42a3 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 17:17:31 -0500 Subject: [PATCH 052/227] Add GetStatusInfo to WebServiceEntity (but it isn't finished yet) --- .../WebServices/Entity/WebServiceEntity.ahk | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index 506bdd28..11c29b32 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -224,4 +224,33 @@ class WebServiceEntity extends AppEntityBase { return modified } + + GetStatusInfo() { + ; @todo fix this data + statusText := "Not logged in" + imgPath := "" + email := "" + + if (this.Authenticated) { + playerName := this.app.Config["player_name"] + email := this.AuthData["email"] + + if (playerName) { + statusText := playerName + } else if (email) { + statusText := email + } else { + statusText := "Logged in" + } + + imgPath := this.AuthData["photo"] + + if (SubStr(imgPath, 1, 4) == "http") { + cachePath := "account--profile.jpg" + imgPath := this.app.Service("manager.cache")["file"].GetCachedDownload(cachePath, imgPath) + } + } + + return Map("name", statusText, "email", email, "photo", imgPath) + } } From c9b41a6def7c0a48878e82a764314b108a71c7a2 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 17:17:57 -0500 Subject: [PATCH 053/227] Update ApiDataSource to get data through the API web service --- .../LaunchpadApi/DataSource/ApiDataSource.ahk | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk b/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk index bf0cf3aa..d8c4363b 100644 --- a/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk +++ b/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk @@ -35,8 +35,14 @@ class ApiDataSource extends DataSourceBase { if (private) { request.requestHeaders["Cache-Control"] := "no-cache" - if (this.app.Config["api_authentication"] && this.app.Services.Has("Api")) { - this.app.Service("Auth").AlterApiRequest(request) + if (this.app.Config["api_authentication"]) { + entityMgr := webService := this.app.Service("entity_manager.web_service") + + if (entityMgr.Has("api") && entityMgr["api"]["Enabled"]) { + webService := this.app.Service("entity_manager.web_service")["api"] + webService["Provider"]["Authenticator"].AlterRequest(webService, request) + } + } } @@ -85,12 +91,15 @@ class ApiDataSource extends DataSourceBase { status := Map("authenticated", false, "email", "", "photo", "") - if (this.app.Config["api_authentication"] && this.app.Service("Auth").IsAuthenticated()) { - statusResult := this.ReadItem(path, true) + if (this.app.Config["api_authentication"]) { + entityMgr := webService := this.app.Service("entity_manager.web_service") - if (statusResult) { - json := JsonData() - status := json.FromString(&statusResult) + if (entityMgr.Has("api") && entityMgr["api"]["Enabled"] && entityMgr["api"]["Authenticated"]) { + statusResult := this.ReadItem(path, true) + + if (statusResult) { + status := JsonData().FromString(&statusResult) + } } } From 343e1335167088835808c405f2e3519d790409e1 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 17:18:19 -0500 Subject: [PATCH 054/227] Update account info window to use API web service --- .../Gui/Dialog/AccountInfoWindow.ahk | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/Lib/Launchpad/Gui/Dialog/AccountInfoWindow.ahk b/Lib/Launchpad/Gui/Dialog/AccountInfoWindow.ahk index 79b752ba..cdf5ba42 100644 --- a/Lib/Launchpad/Gui/Dialog/AccountInfoWindow.ahk +++ b/Lib/Launchpad/Gui/Dialog/AccountInfoWindow.ahk @@ -9,13 +9,23 @@ Controls() { super.Controls() - if (this.app.Services.Has("Auth")) { - info := this.app.Service("Auth").GetStatusInfo() + if (this.app.Services.Has("entity_manager.web_service")) { + entityMgr := this.app.Services["entity_manager.web_service"] - if (info) { - opts := "w" . this.windowSettings["contentWidth"] . " x" . this.margin . " y+" . this.margin - this.guiObj.AddPicture("x" . this.margin . " y+" . this.margin, info["photo"]) - this.guiObj.AddText(opts, "Email: " . info["email"]) + if (entityMgr.Has("api") && entityMgr["api"]["Enabled"]) { + info := Map( + "name", "", + "email", "", + "photo", "" + ) + + ; @todo Pull this information from the API web service + + if (info) { + opts := "w" . this.windowSettings["contentWidth"] . " x" . this.margin . " y+" . this.margin + this.guiObj.AddPicture("x" . this.margin . " y+" . this.margin, info["photo"]) + this.guiObj.AddText(opts, "Email: " . info["email"]) + } } } @@ -29,9 +39,15 @@ } ProcessResult(result, submittedData := "") { + + if (result == "Logout") { - if (this.app.Services.Has("Auth")) { - this.app.Service("Auth").Logout() + if (this.app.Services.Has("entity_manager.web_service")) { + entityMgr := this.app.Services["entity_manager.web_service"] + + if (entityMgr.Has("api") && entityMgr["api"]["Enabled"]) { + entityMgr["api"].Logout() + } } } else if (result == "Save" && submittedData) { this.app.Config["player_name"] := submittedData.PlayerName From 23ec7a90793d36cd98be3be04a56119af1b86c6d Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 17:18:34 -0500 Subject: [PATCH 055/227] Remove unused Auth module --- Lib/Shared/Includes.ahk | 6 - Lib/Shared/Modules/Auth/Auth.module.json | 27 --- Lib/Shared/Modules/Auth/AuthInfo/AuthInfo.ahk | 40 ----- .../Modules/Auth/AuthInfo/JwtAuthInfo.ahk | 42 ----- .../Auth/AuthProvider/AuthProviderBase.ahk | 37 ----- .../Auth/AuthProvider/JwtAuthProvider.ahk | 150 ----------------- .../Volantis.App/Service/AuthService.ahk | 154 ------------------ 7 files changed, 456 deletions(-) delete mode 100644 Lib/Shared/Modules/Auth/Auth.module.json delete mode 100644 Lib/Shared/Modules/Auth/AuthInfo/AuthInfo.ahk delete mode 100644 Lib/Shared/Modules/Auth/AuthInfo/JwtAuthInfo.ahk delete mode 100644 Lib/Shared/Modules/Auth/AuthProvider/AuthProviderBase.ahk delete mode 100644 Lib/Shared/Modules/Auth/AuthProvider/JwtAuthProvider.ahk delete mode 100644 Lib/Shared/Volantis.App/Service/AuthService.ahk diff --git a/Lib/Shared/Includes.ahk b/Lib/Shared/Includes.ahk index a426f7cc..129aa118 100644 --- a/Lib/Shared/Includes.ahk +++ b/Lib/Shared/Includes.ahk @@ -1,9 +1,4 @@ ; Automatically-generated file. Manual edits will be overwritten. -#Include Modules\Auth\AuthInfo\AuthInfo.ahk -#Include Modules\Auth\AuthInfo\JwtAuthInfo.ahk -#Include Modules\Auth\AuthProvider\AuthProviderBase.ahk -#Include Modules\Auth\AuthProvider\JwtAuthProvider.ahk -#Include Modules\LaunchpadApi\AuthProvider\LaunchpadApiAuthProvider.ahk #Include Modules\LaunchpadApi\DataSource\ApiDataSource.ahk #Include Modules\WebServices\Entity\WebServiceEntity.ahk #Include Modules\WebServices\Entity\WebServiceProviderEntity.ahk @@ -98,7 +93,6 @@ #Include Volantis.App\Installer\InstallerComponent\GitHubReleaseInstallerComponent.ahk #Include Volantis.App\Installer\InstallerComponent\InstallerComponentBase.ahk #Include Volantis.App\Service\AppServiceBase.ahk -#Include Volantis.App\Service\AuthService.ahk #Include Volantis.App\Service\EventManager.ahk #Include Volantis.App\Service\LoggerService.ahk #Include Volantis.App\Service\NotificationService.ahk diff --git a/Lib/Shared/Modules/Auth/Auth.module.json b/Lib/Shared/Modules/Auth/Auth.module.json deleted file mode 100644 index 27e65909..00000000 --- a/Lib/Shared/Modules/Auth/Auth.module.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "module": { - "name": "Authentication", - "type": "AppModule", - "icon": "", - "category": "Web Services", - "tags": ["Launchpad", "LaunchpadBuilder"], - "description": "Authenticate with remote accounts, such as the Launchpad API", - "author": { - "name": "Ben McClure, Volantis Dev", - "url": "https://volantisdev.com" - }, - "website": "https://launchpad.games", - "version": "{{VERSION}}", - "appVersion": "", - "dependencies": [] - }, - "parameters": { - "config.auth_service": "" - }, - "services": { - "Auth": { - "class": "AuthService", - "arguments": ["@{App}", "@@config.auth_service", "@state.app"] - } - } -} diff --git a/Lib/Shared/Modules/Auth/AuthInfo/AuthInfo.ahk b/Lib/Shared/Modules/Auth/AuthInfo/AuthInfo.ahk deleted file mode 100644 index 3428b9e0..00000000 --- a/Lib/Shared/Modules/Auth/AuthInfo/AuthInfo.ahk +++ /dev/null @@ -1,40 +0,0 @@ -class AuthInfo { - isAuthenticated := false - secureData := Map() - persistentData := Map() - userIdField := "userId" - - Authenticated { - get => this.isAuthenticated - set => this.isAuthenticated := !!(value) - } - - UserId { - get => this.Get(this.userIdField) - } - - __New() { - - } - - Get(key) { - value := "" - - if (this.secureData.Has(key)) { - value := this.secureData[key] - } else if (this.persistentData.Has(key)) { - value := this.persistentData[key] - } - - return value - } - - Set(key, value, persist := false) { - mapObj := persist ? this.persistentData : this.secureData - mapObj[key] := value - } - - GetPersistentData() { - return this.persistentData - } -} diff --git a/Lib/Shared/Modules/Auth/AuthInfo/JwtAuthInfo.ahk b/Lib/Shared/Modules/Auth/AuthInfo/JwtAuthInfo.ahk deleted file mode 100644 index 57b560ca..00000000 --- a/Lib/Shared/Modules/Auth/AuthInfo/JwtAuthInfo.ahk +++ /dev/null @@ -1,42 +0,0 @@ -class JwtAuthInfo extends AuthInfo { - __New(userInfo) { - super.__New() - - this.Authenticated := false - - added := Map() - - if (userInfo.Has("user_id")) { - this.Authenticated := !!(userInfo["user_id"]) - this.Set("userId", userInfo["user_id"], true) - added["user_id"] := true - } - - if (userInfo.Has("refresh_token")) { - this.Set("refresh", userInfo["refresh_token"], true) - added["refresh_token"] := true - } - - if (userInfo.Has("expires_in")) { - timestamp := DateAdd(A_Now, userInfo["expires_in"], "S") - this.Set("expires", timestamp, true) - added["expires_in"] := true - } - - if (userInfo.Has("id_token")) { - this.Set("authToken", userInfo["id_token"]) - added["id_token"] := true - } - - if (userInfo.Has("access_token")) { - this.Set("accessToken", userInfo["access_token"]) - added["authToken"] := true - } - - for key, value in userInfo { - if (!added.Has(key) || !added[key]) { - this.Set(key, value) - } - } - } -} diff --git a/Lib/Shared/Modules/Auth/AuthProvider/AuthProviderBase.ahk b/Lib/Shared/Modules/Auth/AuthProvider/AuthProviderBase.ahk deleted file mode 100644 index 475bc81f..00000000 --- a/Lib/Shared/Modules/Auth/AuthProvider/AuthProviderBase.ahk +++ /dev/null @@ -1,37 +0,0 @@ -class AuthProviderBase { - __New(persistentData := "") { - - } - - Login() { - - } - - Logout(authInfoObj) { - - } - - NeedsRefresh(authInfoObj) { - - } - - RefreshAuthentication(authInfoObj) { - - } - - AddRefreshInfoToRequest(authInfoObj, httpReqObj) { - ; Add refresh token to the request - } - - AddLoginInfoToRequest(authToken, httpReqObj) { - ; Add auth info needed to complete login to the request - } - - AddAuthInfoToRequest(authInfoObj, httpReqObj) { - ; Add auth info from authInfoObj to httpReqObj as needed - } - - ExtractAuthInfoFromResponse(httpReqObj) { - - } -} diff --git a/Lib/Shared/Modules/Auth/AuthProvider/JwtAuthProvider.ahk b/Lib/Shared/Modules/Auth/AuthProvider/JwtAuthProvider.ahk deleted file mode 100644 index 5fde6ad2..00000000 --- a/Lib/Shared/Modules/Auth/AuthProvider/JwtAuthProvider.ahk +++ /dev/null @@ -1,150 +0,0 @@ -class JwtAuthProvider extends AuthProviderBase { - dataSourceObj := "" - authEndpointUrl := "" - webApiKey := "" - refreshPath := "token" - authToken := "" - refreshToken := "" - - __New(dataSourceObj, authEndpointUrl, webApiKey, persistentData := "") { - this.dataSourceObj := dataSourceObj - this.authEndpointUrl := authEndpointUrl - this.webApiKey := webApiKey - - if (persistentData != "" and persistentData.Has("authToken")) { - this.authToken := persistentData["authToken"] - } - } - - Login(isRetry := false) { - refreshToken := this.refreshToken - - if (!refreshToken) { - refreshToken := this.ShowLoginWindow() - - if (refreshToken) { - this.refreshToken := refreshToken - } - } - - userInfo := "" - - if (refreshToken) { - url := this.GetAuthUrl(this.refreshPath) - request := WinHttpReq(url) - payload := Map("grant_type", "refresh_token", "refresh_token", refreshToken) - response := request.Send("POST", payload) - - if (request.GetStatusCode() == 200) { - userInfo := this.ExtractAuthInfoFromResponse(request) - } else { - this.refreshToken := "" - - if (isRetry) { - ; TODO: Log user out instead of throwing an exception - throw OperationFailedException("Login failed.") - } else { - return this.Login(true) - } - } - } - - return userInfo - } - - GetAuthUrl(path) { - return this.authEndpointUrl . "/" . path . "?key=" . this.webApiKey - } - - ShowLoginWindow() { - return "" - } - - Logout(authInfoObj) { - this.authToken := "" - this.refreshToken := "" - return true - } - - RefreshAuthentication(authInfoObj) { - refreshToken := authInfoObj.Get("refresh") - - if (refreshToken) { - this.refreshToken := refreshToken - } - - return this.Login() - } - - IsAuthenticationValid(authInfoObj) { - isValid := false - - if (authInfoObj && authInfoObj.Authenticated) { - isValid := !this.IsAuthenticationExpired(authInfoObj) - } - - return isValid - } - - AddAuthInfoToRequest(authInfoObj, httpReqObj) { - authToken := authInfoObj.Get("authToken") - - if (authToken) { - this.AddAuthorizationHeader(authToken, httpReqObj) - } - } - - AddAuthorizationHeader(bearerToken, httpReqObj) { - if (bearerToken) { - httpReqObj.requestHeaders["Authorization"] := "Bearer " . bearerToken - } - } - - ExtractAuthInfoFromResponse(httpReqObj) { - responseData := Trim(httpReqObj.GetResponseData()) - - userInfo := Map() - - if (responseData) { - data := JsonData() - userInfo := data.FromString(&responseData) - } - - return JwtAuthInfo(userInfo) - } - - IsAuthenticationExpired(authInfoObj) { - expired := true - - if (authInfoObj and authInfoObj.Authenticated) { - expires := authInfoObj.Get("expires") - - if (expires) { - diff := DateDiff(A_Now, expires, "S") - expired := (diff >= 0) - } - } - - return expired - } - - NeedsRefresh(authInfoObj) { - needsRefresh := false - thresholdSeconds := -600 - - if (!this.IsAuthenticationValid(authInfoObj)) { - needsRefresh := true - } else { - expires := authInfoObj.Get("expires") - - if (expires) { - diff := DateDiff(A_Now, expires, "S") - needsRefresh := (diff >= thresholdSeconds) - } else { - needsRefresh := true - } - } - - return needsRefresh - } -} diff --git a/Lib/Shared/Volantis.App/Service/AuthService.ahk b/Lib/Shared/Volantis.App/Service/AuthService.ahk deleted file mode 100644 index 9d9b4cd2..00000000 --- a/Lib/Shared/Volantis.App/Service/AuthService.ahk +++ /dev/null @@ -1,154 +0,0 @@ -class AuthService extends AppServiceBase { - authProviderObj := "" - stateObj := "" - authenticationEnabled := false - authInfoObj := "" - - __New(app, authProviderObj, stateObj) { - InvalidParameterException.CheckTypes("AuthenticationService", "stateObj", stateObj, "StateBase") - - if (authProviderObj && Type(authProviderObj) == "String") { - authProviderObj := app.Services.Get(authProviderObj) - } - - this.authProviderObj := authProviderObj - this.stateObj := stateObj - - authState := this.stateObj.Authentication - - if (authState && authState.Count > 0) { - authInfoObj := AuthInfo() - authInfoObj.Authenticated := true - - for key, value in authState { - authInfoObj.Set(key, value, true) - } - - this.authInfoObj := authInfoObj - } - - super.__New(app) - } - - SetAuthProvider(authProviderObj) { - this.authProviderObj := authProviderObj - } - - SetState(stateObj) { - this.stateObj := stateObj - } - - Login() { - if (this.app.Config["api_authentication"] && this.authProviderObj) { - authInfoObj := "" - - if (!this.IsAuthenticated()) { - authInfoObj := this.authProviderObj.Login() - } else if (this.AuthenticationNeedsRefresh()) { - authInfoObj := this.RefreshAuthentication() - } - - if (authInfoObj) { - this.UpdateAuthState(authInfoObj) - this.app.UpdateStatusIndicators() - } - } - } - - GetStatusInfo() { - statusText := "Not logged in" - imgPath := "" - email := "" - - if (this.IsAuthenticated()) { - playerName := this.app.Config["player_name"] - email := this.authInfoObj.Get("email") - - if (playerName) { - statusText := playerName - } else if (email) { - statusText := email - } else { - statusText := "Logged in" - } - - imgPath := this.authInfoObj.Get("photo") - - if (SubStr(imgPath, 1, 4) == "http") { - cachePath := "account--profile.jpg" - imgPath := this.app.Service("manager.cache")["file"].GetCachedDownload(cachePath, imgPath) - } - } - - return Map("name", statusText, "email", email, "photo", imgPath) - } - - Logout() { - if (this.app.Config["api_authentication"] && this.authProviderObj && this.authInfoObj) { - this.authProviderObj.Logout(this.authInfoObj) - this.authInfoObj := "" - this.stateObj.Authentication := Map() - this.app.UpdateStatusIndicators() - } - } - - IsAuthenticated() { - return this.app.Config["api_authentication"] && this.authProviderObj && this.authInfoObj && this.authInfoObj.Authenticated - } - - AuthenticationNeedsRefresh() { - needsRefresh := false - - if (this.app.Config["api_authentication"] && this.authProviderObj && this.IsAuthenticated()) { - needsRefresh := this.authProviderObj.NeedsRefresh(this.authInfoObj) - } - - return needsRefresh - } - - RefreshAuthentication() { - if (this.app.Config["api_authentication"] && this.authProviderObj && this.IsAuthenticated()) { - authInfoObj := this.authProviderObj.RefreshAuthentication(this.authInfoObj) - - if (authInfoObj) { - this.UpdateAuthState(authInfoObj) - } - } - } - - UpdateAuthState(authInfoObj) { - if (this.app.Config["api_authentication"] && this.authProviderObj && authInfoObj) { - this.authInfoObj := authInfoObj - this.AddUserInfoFromApi(authInfoObj) - this.stateObj.SetAuthentication(authInfoObj.GetPersistentData()) - } - } - - AddUserInfoFromApi(authInfoObj) { - dataSource := this.app.Service("manager.data_source").GetDefaultDataSource() - - if (dataSource) { - apiStatus := dataSource.GetStatus() - - if (apiStatus) { - if (apiStatus.Has("email")) { - authInfoObj.Set("email", apiStatus["email"], true) - } - - if (apiStatus.Has("photo")) { - authInfoObj.Set("photo", apiStatus["photo"], true) - } - } - } - } - - AlterApiRequest(request) { - if (this.IsAuthenticated()) { - if (this.AuthenticationNeedsRefresh()) { - this.RefreshAuthentication() - } - - this.authProviderObj.AddAuthInfoToRequest(this.authInfoObj, request) - } - } -} From 981d4023ad499c52d2cda8d372a02739f66bd7bd Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 18:48:15 -0500 Subject: [PATCH 056/227] Refactor status indicators to show one per account, work in progress --- Lib/Launchpad/App/Launchpad.ahk | 12 ++++- Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk | 8 ++- .../WebServices/Entity/WebServiceEntity.ahk | 27 ++++++++++ Lib/Shared/Volantis.App/Gui/GuiBase.ahk | 8 +-- .../GuiControl/GuiControlBase.ahk | 2 + .../GuiControl/StatusIndicatorControl.ahk | 37 ++++++++----- .../GuiControl/TitlebarControl.ahk | 53 +++++++++++++------ 7 files changed, 108 insertions(+), 39 deletions(-) diff --git a/Lib/Launchpad/App/Launchpad.ahk b/Lib/Launchpad/App/Launchpad.ahk index 767330d7..b752ac6e 100644 --- a/Lib/Launchpad/App/Launchpad.ahk +++ b/Lib/Launchpad/App/Launchpad.ahk @@ -155,7 +155,17 @@ UpdateStatusIndicators() { if (this.Service("manager.gui").Has("MainWindow")) { - this.Service("manager.gui")["MainWindow"].UpdateStatusIndicator() + serviceMgr := this.container["entity_manager.web_service"] + webServices := serviceMgr.EntityQuery(EntityQuery.RESULT_TYPE_ENTITIES) + .Condition(IsTrueCondition(), "Enabled") + .Condition(IsTrueCondition(), "StatusIndicator") + .Execute() + + windowObj := this.Service("manager.gui")["MainWindow"] + + for serviceId, webService in webServices { + windowObj.UpdateStatusIndicator(webService) + } } } diff --git a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk index 066e43a4..082f57dd 100644 --- a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk @@ -191,9 +191,8 @@ return webService } - GetStatusInfo() { + GetStatusInfo(webService) { info := "" - webService := this._getApiWebService() if (webService) { info := webService.GetStatusInfo() @@ -225,7 +224,7 @@ )) if (accountResult == "OK") { - this.UpdateStatusIndicator() + this.UpdateStatusIndicator(webService) } } else if (result == "Logout") { if (webService) { @@ -238,9 +237,8 @@ } } - StatusWindowIsOnline() { + StatusWindowIsOnline(webService) { isOnline := false - webService := this._getApiWebService() if (webService) { isOnline := webService.Authenticated diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index 11c29b32..6d5fb68f 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -3,6 +3,7 @@ class WebServiceEntity extends AppEntityBase { stateObj := "" persistentStateObj := "" mergeDataFromApi := false + statusIndicators := [] Authenticated { get => this.IsAuthenticated() @@ -70,9 +71,35 @@ class WebServiceEntity extends AppEntityBase { "default", true ) + definitions["StatusIndicator"] := Map( + "type", "boolean", + "required", false, + "default", (this.idVal == "api") + ) + + definitions["StatusIndicatorExpanded"] := Map( + "type", "boolean", + "required", false, + "default", (this.idVal == "api") + ) + return definitions } + GetStatusIndicators() { + return this.statusIndicators + } + + AddStatusIndicator(statusIndicatorCtl) { + this.statusIndicators.Push(statusIndicatorCtl) + } + + UpdateStatusIndicators() { + for , statusIndicatorCtl in this.statusIndicators { + statusIndicatorCtl.UpdateStatusIndicator() + } + } + IsAuthenticated() { isAuthenticated := false diff --git a/Lib/Shared/Volantis.App/Gui/GuiBase.ahk b/Lib/Shared/Volantis.App/Gui/GuiBase.ahk index 0cb58f9e..8a89cabc 100644 --- a/Lib/Shared/Volantis.App/Gui/GuiBase.ahk +++ b/Lib/Shared/Volantis.App/Gui/GuiBase.ahk @@ -349,17 +349,17 @@ class GuiBase { return this.guiObj.AddEdit(opts, defaultValue) } - UpdateStatusIndicator() { + UpdateStatusIndicator(webService) { if (this.config["showStatusIndicator"]) { - this.titlebar.statusIndicator.UpdateStatusIndicator(this.GetStatusInfo(), this.StatusWindowIsOnline() ? "status" : "statusOffline") + webService.UpdateStatusIndicators() } } - StatusWindowIsOnline() { + StatusWindowIsOnline(webService) { return false } - GetStatusInfo() { + GetStatusInfo(webService) { return Map("name", "", "photo", "") } diff --git a/Lib/Shared/Volantis.App/GuiControl/GuiControlBase.ahk b/Lib/Shared/Volantis.App/GuiControl/GuiControlBase.ahk index 399d18cc..9d58675e 100644 --- a/Lib/Shared/Volantis.App/GuiControl/GuiControlBase.ahk +++ b/Lib/Shared/Volantis.App/GuiControl/GuiControlBase.ahk @@ -1,5 +1,6 @@ class GuiControlBase { app := "" + container := "" guiObj := "" ctl := "" defaultH := 20 @@ -12,6 +13,7 @@ class GuiControlBase { __New(guiObj, options := "", heading := "", params*) { InvalidParameterException.CheckTypes("GuiControlBase", "guiObj", guiObj, "GuiBase") this.app := guiObj.app + this.container := this.app.Services this.guiObj := guiObj if (HasBase(options, GuiControlParameters.Prototype)) { diff --git a/Lib/Shared/Volantis.App/GuiControl/StatusIndicatorControl.ahk b/Lib/Shared/Volantis.App/GuiControl/StatusIndicatorControl.ahk index bec24d81..a3aebcef 100644 --- a/Lib/Shared/Volantis.App/GuiControl/StatusIndicatorControl.ahk +++ b/Lib/Shared/Volantis.App/GuiControl/StatusIndicatorControl.ahk @@ -1,32 +1,42 @@ class StatusIndicatorControl extends GuiControlBase { - statusIndicatorW := 120 - statusIndicatorMinW := 120 + statusIndicatorW := 40 + statusIndicatorMinW := 40 + statusIndicatorExpandedMinW := 120 innerControl := "" + webService := "" - CreateControl(statusInfo, handler := "", statusStyle := "status") { + CreateControl(webService, handler := "") { super.CreateControl(false) + this.webService := webService if (handler == "" && HasMethod(this.guiObj, "OnStatusIndicatorClick")) { handler := "OnStatusIndicatorClick" } + this.statusIndicatorW := webService["StatusIndicatorExpanded"] ? this.statusIndicatorExpandedMinW : this.statusIndicatorMinW + options := this.parameters.SetDefaultOptions(this.parameters["options"].Clone(), [ "x+" . this.guiObj.margin, "yp", "w" . this.statusIndicatorW, "h26", - "vStatusIndicator" + "vStatusIndicator" . webService.Id ]) - - name := statusInfo && statusInfo.Has("name") ? statusInfo["name"] : "" - photo := statusInfo && statusInfo.Has("photo") ? statusInfo["photo"] : "" + + statusInfo := this.guiObj.GetStatusInfo(webService) + statusStyle := this.guiObj.StatusWindowIsOnline(webService) ? "status" : "statusOffline" + name := (statusInfo && statusInfo.Has("name") && webService["StatusIndicatorExpanded"]) ? statusInfo["name"] : "" + photo := (statusInfo && statusInfo.Has("photo")) ? statusInfo["photo"] : "" this.innerControl := this.guiObj.Add("ButtonControl", options, name, handler, statusStyle, Map("photo", photo)) this.ctl := this.innerControl.ctl return this.ctl } - UpdateStatusIndicator(statusInfo, statusStyle := "status") { + UpdateStatusIndicator() { + statusInfo := this.guiObj.GetStatusInfo(this.webService) + statusStyle := this.guiObj.StatusWindowIsOnline(this.webService) ? "status" : "statusOffline" + oldW := this.statusIndicatorW newW := this.CalculateWidth(statusInfo) this.statusIndicatorW := newW @@ -38,23 +48,24 @@ class StatusIndicatorControl extends GuiControlBase { this.ctl.Move(statusX - difference,, statusW + difference) } - name := statusInfo && statusInfo.Has("name") ? statusInfo["name"] : "" - photo := statusInfo && statusInfo.Has("photo") ? statusInfo["photo"] : "" + name := (statusInfo && statusInfo.Has("name") && this.webService["StatusIndicatorExpanded"]) ? statusInfo["name"] : "" + photo := (statusInfo && statusInfo.Has("photo")) ? statusInfo["photo"] : "" this.guiObj.themeObj.DrawButton(this.ctl, name, statusStyle, Map("photo", photo)) return difference } CalculateWidth(statusInfo) { - width := this.statusIndicatorMinW + expanded := this.webService["StatusIndicatorExpanded"] + width := expanded ? this.statusIndicatorExpandedMinW : this.statusIndicatorMinW requiredW := 10 if (statusInfo) { - if (statusInfo.Has("name")) { + if (statusInfo.Has("name") && expanded) { requiredW += this.guiObj.themeObj.CalculateTextWidth(statusInfo["name"]) } - if (StatusInfo.Has("photo")) { + if (StatusInfo.Has("photo") || !expanded) { requiredW += 26 } } diff --git a/Lib/Shared/Volantis.App/GuiControl/TitlebarControl.ahk b/Lib/Shared/Volantis.App/GuiControl/TitlebarControl.ahk index 44cadd03..872dccae 100644 --- a/Lib/Shared/Volantis.App/GuiControl/TitlebarControl.ahk +++ b/Lib/Shared/Volantis.App/GuiControl/TitlebarControl.ahk @@ -5,10 +5,12 @@ class TitlebarControl extends GuiControlBase { iconW := 16 titlebarH := 31 titlebarButtonW := 16 - initialStatusIndicatorW := 120 + initialStatusIndicatorW := 40 + initialStatusIndicatorExpandedW := 120 titleButton := "" titleText := "" statusIndicator := "" + statusIndicators := [] minBtn := "" maxBtn := "" unmaxBtn := "" @@ -33,9 +35,21 @@ class TitlebarControl extends GuiControlBase { buttonsW := 0 statusIndicatorW := this.guiObj.config["showStatusIndicator"] ? this.initialStatusIndicatorW : 0 + statusIndicatorExpandedW := this.guiObj.config["showStatusIndicator"] ? this.initialStatusIndicatorExpandedW : 0 + + serviceMgr := this.container["entity_manager.web_service"] + webServices := Map() if (this.guiObj.config["showStatusIndicator"]) { - buttonsW += statusIndicatorW + (this.guiObj.margin * 2) + webServices := serviceMgr.EntityQuery(EntityQuery.RESULT_TYPE_ENTITIES) + .Condition(IsTrueCondition(), "Enabled") + .Condition(IsTrueCondition(), "StatusIndicator") + .Execute() + + for serviceId, webService in webServices { + serviceStatusW := (webService["StatusIndicatorExpanded"]) ? statusIndicatorExpandedW : statusIndicatorW + buttonsW += serviceStatusW + (this.guiObj.margin * 2) + } } if (this.guiObj.config["showMinimize"]) { @@ -84,20 +98,19 @@ class TitlebarControl extends GuiControlBase { this.titleText := this.guiObj.guiObj.AddText(opts, titleText) } - if (this.guiObj.config["showStatusIndicator"]) { - opts := "x" . buttonsX . " y" . (this.topMargin - 5) . " w" . statusIndicatorW - statusStyle := this.guiObj.StatusWindowIsOnline() ? "status" : "statusOffline" - initialInfo := Map() - statusInfo := this.guiObj.GetStatusInfo() - - if (statusInfo) { - initialInfo := statusInfo.Clone() - initialInfo["name"] := "" + if (this.guiObj.config["showStatusIndicator"] && webServices.Count) { + for serviceId, service in webServices { + expanded := service["StatusIndicatorExpanded"] + serviceStatusW := expanded ? statusIndicatorExpandedW : statusIndicatorW + opts := "x" . buttonsX . " y" . (this.topMargin - 5) . " w" . serviceStatusW + + statusIndicator := this.guiObj.Add("StatusIndicatorControl", opts, "", service, "") + this.statusIndicators.Push(statusIndicator) + service.AddStatusIndicator(statusIndicator) + + difference := statusIndicator.UpdateStatusIndicator() + buttonsX += serviceStatusW + (this.guiObj.margin * 2) } - - this.statusIndicator := this.guiObj.Add("StatusIndicatorControl", opts, "", initialInfo, "", statusStyle) - difference := this.statusIndicator.UpdateStatusIndicator(statusInfo, statusStyle) - buttonsX += this.initialStatusIndicatorW + (this.guiObj.margin * 2) } handler := this.RegisterCallback("OnTitlebarButtonClick") @@ -198,7 +211,15 @@ class TitlebarControl extends GuiControlBase { this.guiObj.AutoXYWH("w", ["WindowTitlebar"]) if (this.guiObj.config["showStatusIndicator"]) { - this.guiObj.AutoXYWH("x*", ["StatusIndicator"]) + indicatorCtlNames := [] + + for index, statusIndicator in this.statusIndicators { + indicatorCtlNames.Push(statusIndicator.ctl.Name) + } + + if (indicatorCtlNames.Length) { + this.guiObj.AutoXYWH("x*", indicatorCtlNames) + } } if (this.guiObj.config["showClose"]) { From 9fd1e786f2655ee51cfba9a59c608e08ab908d5a Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 19:41:21 -0500 Subject: [PATCH 057/227] Remove player_name setting --- Launchpad.services.json | 1 - Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk | 5 +---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/Launchpad.services.json b/Launchpad.services.json index 233455d7..704774b4 100644 --- a/Launchpad.services.json +++ b/Launchpad.services.json @@ -21,7 +21,6 @@ "config.platforms_file": "@@{data_dir}\\Platforms.json", "config.platforms_view_mode": "Report", "config.tasks_view_mode": "Report", - "config.player_name": "", "config.rebuild_existing_launchers": false, "config.tasks_file": "@@{data_dir}\\Tasks.json", "config.use_advanced_launcher_editor": false, diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index 6d5fb68f..2307d117 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -259,12 +259,9 @@ class WebServiceEntity extends AppEntityBase { email := "" if (this.Authenticated) { - playerName := this.app.Config["player_name"] email := this.AuthData["email"] - if (playerName) { - statusText := playerName - } else if (email) { + if (email) { statusText := email } else { statusText := "Logged in" From b87c2c9bb826988b121540657e4060e88258c07a Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Mon, 12 Dec 2022 19:41:57 -0500 Subject: [PATCH 058/227] Move callbacks for status indicators into the StatusIndicator control and WebServiceEntity object --- .../Gui/Dialog/AccountInfoWindow.ahk | 51 ++++++++--------- Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk | 56 ------------------- .../WebServices/Entity/WebServiceEntity.ahk | 13 +++++ Lib/Shared/Volantis.App/Gui/GuiBase.ahk | 8 --- .../GuiControl/StatusIndicatorControl.ahk | 49 ++++++++++++++-- .../Service/ComponentManager/GuiManager.ahk | 2 +- 6 files changed, 82 insertions(+), 97 deletions(-) diff --git a/Lib/Launchpad/Gui/Dialog/AccountInfoWindow.ahk b/Lib/Launchpad/Gui/Dialog/AccountInfoWindow.ahk index cdf5ba42..942ca295 100644 --- a/Lib/Launchpad/Gui/Dialog/AccountInfoWindow.ahk +++ b/Lib/Launchpad/Gui/Dialog/AccountInfoWindow.ahk @@ -3,6 +3,7 @@ defaults := super.GetDefaultConfig(container, config) defaults["title"] := "Account Info" defaults["buttons"] := "*&Save|&Cancel|&Logout" + defaults["webService"] := "" return defaults } @@ -12,26 +13,20 @@ if (this.app.Services.Has("entity_manager.web_service")) { entityMgr := this.app.Services["entity_manager.web_service"] - if (entityMgr.Has("api") && entityMgr["api"]["Enabled"]) { - info := Map( - "name", "", - "email", "", - "photo", "" - ) - - ; @todo Pull this information from the API web service - - if (info) { - opts := "w" . this.windowSettings["contentWidth"] . " x" . this.margin . " y+" . this.margin - this.guiObj.AddPicture("x" . this.margin . " y+" . this.margin, info["photo"]) - this.guiObj.AddText(opts, "Email: " . info["email"]) - } + webService := this.config["webService"] + + if (!webService) { + throw AppException("Opened AccountInfoWindow without a webService specified.") } - } - this.AddHeading("Player Name") - this.AddEdit("PlayerName", this.app.Config["player_name"], "", 250) - this.guiObj.AddText("w" . this.windowSettings["contentWidth"], "Note: Player name is stored locally and not synced with your online Launchpad account yet.") + info := webService.GetStatusInfo() + + if (info) { + opts := "w" . this.windowSettings["contentWidth"] . " x" . this.margin . " y+" . this.margin + this.guiObj.AddPicture("x" . this.margin . " y+" . this.margin, info["photo"]) + this.guiObj.AddText(opts, "Email: " . info["email"]) + } + } position := "Wrap x" . this.margin . " y+" . this.margin options := position . " w" . this.windowSettings["contentWidth"] . " +0x200 c" . this.themeObj.GetColor("textLink") @@ -39,18 +34,18 @@ } ProcessResult(result, submittedData := "") { - - if (result == "Logout") { - if (this.app.Services.Has("entity_manager.web_service")) { - entityMgr := this.app.Services["entity_manager.web_service"] - - if (entityMgr.Has("api") && entityMgr["api"]["Enabled"]) { - entityMgr["api"].Logout() - } + webService := this.config["webService"] + + if (webService) { + webService.Logout() + } + } else if (result == "Login") { + webService := this.config["webService"] + + if (webService) { + webService.Login() } - } else if (result == "Save" && submittedData) { - this.app.Config["player_name"] := submittedData.PlayerName } return result diff --git a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk index 082f57dd..65108178 100644 --- a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk @@ -191,62 +191,6 @@ return webService } - GetStatusInfo(webService) { - info := "" - - if (webService) { - info := webService.GetStatusInfo() - } - - return info - } - - OnStatusIndicatorClick(btn, info) { - menuItems := [] - webService := this._getApiWebService() - - if (webService) { - if (webService.Authenticated) { - menuItems.Push(Map("label", "Account Details", "name", "AccountDetails")) - menuItems.Push(Map("label", "Logout", "name", "Logout")) - } else { - menuItems.Push(Map("label", "Login", "name", "Login")) - } - } - - result := this.container["manager.gui"].Menu(menuItems, this, btn) - - if (result == "AccountDetails") { - accountResult := this.container["manager.gui"].Dialog(Map( - "type", "AccountInfoWindow", - "ownerOrParent", this.guiId, - "child", true - )) - - if (accountResult == "OK") { - this.UpdateStatusIndicator(webService) - } - } else if (result == "Logout") { - if (webService) { - webService.Logout() - } - } else if (result == "Login") { - if (webService) { - webService.Login() - } - } - } - - StatusWindowIsOnline(webService) { - isOnline := false - - if (webService) { - isOnline := webService.Authenticated - } - - return isOnline - } - FormatDate(timestamp) { shortDate := FormatTime(timestamp, "ShortDate") shortTime := FormatTime(timestamp, "Time") diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index 2307d117..c55ae3ea 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -277,4 +277,17 @@ class WebServiceEntity extends AppEntityBase { return Map("name", statusText, "email", email, "photo", imgPath) } + + ShowAccountDetails() { + accountResult := this.container["manager.gui"].Dialog(Map( + "type", "AccountInfoWindow", + "ownerOrParent", this.guiId, + "child", true, + "webService", this + )) + + if (accountResult == "OK" || accountResult == "Logout" || accountResult == "Login") { + this.UpdateStatusIndicators() + } + } } diff --git a/Lib/Shared/Volantis.App/Gui/GuiBase.ahk b/Lib/Shared/Volantis.App/Gui/GuiBase.ahk index 8a89cabc..1c59feaf 100644 --- a/Lib/Shared/Volantis.App/Gui/GuiBase.ahk +++ b/Lib/Shared/Volantis.App/Gui/GuiBase.ahk @@ -355,14 +355,6 @@ class GuiBase { } } - StatusWindowIsOnline(webService) { - return false - } - - GetStatusInfo(webService) { - return Map("name", "", "photo", "") - } - SetFont(fontPreset := "normal", extraStyles := "", colorName := "text") { this.guiObj.SetFont() this.guiObj.SetFont("c" . this.themeObj.GetColor(colorName) . " " . this.themeObj.GetFont(fontPreset) . " " . extraStyles) diff --git a/Lib/Shared/Volantis.App/GuiControl/StatusIndicatorControl.ahk b/Lib/Shared/Volantis.App/GuiControl/StatusIndicatorControl.ahk index a3aebcef..12bceb51 100644 --- a/Lib/Shared/Volantis.App/GuiControl/StatusIndicatorControl.ahk +++ b/Lib/Shared/Volantis.App/GuiControl/StatusIndicatorControl.ahk @@ -9,6 +9,10 @@ class StatusIndicatorControl extends GuiControlBase { super.CreateControl(false) this.webService := webService + if (!handler) { + handler := ObjBindMethod(this, "OnStatusIndicatorClick") + } + if (handler == "" && HasMethod(this.guiObj, "OnStatusIndicatorClick")) { handler := "OnStatusIndicatorClick" } @@ -23,8 +27,8 @@ class StatusIndicatorControl extends GuiControlBase { "vStatusIndicator" . webService.Id ]) - statusInfo := this.guiObj.GetStatusInfo(webService) - statusStyle := this.guiObj.StatusWindowIsOnline(webService) ? "status" : "statusOffline" + statusInfo := webService.GetStatusInfo() + statusStyle := webService.Authenticated ? "status" : "statusOffline" name := (statusInfo && statusInfo.Has("name") && webService["StatusIndicatorExpanded"]) ? statusInfo["name"] : "" photo := (statusInfo && statusInfo.Has("photo")) ? statusInfo["photo"] : "" @@ -33,9 +37,46 @@ class StatusIndicatorControl extends GuiControlBase { return this.ctl } + OnStatusIndicatorClick(btn, info) { + webService := this.webService + menuItems := [] + + if (webService) { + if (webService.Authenticated) { + menuItems.Push(Map("label", "Account Details", "name", "AccountDetails")) + menuItems.Push(Map("label", "Logout", "name", "Logout")) + } else { + menuItems.Push(Map("label", "Login", "name", "Login")) + } + } + + result := this.container["manager.gui"].Menu(menuItems, this, btn) + + if (result == "AccountDetails") { + accountResult := this.container["manager.gui"].Dialog(Map( + "type", "AccountInfoWindow", + "webService", this.webService, + "ownerOrParent", this.guiObj.guiId, + "child", true + )) + + if (accountResult == "OK") { + this.UpdateStatusIndicator(webService) + } + } else if (result == "Logout") { + if (webService) { + webService.Logout() + } + } else if (result == "Login") { + if (webService) { + webService.Login() + } + } + } + UpdateStatusIndicator() { - statusInfo := this.guiObj.GetStatusInfo(this.webService) - statusStyle := this.guiObj.StatusWindowIsOnline(this.webService) ? "status" : "statusOffline" + statusInfo := this.webService.GetStatusInfo() + statusStyle := this.webService.Authenticated ? "status" : "statusOffline" oldW := this.statusIndicatorW newW := this.CalculateWidth(statusInfo) diff --git a/Lib/Shared/Volantis.App/Service/ComponentManager/GuiManager.ahk b/Lib/Shared/Volantis.App/Service/ComponentManager/GuiManager.ahk index cfbcfd9e..022e8688 100644 --- a/Lib/Shared/Volantis.App/Service/ComponentManager/GuiManager.ahk +++ b/Lib/Shared/Volantis.App/Service/ComponentManager/GuiManager.ahk @@ -278,7 +278,7 @@ class GuiManager extends ComponentManagerBase { guiObj := obj } else if (HasBase(obj, GuiBase.Prototype)) { guiObj := obj.guiObj - } else if (Type(guiObj) == "String" && this.Has(obj)) { + } else if (Type(obj) == "String" && this.Has(obj)) { guiObj := this[obj].guiObj } From dd9dba1bd05002d7a9e0c5278509eb22d404f71c Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Tue, 13 Dec 2022 02:57:29 -0500 Subject: [PATCH 059/227] Change "email" to "account" in authentication data so it is more generic --- Lib/Launchpad/Gui/Dialog/AccountInfoWindow.ahk | 2 +- .../Modules/LaunchpadApi/DataSource/ApiDataSource.ahk | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/Launchpad/Gui/Dialog/AccountInfoWindow.ahk b/Lib/Launchpad/Gui/Dialog/AccountInfoWindow.ahk index 942ca295..11299d17 100644 --- a/Lib/Launchpad/Gui/Dialog/AccountInfoWindow.ahk +++ b/Lib/Launchpad/Gui/Dialog/AccountInfoWindow.ahk @@ -24,7 +24,7 @@ if (info) { opts := "w" . this.windowSettings["contentWidth"] . " x" . this.margin . " y+" . this.margin this.guiObj.AddPicture("x" . this.margin . " y+" . this.margin, info["photo"]) - this.guiObj.AddText(opts, "Email: " . info["email"]) + this.guiObj.AddText(opts, "Account: " . info["account"]) } } diff --git a/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk b/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk index d8c4363b..479951ef 100644 --- a/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk +++ b/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk @@ -89,7 +89,7 @@ class ApiDataSource extends DataSourceBase { path := "status" statusExpire := 5 ;60 - status := Map("authenticated", false, "email", "", "photo", "") + status := Map("authenticated", false, "account", "", "photo", "") if (this.app.Config["api_authentication"]) { entityMgr := webService := this.app.Service("entity_manager.web_service") @@ -99,6 +99,11 @@ class ApiDataSource extends DataSourceBase { if (statusResult) { status := JsonData().FromString(&statusResult) + + if (status.Has("email")) { + status["account"] := status["email"] + status.Delete("email") + } } } } From 383d2e295ea77cf7766dcfbbd0de037c92bcc3b0 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Tue, 13 Dec 2022 03:42:58 -0500 Subject: [PATCH 060/227] Committing WebServiceLayerSource for posterity because I'm about to rewrite it --- .../LayerSource/WebServiceLayerSource.ahk | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 Lib/Shared/Modules/WebServices/LayerSource/WebServiceLayerSource.ahk diff --git a/Lib/Shared/Modules/WebServices/LayerSource/WebServiceLayerSource.ahk b/Lib/Shared/Modules/WebServices/LayerSource/WebServiceLayerSource.ahk new file mode 100644 index 00000000..37c4a943 --- /dev/null +++ b/Lib/Shared/Modules/WebServices/LayerSource/WebServiceLayerSource.ahk @@ -0,0 +1,59 @@ +class WebServiceLayerSource extends LayerSourceBase { + webService := "" + path := "" + useAuthentication := false + + ; A map in the format oldKey => newKey + fieldMap := Map() + + __New(webService, path, useAuthentication := false) { + this.webService := webService + this.path := path + this.useAuthentication := useAuthentication + } + + SaveData(data := "") { + ; Web services don't supoport saving layer data yet. + return this + } + + LoadData() { + request := this.webService.Request(this.path, "GET", "", this.useAuthentication, true) + response := request.Send() + responseBody := response.GetResponseBody() + + data := "" + + if (responseBody) { + data := JsonData().FromString(&responseBody) + } else { + data := Map() + } + + for oldKey, newKey in this.fieldMap { + if data.Has(oldKey) { + data[newKey] := data[oldKey] + data.Delete(oldKey) + } + } + + return data + } + + HasData() { + request := this.webService.Request(this.path, "HEAD", "", this.useAuthentication, false) + response := request.Send() + exists := response.GetHttpStatusCode() + + if (!exists) { + request.cacheObj.SetNotFound(this.path) + } + + return exists + } + + DeleteData() { + ; Web services don't support deleting layer data yet. + return this + } +} From f10500f36c9aa17e9dd679e7e7849a0a5ffea617 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Tue, 13 Dec 2022 04:23:23 -0500 Subject: [PATCH 061/227] Rename "api" services to "launchpad_api" --- Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk | 4 ++-- Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk | 4 ++-- .../LaunchpadApi/DataSource/ApiDataSource.ahk | 6 +++--- .../LaunchpadApi/LaunchpadApi.module.json | 20 +++++++++---------- .../ManageWindow/ManageWebServicesWindow.ahk | 2 +- 5 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk index 65108178..d5f1b6bc 100644 --- a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk @@ -183,8 +183,8 @@ if (this.app.Services.Has("entity_manager.web_service")) { entityMgr := this.app.Services["entity_manager.web_service"] - if (entityMgr.Has("api") && entityMgr["api"]["Enabled"]) { - webService := entityMgr["api"] + if (entityMgr.Has("launchpad_api") && entityMgr["launchpad_api"]["Enabled"]) { + webService := entityMgr["launchpad_api"] } } diff --git a/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk b/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk index 0f1b87c7..95b11baa 100644 --- a/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk +++ b/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk @@ -63,8 +63,8 @@ class LaunchpadBuilder extends AppBase { if (buildInfo.DeployToApi && this.Services.Has("entity_manager.web_service")) { entityMgr := this.Services["entity_manager.web_service"] - if (entityMgr.Has("api") && entityMgr["api"]["Enabled"]) { - entityMgr["api"].Login() + if (entityMgr.Has("launchpad_api") && entityMgr["launchpad_api"]["Enabled"]) { + entityMgr["launchpad_api"].Login() } } diff --git a/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk b/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk index 479951ef..83747962 100644 --- a/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk +++ b/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk @@ -38,8 +38,8 @@ class ApiDataSource extends DataSourceBase { if (this.app.Config["api_authentication"]) { entityMgr := webService := this.app.Service("entity_manager.web_service") - if (entityMgr.Has("api") && entityMgr["api"]["Enabled"]) { - webService := this.app.Service("entity_manager.web_service")["api"] + if (entityMgr.Has("launchpad_api") && entityMgr["launchpad_api"]["Enabled"]) { + webService := this.app.Service("entity_manager.web_service")["launchpad_api"] webService["Provider"]["Authenticator"].AlterRequest(webService, request) } @@ -94,7 +94,7 @@ class ApiDataSource extends DataSourceBase { if (this.app.Config["api_authentication"]) { entityMgr := webService := this.app.Service("entity_manager.web_service") - if (entityMgr.Has("api") && entityMgr["api"]["Enabled"] && entityMgr["api"]["Authenticated"]) { + if (entityMgr.Has("launchpad_api") && entityMgr["launchpad_api"]["Enabled"] && entityMgr["launchpad_api"]["Authenticated"]) { statusResult := this.ReadItem(path, true) if (statusResult) { diff --git a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json index bc9da04e..ad878fd5 100644 --- a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json +++ b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json @@ -14,23 +14,23 @@ "appVersion": "", "dependencies": ["WebServices"] }, + "parameters": { + "config.data_source_key": "launchpad_api", + "config.api_endpoint": "https://api.launchpad.games/v1", + "config.api_authentication": true + }, "services": { - "data_source.api": { + "data_source.launchpad_api": { "class": "ApiDataSource", - "arguments": ["@{App}", "@manager.cache", "api", "@@config.api_endpoint"] + "arguments": ["@{App}", "@manager.cache", "launchpad_api", "@@config.api_endpoint"] }, - "cache_state.api": { + "cache_state.launchpad_api": { "class": "CacheState", "arguments": ["@{App}", "@@config.cache_dir", "API.json"] }, - "cache.api": { + "cache.launchpad_api": { "class": "FileCache", - "arguments": ["@{App}", "@cache_state.api", "@@config.cache_dir", "API"] + "arguments": ["@{App}", "@cache_state.launchpad_api", "@@config.cache_dir", "API"] } - }, - "parameters": { - "config.data_source_key": "api", - "config.api_endpoint": "https://api.launchpad.games/v1", - "config.api_authentication": true } } diff --git a/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk b/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk index 83a36400..873a9596 100644 --- a/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk +++ b/Lib/Shared/Modules/WebServices/Gui/ManageWindow/ManageWebServicesWindow.ahk @@ -39,7 +39,7 @@ class ManageWebServicesWindow extends ManageEntitiesWindow { shouldShow := super._shouldShowButton(entityObj, buttonName) if (shouldShow && buttonName == "DeleteEntity") { - shouldShow := entityObj.Id != "api" + shouldShow := entityObj.Id != "launchpad_api" } return shouldShow From a882b8cfb224fb7759afe5397791466bdbee4a7f Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Tue, 13 Dec 2022 04:30:37 -0500 Subject: [PATCH 062/227] Add WebServiceAdapter entity types, the concept of web service data types, and move launchpad api definitions into the LaunchpadApi module --- Lib/Shared/Includes.ahk | 4 + .../LaunchpadApi/LaunchpadApi.module.json | 95 +++++++- .../WebServices/Entity/WebServiceEntity.ahk | 63 +++++- .../Factory/WebServiceAdapterFactory.ahk | 41 ++++ .../FileWebServiceAdapter.ahk | 5 + .../JsonWebServiceAdapter.ahk | 3 + .../WebServiceAdapterBase.ahk | 210 ++++++++++++++++++ .../WebServices/WebServices.module.json | 65 ++++-- 8 files changed, 459 insertions(+), 27 deletions(-) create mode 100644 Lib/Shared/Modules/WebServices/Factory/WebServiceAdapterFactory.ahk create mode 100644 Lib/Shared/Modules/WebServices/WebServiceAdapter/FileWebServiceAdapter.ahk create mode 100644 Lib/Shared/Modules/WebServices/WebServiceAdapter/JsonWebServiceAdapter.ahk create mode 100644 Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk diff --git a/Lib/Shared/Includes.ahk b/Lib/Shared/Includes.ahk index 129aa118..46f370e3 100644 --- a/Lib/Shared/Includes.ahk +++ b/Lib/Shared/Includes.ahk @@ -6,8 +6,12 @@ #Include Modules\WebServices\Event\WebServiceResponseEvent.ahk #Include Modules\WebServices\Events\WebServicesEvents.ahk #Include Modules\WebServices\EventSubscriber\WebServicesEventSubscriber.ahk +#Include Modules\WebServices\Factory\WebServiceDataAdapterFactory.ahk #Include Modules\WebServices\Gui\AuthenticationGui\LaunchpadLoginWindow.ahk #Include Modules\WebServices\Gui\ManageWindow\ManageWebServicesWindow.ahk +#Include Modules\WebServices\WebServiceAdapter\FileWebServiceAdapter.ahk +#Include Modules\WebServices\WebServiceAdapter\JsonWebServiceAdapter.ahk +#Include Modules\WebServices\WebServiceAdapter\WebServiceAdapterBase.ahk #Include Modules\WebServices\WebServiceAuthenticator\JwtWebServiceAuthenticator.ahk #Include Modules\WebServices\WebServiceAuthenticator\WebServiceAuthenticatorBase.ahk #Include Modules\WebServices\WebServiceRequest\BasicWebServiceRequest.ahk diff --git a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json index ad878fd5..81f1c228 100644 --- a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json +++ b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json @@ -17,7 +17,100 @@ "parameters": { "config.data_source_key": "launchpad_api", "config.api_endpoint": "https://api.launchpad.games/v1", - "config.api_authentication": true + "config.api_authentication": true, + "web_services.providers.launchpad_api": { + "name": "Launchpad API", + "EndpointUrl": "https://api.launchpad.games/v1", + "AuthenticationEndpointUrl": "https://securetoken.googleapis.com/v1", + "AuthenticationRefreshPath": "token", + "IconSrc": "logo", + "SupportsAuthentication": true, + "Authenticator": "jwt", + "AppKey": "AIzaSyCbwzOWJjTft77P96dV5VB3dAx9TjdDowQ", + "LoginWindow": "LaunchpadLoginWindow" + }, + "web_services.services.launchpad_api": { + "name": "Launchpad API", + "Provider": "launchpad_api" + }, + "web_services.adapters.launchpad_api.account_info": { + "dataType": "account_info", + "requestPath": "/status", + "cacheResponse": false, + "readAuth": true, + "dataMap": { + "email": "account" + } + }, + "web_services.adapters.launchpad_api.error_submission": { + "dataType": "error_submission", + "requestPath": "/error-submissions/{submission}", + "cacheResponse": false, + "readAllow": false, + "createAllow": true, + "createAuth": false + }, + "web_services.adapters.launchpad_api.feedback_submission": { + "dataType": "feedback_submission", + "requestPath": "/feedback-submissions/{submission}", + "cacheResponse": false, + "readAllow": false, + "createAllow": true, + "createAuth": false + }, + "web_services.adapters.launchpad_api.game_submission": { + "dataType": "game_submission", + "requestPath": "/game-submissions/{submission}", + "cacheResponse": false, + "readAllow": false, + "createAllow": true, + "createAuth": false + }, + "web_services.adapters.launchpad_api.release_info": { + "dataType": "release_info", + "requestPath": "/release-info/{tag}", + "cacheMaxAge": 1800 + }, + "web_services.adapters.launchpad_api.platform_list": { + "dataType": "entity_list", + "requestPath": "/game-platforms", + "entityType": "platform" + }, + "web_services.adapters.launchpad_api.platform_data": { + "dataType": "entity_data", + "requestPath": "/game-platforms/{id}", + "entityType": "platform" + }, + "web_services.adapters.launchpad_api.game_type_list": { + "dataType": "entity_list", + "requestPath": "/game-types", + "entityType": "managed_game" + }, + "web_services.adapters.launchpad_api.game_type_data": { + "dataType": "entity_data", + "requestPath": "/game-types/{id}", + "entityType": "managed_game" + }, + "web_services.adapters.launchpad_api.launcher_list": { + "dataType": "entity_list", + "requestPath": "/games", + "entityType": "launcher" + }, + "web_services.adapters.launchpad_api.launcher_data": { + "dataType": "entity_data", + "requestPath": "/games/{id}", + "entityType": "launcher" + }, + "web_services.adapters.launchpad_api.launcher_type_list": { + "dataType": "entity_list", + "requestPath": "/launcher-types", + "entityType": "managed_launcher" + }, + "web_services.adapters.launchpad_api.launcher_type_data": { + "dataType": "entity_data", + "requestPath": "/launcher-types/{id}", + "entityType": "managed_launcher" + } }, "services": { "data_source.launchpad_api": { diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index c55ae3ea..530c0603 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -4,6 +4,8 @@ class WebServiceEntity extends AppEntityBase { persistentStateObj := "" mergeDataFromApi := false statusIndicators := [] + adapters := Map() + adapterFactory := "" Authenticated { get => this.IsAuthenticated() @@ -18,10 +20,11 @@ class WebServiceEntity extends AppEntityBase { set => this.SetAuthData(key, value) } - __New(app, id, entityTypeId, container, cacheObj, stateObj, persistentStateObj, eventMgr, storageObj, idSanitizer, parentEntity := "") { + __New(app, id, entityTypeId, container, adapterFactory, cacheObj, stateObj, persistentStateObj, eventMgr, storageObj, idSanitizer, parentEntity := "") { this.cacheObj := cacheObj this.stateObj := stateObj this.persistentStateObj := persistentStateObj + this.adapterFactory := adapterFactory super.__New(app, id, entityTypeId, container, eventMgr, storageObj, idSanitizer, parentEntity) } @@ -34,6 +37,7 @@ class WebServiceEntity extends AppEntityBase { id, entityTypeId, container, + container.Get("web_services.adapter_factory"), container.Get("cache.web_services"), container.Get("state.web_services_tmp"), container.Get("state.web_services"), @@ -44,10 +48,59 @@ class WebServiceEntity extends AppEntityBase { ) } + GetAdapters(filters := "") { + if (!filters) { + filters := Map() + } + + adapterData := this.container.GetParameter("web_services.adapters." . this["Provider"]["id"]) + + adapters := Map() + + for key, definition in adapterData { + adapter := this.GetAdapter(key) + definition := adapter.definition + include := true + + for filterKey, filterVal in filters { + if (!definition.Has(filterKey) || definition[filterKey] != filterVal) { + include := false + + break + } + } + + if (include) { + adapters[key] := adapter + } + } + + return adapters + } + + GetAdapter(key) { + adapter := "" + + if (this.adapters.Has(key)) { + adapter := this.adapters[key] + } + + if (!adapter) { + param := "web_services.adapters." . this["Provider"]["id"] + + if (this.container.HasParameter(param)) { + adapter := this.adapterFactory.CreateWebServiceAdapter(this, this.container.GetParameter(param)) + this.adapters[key] := adapter + } + } + + return adapter + } + BaseFieldDefinitions() { definitions := super.BaseFieldDefinitions() - if (this.idVal == "api" && definitions.Has("name")) { + if (this.idVal == "launchpad_api" && definitions.Has("name")) { definitions["name"]["editable"] := false } @@ -62,7 +115,7 @@ class WebServiceEntity extends AppEntityBase { "type", "boolean", "description", "Automatically authenticate with this service when Launchpad starts.", "required", false, - "default", (this.idVal == "api") + "default", (this.idVal == "launchpad_api") ) definitions["Enabled"] := Map( @@ -74,13 +127,13 @@ class WebServiceEntity extends AppEntityBase { definitions["StatusIndicator"] := Map( "type", "boolean", "required", false, - "default", (this.idVal == "api") + "default", (this.idVal == "launchpad_api") ) definitions["StatusIndicatorExpanded"] := Map( "type", "boolean", "required", false, - "default", (this.idVal == "api") + "default", (this.idVal == "launchpad_api") ) return definitions diff --git a/Lib/Shared/Modules/WebServices/Factory/WebServiceAdapterFactory.ahk b/Lib/Shared/Modules/WebServices/Factory/WebServiceAdapterFactory.ahk new file mode 100644 index 00000000..d1c423a4 --- /dev/null +++ b/Lib/Shared/Modules/WebServices/Factory/WebServiceAdapterFactory.ahk @@ -0,0 +1,41 @@ +class WebServiceAdapterFactory { + container := "" + + __New(container) { + this.container := container + } + + CreateWebServiceAdapter(webService, definition) { + adapterTypes := this.container.GetParameter("web_services.adapter_types") + + if (!definition.Has("adapterType") || !definition["adapterType"]) { + definition["adapterType"] := "json" + } + + if (adapterTypes.Has(definition["adapterType"])) { + defaults := adapterTypes[definition["adapterType"]] + + if (Type(defaults) == "String") { + defaults := Map("class", defaults) + } + + for key, val in defaults { + if (!definition.Has(key)) { + definition[key] := val + } + } + } + + if (!definition.Has("class")) { + throw AppException("Adapter class not known.") + } + + adapterClass := definition["class"] + + if (!HasMethod(%adapterClass%)) { + throw AppException("Adapter class " . adapterClass . " was not found.") + } + + return %adapterClass%.Create(this.container, webService, definition) + } +} diff --git a/Lib/Shared/Modules/WebServices/WebServiceAdapter/FileWebServiceAdapter.ahk b/Lib/Shared/Modules/WebServices/WebServiceAdapter/FileWebServiceAdapter.ahk new file mode 100644 index 00000000..22d44841 --- /dev/null +++ b/Lib/Shared/Modules/WebServices/WebServiceAdapter/FileWebServiceAdapter.ahk @@ -0,0 +1,5 @@ +class FileWebServiceAdapter extends WebServiceAdapterBase { + dataType := "" + + ; @todo Implement file downloading +} diff --git a/Lib/Shared/Modules/WebServices/WebServiceAdapter/JsonWebServiceAdapter.ahk b/Lib/Shared/Modules/WebServices/WebServiceAdapter/JsonWebServiceAdapter.ahk new file mode 100644 index 00000000..7c61899b --- /dev/null +++ b/Lib/Shared/Modules/WebServices/WebServiceAdapter/JsonWebServiceAdapter.ahk @@ -0,0 +1,3 @@ +class JsonWebServiceAdapter extends WebServiceAdapterBase { + dataType := "JsonData" +} \ No newline at end of file diff --git a/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk b/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk new file mode 100644 index 00000000..a7b3e7d6 --- /dev/null +++ b/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk @@ -0,0 +1,210 @@ +class WebServiceAdapterBase { + container := "" + webService := "" + definition := "" + dataType := "" + merger := "" + + __New(container, merger, webService, definition) { + this.container := container + this.merger := merger + this.webService := webService + + if (!definition) { + definition := Map() + } + + if (!HasBase(definition, Map.Prototype)) { + throw AppException("Definition must be a Map-like object.") + } + + this.definition := this.merger.Merge(this.GetDefaultDefinition(), definition) + } + + static Create(container, webService, definition) { + className := this.Prototype.__Class + + return %className%( + container, + container.Get("merger.list"), + webService, + definition + ) + } + + GetDefaultDefinition() { + return Map( + "dataType", "", + "adapterType", "json", + "requestPath", "", + "requestData", "", + "cacheResponse", true, + "cacheMaxAge", 186400, + "createAllow", false, + "createMethod", "POST", + "createAuth", true, + "readAllow", true, + "readMethod", "GET", + "readAuth", false, + "updateAllow", false, + "updateMethod", "POST", + "updateAuth", true, + "deleteAllow", false, + "deleteMethod", "PUT", + "deleteAuth", true, + "dataMap", Map() + ) + } + + CreateData(data, params := "") { + if (!this.definition["createAllow"]) { + throw AppException("The 'create' operation is not allowed on this data adapter.") + } + + return this._request( + params, + this.definition["createMethod"], + data ? data : this._getData(params), + this.definition["createAuth"], + false + ).Send().IsSuccessful() + } + + DataExists(params := "") { + if (!this.definition["readAllow"]) { + throw AppException("The 'read' operation is not allowed on this data adapter.") + } + + return this._request( + params, + this.definition["readMethod"], + this._getData(params), + this.definition["readAuth"], + false + ).Send().IsSuccessful() + } + + ReadData(params := "") { + if (!this.definition["readAllow"]) { + throw AppException("The 'read' operation is not allowed on this data adapter.") + } + + response := this._request( + params, + this.definition["readMethod"], + this._getData(params), + this.definition["readAuth"], + this.definition["cacheResponse"] + ).Send() + + data := "" + + if (response.IsSuccessful()) { + data := response.GetResponseBody() + data := this._parseData(data, params) + this._mapData(data, params) + } + + return data + } + + UpdateData(data, params := "") { + if (!this.definition["updateAllow"]) { + throw AppException("The 'update' operation is not allowed on this data adapter.") + } + + return this._request( + params, + this.definition["updateMethod"], + data ? data : this._getData(params), + this.definition["updateAuth"], + false + ).Send().IsSuccessful() + } + + DeleteData(params := "") { + if (!this.definition["deleteAllow"]) { + throw AppException("The 'delete' operation is not allowed on this data adapter.") + } + + return this._request( + params, + this.definition["deleteMethod"], + this._getData(params), + this.definition["deleteAuth"], + false + ).Send().IsSuccessful() + } + + _requestPath(params) { + requestPath := this.definition["requestPath"] + isFound := true + + while isFound { + match := "" + isFound := RegExMatch(requestPath, "({([^}]+)})", &match) + + if (isFound) { + key := match[2] + + replacement := (params && params.Has(key)) + ? params[key] + : "" + + requestPath := StrReplace(requestPath, match[1], replacement) + } + } + + return requestPath + } + + _request(params, method, data, cacheResponse) { + return this.webService.Request( + this.definition["requestPath"], + method, + data, + this.definition["useAuthentication"], + cacheResponse + ) + } + + _mapData(data, params, reverse := false) { + if ( + data + && HasBase(data, Map.Prototype) + && this.definition["dataMap"] + && HasBase(this.definition["dataMap"], Map.Prototype) + ) { + for key1, key2 in this.definition["dataMap"] { + oldKey := reverse ? key2 : key1 + newKey := reverse ? key1 : key2 + + if (data.Has(oldKey)) { + data[newKey] := data[oldKey] + data.Delete(oldKey) + } + } + } + } + + _parseData(data, params) { + if (data && this.dataType) { + dataType := this.dataType + data := %dataType%().FromString(data) + } + + return data + } + + _getData(params, data := "") { + if (!data) { + if (params.Has("data") && params["data"]) { + data := params["data"] + } else if (this.definition["requestData"]) { + data := this.definition["requestData"] + } + } + + return data + } +} diff --git a/Lib/Shared/Modules/WebServices/WebServices.module.json b/Lib/Shared/Modules/WebServices/WebServices.module.json index 74985cb7..ec745348 100644 --- a/Lib/Shared/Modules/WebServices/WebServices.module.json +++ b/Lib/Shared/Modules/WebServices/WebServices.module.json @@ -42,30 +42,54 @@ "definition_loader_parameter_key": "web_services.providers", "storage_type": "runtime" }, - "web_services.providers.launchpad_api": { - "name": "Launchpad API", - "EndpointUrl": "https://api.launchpad.games/v1", - "AuthenticationEndpointUrl": "https://securetoken.googleapis.com/v1", - "AuthenticationRefreshPath": "token", - "IconSrc": "logo", - "SupportsAuthentication": true, - "Authenticator": "jwt", - "AppKey": "AIzaSyCbwzOWJjTft77P96dV5VB3dAx9TjdDowQ", - "LoginWindow": "LaunchpadLoginWindow" - }, - "web_services.services.api": { - "name": "Launchpad API", - "Provider": "launchpad_api" + "web_services.data_types.account_info": { + "name": "Account Info", + "description": "Account information related to an authentication session." + }, + "web_services.data_types.error_submission": { + "name": "Error Submission", + "description": "An endpoint to submit application errors to." + }, + "web_services.data_types.feedback_submission": { + "name": "Feedback Submission", + "description": "An endpoint to submit feedback to." + }, + "web_services.data_types.game_submission": { + "name": "Game Submission", + "description": "An endpoint to game launcher data to for sharing with the community." + }, + "web_services.data_types.release_info": { + "name": "Release Info", + "description": "Information about the latest (or any) version of Launchpad" + }, + "web_services.data_types.entity_list": { + "name": "Entity List", + "description": "A listing of entities from a web service." + }, + "web_services.data_types.entity_data": { + "name": "Entity Data", + "description": "Data to be imported into an entity within the application." + }, + "web_services.adapter_types.json": { + "class": "JsonWebServiceAdapter" + }, + "web_services.adapter_types.file": { + "class": "FileWebServiceAdapter" } }, "services": { + "cache.web_services": { + "class": "FileCache", + "arguments": ["@{App}", "@cache_state.web_services", "@@config.cache_dir", "WebServices"] + }, "cache_state.web_services": { "class": "CacheState", "arguments": ["@{App}", "@@config.cache_dir", "WebServices.json"] }, - "cache.web_services": { - "class": "FileCache", - "arguments": ["@{App}", "@cache_state.web_services", "@@config.cache_dir", "WebServices"] + "event_subscriber.web_services": { + "class": "WebServicesEventSubscriber", + "arguments": ["@{}"], + "tags": ["event_subscriber"] }, "state.web_services": { "class": "JsonState", @@ -79,10 +103,9 @@ "class": "JwtWebServiceAuthenticator", "arguments": ["@manager.gui"] }, - "event_subscriber.web_services": { - "class": "WebServicesEventSubscriber", - "arguments": ["@{}"], - "tags": ["event_subscriber"] + "web_services.adapter_factory": { + "class": "WebServiceAdapterFactory", + "arguments": ["@{}"] } } } From 65af969a3bfbbee094fe7ef27aa59874dc640866 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Tue, 13 Dec 2022 04:30:51 -0500 Subject: [PATCH 063/227] Fix extra function in TabsControl --- Lib/Shared/Volantis.App/GuiControl/TabsControl.ahk | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/Shared/Volantis.App/GuiControl/TabsControl.ahk b/Lib/Shared/Volantis.App/GuiControl/TabsControl.ahk index 3b6a1e62..591a2a05 100644 --- a/Lib/Shared/Volantis.App/GuiControl/TabsControl.ahk +++ b/Lib/Shared/Volantis.App/GuiControl/TabsControl.ahk @@ -122,6 +122,5 @@ class TabsControl extends GuiControlBase { } this.guiObj.AutoXYWH("wh", [this.ctl.Name]) - this.ResizeColumns() } } From e33084f3924b3efef8f38ce24da09add409c083b Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Tue, 13 Dec 2022 04:31:34 -0500 Subject: [PATCH 064/227] Fix include --- Lib/Shared/Includes.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/Shared/Includes.ahk b/Lib/Shared/Includes.ahk index 46f370e3..28258306 100644 --- a/Lib/Shared/Includes.ahk +++ b/Lib/Shared/Includes.ahk @@ -6,7 +6,7 @@ #Include Modules\WebServices\Event\WebServiceResponseEvent.ahk #Include Modules\WebServices\Events\WebServicesEvents.ahk #Include Modules\WebServices\EventSubscriber\WebServicesEventSubscriber.ahk -#Include Modules\WebServices\Factory\WebServiceDataAdapterFactory.ahk +#Include Modules\WebServices\Factory\WebServiceAdapterFactory.ahk #Include Modules\WebServices\Gui\AuthenticationGui\LaunchpadLoginWindow.ahk #Include Modules\WebServices\Gui\ManageWindow\ManageWebServicesWindow.ahk #Include Modules\WebServices\WebServiceAdapter\FileWebServiceAdapter.ahk From 44759b2eed8d3b2a27624d7f0774ffde7d000c15 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Tue, 13 Dec 2022 04:32:07 -0500 Subject: [PATCH 065/227] Replace WebServiceLayerSource with WebServiceAdapterLayerSource to simplify the interface --- .../WebServiceAdapterLayerSource.ahk | 51 ++++++++++++++++ .../LayerSource/WebServiceLayerSource.ahk | 59 ------------------- 2 files changed, 51 insertions(+), 59 deletions(-) create mode 100644 Lib/Shared/Modules/WebServices/LayerSource/WebServiceAdapterLayerSource.ahk delete mode 100644 Lib/Shared/Modules/WebServices/LayerSource/WebServiceLayerSource.ahk diff --git a/Lib/Shared/Modules/WebServices/LayerSource/WebServiceAdapterLayerSource.ahk b/Lib/Shared/Modules/WebServices/LayerSource/WebServiceAdapterLayerSource.ahk new file mode 100644 index 00000000..c4dab738 --- /dev/null +++ b/Lib/Shared/Modules/WebServices/LayerSource/WebServiceAdapterLayerSource.ahk @@ -0,0 +1,51 @@ +class WebServiceAdapterLayerSource extends LayerSourceBase { + adapter := "" + params := "" + + __New(adapter, params := "") { + if (!params) { + params := Map() + } + + this.adapter := adapter + this.params := params + } + + SaveData(data := "") { + if (this.HasData()) { + if (this.adapter.definition["updateAllow"]) { + this.adapter.UpdateData(data, this.params) + } + } else if (this.adapter.definition["createAllow"]) { + this.adapter.CreateData(data, this.params) + } + + return this + } + + LoadData() { + data := "" + + if (this.adapter.definition["readAllow"]) { + data := this.adapter.ReadData(this.params) + } + + if (!data) { + data := Map() + } + + return data + } + + HasData() { + return this.adapter.DataExists(this.params) + } + + DeleteData() { + if (this.adapter.definitions["deleteAllow"]) { + this.adapter.DeleteData(this.params) + } + + return this + } +} diff --git a/Lib/Shared/Modules/WebServices/LayerSource/WebServiceLayerSource.ahk b/Lib/Shared/Modules/WebServices/LayerSource/WebServiceLayerSource.ahk deleted file mode 100644 index 37c4a943..00000000 --- a/Lib/Shared/Modules/WebServices/LayerSource/WebServiceLayerSource.ahk +++ /dev/null @@ -1,59 +0,0 @@ -class WebServiceLayerSource extends LayerSourceBase { - webService := "" - path := "" - useAuthentication := false - - ; A map in the format oldKey => newKey - fieldMap := Map() - - __New(webService, path, useAuthentication := false) { - this.webService := webService - this.path := path - this.useAuthentication := useAuthentication - } - - SaveData(data := "") { - ; Web services don't supoport saving layer data yet. - return this - } - - LoadData() { - request := this.webService.Request(this.path, "GET", "", this.useAuthentication, true) - response := request.Send() - responseBody := response.GetResponseBody() - - data := "" - - if (responseBody) { - data := JsonData().FromString(&responseBody) - } else { - data := Map() - } - - for oldKey, newKey in this.fieldMap { - if data.Has(oldKey) { - data[newKey] := data[oldKey] - data.Delete(oldKey) - } - } - - return data - } - - HasData() { - request := this.webService.Request(this.path, "HEAD", "", this.useAuthentication, false) - response := request.Send() - exists := response.GetHttpStatusCode() - - if (!exists) { - request.cacheObj.SetNotFound(this.path) - } - - return exists - } - - DeleteData() { - ; Web services don't support deleting layer data yet. - return this - } -} From 4f1bf74db5e376486c22cc2a26df98dc10d29b7e Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Tue, 13 Dec 2022 04:32:29 -0500 Subject: [PATCH 066/227] Missing change from last commit --- Lib/Shared/Includes.ahk | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/Shared/Includes.ahk b/Lib/Shared/Includes.ahk index 28258306..56c04e44 100644 --- a/Lib/Shared/Includes.ahk +++ b/Lib/Shared/Includes.ahk @@ -9,6 +9,7 @@ #Include Modules\WebServices\Factory\WebServiceAdapterFactory.ahk #Include Modules\WebServices\Gui\AuthenticationGui\LaunchpadLoginWindow.ahk #Include Modules\WebServices\Gui\ManageWindow\ManageWebServicesWindow.ahk +#Include Modules\WebServices\LayerSource\WebServiceAdapterLayerSource.ahk #Include Modules\WebServices\WebServiceAdapter\FileWebServiceAdapter.ahk #Include Modules\WebServices\WebServiceAdapter\JsonWebServiceAdapter.ahk #Include Modules\WebServices\WebServiceAdapter\WebServiceAdapterBase.ahk From 0cb1beda79a19396dcfdde42143786b4081d97d4 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Tue, 13 Dec 2022 04:36:43 -0500 Subject: [PATCH 067/227] Remove the concept of a "local" data source for now --- Launchpad.services.json | 6 +----- Lib/Launchpad/DataSource/LocalDataSource.ahk | 2 -- Lib/Launchpad/Includes.ahk | 1 - 3 files changed, 1 insertion(+), 8 deletions(-) delete mode 100644 Lib/Launchpad/DataSource/LocalDataSource.ahk diff --git a/Launchpad.services.json b/Launchpad.services.json index 704774b4..7871e55d 100644 --- a/Launchpad.services.json +++ b/Launchpad.services.json @@ -11,7 +11,7 @@ "config.clean_launchers_on_build": false, "config.clean_launchers_on_exit": true, "config.create_desktop_shortcuts": true, - "config.data_source_key": "local", + "config.data_source_key": "launchpad_api", "config.default_launcher_theme": "", "config.destination_dir": "@@{data_dir}\\Launchers", "config.launcher_double_click_action": "Edit", @@ -110,10 +110,6 @@ "class": "LaunchpadConfig", "arguments": ["@config_storage.app_config", "@{}", "@@config_key"] }, - "data_source.local": { - "class": "LocalDataSource", - "arguments": ["@manager.cache", "local"] - }, "ini_migrator": { "class": "LaunchpadIniMigrator", "arguments": ["@{App}", "@manager.gui"] diff --git a/Lib/Launchpad/DataSource/LocalDataSource.ahk b/Lib/Launchpad/DataSource/LocalDataSource.ahk deleted file mode 100644 index 7ac01421..00000000 --- a/Lib/Launchpad/DataSource/LocalDataSource.ahk +++ /dev/null @@ -1,2 +0,0 @@ -class LocalDataSource extends DataSourceBase { -} diff --git a/Lib/Launchpad/Includes.ahk b/Lib/Launchpad/Includes.ahk index cfacabc5..775a4966 100644 --- a/Lib/Launchpad/Includes.ahk +++ b/Lib/Launchpad/Includes.ahk @@ -21,7 +21,6 @@ #Include Config\LaunchpadConfig.ahk #Include Config\PlatformsConfig.ahk #Include ConfigMigrator\LaunchpadIniMigrator.ahk -#Include DataSource\LocalDataSource.ahk #Include DetectedGame\DetectedGame.ahk #Include Entity\LauncherEntity.ahk #Include Entity\ManagedEntityBase.ahk From a6f746c0b76d6c82f3bb27d862bbd4695d1891be Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Tue, 13 Dec 2022 14:27:40 -0500 Subject: [PATCH 068/227] Remove old api_endpoint config value --- Launchpad.services.json | 1 - Lib/Launchpad/Gui/Form/SettingsWindow.ahk | 12 ------- Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk | 1 - .../LaunchpadApi/DataSource/ApiDataSource.ahk | 35 ------------------- .../LaunchpadApi/LaunchpadApi.module.json | 3 +- .../Volantis.App/Gui/Form/FeedbackWindow.ahk | 2 -- 6 files changed, 1 insertion(+), 53 deletions(-) diff --git a/Launchpad.services.json b/Launchpad.services.json index 7871e55d..4323ef7f 100644 --- a/Launchpad.services.json +++ b/Launchpad.services.json @@ -2,7 +2,6 @@ "parameters": { "backups_config": {}, "config.api_authentication": false, - "config.api_endpoint": "", "config.assets_dir": "@@{data_dir}\\Launcher Assets", "config.auto_backup_config_files": true, "config.backups_to_keep": 5, diff --git a/Lib/Launchpad/Gui/Form/SettingsWindow.ahk b/Lib/Launchpad/Gui/Form/SettingsWindow.ahk index 5ffa8e5c..dd9e9941 100644 --- a/Lib/Launchpad/Gui/Form/SettingsWindow.ahk +++ b/Lib/Launchpad/Gui/Form/SettingsWindow.ahk @@ -122,8 +122,6 @@ ctl := this.guiObj.AddDDL("vlogging_level xs y+m Choose" . chosen . " w200 c" . this.themeObj.GetColor("editText"), this.container.Get("logger").GetLogLevels()) ctl.OnEvent("Change", "OnLoggingLevelChange") - this.AddConfigLocationBlock("API Endpoint", "api_endpoint") - this.AddHeading("API Settings") ctl := this.AddConfigCheckBox("Enable API login for enhanced functionality", "api_authentication") ctl.ctl.NeedsRestart := true @@ -244,16 +242,6 @@ } } - OnApiEndpointMenuClick(btn) { - if (btn == "ChangeApiEndpoint") { - this.app.Service("manager.data_source").GetDefaultDataSource().ChangeApiEndpoint("", "") - this.SetText("ApiEndpoint", this.app.Config["api_endpoint"], "Bold") - this.needsRestart := true - } else if (btn == "OpenApiEndpoint") { - this.app.Service("manager.data_source").GetDefaultDataSource().Open() - } - } - OnCacheDirMenuClick(btn) { if (btn == "ChangeCacheDir") { this.app.Service("manager.cache").ChangeCacheDir() diff --git a/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk b/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk index 95b11baa..36a1ec84 100644 --- a/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk +++ b/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk @@ -2,7 +2,6 @@ class LaunchpadBuilder extends AppBase { GetParameterDefinitions(config) { parameters := super.GetParameterDefinitions(config) parameters["config_path"] := this.appDir . "\Launchpad.build.json" - parameters["config.api_endpoint"] := "https://api.launchpad.games/v1" parameters["config.data_source_key"] := "" parameters["config.api_authentication"] := true parameters["config.dist_dir"] := this.appDir . "\Dist" diff --git a/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk b/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk index 83747962..5e14fd36 100644 --- a/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk +++ b/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk @@ -118,39 +118,4 @@ class ApiDataSource extends DataSourceBase { Open() { Run(this.endpointUrl) } - - ChangeApiEndpoint(existingEndpoint := "", owner := "", parent := "") { - if (existingEndpoint == "") { - existingEndpoint := this.app.Config["api_endpoint"] - } - - ownerOrParent := "" - - if (parent) { - ownerOrParent := parent - } else if (owner) { - ownerOrParent := owner - } - - apiEndpointUrl := this.app.Service("manager.gui").Dialog(Map( - "type", "SingleInputBox", - "title", "API Endpoint URL", - "text", "Enter the base URL of the API endpoint you would like Launchpad to connect to. Leave blank to revert to the default.", - "defaultValue", existingEndpoint, - "ownerOrParent", ownerOrParent, - "child", !!(parent) - )) - - if (apiEndpointUrl != existingEndpoint) { - this.app.Config["api_endpoint"] := apiEndpointUrl - apiEndpointUrl := this.app.Config["api_endpoint"] - - if (apiEndpointUrl != existingEndpoint) { - this.endpointUrl := apiEndpointUrl - this.cache.FlushCache() - } - } - - return apiEndpointUrl - } } diff --git a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json index 81f1c228..ba9386f9 100644 --- a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json +++ b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json @@ -16,7 +16,6 @@ }, "parameters": { "config.data_source_key": "launchpad_api", - "config.api_endpoint": "https://api.launchpad.games/v1", "config.api_authentication": true, "web_services.providers.launchpad_api": { "name": "Launchpad API", @@ -115,7 +114,7 @@ "services": { "data_source.launchpad_api": { "class": "ApiDataSource", - "arguments": ["@{App}", "@manager.cache", "launchpad_api", "@@config.api_endpoint"] + "arguments": ["@{App}", "@manager.cache", "launchpad_api", "https://api.launchpad.games/v1"] }, "cache_state.launchpad_api": { "class": "CacheState", diff --git a/Lib/Shared/Volantis.App/Gui/Form/FeedbackWindow.ahk b/Lib/Shared/Volantis.App/Gui/Form/FeedbackWindow.ahk index ba27dc1c..bcf5b290 100644 --- a/Lib/Shared/Volantis.App/Gui/Form/FeedbackWindow.ahk +++ b/Lib/Shared/Volantis.App/Gui/Form/FeedbackWindow.ahk @@ -1,11 +1,9 @@ class FeedbackWindow extends DialogBox { errorObj := "" notifierObj := "" - apiEndpointUrl := "" __New(container, themeObj, config) { this.notifierObj := container.Get("notifier").notifierObj - this.apiEndpointUrl := container.Get("config.app")["api_endpoint"] super.__New(container, themeObj, config) } From d4530c33ef6d2e842e59165cb40947eeb383e171 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Tue, 13 Dec 2022 14:28:26 -0500 Subject: [PATCH 069/227] Allow generically making requests to web service adapters based on filtering --- .../WebServices/Entity/WebServiceEntity.ahk | 32 ++++++++++++++++++- .../WebServiceAdapterBase.ahk | 32 +++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index 530c0603..dc6e135e 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -48,7 +48,33 @@ class WebServiceEntity extends AppEntityBase { ) } - GetAdapters(filters := "") { + AdapterRequest(params, adapterFilters, operation := "read", limit := false) { + if (!adapterFilters) { + adapterFilters := Map() + } + + if (!adapterFilters) { + adapterFilters := Map() + } + + if (Type(adapterFilters) == "String") { + adapterFilters := Map("adapterType", adapterFilters) + } + + results := Map() + + for adapterKey, adapter in this.GetAdapters(adapterFilters, operation) { + results[adapterKey] := adapter.SendRequest(operation, params) + + if (limit && results.Count >= limit) { + break + } + } + + return results + } + + GetAdapters(filters := "", operation := "") { if (!filters) { filters := Map() } @@ -70,6 +96,10 @@ class WebServiceEntity extends AppEntityBase { } } + if (include && operation) { + include := adapter.SupportsOperation(operation) + } + if (include) { adapters[key] := adapter } diff --git a/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk b/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk index a7b3e7d6..f7002611 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk @@ -4,6 +4,7 @@ class WebServiceAdapterBase { definition := "" dataType := "" merger := "" + operationTypes := ["create", "read", "update", "delete"] __New(container, merger, webService, definition) { this.container := container @@ -56,6 +57,37 @@ class WebServiceAdapterBase { ) } + SupportsOperation(operation) { + supported := false + + if (this.operationTypes.Contains(operation)) { + supported := this.definition[operation + "Allow"] + } + + return supported + } + + SendRequest(operation, params := "") { + if (!this.SupportsOperation(operation)) { + throw AppException("The '" . operation . "' operation is not supported by this data adapter.") + } + + result := "" + data := params.Has("data") ? params["data"] : "" + + if (operation == "create") { + result := this.CreateData(data, params) + } else if (operation == "read") { + result := this.ReadData(params) + } else if (operation == "update") { + result := this.UpdateData(data, params) + } else if (operation == "delete") { + result := this.DeleteData(params) + } + + return result + } + CreateData(data, params := "") { if (!this.definition["createAllow"]) { throw AppException("The 'create' operation is not allowed on this data adapter.") From 744bd9b4e7f4e372b8a0c89f12bbb2eac946eac4 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Tue, 13 Dec 2022 17:27:41 -0500 Subject: [PATCH 070/227] Move main menu definitions to the main app class, and use a series of events to allow other code to populate it. Additionally, refactor a number of attributes to be parameters and simplify calls to services and parameters --- Launchpad.ahk | 1 - Launchpad.services.json | 9 + Lib/Launchpad/App/Launchpad.ahk | 90 +++-- .../Builder/BuildFile/CopyableBuildFile.ahk | 2 +- .../Builder/BuildFile/GameAhkFile.ahk | 2 +- Lib/Launchpad/Builder/BuildFile/IconFile.ahk | 4 +- .../LauncherBuilderOpBase.ahk | 2 +- .../LauncherGameOp/LauncherGameOpBase.ahk | 6 +- .../BulkOperation/LoadOp/LoadEntitiesOp.ahk | 4 +- Lib/Launchpad/Entity/LauncherEntity.ahk | 4 +- Lib/Launchpad/Entity/ManagedEntityBase.ahk | 2 +- .../GamePlatform/GamePlatformBase.ahk | 4 +- Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk | 2 +- Lib/Launchpad/Gui/Form/ImportShortcutForm.ahk | 2 +- Lib/Launchpad/Gui/Form/LauncherWizard.ahk | 2 +- Lib/Launchpad/Gui/Form/SettingsWindow.ahk | 26 +- Lib/Launchpad/Gui/Form/SetupWindow.ahk | 10 +- .../Gui/ManageWindow/DetectedGamesWindow.ahk | 4 +- Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk | 102 +----- .../Gui/ManageWindow/ManageBackupsWindow.ahk | 2 +- .../Gui/ManageWindow/ManageModulesWindow.ahk | 4 +- .../Gui/ManageWindow/PlatformsWindow.ahk | 2 +- .../GamePlatform/BlizzardPlatform.ahk | 2 +- Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk | 8 +- .../AppBuilder/ChocoPkgBuilder.ahk | 2 +- .../BuildDeployer/ApiBuildDeployer.ahk | 2 +- .../App/LaunchpadLauncher.ahk | 6 +- .../Condition/SteamConditionBase.ahk | 2 +- Lib/LaunchpadLauncher/Game/BlizzardGame.ahk | 6 +- Lib/LaunchpadLauncher/Game/GameBase.ahk | 10 +- Lib/LaunchpadLauncher/Game/RiotGame.ahk | 2 +- Lib/Shared/Includes.ahk | 4 +- .../LaunchpadApi/DataSource/ApiDataSource.ahk | 6 +- .../WebServices/Entity/WebServiceEntity.ahk | 2 +- .../WebServicesEventSubscriber.ahk | 22 ++ .../WebServices}/Gui/Form/FeedbackWindow.ahk | 38 +- Lib/Shared/Volantis.App/App/AppBase.ahk | 332 +++++++++++++++--- Lib/Shared/Volantis.App/App/TestAppBase.ahk | 4 +- .../BulkOperation/BulkOperationBase.ahk | 8 +- .../BulkOperation/InstallOp/InstallOp.ahk | 2 +- .../Volantis.App/Entity/AppEntityBase.ahk | 4 +- .../Volantis.App/Entity/BackupEntity.ahk | 2 +- .../Volantis.App/Event/MenuItemsEvent.ahk | 16 + .../Volantis.App/Event/MenuResultEvent.ahk | 20 ++ Lib/Shared/Volantis.App/Events/Events.ahk | 8 + .../Gui/EntityEditor/EntityEditorBase.ahk | 2 +- Lib/Shared/Volantis.App/Gui/GuiBase.ahk | 20 +- .../Gui/ManageWindow/ManageEntitiesWindow.ahk | 2 +- Lib/Shared/Volantis.App/Gui/Menu/MenuGui.ahk | 2 +- .../Volantis.App/GuiControl/LocationBlock.ahk | 2 +- Lib/Shared/Volantis.App/State/StateBase.ahk | 2 +- .../Volantis.Entity/Entity/EntityBase.ahk | 2 +- Lib/TestLib/Test/AppTestBase.ahk | 2 +- Scripts/Build.ahk | 1 - 54 files changed, 545 insertions(+), 284 deletions(-) rename Lib/Shared/{Volantis.App => Modules/WebServices}/Gui/Form/FeedbackWindow.ahk (61%) create mode 100644 Lib/Shared/Volantis.App/Event/MenuItemsEvent.ahk create mode 100644 Lib/Shared/Volantis.App/Event/MenuResultEvent.ahk diff --git a/Launchpad.ahk b/Launchpad.ahk index 6e5d905f..d34cd5e1 100644 --- a/Launchpad.ahk +++ b/Launchpad.ahk @@ -13,7 +13,6 @@ appVersion := "{{VERSION}}" Launchpad(Map( "appName", "Launchpad", - "developer", "Volantis Development", "version", appVersion, "trayIcon", "Resources\Graphics\launchpad.ico", "console", true diff --git a/Launchpad.services.json b/Launchpad.services.json index 4323ef7f..f7c00607 100644 --- a/Launchpad.services.json +++ b/Launchpad.services.json @@ -1,5 +1,14 @@ { "parameters": { + "app.website_url": "https://launchpad.games", + "app.custom_tray_menu": true, + "app.developer": "Volantis Development", + "app.has_settings": true, + "app.settings_window": "SettingsWindow", + "app.supports_update_check": true, + "app.show_about_menu_item": true, + "app.about_window": "AboutWindow", + "app.show_website_menu_item": true, "backups_config": {}, "config.api_authentication": false, "config.assets_dir": "@@{data_dir}\\Launcher Assets", diff --git a/Lib/Launchpad/App/Launchpad.ahk b/Lib/Launchpad/App/Launchpad.ahk index b752ac6e..661c64f3 100644 --- a/Lib/Launchpad/App/Launchpad.ahk +++ b/Lib/Launchpad/App/Launchpad.ahk @@ -1,28 +1,26 @@ class Launchpad extends AppBase { - customTrayMenu := true detectGames := false - isSetup := false CheckForUpdates(notify := true) { updateAvailable := false - if (this.Version != "{{VERSION}}" && this.Service("manager.data_source").GetDefaultDataSource()) { - dataSource := this.Service("manager.data_source").GetDefaultDataSource() + if (this.Version != "{{VERSION}}" && this["manager.data_source"].GetDefaultDataSource()) { + dataSource := this["manager.data_source"].GetDefaultDataSource() releaseInfoStr := dataSource.ReadItem("release-info") if (releaseInfoStr) { data := JsonData() releaseInfo := data.FromString(&releaseInfoStr) - if (releaseInfo && releaseInfo["data"].Has("version") && releaseInfo["data"]["version"] && this.Service("version_checker").VersionIsOutdated(releaseInfo["data"]["version"], this.Version)) { + if (releaseInfo && releaseInfo["data"].Has("version") && releaseInfo["data"]["version"] && this["version_checker"].VersionIsOutdated(releaseInfo["data"]["version"], this.Version)) { updateAvailable := true - this.Service("manager.gui").Dialog(Map("type", "UpdateAvailableWindow"), releaseInfo) + this["manager.gui"].Dialog(Map("type", "UpdateAvailableWindow"), releaseInfo) } } } if (!updateAvailable && notify) { - this.Service("notifier").Info("You're running the latest version of Launchpad. Shiny!") + this["notifier"].Info("You're running the latest version of Launchpad. Shiny!") } } @@ -32,7 +30,7 @@ } InitializeApp(config) { - eventMgr := this.Service("manager.event") + eventMgr := this["manager.event"] eventMgr.Register(EntityEvents.ENTITY_CREATED, "LaunchpadEntityCreated", ObjBindMethod(this, "OnEntityCreated")) eventMgr.Register(EntityEvents.ENTITY_UPDATED, "LaunchpadEntityUpdated", ObjBindMethod(this, "OnEntityUpdated")) eventMgr.Register(EntityEvents.ENTITY_DELETED, "LaunchpadEntityDeleted", ObjBindMethod(this, "OnEntityDeleted")) @@ -112,28 +110,28 @@ super.RunApp(config) - this.Service("entity_manager.platform").LoadComponents() - this.Service("entity_manager.launcher").LoadComponents() - this.Service("entity_manager.backup").LoadComponents() + this["entity_manager.platform"].LoadComponents() + this["entity_manager.launcher"].LoadComponents() + this["entity_manager.backup"].LoadComponents() this.OpenApp() if (this.detectGames) { - this.Service("entity_manager.platform").DetectGames() + this["entity_manager.platform"].DetectGames() } } MigrateConfiguration() { - configFile := this.Parameter("previous_config_file") + configFile := this.Parameter["previous_config_file"] if (configFile && FileExist(configFile)) { - response := this.Service("manager.gui").Dialog(Map( + response := this["manager.gui"].Dialog(Map( "title", "Migrate settings?", "text", this.appName . " uses a new configuration file format, and has detected that you have a previous configuration file.`n`nWould you like to automatically migrate your settings?`n`nChoose Yes to migrate your previous configuration. Choose no to simply delete it and start from scratch." )) if (response == "Yes") { - this.Service("ini_migrator").Migrate(configFile, this.Config) + this["ini_migrator"].Migrate(configFile, this.Config) } else { FileDelete(configFile) } @@ -141,7 +139,7 @@ } InitialSetup(config) { - result := this.Service("manager.gui").Dialog(Map("type", "SetupWindow")) + result := this["manager.gui"].Dialog(Map("type", "SetupWindow")) if (result == "Exit") { this.ExitApp() @@ -149,19 +147,18 @@ this.detectGames := true } - this.isSetup := true super.InitialSetup(config) } UpdateStatusIndicators() { - if (this.Service("manager.gui").Has("MainWindow")) { + if (this["manager.gui"].Has("MainWindow")) { serviceMgr := this.container["entity_manager.web_service"] webServices := serviceMgr.EntityQuery(EntityQuery.RESULT_TYPE_ENTITIES) .Condition(IsTrueCondition(), "Enabled") .Condition(IsTrueCondition(), "StatusIndicator") .Execute() - windowObj := this.Service("manager.gui")["MainWindow"] + windowObj := this["manager.gui"]["MainWindow"] for serviceId, webService in webServices { windowObj.UpdateStatusIndicator(webService) @@ -171,33 +168,66 @@ ExitApp() { if (this.isSetup && this.Config["clean_launchers_on_exit"]) { - this.Service("manager.builder").CleanLaunchers() + this["manager.builder"].CleanLaunchers() } if (this.isSetup && this.Config["flush_cache_on_exit"]) { - this.Service("manager.cache").FlushCaches(false, false) + this["manager.cache"].FlushCaches(false, false) } super.ExitApp() } - OpenWebsite() { - Run("https://launchpad.games") + RestartApp() { + if (this.Services.Has("manager.gui")) { + guiMgr := this["manager.gui"] + + if (guiMgr.Has("MainWindow")) { + guiMgr.StoreWindowState(this["manager.gui"]["MainWindow"]) + } + } + + super.RestartApp() } - ProvideFeedback() { - this.Service("manager.gui").Dialog(Map("type", "FeedbackWindow")) + AddMainMenuEarlyItems(menuItems, showOpenItem := false) { + menuItems := super.AddMainMenuEarlyItems(menuItems, showOpenItem) + + launchersItems := [] + launchersItems.Push(Map("label", "&Clean Launchers", "name", "CleanLaunchers")) + launchersItems.Push(Map("label", "&Reload Launchers", "name", "ReloadLaunchers")) + + menuItems.Push(Map("label", "&Launchers", "name", "LaunchersMenu", "childItems", launchersItems)) + + return menuItems } - RestartApp() { - if (this.Services.Has("manager.gui")) { - guiMgr := this.Service("manager.gui") + HandleMainMenuClick(result) { + if (result == "CleanLaunchers") { + this["manager.builder"].CleanLaunchers() + } else if (result == "ReloadLaunchers") { + this["entity_manager.launcher"].LoadComponents(true) + guiMgr := this["manager.gui"] if (guiMgr.Has("MainWindow")) { - guiMgr.StoreWindowState(this.Service("manager.gui")["MainWindow"]) + guiMgr["MainWindow"].UpdateListView() } + } else if (result == "ManageModules") { + this["manager.gui"].OpenWindow("ManageModulesWindow") + } else if (result == "FlushCache") { + this["manager.cache"].FlushCaches(true, true) + } else { + super.HandleMainMenuClick(result) } - super.RestartApp() + return result + } + + GetToolsMenuItems() { + toolsItems := super.GetToolsMenuItems() + toolsItems.Push(Map("label", "&Modules", "name", "ManageModules")) + toolsItems.Push(Map("label", "&Flush Cache", "name", "FlushCache")) + + return toolsItems } } diff --git a/Lib/Launchpad/Builder/BuildFile/CopyableBuildFile.ahk b/Lib/Launchpad/Builder/BuildFile/CopyableBuildFile.ahk index 2bd1146e..aaa77f98 100644 --- a/Lib/Launchpad/Builder/BuildFile/CopyableBuildFile.ahk +++ b/Lib/Launchpad/Builder/BuildFile/CopyableBuildFile.ahk @@ -56,7 +56,7 @@ class CopyableBuildFile extends BuildFileBase { filePath := FileSelect(1,, this.launcherEntityObj.Id . ": " . this.RequestMessage, this.SelectFilter) if (filePath == "") { - this.app.Service("notifier").Warning("No file selected. Skipping build file.") + this.app["notifier"].Warning("No file selected. Skipping build file.") } return filePath diff --git a/Lib/Launchpad/Builder/BuildFile/GameAhkFile.ahk b/Lib/Launchpad/Builder/BuildFile/GameAhkFile.ahk index ed0eaedc..230f51e6 100644 --- a/Lib/Launchpad/Builder/BuildFile/GameAhkFile.ahk +++ b/Lib/Launchpad/Builder/BuildFile/GameAhkFile.ahk @@ -35,7 +35,7 @@ class GameAhkFile extends ComposableBuildFile { GetPlatforms() { platforms := Map() - for key, platform in this.app.Service("entity_manager.platform").GetActivePlatforms(EntityQuery.RESULT_TYPE_ENTITIES) { + for key, platform in this.app["entity_manager.platform"].GetActivePlatforms(EntityQuery.RESULT_TYPE_ENTITIES) { platforms[key] := platform.FieldData } diff --git a/Lib/Launchpad/Builder/BuildFile/IconFile.ahk b/Lib/Launchpad/Builder/BuildFile/IconFile.ahk index 333333a4..729d4c01 100644 --- a/Lib/Launchpad/Builder/BuildFile/IconFile.ahk +++ b/Lib/Launchpad/Builder/BuildFile/IconFile.ahk @@ -53,7 +53,7 @@ class IconFile extends CopyableBuildFile { } if (iconsCount == 0) { - this.app.Service("notifier").Warning("No icons could be extracted from %exeFile%. Please try another file.") + this.app["notifier"].Warning("No icons could be extracted from %exeFile%. Please try another file.") iconFilePath := "" this.Cleanup() } else { @@ -61,7 +61,7 @@ class IconFile extends CopyableBuildFile { iconFilePath := FileSelect(, iconsDir, "Select the correct icon from the extracted files", "Icons (*.ico)") if (iconFilePath == "") { - this.app.Service("notifier").Warning("Canceled icon selection. Please try again.") + this.app["notifier"].Warning("Canceled icon selection. Please try again.") this.Cleanup() } } diff --git a/Lib/Launchpad/BulkOperation/LauncherBuilderOp/LauncherBuilderOpBase.ahk b/Lib/Launchpad/BulkOperation/LauncherBuilderOp/LauncherBuilderOpBase.ahk index f1194602..f9f976b3 100644 --- a/Lib/Launchpad/BulkOperation/LauncherBuilderOp/LauncherBuilderOpBase.ahk +++ b/Lib/Launchpad/BulkOperation/LauncherBuilderOp/LauncherBuilderOpBase.ahk @@ -11,7 +11,7 @@ class LauncherBuilderOpBase extends LauncherGameOpBase { } if (Type(builder) == "String" && builder != "") { - builder := app.Service("manager.builder")[builder] + builder := app["manager.builder"][builder] } InvalidParameterException.CheckTypes("LauncherBuilderOpBase", "builder", builder, "BuilderBase") diff --git a/Lib/Launchpad/BulkOperation/LauncherGameOp/LauncherGameOpBase.ahk b/Lib/Launchpad/BulkOperation/LauncherGameOp/LauncherGameOpBase.ahk index 558ee665..f9193b3d 100644 --- a/Lib/Launchpad/BulkOperation/LauncherGameOp/LauncherGameOpBase.ahk +++ b/Lib/Launchpad/BulkOperation/LauncherGameOp/LauncherGameOpBase.ahk @@ -14,7 +14,7 @@ class LauncherGameOpBase extends BulkOperationBase { itemFailedText := "Failed." __New(app, launcherEntities := "", owner := "") { - this.launcherManager := app.Service("entity_manager.launcher") + this.launcherManager := app["entity_manager.launcher"] if (launcherEntities == "") { launcherEntities := this.launcherManager.All() @@ -63,12 +63,12 @@ class LauncherGameOpBase extends BulkOperationBase { VerifyRequirements() { if (this.app.Config["destination_dir"] == "") { - this.app.Service("notifier").Error("Launcher directory is not set.") + this.app["notifier"].Error("Launcher directory is not set.") return false } if (this.app.Config["assets_dir"] == "") { - this.app.Service("notifier").Error("Assets directory is not set.") + this.app["notifier"].Error("Assets directory is not set.") return false } diff --git a/Lib/Launchpad/BulkOperation/LoadOp/LoadEntitiesOp.ahk b/Lib/Launchpad/BulkOperation/LoadOp/LoadEntitiesOp.ahk index fab21b04..c51002ac 100644 --- a/Lib/Launchpad/BulkOperation/LoadOp/LoadEntitiesOp.ahk +++ b/Lib/Launchpad/BulkOperation/LoadOp/LoadEntitiesOp.ahk @@ -10,7 +10,7 @@ class LoadEntitiesOp extends BulkOperationBase { failedMessage := "{n} launcher(s) could not be loaded due to errors." __New(app, launcherConfigObj := "", owner := "") { - this.launcherManager := app.Service("entity_manager.launcher") + this.launcherManager := app["entity_manager.launcher"] if (launcherConfigObj == "") { launcherConfigObj := this.launcherManager.GetConfig() @@ -29,7 +29,7 @@ class LoadEntitiesOp extends BulkOperationBase { } ; @todo replace this since EntityFactory is no longer used - factory := this.app.Service("EntityFactory") + factory := this.app["EntityFactory"] for key, config in this.launcherConfigObj { this.StartItem(key, key) diff --git a/Lib/Launchpad/Entity/LauncherEntity.ahk b/Lib/Launchpad/Entity/LauncherEntity.ahk index 1122a0ac..9477392c 100644 --- a/Lib/Launchpad/Entity/LauncherEntity.ahk +++ b/Lib/Launchpad/Entity/LauncherEntity.ahk @@ -320,7 +320,7 @@ class LauncherEntity extends AppEntityBase { if (filePath && FileExist(filePath)) { launcherVersion := FileGetVersion(this.GetLauncherFile(this.Id)) - if (launcherVersion && !this.app.Service("version_checker").VersionIsOutdated(this.app.Version, launcherVersion)) { + if (launcherVersion && !this.app["version_checker"].VersionIsOutdated(this.app.Version, launcherVersion)) { outdated := false } @@ -330,7 +330,7 @@ class LauncherEntity extends AppEntityBase { if (!buildInfo["Version"] || !buildInfo["Timestamp"]) { outdated := true } else { - if (configInfo["Version"] && this.app.Service("version_checker").VersionIsOutdated(configInfo["Version"], buildInfo["Version"])) { + if (configInfo["Version"] && this.app["version_checker"].VersionIsOutdated(configInfo["Version"], buildInfo["Version"])) { outdated := true } else if (configInfo["Timestamp"] && DateDiff(configInfo["Timestamp"], buildInfo["Timestamp"], "S") > 0) { outdated := true diff --git a/Lib/Launchpad/Entity/ManagedEntityBase.ahk b/Lib/Launchpad/Entity/ManagedEntityBase.ahk index 976c6489..99926db4 100644 --- a/Lib/Launchpad/Entity/ManagedEntityBase.ahk +++ b/Lib/Launchpad/Entity/ManagedEntityBase.ahk @@ -552,7 +552,7 @@ class ManagedEntityBase extends AppEntityBase { productCode := this.GetBlizzardProductKey() if (productCode != "" && this.app.Services.Has("BlizzardProductDb")) { - path := this.app.Service("BlizzardProductDb").GetProductInstallPath(productCode) + path := this.app["BlizzardProductDb"].GetProductInstallPath(productCode) } return path diff --git a/Lib/Launchpad/GamePlatform/GamePlatformBase.ahk b/Lib/Launchpad/GamePlatform/GamePlatformBase.ahk index d5b3f944..f9d674ef 100644 --- a/Lib/Launchpad/GamePlatform/GamePlatformBase.ahk +++ b/Lib/Launchpad/GamePlatform/GamePlatformBase.ahk @@ -87,7 +87,7 @@ class GamePlatformBase { } NeedsUpdate() { - return this.app.Service("version_checker").VersionIsOutdated(this.GetLatestVersion(), this.GetInstalledVersion()) + return this.app["version_checker"].VersionIsOutdated(this.GetLatestVersion(), this.GetInstalledVersion()) } GetInstalledVersion() { @@ -179,7 +179,7 @@ class GamePlatformBase { } DetermineMainExe(key, possibleExes) { - dataSource := this.app.Service("manager.data_source").GetDefaultDataSource() + dataSource := this.app["manager.data_source"].GetDefaultDataSource() dsData := this.GetDataSourceDefaults(dataSource, key) mainExe := "" diff --git a/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk b/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk index 04119844..3513534a 100644 --- a/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk +++ b/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk @@ -22,7 +22,7 @@ Create() { super.Create() - this.dataSource := this.app.Service("manager.data_source").GetDefaultDataSource() + this.dataSource := this.app["manager.data_source"].GetDefaultDataSource() this.knownPlatforms := this.dataSource.ReadListing("platforms") this.knownGames := this.dataSource.ReadListing("game-keys") this.launcherTypes := this.dataSource.ReadListing("launcher-types") diff --git a/Lib/Launchpad/Gui/Form/ImportShortcutForm.ahk b/Lib/Launchpad/Gui/Form/ImportShortcutForm.ahk index 82c7b008..de956383 100644 --- a/Lib/Launchpad/Gui/Form/ImportShortcutForm.ahk +++ b/Lib/Launchpad/Gui/Form/ImportShortcutForm.ahk @@ -29,7 +29,7 @@ GetLauncherConfig() { platformKey := Trim(this.guiObj["Platform"].Text) config := Map("Platform", platformKey) - platform := this.app.Service("entity_manager.platform")[platformKey] + platform := this.app["entity_manager.platform"][platformKey] if (platform) { config["LauncherType"] := platform.Platform.launcherType diff --git a/Lib/Launchpad/Gui/Form/LauncherWizard.ahk b/Lib/Launchpad/Gui/Form/LauncherWizard.ahk index 1f1889b7..97bcd11a 100644 --- a/Lib/Launchpad/Gui/Form/LauncherWizard.ahk +++ b/Lib/Launchpad/Gui/Form/LauncherWizard.ahk @@ -25,7 +25,7 @@ GetLauncherConfig() { platformKey := Trim(this.guiObj["Platform"].Text) config := Map("Platform", platformKey, "GameInstallDir", this.installDir, "GameExe", this.exe) - platform := this.app.Service("entity_manager.platform")[platformKey] + platform := this.app["entity_manager.platform"][platformKey] if (platform) { config["LauncherType"] := platform.Platform.launcherType diff --git a/Lib/Launchpad/Gui/Form/SettingsWindow.ahk b/Lib/Launchpad/Gui/Form/SettingsWindow.ahk index dd9e9941..605f639f 100644 --- a/Lib/Launchpad/Gui/Form/SettingsWindow.ahk +++ b/Lib/Launchpad/Gui/Form/SettingsWindow.ahk @@ -133,11 +133,11 @@ } OnManageBackups(btn, info) { - this.app.Service("entity_type.backup").OpenManageWindow() + this.app["entity_type.backup"].OpenManageWindow() } OnManagePlatforms(btn, info) { - this.app.Service("entity_type.platform").OpenManageWindow() + this.app["entity_type.platform"].OpenManageWindow() } AddConfigLocationBlock(heading, settingName, extraButton := "", helpText := "") { @@ -190,7 +190,7 @@ } else if (btn == "OpenLauncherFile") { this.app.Config.OpenLauncherFile() } else if (btn == "ReloadLauncherFile") { - this.app.Service("entity_manager.launcher").LoadComponents(true) + this.app["entity_manager.launcher"].LoadComponents(true) } } @@ -201,7 +201,7 @@ } else if (btn == "OpenBackupsFile") { this.app.Config.OpenBackupsFile() } else if (btn == "ReloadBackupsFile") { - this.app.Service("entity_manager.backup").LoadComponents(true) + this.app["entity_manager.backup"].LoadComponents(true) } } @@ -212,7 +212,7 @@ } else if (btn == "OpenPlatformsFile") { this.app.Config.OpenPlatformsFile() } else if (btn == "ReloadPlatformsFile") { - this.app.Service("entity_manager.platform").LoadComponents(true) + this.app["entity_manager.platform"].LoadComponents(true) } } @@ -244,22 +244,22 @@ OnCacheDirMenuClick(btn) { if (btn == "ChangeCacheDir") { - this.app.Service("manager.cache").ChangeCacheDir() + this.app["manager.cache"].ChangeCacheDir() this.SetText("CacheDir", this.app.Config["cache_dir"], "Bold") } else if (btn == "OpenCacheDir") { - this.app.Service("manager.cache").OpenCacheDir() + this.app["manager.cache"].OpenCacheDir() } else if (btn == "FlushCacheDir") { - this.app.Service("manager.cache").FlushCaches(true, true) + this.app["manager.cache"].FlushCaches(true, true) } } OnBackupDirMenuClick(btn) { if (btn == "ChangeBackupDir") { - this.app.Service("entity_manager.backup").ChangeBackupDir() + this.app["entity_manager.backup"].ChangeBackupDir() this.SetText("BackupDir", this.app.Config["backup_dir"], "Bold") this.needsRestart := true } else if (btn == "OpenBackupDir") { - this.app.Service("entity_manager.backup").OpenBackupDir() + this.app["entity_manager.backup"].OpenBackupDir() } } @@ -304,7 +304,7 @@ this.app.Config.SaveConfig() if (this.needsRestart) { - response := this.app.Service("manager.gui").Dialog(Map( + response := this.app["manager.gui"].Dialog(Map( "title", "Restart " . this.app.appName . "?", "text", "One or more settings that have been changed require restarting " . this.app.appName . " to fully take effect.`n`nWould you like to restart " . this.app.appName . " now?" )) @@ -314,8 +314,8 @@ } } - if (this.app.Service("manager.gui").Has("MainWindow")) { - this.app.Service("manager.gui")["MainWindow"].UpdateListView() + if (this.app["manager.gui"].Has("MainWindow")) { + this.app["manager.gui"]["MainWindow"].UpdateListView() } return result diff --git a/Lib/Launchpad/Gui/Form/SetupWindow.ahk b/Lib/Launchpad/Gui/Form/SetupWindow.ahk index 1ebd5784..0be2ca76 100644 --- a/Lib/Launchpad/Gui/Form/SetupWindow.ahk +++ b/Lib/Launchpad/Gui/Form/SetupWindow.ahk @@ -16,7 +16,7 @@ Create() { super.Create() - this.availableThemes := this.app.Service("manager.theme").GetAvailableThemes() + this.availableThemes := this.app["manager.theme"].GetAvailableThemes() } AddDescription(text) { @@ -58,7 +58,7 @@ } GetInstalledPlatforms() { - platformMgr := this.app.Service("entity_manager.platform") + platformMgr := this.app["entity_manager.platform"] platformQuery := platformMgr.EntityQuery(EntityQuery.RESULT_TYPE_ENTITIES) .Condition(IsTrueCondition(), "IsInstalled") return platformQuery.Execute() @@ -80,7 +80,7 @@ this.guiObj.Submit(false) len := StrLen("PlatformToggle") name := SubStr(chk.Name, len + 1) - platformMgr := this.app.Service("entity_manager.platform") + platformMgr := this.app["entity_manager.platform"] if (platformMgr.Has(name)) { platform := platformMgr[name] @@ -115,8 +115,8 @@ ProcessResult(result, submittedData := "") { if (result == "Start") { - this.app.Service("config.app").SaveConfig() - this.app.Service("entity_manager.platform").SaveModifiedEntities() + this.app["config.app"].SaveConfig() + this.app["entity_manager.platform"].SaveModifiedEntities() if (submittedData.DetectGames) { result := "Detect" diff --git a/Lib/Launchpad/Gui/ManageWindow/DetectedGamesWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/DetectedGamesWindow.ahk index 5d9aed4f..98f10890 100644 --- a/Lib/Launchpad/Gui/ManageWindow/DetectedGamesWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/DetectedGamesWindow.ahk @@ -200,7 +200,7 @@ detectedGameObj := this.detectedGames[key] - result := this.app.Service("manager.gui").Dialog(Map( + result := this.app["manager.gui"].Dialog(Map( "type", "DetectedGameEditor", "ownerOrParent", this.guiId, "child", true @@ -233,7 +233,7 @@ menuItems := [] menuItems.Push(Map("label", "Edit", "name", "EditDetectedGame")) - result := this.app.Service("manager.gui").Menu(menuItems, this) + result := this.app["manager.gui"].Menu(menuItems, this) if (result == "EditDetectedGame") { this.EditDetectedGame(key) diff --git a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk index d5f1b6bc..81221664 100644 --- a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk @@ -91,104 +91,12 @@ } } - _getToolsMenuEntityTypes() { - entityTypes := Map() - - for key, entityType in this.container["manager.entity_type"] { - if (entityType.definition["manager_link_in_tools_menu"]) { - entityTypes[key] := entityType - } - } - - return entityTypes - } - ShowTitleMenu() { - menuEntityTypes := this._getToolsMenuEntityTypes() - toolsItems := [] - - for key, entityType in menuEntityTypes { - menuLinkText := entityType.definition["manager_menu_link_text"] - - if (!menuLinkText) { - menuLinkText := "&" . entityType.definition["name_plural"] - } - - toolsItems.Push(Map("label", menuLinkText, "name", "manage_" . key)) - } - - toolsItems.Push(Map("label", "&Modules", "name", "ManageModules")) - toolsItems.Push(Map("label", "&Flush Cache", "name", "FlushCache")) - - launchersItems := [] - launchersItems.Push(Map("label", "&Clean Launchers", "name", "CleanLaunchers")) - launchersItems.Push(Map("label", "&Reload Launchers", "name", "ReloadLaunchers")) - - aboutItems := [] - aboutItems.Push(Map("label", "&About Launchpad", "name", "About")) - aboutItems.Push(Map("label", "&Open Website", "name", "OpenWebsite")) - - menuItems := [] - menuItems.Push(Map("label", "&Tools", "name", "ToolsMenu", "childItems", toolsItems)) - menuItems.Push(Map("label", "&Launchers", "name", "LaunchersMenu", "childItems", launchersItems)) - menuItems.Push("") - menuItems.Push(Map("label", "&About", "name", "About", "childItems", aboutItems)) - menuItems.Push("") - menuItems.Push(Map("label", "&Settings", "name", "Settings")) - menuItems.Push(Map("label", "Check for &Updates", "name", "CheckForUpdates")) - menuItems.Push("") - menuItems.Push(Map("label", "Provide &Feedback", "name", "ProvideFeedback")) - menuItems.Push("") - menuItems.Push(Map("label", "&Restart", "name", "Reload")) - menuItems.Push(Map("label", "E&xit", "name", "Exit")) - - result := this.container["manager.gui"].Menu(menuItems, this, this.guiObj["WindowTitleText"]) - - if (result == "ManageModules") { - this.container["manager.gui"].OpenWindow("ManageModulesWindow") - } else if (result == "FlushCache") { - this.container["manager.cache"].FlushCaches(true, true) - } else if (result == "CleanLaunchers") { - this.container["manager.builder"].CleanLaunchers() - } else if (result == "ReloadLaunchers") { - this.launcherManager.LoadComponents(true) - this.UpdateListView() - } else if (result == "About") { - this.container["manager.gui"].Dialog(Map("type", "AboutWindow")) - } else if (result == "OpenWebsite") { - this.app.OpenWebsite() - } else if (result == "ProvideFeedback") { - this.app.ProvideFeedback() - } else if (result == "Settings") { - this.container["manager.gui"].Dialog(Map("type", "SettingsWindow", "unique", false)) - } else if (result == "CheckForUpdates") { - this.app.CheckForUpdates() - } else if (result == "Reload") { - this.app.restartApp() - } else if (result == "Exit") { - this.app.ExitApp() - } else { - for key, entityType in menuEntityTypes { - if (result == "manage_" . key) { - this.container["entity_type." . key].OpenManageWindow() - break - } - } - } - } - - _getApiWebService() { - webService := "" - - if (this.app.Services.Has("entity_manager.web_service")) { - entityMgr := this.app.Services["entity_manager.web_service"] - - if (entityMgr.Has("launchpad_api") && entityMgr["launchpad_api"]["Enabled"]) { - webService := entityMgr["launchpad_api"] - } - } - - return webService + this.app.MainMenu( + this, + this.guiObj["WindowTitleText"], + false + ) } FormatDate(timestamp) { diff --git a/Lib/Launchpad/Gui/ManageWindow/ManageBackupsWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/ManageBackupsWindow.ahk index 3ceb36f4..be595c3e 100644 --- a/Lib/Launchpad/Gui/ManageWindow/ManageBackupsWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/ManageBackupsWindow.ahk @@ -109,7 +109,7 @@ class ManageBackupsWindow extends ManageWindowBase { menuItems.Push(Map("label", "Restore", "name", "RestoreBackup")) menuItems.Push(Map("label", "Delete", "name", "DeleteBackup")) - result := this.app.Service("manager.gui").Menu(menuItems, this) + result := this.app["manager.gui"].Menu(menuItems, this) if (result == "EditBackup") { this.EditBackup(key) diff --git a/Lib/Launchpad/Gui/ManageWindow/ManageModulesWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/ManageModulesWindow.ahk index ec0e10b3..7597c34b 100644 --- a/Lib/Launchpad/Gui/ManageWindow/ManageModulesWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/ManageModulesWindow.ahk @@ -84,7 +84,7 @@ class ManageModulesWindow extends ManageWindowBase { this.Submit(false) } - response := this.app.Service("manager.gui").Dialog(Map( + response := this.app["manager.gui"].Dialog(Map( "title", "Restart " . this.app.appName . "?", "text", "One or more module changes require restarting " . this.app.appName . " to fully take effect.`n`nWould you like to restart " . this.app.appName . " now?" )) @@ -153,7 +153,7 @@ class ManageModulesWindow extends ManageWindowBase { menuItems.Push(Map("label", "Delete", "name", "DeleteModule")) } - result := this.app.Service("manager.gui").Menu(menuItems, this) + result := this.app["manager.gui"].Menu(menuItems, this) if (result == "EnableModule") { this.EnableModule(key) diff --git a/Lib/Launchpad/Gui/ManageWindow/PlatformsWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/PlatformsWindow.ahk index 845423b6..b3b54fe7 100644 --- a/Lib/Launchpad/Gui/ManageWindow/PlatformsWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/PlatformsWindow.ahk @@ -115,7 +115,7 @@ class PlatformsWindow extends ManageWindowBase { menuItems.Push(Map("label", "Install", "name", "InstallPlatform")) } - result := this.app.Service("manager.gui").Menu(menuItems, this) + result := this.app["manager.gui"].Menu(menuItems, this) if (result == "EditPlatform") { this.EditPlatform(key) diff --git a/Lib/Launchpad/Modules/Blizzard/GamePlatform/BlizzardPlatform.ahk b/Lib/Launchpad/Modules/Blizzard/GamePlatform/BlizzardPlatform.ahk index 5d326086..51c997e3 100644 --- a/Lib/Launchpad/Modules/Blizzard/GamePlatform/BlizzardPlatform.ahk +++ b/Lib/Launchpad/Modules/Blizzard/GamePlatform/BlizzardPlatform.ahk @@ -22,7 +22,7 @@ class BlizzardPlatform extends RegistryLookupGamePlatformBase { productInstalls := [] if (this.app.Services.Has("BlizzardProductDb")) { - productInstalls := this.app.Service("BlizzardProductDb").GetProductInstalls() + productInstalls := this.app["BlizzardProductDb"].GetProductInstalls() } games := [] diff --git a/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk b/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk index 36a1ec84..13f7d8a8 100644 --- a/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk +++ b/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk @@ -49,8 +49,8 @@ class LaunchpadBuilder extends AppBase { RunApp(config) { super.RunApp(config) - version := this.Service("GitTagVersionIdentifier").IdentifyVersion() - buildInfo := this.Service("manager.gui").Dialog(Map( + version := this["GitTagVersionIdentifier"].IdentifyVersion() + buildInfo := this["manager.gui"].Dialog(Map( "type", "BuildSettingsForm", "version", version )) @@ -83,7 +83,7 @@ class LaunchpadBuilder extends AppBase { } if (buildInfo.DeployToGitHub || buildInfo.DeployToApi || buildInfo.DeployToChocolatey) { - releaseInfo := this.Service("manager.gui").Dialog(Map("type", "ReleaseInfoForm")) + releaseInfo := this["manager.gui"].Dialog(Map("type", "ReleaseInfoForm")) if (!releaseInfo) { this.ExitApp() @@ -148,7 +148,7 @@ class LaunchpadBuilder extends AppBase { if (!this.GetCmdOutput("git show-ref " . version)) { RunWait("git tag " . version, this.appDir) - response := this.Service("manager.gui").Dialog(Map( + response := this["manager.gui"].Dialog(Map( "title", "Push git tag?", "text", "Would you like to push the git tag that was just created (" . version . ") to origin?" )) diff --git a/Lib/LaunchpadBuilder/AppBuilder/ChocoPkgBuilder.ahk b/Lib/LaunchpadBuilder/AppBuilder/ChocoPkgBuilder.ahk index 8ea2644b..a855d436 100644 --- a/Lib/LaunchpadBuilder/AppBuilder/ChocoPkgBuilder.ahk +++ b/Lib/LaunchpadBuilder/AppBuilder/ChocoPkgBuilder.ahk @@ -9,7 +9,7 @@ class ChocoPkgBuilder extends AppBuilderBase { throw AppException("Installer file doesn't exist, cannot build chocolatey package.") } - hash := this.app.Service("FileHasher").Hash(installer, FileHasher.HASH_TYPE_SHA256) + hash := this.app["FileHasher"].Hash(installer, FileHasher.HASH_TYPE_SHA256) if (!hash) { throw AppException("Failed to create an SHA256 hash of the installer file.") diff --git a/Lib/LaunchpadBuilder/BuildDeployer/ApiBuildDeployer.ahk b/Lib/LaunchpadBuilder/BuildDeployer/ApiBuildDeployer.ahk index 330209b9..5173dc95 100644 --- a/Lib/LaunchpadBuilder/BuildDeployer/ApiBuildDeployer.ahk +++ b/Lib/LaunchpadBuilder/BuildDeployer/ApiBuildDeployer.ahk @@ -2,7 +2,7 @@ class ApiBuildDeployer extends BuildDeployerBase { Deploy(deployInfo) { apiUrl := "https://api.launchpad.games/v1/release-info" - this.app.Service("manager.gui").Dialog(Map( + this.app["manager.gui"].Dialog(Map( "title", "Not Yet Available", "text", "Release info pushing is not yet available. Please update release info manually." )) diff --git a/Lib/LaunchpadLauncher/App/LaunchpadLauncher.ahk b/Lib/LaunchpadLauncher/App/LaunchpadLauncher.ahk index ec563367..747feefa 100644 --- a/Lib/LaunchpadLauncher/App/LaunchpadLauncher.ahk +++ b/Lib/LaunchpadLauncher/App/LaunchpadLauncher.ahk @@ -64,11 +64,11 @@ class LaunchpadLauncher extends AppBase { RunApp(config) { super.RunApp(config) - this.Service("Launcher").LaunchGame() + this["Launcher"].LaunchGame() } RestartApp() { - game := this.Service("Game") + game := this["Game"] if (game) { game.StopOverlay() @@ -78,7 +78,7 @@ class LaunchpadLauncher extends AppBase { } ExitApp() { - game := this.Service("Game") + game := this["Game"] if (game) { game.StopOverlay() diff --git a/Lib/LaunchpadLauncher/Condition/SteamConditionBase.ahk b/Lib/LaunchpadLauncher/Condition/SteamConditionBase.ahk index 43f0ebbe..765a5369 100644 --- a/Lib/LaunchpadLauncher/Condition/SteamConditionBase.ahk +++ b/Lib/LaunchpadLauncher/Condition/SteamConditionBase.ahk @@ -11,7 +11,7 @@ class SteamConditionBase extends ConditionBase { GetSteamPath(app) { steamPath := "" - platforms := app.Parameter("platforms") + platforms := app.Parameter["platforms"] if (platforms.Has("Steam") && platforms["Steam"].Has("InstallDir")) { steamPath := platforms["Steam"]["InstallDir"] diff --git a/Lib/LaunchpadLauncher/Game/BlizzardGame.ahk b/Lib/LaunchpadLauncher/Game/BlizzardGame.ahk index 15a0fc83..5eafec37 100644 --- a/Lib/LaunchpadLauncher/Game/BlizzardGame.ahk +++ b/Lib/LaunchpadLauncher/Game/BlizzardGame.ahk @@ -4,7 +4,7 @@ class BlizzardGame extends SimpleGame { playButtonColors := ["0074E0", "148EFF"] GetRunCmd() { - launcherPath := this.app.Service("Launcher").config["LauncherInstallDir"] . "\" . this.app.Service("Launcher").config["LauncherExe"] + launcherPath := this.app["Launcher"].config["LauncherInstallDir"] . "\" . this.app["Launcher"].config["LauncherExe"] if (launcherPath != "") { gameKey := this.config["GameLauncherSpecificId"] @@ -16,7 +16,7 @@ class BlizzardGame extends SimpleGame { RunGameRun() { pid := super.RunGameRun() - winTitle := this.app.Service("Launcher").config["LauncherWindowTitle"] + winTitle := this.app["Launcher"].config["LauncherWindowTitle"] if (!WinExist(winTitle)) { WinWait(winTitle) @@ -53,7 +53,7 @@ class BlizzardGame extends SimpleGame { } CleanupAfterRun(progress := "") { - winTitle := this.app.Service("Launcher").config["LauncherWindowTitle"] + winTitle := this.app["Launcher"].config["LauncherWindowTitle"] if (WinExist(winTitle)) { WinClose("ahk_id" . WinGetID(winTitle)) } diff --git a/Lib/LaunchpadLauncher/Game/GameBase.ahk b/Lib/LaunchpadLauncher/Game/GameBase.ahk index f50dd053..e943ab16 100644 --- a/Lib/LaunchpadLauncher/Game/GameBase.ahk +++ b/Lib/LaunchpadLauncher/Game/GameBase.ahk @@ -23,7 +23,7 @@ class GameBase { config := Map() } - this.launcherConfig := app.Service("config.app") + this.launcherConfig := app["config.app"] InvalidParameterException.CheckTypes("GameBase", "app", app, "AppBase", "key", key, "", "config", config, "Map") this.app := app this.key := key @@ -33,7 +33,7 @@ class GameBase { Log(message, level := "Debug") { if (this.app.Services.Has("logger") && this.launcherConfig["LoggingLevel"] != "None") { - this.app.Service("logger").Log(this.key . ": " . message, level) + this.app["logger"].Log(this.key . ": " . message, level) } } @@ -180,14 +180,14 @@ class GameBase { StartOverlay() { SetTimer(this.overlayCallbackObj, 0) this.Log("Starting Launchpad Overlay...") - this.app.Service("manager.overlay").Start(this.launcherConfig["OverlayHotkey"]) + this.app["manager.overlay"].Start(this.launcherConfig["OverlayHotkey"]) this.overlayStarted := true } StopOverlay() { if (this.overlayStarted) { this.Log("Shutting down Launchpad Overlay...") - this.app.Service("manager.overlay").Close() + this.app["manager.overlay"].Close() } } @@ -198,7 +198,7 @@ class GameBase { } this.Log("Closing overlay if running...") - this.app.Service("manager.overlay").Close() + this.app["manager.overlay"].Close() this.Log("Cleaning up scheduled task(s)...") this.CleanupScheduledTask() } diff --git a/Lib/LaunchpadLauncher/Game/RiotGame.ahk b/Lib/LaunchpadLauncher/Game/RiotGame.ahk index 0a63e8ec..e4edda9b 100644 --- a/Lib/LaunchpadLauncher/Game/RiotGame.ahk +++ b/Lib/LaunchpadLauncher/Game/RiotGame.ahk @@ -1,6 +1,6 @@ class RiotGame extends SimpleGame { GetRunCmd() { - launcherPath := "`"" . this.app.Service("Launcher").config["LauncherInstallDir"] . "\" . this.app.Service("Launcher").config["LauncherExe"] . "`"" + launcherPath := "`"" . this.app["Launcher"].config["LauncherInstallDir"] . "\" . this.app["Launcher"].config["LauncherExe"] . "`"" if (launcherPath != "") { gameKey := this.config["GameLauncherSpecificId"] diff --git a/Lib/Shared/Includes.ahk b/Lib/Shared/Includes.ahk index 56c04e44..392be1b7 100644 --- a/Lib/Shared/Includes.ahk +++ b/Lib/Shared/Includes.ahk @@ -8,6 +8,7 @@ #Include Modules\WebServices\EventSubscriber\WebServicesEventSubscriber.ahk #Include Modules\WebServices\Factory\WebServiceAdapterFactory.ahk #Include Modules\WebServices\Gui\AuthenticationGui\LaunchpadLoginWindow.ahk +#Include Modules\WebServices\Gui\Form\FeedbackWindow.ahk #Include Modules\WebServices\Gui\ManageWindow\ManageWebServicesWindow.ahk #Include Modules\WebServices\LayerSource\WebServiceAdapterLayerSource.ahk #Include Modules\WebServices\WebServiceAdapter\FileWebServiceAdapter.ahk @@ -53,6 +54,8 @@ #Include Volantis.App\Event\ComponentInfoEvent.ahk #Include Volantis.App\Event\DefineComponentsEvent.ahk #Include Volantis.App\Event\LoadComponentEvent.ahk +#Include Volantis.App\Event\MenuItemsEvent.ahk +#Include Volantis.App\Event\MenuResultEvent.ahk #Include Volantis.App\Event\RegisterComponentsEvent.ahk #Include Volantis.App\Event\ServiceDefinitionsEvent.ahk #Include Volantis.App\Events\Events.ahk @@ -68,7 +71,6 @@ #Include Volantis.App\Gui\Dialog\UpdateAvailableWindow.ahk #Include Volantis.App\Gui\EntityEditor\EntityEditorBase.ahk #Include Volantis.App\Gui\EntityEditor\SimpleEntityEditor.ahk -#Include Volantis.App\Gui\Form\FeedbackWindow.ahk #Include Volantis.App\Gui\Form\FormGuiBase.ahk #Include Volantis.App\Gui\Form\IconSelector.ahk #Include Volantis.App\Gui\ManageWindow\ManageEntitiesWindow.ahk diff --git a/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk b/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk index 5e14fd36..1f84afbd 100644 --- a/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk +++ b/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk @@ -36,10 +36,10 @@ class ApiDataSource extends DataSourceBase { request.requestHeaders["Cache-Control"] := "no-cache" if (this.app.Config["api_authentication"]) { - entityMgr := webService := this.app.Service("entity_manager.web_service") + entityMgr := webService := this.app["entity_manager.web_service"] if (entityMgr.Has("launchpad_api") && entityMgr["launchpad_api"]["Enabled"]) { - webService := this.app.Service("entity_manager.web_service")["launchpad_api"] + webService := this.app["entity_manager.web_service"]["launchpad_api"] webService["Provider"]["Authenticator"].AlterRequest(webService, request) } @@ -92,7 +92,7 @@ class ApiDataSource extends DataSourceBase { status := Map("authenticated", false, "account", "", "photo", "") if (this.app.Config["api_authentication"]) { - entityMgr := webService := this.app.Service("entity_manager.web_service") + entityMgr := webService := this.app["entity_manager.web_service"] if (entityMgr.Has("launchpad_api") && entityMgr["launchpad_api"]["Enabled"] && entityMgr["launchpad_api"]["Authenticated"]) { statusResult := this.ReadItem(path, true) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index dc6e135e..85a1b0ab 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -354,7 +354,7 @@ class WebServiceEntity extends AppEntityBase { if (SubStr(imgPath, 1, 4) == "http") { cachePath := "account--profile.jpg" - imgPath := this.app.Service("manager.cache")["file"].GetCachedDownload(cachePath, imgPath) + imgPath := this.app["manager.cache"]["file"].GetCachedDownload(cachePath, imgPath) } } diff --git a/Lib/Shared/Modules/WebServices/EventSubscriber/WebServicesEventSubscriber.ahk b/Lib/Shared/Modules/WebServices/EventSubscriber/WebServicesEventSubscriber.ahk index 66ea089b..0924015c 100644 --- a/Lib/Shared/Modules/WebServices/EventSubscriber/WebServicesEventSubscriber.ahk +++ b/Lib/Shared/Modules/WebServices/EventSubscriber/WebServicesEventSubscriber.ahk @@ -3,6 +3,12 @@ class WebServicesEventSubscriber extends EventSubscriberBase { return Map( Events.APP_POST_STARTUP, [ ObjBindMethod(this, "OnPostStartup") + ], + Events.APP_MENU_ITEMS_LATE, [ + ObjBindMethod(this, "OnMenuItemsLate") + ], + Events.APP_MENU_PROCESS_RESULT, [ + ObjBindMethod(this, "OnMenuProcessResult") ] ) } @@ -18,4 +24,20 @@ class WebServicesEventSubscriber extends EventSubscriberBase { webService.Login() } } + + OnMenuItemsLate(event, extra, eventName, hwnd) { + event.MenuItems.Push(Map( + "label", "Provide &Feedback", + "name", "ProvideFeedback" + )) + } + + OnMenuProcessResult(event, extra, eventName, hwnd) { + if (!event.IsFinished) { + if (event.Result == "ProvideFeedback") { + this.container["manager.gui"].Dialog(Map("type", "FeedbackWindow")) + event.IsFinished := true + } + } + } } diff --git a/Lib/Shared/Volantis.App/Gui/Form/FeedbackWindow.ahk b/Lib/Shared/Modules/WebServices/Gui/Form/FeedbackWindow.ahk similarity index 61% rename from Lib/Shared/Volantis.App/Gui/Form/FeedbackWindow.ahk rename to Lib/Shared/Modules/WebServices/Gui/Form/FeedbackWindow.ahk index bcf5b290..c508904b 100644 --- a/Lib/Shared/Volantis.App/Gui/Form/FeedbackWindow.ahk +++ b/Lib/Shared/Modules/WebServices/Gui/Form/FeedbackWindow.ahk @@ -36,20 +36,44 @@ class FeedbackWindow extends DialogBox { SendFeedback() { global appVersion - if (this.apiEndpointUrl) { - endpoint := this.apiEndpointUrl . "/submit-feedback" + webServiceId := "launchpad_api" + entityMgr := this.container["entity_manager.web_service"] + + results := Map() + success := false + + if (entityMgr.Has(webServiceId) && entityMgr[webServiceId]["Enabled"]) { + webService := entityMgr[webServiceId] body := Map() body["email"] := this.guiObj["Email"].Text body["version"] := appVersion body["feedback"] := this.guiObj["Feedback"].Text - request := WinHttpReq(endpoint) - response := request.Send("POST", body) - success := !!(request.GetStatusCode() == 200) + results := webService.AdapterRequest(Map("data", body), "feedback_submission", "create") + } - notification := success ? "Successfully sent feedback to Volantis Development" : "Failed to send feedback to Volantis Development" - this.notifierObj.Notify(notification, "Feedback Sent", success ? "info" : "error") + for key, result in results { + if (result) { + success := true + break + } } + + message := "" + + if (success) { + message := "Successfully sent feedback" + } else if (results.Count) { + message := "Failed to send feedback" + } else { + message := "No feedback adapters are enabled" + } + + this.notifierObj.Notify( + message, + "Feedback Submission", + success ? "info" : "error" + ) } } diff --git a/Lib/Shared/Volantis.App/App/AppBase.ahk b/Lib/Shared/Volantis.App/App/AppBase.ahk index 082990c8..5b75f081 100644 --- a/Lib/Shared/Volantis.App/App/AppBase.ahk +++ b/Lib/Shared/Volantis.App/App/AppBase.ahk @@ -1,5 +1,4 @@ class AppBase { - developer := "" versionStr := "" appName := "" appDir := "" @@ -8,9 +7,9 @@ class AppBase { configObj := "" stateObj := "" serviceContainerObj := "" - customTrayMenu := false themeReady := false startConfig := "" + isSetup := false static Instance := "" @@ -25,11 +24,20 @@ class AppBase { } Config { - get => this.Service("config.app") + get => this["config.app"] } State { - get => this.Service("state.app") + get => this["state.app"] + } + + Parameter[key] { + get => this.Services.GetParameter(key) + set => this.Services.SetParameter(key, value) + } + + __Item[serviceId] { + get => this.Service(serviceId) } __New(config := "", autoStart := true) { @@ -57,6 +65,16 @@ class AppBase { "app_dir", this.appDir, "data_dir", this.dataDir, "tmp_dir", this.tmpDir, + "app.website_url", "", + "app.custom_tray_menu", false, + "app.developer", "", + "app.has_settings", false, + "app.settings_window", "", + "app.show_restart_menu_item", true, + "app.supports_update_check", false, + "app.show_about_menu_item", false, + "app.about_window", "", + "app.show_website_menu_item", false, "resources_dir", "@@{app_dir}\Resources", "config_path", "@@{app_dir}\" . this.appName . ".json", "config_key", "config", @@ -374,10 +392,6 @@ class AppBase { config["appName"] := appBaseName } - if (!config.Has("developer")) { - config["developer"] := "" - } - if (!config.Has("appDir") || !config["appDir"]) { config["appDir"] := A_ScriptDir } @@ -396,7 +410,6 @@ class AppBase { this.appName := config["appName"] this.versionStr := config["version"] - this.developer := config["developer"] this.appDir := config["appDir"] this.tmpDir := config["tmpDir"] this.dataDir := config["dataDir"] @@ -412,26 +425,26 @@ class AppBase { this.LoadServices(config) if (!config.Has("useShell") || config("useShell")) { - this.Service("shell") + this["shell"] } OnError(ObjBindMethod(this, "OnException")) event := AppRunEvent(Events.APP_PRE_INITIALIZE, this, config) - this.Service("manager.event").DispatchEvent(event) + this["manager.event"].DispatchEvent(event) this.InitializeApp(config) event := AppRunEvent(Events.APP_POST_INITIALIZE, this, config) - this.Service("manager.event").DispatchEvent(event) + this["manager.event"].DispatchEvent(event) event := AppRunEvent(Events.APP_PRE_RUN, this, config) - this.Service("manager.event").DispatchEvent(event) + this["manager.event"].DispatchEvent(event) this.RunApp(config) event := AppRunEvent(Events.APP_POST_STARTUP, this, config) - this.Service("manager.event").DispatchEvent(event) + this["manager.event"].DispatchEvent(event) } LoadServices(config) { @@ -441,18 +454,18 @@ class AppBase { )) this.Services.LoadDefinitions(MapDefinitionLoader(config)) - sdFactory := this.Service("factory.structured_data") - serviceFile := this.Services.GetParameter("service_files.app") + sdFactory := this["factory.structured_data"] + serviceFile := this.Parameter["service_files.app"] if (FileExist(serviceFile)) { this.Services.LoadDefinitions(FileDefinitionLoader(sdFactory, serviceFile)) } - this.Service("config.app") + this["config.app"] this.InitializeTheme() this.InitializeModules(config) - for index, moduleServiceFile in this.Service("manager.module").GetModuleServiceFiles() { + for index, moduleServiceFile in this["manager.module"].GetModuleServiceFiles() { if (FileExist(moduleServiceFile)) { this.Services.LoadDefinitions(FileDefinitionLoader(sdFactory, moduleServiceFile)) } else { @@ -461,49 +474,49 @@ class AppBase { } ; Reload user config files to ensure they are the active values - this.Service("config.app").LoadConfig(true) + this["config.app"].LoadConfig(true) ; Register early event subscribers (e.g. modules) - this.Service("manager.event").RegisterServiceSubscribers(this.Services) + this["manager.event"].RegisterServiceSubscribers(this.Services) - this.Service("manager.event").Register(Events.APP_SERVICES_LOADED, "AppServices", ObjBindMethod(this, "OnServicesLoaded")) + this["manager.event"].Register(Events.APP_SERVICES_LOADED, "AppServices", ObjBindMethod(this, "OnServicesLoaded")) event := ServiceDefinitionsEvent(Events.APP_SERVICE_DEFINITIONS, "", "", config) - this.Service("manager.event").DispatchEvent(event) + this["manager.event"].DispatchEvent(event) if (event.Services.Count || event.Parameters.Count) { this.Services.LoadDefinitions(SimpleDefinitionLoader(event.Services, event.Parameters)) } - serviceFile := this.Services.GetParameter("service_files.user") + serviceFile := this.Parameter["service_files.user"] if (FileExist(serviceFile)) { this.Services.LoadDefinitions(FileDefinitionLoader(sdFactory, serviceFile)) } ; Register any missing late-loading event subscribers - this.Service("manager.event").RegisterServiceSubscribers(this.Services) + this["manager.event"].RegisterServiceSubscribers(this.Services) event := AppRunEvent(Events.APP_SERVICES_LOADED, this, config) - this.Service("manager.event").DispatchEvent(event) + this["manager.event"].DispatchEvent(event) } OnServicesLoaded(event, extra, eventName, hwnd) { - this.Service("manager.cache") - this.Service("manager.entity_type").All() - this.Service("manager.installer").RunInstallers(InstallerBase.INSTALLER_TYPE_REQUIREMENT) + this["manager.cache"] + this["manager.entity_type"].All() + this["manager.installer"].RunInstallers(InstallerBase.INSTALLER_TYPE_REQUIREMENT) } InitializeModules(config) { - includeFiles := this.Services.GetParameter("include_files") - updated := this.Service("manager.module").UpdateModuleIncludes(includeFiles["modules"], includeFiles["module_tests"]) + includeFiles := this.Parameter["include_files"] + updated := this["manager.module"].UpdateModuleIncludes(includeFiles["modules"], includeFiles["module_tests"]) if (updated) { message := A_IsCompiled ? "Your modules have been updated. Currently, you must recompile " this.appName . " yourself for the changes to take effect. Would you like to exit now (highly recommended)?" : "Your modules have been updated, and " this.appName . " must be restarted for the changes to take effect. Would you like to restart now?" - response := this.app.Service("manager.gui").Dialog(Map( + response := this.app["manager.gui"].Dialog(Map( "title", "Module Includes Updated", "text", message )) @@ -519,16 +532,16 @@ class AppBase { } InitializeTheme() { - this.Service("gdip", "manager.gui", "manager.theme") + this[["gdip", "manager.gui", "manager.theme"]] this.themeReady := true } InitializeApp(config) { A_AllowMainWindow := false - if (this.customTrayMenu) { + if (this.Parameter["app.custom_tray_menu"]) { A_TrayMenu.Delete() - this.Service("manager.event").Register(Events.AHK_NOTIFYICON, "TrayClick", ObjBindMethod(this, "OnTrayIconRightClick"), 1) + this["manager.event"].Register(Events.AHK_NOTIFYICON, "TrayClick", ObjBindMethod(this, "OnTrayIconRightClick"), 1) } } @@ -537,19 +550,19 @@ class AppBase { this.CheckForUpdates(false) } - if (this.Services.HasParameter("config_path") && !FileExist(this.Parameter("config_path"))) { + if (this.Services.HasParameter("config_path") && !FileExist(this.Parameter["config_path"])) { this.InitialSetup(config) } } OpenApp() { - mainWin := this.Parameter("config.main_window") + mainWin := this.Parameter["config.main_window"] if (mainWin) { - if (this.Service("manager.gui").Has(mainWin)) { - WinActivate("ahk_id " . this.Service("manager.gui")[mainWin].GetHwnd()) + if (this["manager.gui"].Has(mainWin)) { + WinActivate("ahk_id " . this["manager.gui"][mainWin].GetHwnd()) } else { - this.Service("manager.gui").OpenWindow(Map( + this["manager.gui"].OpenWindow(Map( "type", mainWin, "title", this.appName )) @@ -559,7 +572,7 @@ class AppBase { ExitApp() { event := AppRunEvent(Events.APP_SHUTDOWN, this) - this.Service("manager.event").DispatchEvent(event) + this["manager.event"].DispatchEvent(event) if (this.Services.Has("gdip")) { Gdip_Shutdown(this.Services["gdip"].GetHandle()) @@ -570,7 +583,7 @@ class AppBase { RestartApp() { event := AppRunEvent(Events.APP_RESTART, this) - this.Service("manager.event").DispatchEvent(event) + this["manager.event"].DispatchEvent(event) if (this.Services.Has("gdip")) { Gdip_Shutdown(this.Services["gdip"].GetHandle()) @@ -586,7 +599,7 @@ class AppBase { throw AppException("The shell is disabled, so shell commands cannot currently be run.") } - result := this.Service("shell").Exec(A_ComSpec . " /C " . command).StdOut.ReadAll() + result := this["shell"].Exec(A_ComSpec . " /C " . command).StdOut.ReadAll() if (trimOutput) { result := Trim(result, " `r`n`t") @@ -606,12 +619,12 @@ class AppBase { } for index, arrName in name { - results[arrName] := this.Service(arrName) + results[arrName] := this[arrName] } if (params && params.Length) { for index, arrName in params { - results[arrName] := this.Service(arrName) + results[arrName] := this[arrName] } } @@ -621,14 +634,15 @@ class AppBase { return this.Services.Get(name) } - Parameter(name) { - return this.Services.GetParameter(name) - } - OnException(e, mode) { extra := (e.HasProp("Extra") && e.Extra != "") ? "`n`nExtra information:`n" . e.Extra : "" occurredIn := e.What ? " in " . e.What : "" - developer := this.developer ? this.developer : "the developer(s)" + + developer := this.Parameter["app.developer"] + + if (!developer) { + developer := "the developer(s)" + } errorText := this.appName . " has experienced an unhandled exception. You can find the details below." errorText .= "`n`n" . e.Message . extra @@ -646,7 +660,7 @@ class AppBase { } if (this.Services.Has("logger")) { - this.Service("logger").Error(errorText) + this["logger"].Error(errorText) } errorText .= "`n" @@ -659,7 +673,7 @@ class AppBase { if (this.themeReady) { btns := allowContinue ? "*&Continue|&Reload|&Exit" : "*&Reload|&Exit" - this.Service("manager.gui").Dialog(Map( + this["manager.gui"].Dialog(Map( "type", "ErrorDialog", "title", "Unhandled Exception", "text", errorText, @@ -683,7 +697,7 @@ class AppBase { OnTrayIconRightClick(wParam, lParam, msg, hwnd) { if (lParam == Events.MOUSE_RIGHT_UP) { - if (this.customTrayMenu) { + if (this.Parameter["app.custom_tray_menu"]) { this.ShowTrayMenu() return 0 } @@ -691,7 +705,7 @@ class AppBase { } InitialSetup(config) { - ; Override this to set config values as needed + this.isSetup := true } CheckForUpdates(notify := true) { @@ -706,7 +720,7 @@ class AppBase { menuItems.Push(Map("label", "Restart", "name", "RestartApp")) menuItems.Push(Map("label", "Exit", "name", "ExitApp")) - result := this.Service("manager.gui").Menu(menuItems, this) + result := this["manager.gui"].Menu(menuItems, this) this.HandleTrayMenuClick(result) } @@ -730,4 +744,214 @@ class AppBase { this.ExitApp() super.__Delete() } + + MainMenu(parentGui, parentCtl, showOpenAppItem := false) { + menuItems := this.GetMainMenuItems(showOpenAppItem) + + if (menuItems.Length) { + this.HandleMainMenuClick(this["manager.gui"].Menu( + menuItems, + parentGui, + parentCtl + )) + } + } + + GetMainMenuItems(showOpenAppItem := false) { + menuItems := [] + menuItems := this.AddMainMenuEarlyItems(menuItems, showOpenAppItem) + + if (menuItems.Length) { + menuItems.Push("") + } + + length := menuItems.Length + + toolsItems := this.GetToolsMenuItems() + + if (toolsItems.Length) { + menuItems.Push(Map("label", "&Tools", "name", "ToolsMenu", "childItems", toolsItems)) + } + + aboutItems := this.GetAboutMenuItems() + + if (aboutItems.Length) { + menuItems.Push(Map("label", "&About", "name", "About", "childItems", aboutItems)) + } + + menuItems := this.AddMainMenuMiddleItems(menuItems) + + if (menuItems.Length > length) { + menuItems.Push("") + } + + length := menuItems.Length + menuItems := this.AddMainMenuLateItems(menuItems) + + if (menuItems.Length > length) { + menuItems.Push("") + } + + if (this.Parameter["app.show_restart_menu_item"]) { + menuItems.Push(Map("label", "&Restart", "name", "Reload")) + } + + menuItems.Push(Map("label", "E&xit", "name", "Exit")) + + event := MenuItemsEvent(Events.APP_MENU_ITEMS_ALTER, menuItems) + this.Dispatch(event) + menuItems := event.MenuItems + + return menuItems + } + + GetAboutMenuItems() { + aboutItems := [] + + if (this.Parameter["app.show_about_menu_item"]) { + aboutItems.Push(Map("label", "&About " . this.appName, "name", "About")) + } + + if (this.Parameter["app.show_website_menu_item"]) { + aboutItems.Push(Map("label", "Open &Website", "name", "OpenWebsite")) + } + + event := MenuItemsEvent(Events.APP_MENU_ABOUT_ITEMS_ALTER, aboutItems) + this.Dispatch(event) + aboutItems := event.MenuItems + + return aboutItems + } + + GetToolsMenuItems() { + toolsItems := this.AddEntityManagerMenuLinks([]) + event := MenuItemsEvent(Events.APP_MENU_TOOLS_ITEMS_ALTER, toolsItems) + this.Dispatch(event) + toolsItems := event.MenuItems + + return toolsItems + } + + AddMainMenuEarlyItems(menuItems, showOpenAppItem := false) { + if (showOpenAppItem) { + menuItems.Push(Map("label", "Open " . this.appName, "name", "OpenApp")) + menuItems.Push("") + } + + event := MenuItemsEvent(Events.APP_MENU_ITEMS_EARLY, menuItems) + this.Dispatch(event) + menuItems := event.MenuItems + + return menuItems + } + + AddMainMenuMiddleItems(menuItems) { + event := MenuItemsEvent(Events.APP_MENU_ITEMS_MIDDLE, menuItems) + this.Dispatch(event) + menuItems := event.MenuItems + return menuItems + } + + AddMainMenuLateItems(menuItems) { + if (this.Parameter["app.has_settings"]) { + menuItems.Push(Map("label", "&Settings", "name", "Settings")) + } + + if (this.Parameter["app.supports_update_check"]) { + menuItems.Push(Map("label", "Check for &Updates", "name", "CheckForUpdates")) + } + + event := MenuItemsEvent(Events.APP_MENU_ITEMS_LATE, menuItems) + this.Dispatch(event) + menuItems := event.MenuItems + + return menuItems + } + + AddEntityManagerMenuLinks(menuItems) { + menuEntityTypes := this._getToolsMenuEntityTypes() + + for key, entityType in menuEntityTypes { + menuLinkText := entityType.definition["manager_menu_link_text"] + + if (!menuLinkText) { + menuLinkText := "&" . entityType.definition["name_plural"] + } + + menuItems.Push(Map("label", menuLinkText, "name", "manage_" . key)) + } + + return menuItems + } + + Dispatch(event) { + this["manager.event"].DispatchEvent(event) + } + + _getToolsMenuEntityTypes() { + entityTypes := Map() + + for key, entityType in this["manager.entity_type"] { + if (entityType.definition["manager_link_in_tools_menu"]) { + entityTypes[key] := entityType + } + } + + return entityTypes + } + + HandleMainMenuClick(result) { + event := MenuResultEvent(Events.APP_MENU_PROCESS_RESULT, result) + this.Dispatch(event) + result := event.Result + + if (!event.IsFinished) { + if (result == "About") { + this.ShowAbout() + } else if (result == "OpenWebsite") { + this.OpenWebsite() + } else if (result == "Settings") { + this.ShowSettings() + } else if (result == "CheckForUpdates") { + this.CheckForUpdates() + } else if (result == "Reload") { + this.restartApp() + } else if (result == "Exit") { + this.ExitApp() + } else { + for key, entityType in this._getToolsMenuEntityTypes() { + if (result == "manage_" . key) { + this["entity_type." . key].OpenManageWindow() + break + } + } + } + } + + return result + } + + ShowSettings() { + windowName := this.Parameter["app.settings_window"] + + if (windowName) { + this["manager.gui"].Dialog(Map("type", windowName, "unique", false)) + } + } + + ShowAbout() { + windowName := this.Parameter["app.about_window"] + + if (windowName) { + this["manager.gui"].Dialog(Map("type", windowName)) + } + } + + OpenWebsite() { + websiteUrl := this.Parameter["app.website_url"] + + if (websiteUrl) { + Run(websiteUrl) + } + } } diff --git a/Lib/Shared/Volantis.App/App/TestAppBase.ahk b/Lib/Shared/Volantis.App/App/TestAppBase.ahk index a5263d94..25120777 100644 --- a/Lib/Shared/Volantis.App/App/TestAppBase.ahk +++ b/Lib/Shared/Volantis.App/App/TestAppBase.ahk @@ -1,13 +1,13 @@ class TestAppBase extends AppBase { ExitApp() { event := AppRunEvent(Events.APP_SHUTDOWN, this) - this.Service("manager.event").DispatchEvent(event) + this["manager.event"].DispatchEvent(event) ; Don't actually exit } RestartApp() { event := AppRunEvent(Events.APP_SHUTDOWN, this) - this.Service("manager.event").DispatchEvent(event) + this["manager.event"].DispatchEvent(event) ; Don't actually restart } } diff --git a/Lib/Shared/Volantis.App/BulkOperation/BulkOperationBase.ahk b/Lib/Shared/Volantis.App/BulkOperation/BulkOperationBase.ahk index a9406e35..6f85723d 100644 --- a/Lib/Shared/Volantis.App/BulkOperation/BulkOperationBase.ahk +++ b/Lib/Shared/Volantis.App/BulkOperation/BulkOperationBase.ahk @@ -40,7 +40,7 @@ class BulkOperationBase { } if (this.app.Services.Has("logger")) { - this.app.Service("logger").Debug(Type(this) . ": Starting bulk operation...") + this.app["logger"].Debug(Type(this) . ": Starting bulk operation...") } this.running := true @@ -56,7 +56,7 @@ class BulkOperationBase { LogResults() { if (this.app.Services.Has("logger")) { - this.app.Service("logger").Info(Type(this) . " Results: " . this.GetResultMessage()) + this.app["logger"].Info(Type(this) . " Results: " . this.GetResultMessage()) } } @@ -89,7 +89,7 @@ class BulkOperationBase { ownerOrParent := this.parent } - this.progress := this.app.Service("manager.gui").OpenWindow(Map( + this.progress := this.app["manager.gui"].OpenWindow(Map( "type", "ProgressIndicator", "title", this.progressTitle, "text", this.progressText, @@ -115,7 +115,7 @@ class BulkOperationBase { Notify() { if (this.shouldNotify && this.app.Services.Has("notifier")) { - this.app.Service("notifier").Info(this.GetResultMessage()) + this.app["notifier"].Info(this.GetResultMessage()) } } diff --git a/Lib/Shared/Volantis.App/BulkOperation/InstallOp/InstallOp.ahk b/Lib/Shared/Volantis.App/BulkOperation/InstallOp/InstallOp.ahk index 6c4d0aad..b093857a 100644 --- a/Lib/Shared/Volantis.App/BulkOperation/InstallOp/InstallOp.ahk +++ b/Lib/Shared/Volantis.App/BulkOperation/InstallOp/InstallOp.ahk @@ -17,7 +17,7 @@ class InstallOp extends BulkOperationBase { for index, name in this.installers { name := "installer." . name - installer := this.app.Service(name) + installer := this.app[name] if (!HasBase(installer, InstallerBase.Prototype)) { throw AppException("Provided installer is not valid: " . name) diff --git a/Lib/Shared/Volantis.App/Entity/AppEntityBase.ahk b/Lib/Shared/Volantis.App/Entity/AppEntityBase.ahk index 803fd056..b1be4fd7 100644 --- a/Lib/Shared/Volantis.App/Entity/AppEntityBase.ahk +++ b/Lib/Shared/Volantis.App/Entity/AppEntityBase.ahk @@ -146,8 +146,8 @@ class AppEntityBase extends FieldableEntity { } for index, dataSourceKey in dataSourceKeys { - if (this.app.Service("manager.data_source").Has(dataSourceKey)) { - dataSource := this.app.Service("manager.data_source")[dataSourceKey] + if (this.app["manager.data_source"].Has(dataSourceKey)) { + dataSource := this.app["manager.data_source"][dataSourceKey] if (dataSource) { dataSources[dataSourceKey] := dataSource diff --git a/Lib/Shared/Volantis.App/Entity/BackupEntity.ahk b/Lib/Shared/Volantis.App/Entity/BackupEntity.ahk index 8d25858b..c02ae818 100644 --- a/Lib/Shared/Volantis.App/Entity/BackupEntity.ahk +++ b/Lib/Shared/Volantis.App/Entity/BackupEntity.ahk @@ -25,7 +25,7 @@ class BackupEntity extends AppEntityBase { definitions["IconSrc"] := Map( "type", "icon_file", "description", "The path to this an icon (.ico or .exe).", - "default", this.app.Service("manager.theme")[].GetIconPath("backup") + "default", this.app["manager.theme"][].GetIconPath("backup") ) definitions["Source"] := Map( diff --git a/Lib/Shared/Volantis.App/Event/MenuItemsEvent.ahk b/Lib/Shared/Volantis.App/Event/MenuItemsEvent.ahk new file mode 100644 index 00000000..23c1f89f --- /dev/null +++ b/Lib/Shared/Volantis.App/Event/MenuItemsEvent.ahk @@ -0,0 +1,16 @@ +class MenuItemsEvent extends EventBase { + menuItemsObj := "" + + MenuItems { + get => this.menuItemsObj + } + + __New(eventName, menuItems := "") { + if (!menuItems) { + menuItems := [] + } + + this.menuItemsObj := menuItems + super.__New(eventName) + } +} diff --git a/Lib/Shared/Volantis.App/Event/MenuResultEvent.ahk b/Lib/Shared/Volantis.App/Event/MenuResultEvent.ahk new file mode 100644 index 00000000..8d9011d6 --- /dev/null +++ b/Lib/Shared/Volantis.App/Event/MenuResultEvent.ahk @@ -0,0 +1,20 @@ +class MenuResultEvent extends EventBase { + resultItem := "" + finished := false + + Result { + get => this.resultItem + set => this.resultItem := value + } + + IsFinished { + get => this.finished + set => !!(value) + } + + __New(eventName, result) { + this.resultItem := result + + super.__New(eventName) + } +} diff --git a/Lib/Shared/Volantis.App/Events/Events.ahk b/Lib/Shared/Volantis.App/Events/Events.ahk index 35992f39..0ba53063 100644 --- a/Lib/Shared/Volantis.App/Events/Events.ahk +++ b/Lib/Shared/Volantis.App/Events/Events.ahk @@ -19,5 +19,13 @@ class Events { static APP_SHUTDOWN := 0x1020 static APP_RESTART := 0x1025 + static APP_MENU_ITEMS_EARLY := 0x1030 + static APP_MENU_ITEMS_MIDDLE := 0x1032 + static APP_MENU_ITEMS_LATE := 0x1034 + static APP_MENU_ITEMS_ALTER := 0x1036 + static APP_MENU_TOOLS_ITEMS_ALTER := 0x1038 + static APP_MENU_ABOUT_ITEMS_ALTER := 0x1040 + static APP_MENU_PROCESS_RESULT := 0x1042 + static AHK_NOTIFYICON := 0x404 } diff --git a/Lib/Shared/Volantis.App/Gui/EntityEditor/EntityEditorBase.ahk b/Lib/Shared/Volantis.App/Gui/EntityEditor/EntityEditorBase.ahk index 7ad3903b..467bd165 100644 --- a/Lib/Shared/Volantis.App/Gui/EntityEditor/EntityEditorBase.ahk +++ b/Lib/Shared/Volantis.App/Gui/EntityEditor/EntityEditorBase.ahk @@ -78,6 +78,6 @@ class EntityEditorBase extends FormGuiBase { Create() { super.Create() - this.dataSource := this.app.Service("manager.data_source").GetDefaultDataSource() + this.dataSource := this.app["manager.data_source"].GetDefaultDataSource() } } diff --git a/Lib/Shared/Volantis.App/Gui/GuiBase.ahk b/Lib/Shared/Volantis.App/Gui/GuiBase.ahk index 1c59feaf..412fb7eb 100644 --- a/Lib/Shared/Volantis.App/Gui/GuiBase.ahk +++ b/Lib/Shared/Volantis.App/Gui/GuiBase.ahk @@ -129,7 +129,7 @@ class GuiBase { RegisterCallbacks() { guiId := "Gui" . this.guiId - this.app.Service("manager.event") + this.app["manager.event"] .Register(Events.MOUSE_MOVE, guiId, ObjBindMethod(this, "OnMouseMove")) .Register(Events.WM_NCCALCSIZE, guiId, ObjBindMethod(this, "OnCalcSize")) .Register(Events.WM_NCACTIVATE, guiId, ObjBindMethod(this, "OnActivate")) @@ -142,10 +142,10 @@ class GuiBase { __Delete() { if (this.app) { - this.app.Service("manager.event").Unregister(Events.MOUSE_MOVE, "Gui" . this.guiId) - this.app.Service("manager.event").Unregister(Events.WM_NCCALCSIZE, "Gui" . this.guiId) - this.app.Service("manager.event").Unregister(Events.WM_NCACTIVATE, "Gui" . this.guiId) - this.app.Service("manager.event").Unregister(Events.WM_NCHITTEST, "Gui" . this.guiId) + this.app["manager.event"].Unregister(Events.MOUSE_MOVE, "Gui" . this.guiId) + this.app["manager.event"].Unregister(Events.WM_NCCALCSIZE, "Gui" . this.guiId) + this.app["manager.event"].Unregister(Events.WM_NCACTIVATE, "Gui" . this.guiId) + this.app["manager.event"].Unregister(Events.WM_NCHITTEST, "Gui" . this.guiId) } if (this.activeTooltip) { @@ -567,22 +567,22 @@ class GuiBase { } if (!this.isClosed && WinExist("ahk_id " . this.guiObj.Hwnd)) { - this.app.Service("manager.gui").StoreWindowState(this) + this.app["manager.gui"].StoreWindowState(this) WinClose("ahk_id " . this.guiObj.Hwnd) } else { this.Destroy() } - this.app.Service("manager.gui").CleanupWindow(this.guiId) + this.app["manager.gui"].CleanupWindow(this.guiId) } Destroy() { if (!this.isClosed && this.config["saveWindowState"]) { - this.app.Service("manager.gui").StoreWindowState(this) + this.app["manager.gui"].StoreWindowState(this) } if (this.owner) { - this.app.Service("manager.gui").ReleaseFromParent(this.guiId) + this.app["manager.gui"].ReleaseFromParent(this.guiId) } this.Cleanup() @@ -594,7 +594,7 @@ class GuiBase { } Cleanup() { - this.app.Service("manager.gui").UnloadComponent(this.guiId) + this.app["manager.gui"].UnloadComponent(this.guiId) ; Extend to clear any global variables used } diff --git a/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk b/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk index 3c3950d0..1a4b3c09 100644 --- a/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk +++ b/Lib/Shared/Volantis.App/Gui/ManageWindow/ManageEntitiesWindow.ahk @@ -188,7 +188,7 @@ class ManageEntitiesWindow extends ManageWindowBase { entityObj := this.entityMgr[key] menuItems := this.GetContextMenuItems(entityObj) - result := this.app.Service("manager.gui").Menu(menuItems, this) + result := this.app["manager.gui"].Menu(menuItems, this) this.ProcessContextMenuResult(result, key) } diff --git a/Lib/Shared/Volantis.App/Gui/Menu/MenuGui.ahk b/Lib/Shared/Volantis.App/Gui/Menu/MenuGui.ahk index 13f2c878..a7b5d689 100644 --- a/Lib/Shared/Volantis.App/Gui/Menu/MenuGui.ahk +++ b/Lib/Shared/Volantis.App/Gui/Menu/MenuGui.ahk @@ -133,7 +133,7 @@ if (btn.ChildItems) { this.childOpen := true - this.result := this.app.Service("manager.gui").Menu(Map( + this.result := this.app["manager.gui"].Menu(Map( "parent", this, "child", true, "openAtCtlSide", "right" diff --git a/Lib/Shared/Volantis.App/GuiControl/LocationBlock.ahk b/Lib/Shared/Volantis.App/GuiControl/LocationBlock.ahk index 40f371d8..3f535567 100644 --- a/Lib/Shared/Volantis.App/GuiControl/LocationBlock.ahk +++ b/Lib/Shared/Volantis.App/GuiControl/LocationBlock.ahk @@ -83,7 +83,7 @@ class LocationBlock extends GuiControlBase { } OnLocationOptions(btn, info) { - result := this.app.Service("manager.gui").Menu(Map( + result := this.app["manager.gui"].Menu(Map( "parent", this.guiObj, "child", true ), btn.MenuItems, btn) diff --git a/Lib/Shared/Volantis.App/State/StateBase.ahk b/Lib/Shared/Volantis.App/State/StateBase.ahk index 65f0c2b4..c19f07e9 100644 --- a/Lib/Shared/Volantis.App/State/StateBase.ahk +++ b/Lib/Shared/Volantis.App/State/StateBase.ahk @@ -15,7 +15,7 @@ class StateBase { } IsStateOutdated() { - return this.app.Service("version_checker").VersionIsOutdated(this.app.Version, this.Version) + return this.app["version_checker"].VersionIsOutdated(this.app.Version, this.Version) } __New(app, state := "", autoLoad := false) { diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index 826e1d58..c428f0b1 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -393,7 +393,7 @@ class EntityBase { result := "Cancel" while (mode) { - result := this.app.Service("manager.gui").Dialog(Map( + result := this.app["manager.gui"].Dialog(Map( "type", "SimpleEntityEditor", "mode", mode, "child", !!(ownerOrParent), diff --git a/Lib/TestLib/Test/AppTestBase.ahk b/Lib/TestLib/Test/AppTestBase.ahk index 028d322c..4414cacf 100644 --- a/Lib/TestLib/Test/AppTestBase.ahk +++ b/Lib/TestLib/Test/AppTestBase.ahk @@ -6,12 +6,12 @@ class AppTestBase extends TestBase { GetTestAppConfig() { config := Map( "appName", "Test App", - "developer", "Test Developer", "appDir", A_ScriptDir, "tmpDir", this.testDir . "\Temp", "dataDir", this.testDir . "\Data", "version", this.testAppVersion, "parameters", Map( + "app.developer", "Test Developer", "config.flush_cache_on_exit", false, "config.logging_level", "none", "config.module_dirs", [], diff --git a/Scripts/Build.ahk b/Scripts/Build.ahk index 998e9099..09656b22 100644 --- a/Scripts/Build.ahk +++ b/Scripts/Build.ahk @@ -9,7 +9,6 @@ appVersion := "{{VERSION}}" LaunchpadBuilder(Map( "appDir", appDir, "appName", "Launchpad", - "developer", "Volantis Development", "version", appVersion, "trayIcon", appDir . "\Resources\Graphics\launchpad.ico", "console", true, From 7e45a12cb1e1574d49f1ba7de2734073fe8f4a3f Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Tue, 13 Dec 2022 18:49:02 -0500 Subject: [PATCH 071/227] Turn update checks into an event which is subscribed from the Launchpad API module --- Lib/Launchpad/App/Launchpad.ahk | 23 --------------- Lib/Shared/Includes.ahk | 2 ++ .../LaunchpadApiSubscriber.ahk | 27 ++++++++++++++++++ Lib/Shared/Volantis.App/App/AppBase.ahk | 28 ++++++++++++++++--- .../Volantis.App/Event/ReleaseInfoEvent.ahk | 23 +++++++++++++++ Lib/Shared/Volantis.App/Events/Events.ahk | 2 ++ 6 files changed, 78 insertions(+), 27 deletions(-) create mode 100644 Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk create mode 100644 Lib/Shared/Volantis.App/Event/ReleaseInfoEvent.ahk diff --git a/Lib/Launchpad/App/Launchpad.ahk b/Lib/Launchpad/App/Launchpad.ahk index 661c64f3..8ab4a97d 100644 --- a/Lib/Launchpad/App/Launchpad.ahk +++ b/Lib/Launchpad/App/Launchpad.ahk @@ -1,29 +1,6 @@ class Launchpad extends AppBase { detectGames := false - CheckForUpdates(notify := true) { - updateAvailable := false - - if (this.Version != "{{VERSION}}" && this["manager.data_source"].GetDefaultDataSource()) { - dataSource := this["manager.data_source"].GetDefaultDataSource() - releaseInfoStr := dataSource.ReadItem("release-info") - - if (releaseInfoStr) { - data := JsonData() - releaseInfo := data.FromString(&releaseInfoStr) - - if (releaseInfo && releaseInfo["data"].Has("version") && releaseInfo["data"]["version"] && this["version_checker"].VersionIsOutdated(releaseInfo["data"]["version"], this.Version)) { - updateAvailable := true - this["manager.gui"].Dialog(Map("type", "UpdateAvailableWindow"), releaseInfo) - } - } - } - - if (!updateAvailable && notify) { - this["notifier"].Info("You're running the latest version of Launchpad. Shiny!") - } - } - UpdateIncludes() { this.RunAhkScript(this.appDir . "\Scripts\UpdateIncludes.ahk") this.RestartApp() diff --git a/Lib/Shared/Includes.ahk b/Lib/Shared/Includes.ahk index 392be1b7..1dad575c 100644 --- a/Lib/Shared/Includes.ahk +++ b/Lib/Shared/Includes.ahk @@ -1,5 +1,6 @@ ; Automatically-generated file. Manual edits will be overwritten. #Include Modules\LaunchpadApi\DataSource\ApiDataSource.ahk +#Include Modules\LaunchpadApi\EventSubscriber\LaunchpadApiSubscriber.ahk #Include Modules\WebServices\Entity\WebServiceEntity.ahk #Include Modules\WebServices\Entity\WebServiceProviderEntity.ahk #Include Modules\WebServices\Event\WebServiceRequestEvent.ahk @@ -57,6 +58,7 @@ #Include Volantis.App\Event\MenuItemsEvent.ahk #Include Volantis.App\Event\MenuResultEvent.ahk #Include Volantis.App\Event\RegisterComponentsEvent.ahk +#Include Volantis.App\Event\ReleaseInfoEvent.ahk #Include Volantis.App\Event\ServiceDefinitionsEvent.ahk #Include Volantis.App\Events\Events.ahk #Include Volantis.App\Exception\AppException.ahk diff --git a/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk b/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk new file mode 100644 index 00000000..5d47081a --- /dev/null +++ b/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk @@ -0,0 +1,27 @@ +class LaunchpadApiSubscriber extends EventSubscriberBase { + GetEventSubscribers() { + return Map( + Events.APP_GET_RELEASE_INFO, [ + ObjBindMethod(this, "GetReleaseInfo") + ], + ) + } + + GetReleaseInfo(event, extra, eventName, hwnd) { + releaseInfo := event.ReleaseInfo + + if (!event.ReleaseInfo.Count && this.App.Version != "{{VERSION}}") { + webService := this.App["entity_manager.web_service"]["launchpad_api"] + + if (webService["Enabled"]) { + releaseInfo := webService.AdapterRequest("", "release_info", "read", 1) + + if (releaseInfo && releaseInfo.Has("data")) { + for key, val in releaseInfo["data"] { + event.ReleaseInfo[key] = val + } + } + } + } + } +} diff --git a/Lib/Shared/Volantis.App/App/AppBase.ahk b/Lib/Shared/Volantis.App/App/AppBase.ahk index 5b75f081..07f03fa2 100644 --- a/Lib/Shared/Volantis.App/App/AppBase.ahk +++ b/Lib/Shared/Volantis.App/App/AppBase.ahk @@ -708,10 +708,6 @@ class AppBase { this.isSetup := true } - CheckForUpdates(notify := true) { - ; Optional method to override - } - ShowTrayMenu() { menuItems := [] menuItems.Push(Map("label", "Open " . this.appName, "name", "OpenApp")) @@ -954,4 +950,28 @@ class AppBase { Run(websiteUrl) } } + + CheckForUpdates(notify := true) { + if (this.Parameter["app.supports_update_check"]) { + updateAvailable := false + + event := ReleaseInfoEvent(Events.APP_GET_RELEASE_INFO, this) + this["manager.event"].DispatchEvent(event) + releaseInfo := event.ReleaseInfo + + if ( + releaseInfo + && releaseInfo.Has("version") + && releaseInfo["version"] + && this["version_checker"].VersionIsOutdated(releaseInfo["version"], this.Version) + ) { + updateAvailable := true + this["manager.gui"].Dialog(Map("type", "UpdateAvailableWindow"), releaseInfo) + } + + if (!updateAvailable && notify) { + this["notifier"].Info("You're running the latest version of " . this.appName . ". Shiny!") + } + } + } } diff --git a/Lib/Shared/Volantis.App/Event/ReleaseInfoEvent.ahk b/Lib/Shared/Volantis.App/Event/ReleaseInfoEvent.ahk new file mode 100644 index 00000000..84af7d5c --- /dev/null +++ b/Lib/Shared/Volantis.App/Event/ReleaseInfoEvent.ahk @@ -0,0 +1,23 @@ +class ReleaseInfoEvent extends EventBase { + appObj := "" + releaseInfoObj := Map() + + App { + get => this.appObj + } + + ReleaseInfo { + get => this.releaseInfoObj + set => this.releaseInfoObj := value + } + + __New(eventName, app, releaseInfo := "") { + this.appObj := app + + if (releaseInfo) { + this.releaseInfoObj := releaseInfo + } + + super.__New(eventName) + } +} diff --git a/Lib/Shared/Volantis.App/Events/Events.ahk b/Lib/Shared/Volantis.App/Events/Events.ahk index 0ba53063..74ee9b65 100644 --- a/Lib/Shared/Volantis.App/Events/Events.ahk +++ b/Lib/Shared/Volantis.App/Events/Events.ahk @@ -27,5 +27,7 @@ class Events { static APP_MENU_ABOUT_ITEMS_ALTER := 0x1040 static APP_MENU_PROCESS_RESULT := 0x1042 + static APP_GET_RELEASE_INFO := 0x1050 + static AHK_NOTIFYICON := 0x404 } From b31c437d5dc3b749a0d88d171f9c930f36a83e23 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Tue, 13 Dec 2022 18:50:20 -0500 Subject: [PATCH 072/227] Only enable app.supports_update_check if the LaunchpadApi module is enabled --- Launchpad.services.json | 1 - Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/Launchpad.services.json b/Launchpad.services.json index f7c00607..a4fff85b 100644 --- a/Launchpad.services.json +++ b/Launchpad.services.json @@ -5,7 +5,6 @@ "app.developer": "Volantis Development", "app.has_settings": true, "app.settings_window": "SettingsWindow", - "app.supports_update_check": true, "app.show_about_menu_item": true, "app.about_window": "AboutWindow", "app.show_website_menu_item": true, diff --git a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json index ba9386f9..2830ad34 100644 --- a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json +++ b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json @@ -15,6 +15,7 @@ "dependencies": ["WebServices"] }, "parameters": { + "app.supports_update_check": true, "config.data_source_key": "launchpad_api", "config.api_authentication": true, "web_services.providers.launchpad_api": { From ecc6e2e82bf27c16e35696653a9267b45e5057ba Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Wed, 14 Dec 2022 02:50:32 -0500 Subject: [PATCH 073/227] Rename and add to entity data events --- Lib/Shared/Includes.ahk | 4 ++- .../Event/EntityDetectValuesEvent.ahk | 17 ++++++++++ .../Event/EntityLayerSourcesEvent.ahk | 13 ++++++++ .../Volantis.Entity/Event/EntityListEvent.ahk | 32 +++++++++++++++++++ .../Event/EntityStorageEvent.ahk | 13 -------- .../Volantis.Entity/Events/EntityEvents.ahk | 7 ++-- .../LayeredData/EntityData.ahk | 14 ++++---- 7 files changed, 77 insertions(+), 23 deletions(-) create mode 100644 Lib/Shared/Volantis.Entity/Event/EntityDetectValuesEvent.ahk create mode 100644 Lib/Shared/Volantis.Entity/Event/EntityLayerSourcesEvent.ahk create mode 100644 Lib/Shared/Volantis.Entity/Event/EntityListEvent.ahk delete mode 100644 Lib/Shared/Volantis.Entity/Event/EntityStorageEvent.ahk diff --git a/Lib/Shared/Includes.ahk b/Lib/Shared/Includes.ahk index 1dad575c..4f961270 100644 --- a/Lib/Shared/Includes.ahk +++ b/Lib/Shared/Includes.ahk @@ -232,13 +232,15 @@ #Include Volantis.Entity\EntityType\BasicEntityType.ahk #Include Volantis.Entity\EntityType\EntityTypeBase.ahk #Include Volantis.Entity\Event\EntityDataProcessorsEvent.ahk +#Include Volantis.Entity\Event\EntityDetectValuesEvent.ahk #Include Volantis.Entity\Event\EntityEvent.ahk #Include Volantis.Entity\Event\EntityFieldDefinitionsEvent.ahk #Include Volantis.Entity\Event\EntityFieldGroupsEvent.ahk #Include Volantis.Entity\Event\EntityLayersEvent.ahk +#Include Volantis.Entity\Event\EntityLayerSourcesEvent.ahk +#Include Volantis.Entity\Event\EntityListEvent.ahk #Include Volantis.Entity\Event\EntityReferenceEvent.ahk #Include Volantis.Entity\Event\EntityRefreshEvent.ahk -#Include Volantis.Entity\Event\EntityStorageEvent.ahk #Include Volantis.Entity\Event\EntityValidateEvent.ahk #Include Volantis.Entity\Events\EntityEvents.ahk #Include Volantis.Entity\Exception\EntityException.ahk diff --git a/Lib/Shared/Volantis.Entity/Event/EntityDetectValuesEvent.ahk b/Lib/Shared/Volantis.Entity/Event/EntityDetectValuesEvent.ahk new file mode 100644 index 00000000..2a5bf92c --- /dev/null +++ b/Lib/Shared/Volantis.Entity/Event/EntityDetectValuesEvent.ahk @@ -0,0 +1,17 @@ +class EntityDetectValuesEvent extends EntityEvent { + _valuesMap := "" + + Values { + get => this._valuesMap + } + + __New(eventName, entityTypeId, entityObj, values := "") { + if (!values) { + values := Map() + } + + this._valuesMap := values + + super.__New(eventName, entityTypeId, entityObj) + } +} diff --git a/Lib/Shared/Volantis.Entity/Event/EntityLayerSourcesEvent.ahk b/Lib/Shared/Volantis.Entity/Event/EntityLayerSourcesEvent.ahk new file mode 100644 index 00000000..f8431925 --- /dev/null +++ b/Lib/Shared/Volantis.Entity/Event/EntityLayerSourcesEvent.ahk @@ -0,0 +1,13 @@ +class EntityLayerSourcesEvent extends EntityEvent { + _layerSourcesObj := "" + + LayerSources { + get => this._layerSourcesObj + } + + __New(eventName, entityTypeId, entityObj, layerSourcesObj) { + this._layerSourcesObj := layerSourcesObj + + super.__New(eventName, entityTypeId, entityObj) + } +} diff --git a/Lib/Shared/Volantis.Entity/Event/EntityListEvent.ahk b/Lib/Shared/Volantis.Entity/Event/EntityListEvent.ahk new file mode 100644 index 00000000..9782b0be --- /dev/null +++ b/Lib/Shared/Volantis.Entity/Event/EntityListEvent.ahk @@ -0,0 +1,32 @@ +class EntityListEvent extends EventBase { + _entityTypeId := "" + _entityList := [] + _includeManaged := false + _includeExtended := false + + __New(eventName, entityTypeId, entityList, includeManaged, includeExtended) { + this._entityTypeId := entityTypeId + this._entityList := entityList + this._includeManaged := includeManaged + this._includeExtended := includeExtended + + super.__New(eventName) + } + + EntityTypeId { + get => this._entityTypeId + } + + EntityList { + get => this._entityList + set => this._entityList := value + } + + IncludeManaged { + get => this._includeManaged + } + + IncludeExtended { + get => this._includeExtended + } +} diff --git a/Lib/Shared/Volantis.Entity/Event/EntityStorageEvent.ahk b/Lib/Shared/Volantis.Entity/Event/EntityStorageEvent.ahk deleted file mode 100644 index 643d20fe..00000000 --- a/Lib/Shared/Volantis.Entity/Event/EntityStorageEvent.ahk +++ /dev/null @@ -1,13 +0,0 @@ -class EntityStorageEvent extends EntityEvent { - _storageObj := "" - - Storage { - get => this._storageObj - } - - __New(eventName, entityTypeId, entityObj, storageObj) { - this._storageObj := storageObj - - super.__New(eventName, entityTypeId, entityObj) - } -} diff --git a/Lib/Shared/Volantis.Entity/Events/EntityEvents.ahk b/Lib/Shared/Volantis.Entity/Events/EntityEvents.ahk index 1076b822..3e0a883b 100644 --- a/Lib/Shared/Volantis.Entity/Events/EntityEvents.ahk +++ b/Lib/Shared/Volantis.Entity/Events/EntityEvents.ahk @@ -1,6 +1,6 @@ class EntityEvents { - static ENTITY_STORAGE_OBJECTS := 0x4020 - static ENTITY_STORAGE_OBJECTS_ALTER := 0x4022 + static ENTITY_LAYER_SOURCES := 0x4020 + static ENTITY_LAYER_SOURCES_ALTER := 0x4022 static ENTITY_DATA_PROCESSORS := 0x4030 static ENTITY_DATA_PROCESSORS_ALTER := 0x4032 static ENTITY_PREPARE := 0x4035 @@ -15,10 +15,13 @@ class EntityEvents { static ENTITY_REFRESH := 0x4065 static ENTITY_DATA_LAYERS := 0x4070 static ENTITY_DATA_LAYERS_ALTER := 0x4071 + static ENTITY_DETECT_VALUES := 0x4075 + static ENTITY_DETECT_VALUES_ALTER := 0x4076 static ENTITY_VALIDATE := 0x4080 static ENTITY_FIELD_DEFINITIONS := 0x4085 static ENTITY_FIELD_DEFINITIONS_ALTER := 0x4087 static ENTITY_FIELD_GROUPS := 0x4090 static ENTITY_FIELD_GROUPS_ALTER := 0x4092 static ENTITY_REFERENCE_ENTITY_SAVED := 0x4095 + static ENTITY_LIST_ENTITIES := 0x4098 } diff --git a/Lib/Shared/Volantis.Entity/LayeredData/EntityData.ahk b/Lib/Shared/Volantis.Entity/LayeredData/EntityData.ahk index 2a3c041d..3086142b 100644 --- a/Lib/Shared/Volantis.Entity/LayeredData/EntityData.ahk +++ b/Lib/Shared/Volantis.Entity/LayeredData/EntityData.ahk @@ -11,23 +11,23 @@ class EntityData extends LayeredDataBase { super.__New( entity.cloner, this._createProcessors(), - this._getLayerNames(layerNames), - this._collectEntityStorage(layerSources) + this._collectLayerNames(layerNames), + this._collectSources(layerSources) ) } - _collectEntityStorage(layerSources) { + _collectSources(layerSources) { if (!layerSources.Has("defaults")) { layerSources["defaults"] := ObjBindMethod(this.entity, "InitializeDefaults") } - event := EntityStorageEvent(EntityEvents.ENTITY_STORAGE_OBJECTS, this.entityTypeId, this.entity, layerSources) + event := EntityLayerSourcesEvent(EntityEvents.ENTITY_LAYER_SOURCES, this.entityTypeId, this.entity, layerSources) this.eventMgr.DispatchEvent(event) - event := EntityStorageEvent(EntityEvents.ENTITY_STORAGE_OBJECTS_ALTER, this.entityTypeId, this.entity, event.Storage) + event := EntityLayerSourcesEvent(EntityEvents.ENTITY_LAYER_SOURCES_ALTER, this.entityTypeId, this.entity, event.LayerSources) this.eventMgr.DispatchEvent(event) - return event.Storage + return event.LayerSources } _createProcessors() { @@ -45,7 +45,7 @@ class EntityData extends LayeredDataBase { return event.Processors } - _getLayerNames(layerNames) { + _collectLayerNames(layerNames) { if (!layerNames) { layerNames := [] } From e4d0db897ad0ce8b05a8d12d9af8a3d24e17f8d4 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Wed, 14 Dec 2022 02:50:53 -0500 Subject: [PATCH 074/227] wording change --- Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk b/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk index 56d06d51..423b9889 100644 --- a/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk +++ b/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk @@ -8,7 +8,7 @@ * * Example: * - Layer 1: Initial defaults - * - Layer 2: Defaults from datasource + * - Layer 2: Defaults from external sources * - Layer 3: Auto-detected defaults * - Layer 4: User configuration values * - Processor 1: Token expander From 3eb32cc5ad686b640515de83b67c80c45f4e0954 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Wed, 14 Dec 2022 03:02:09 -0500 Subject: [PATCH 075/227] Remove api_authentication and data_source_key config parameters --- Launchpad.services.json | 2 -- Lib/Launchpad/Gui/Form/SettingsWindow.ahk | 4 ---- Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk | 2 -- Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json | 2 -- 4 files changed, 10 deletions(-) diff --git a/Launchpad.services.json b/Launchpad.services.json index a4fff85b..f4f23a8d 100644 --- a/Launchpad.services.json +++ b/Launchpad.services.json @@ -9,7 +9,6 @@ "app.about_window": "AboutWindow", "app.show_website_menu_item": true, "backups_config": {}, - "config.api_authentication": false, "config.assets_dir": "@@{data_dir}\\Launcher Assets", "config.auto_backup_config_files": true, "config.backups_to_keep": 5, @@ -18,7 +17,6 @@ "config.clean_launchers_on_build": false, "config.clean_launchers_on_exit": true, "config.create_desktop_shortcuts": true, - "config.data_source_key": "launchpad_api", "config.default_launcher_theme": "", "config.destination_dir": "@@{data_dir}\\Launchers", "config.launcher_double_click_action": "Edit", diff --git a/Lib/Launchpad/Gui/Form/SettingsWindow.ahk b/Lib/Launchpad/Gui/Form/SettingsWindow.ahk index 605f639f..ecf2c0cd 100644 --- a/Lib/Launchpad/Gui/Form/SettingsWindow.ahk +++ b/Lib/Launchpad/Gui/Form/SettingsWindow.ahk @@ -122,10 +122,6 @@ ctl := this.guiObj.AddDDL("vlogging_level xs y+m Choose" . chosen . " w200 c" . this.themeObj.GetColor("editText"), this.container.Get("logger").GetLogLevels()) ctl.OnEvent("Change", "OnLoggingLevelChange") - this.AddHeading("API Settings") - ctl := this.AddConfigCheckBox("Enable API login for enhanced functionality", "api_authentication") - ctl.ctl.NeedsRestart := true - tabs.UseTab() closeW := 100 diff --git a/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk b/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk index 13f7d8a8..4b82e62d 100644 --- a/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk +++ b/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk @@ -2,8 +2,6 @@ class LaunchpadBuilder extends AppBase { GetParameterDefinitions(config) { parameters := super.GetParameterDefinitions(config) parameters["config_path"] := this.appDir . "\Launchpad.build.json" - parameters["config.data_source_key"] := "" - parameters["config.api_authentication"] := true parameters["config.dist_dir"] := this.appDir . "\Dist" parameters["config.build_dir"] := this.appDir . "\Build" parameters["config.icon_file"] := this.appDir . "\Resources\Graphics\launchpad.ico" diff --git a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json index 2830ad34..ce2d9c31 100644 --- a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json +++ b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json @@ -16,8 +16,6 @@ }, "parameters": { "app.supports_update_check": true, - "config.data_source_key": "launchpad_api", - "config.api_authentication": true, "web_services.providers.launchpad_api": { "name": "Launchpad API", "EndpointUrl": "https://api.launchpad.games/v1", From db232b16d9dbdaf68a7d593b362be2a104a9e72f Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Wed, 14 Dec 2022 03:07:12 -0500 Subject: [PATCH 076/227] Remove unused dataSource assignments --- Lib/Launchpad/Gui/Dialog/LoginWindow.ahk | 1 - .../WebServices/Gui/AuthenticationGui/LaunchpadLoginWindow.ahk | 1 - Lib/Shared/Volantis.App/Gui/Dialog/EntityDeleteWindow.ahk | 1 - 3 files changed, 3 deletions(-) diff --git a/Lib/Launchpad/Gui/Dialog/LoginWindow.ahk b/Lib/Launchpad/Gui/Dialog/LoginWindow.ahk index 66b256e0..7edc0d12 100644 --- a/Lib/Launchpad/Gui/Dialog/LoginWindow.ahk +++ b/Lib/Launchpad/Gui/Dialog/LoginWindow.ahk @@ -2,7 +2,6 @@ entityObj := "" entityManager := "" missingFields := Map() - dataSource := "" GetDefaultConfig(container, config) { defaults := super.GetDefaultConfig(container, config) diff --git a/Lib/Shared/Modules/WebServices/Gui/AuthenticationGui/LaunchpadLoginWindow.ahk b/Lib/Shared/Modules/WebServices/Gui/AuthenticationGui/LaunchpadLoginWindow.ahk index d6ef8ff1..0145f0cb 100644 --- a/Lib/Shared/Modules/WebServices/Gui/AuthenticationGui/LaunchpadLoginWindow.ahk +++ b/Lib/Shared/Modules/WebServices/Gui/AuthenticationGui/LaunchpadLoginWindow.ahk @@ -2,7 +2,6 @@ entityObj := "" entityManager := "" missingFields := Map() - dataSource := "" GetDefaultConfig(container, config) { defaults := super.GetDefaultConfig(container, config) diff --git a/Lib/Shared/Volantis.App/Gui/Dialog/EntityDeleteWindow.ahk b/Lib/Shared/Volantis.App/Gui/Dialog/EntityDeleteWindow.ahk index 73d274dc..f5a6a6cd 100644 --- a/Lib/Shared/Volantis.App/Gui/Dialog/EntityDeleteWindow.ahk +++ b/Lib/Shared/Volantis.App/Gui/Dialog/EntityDeleteWindow.ahk @@ -2,7 +2,6 @@ entityObj := "" entityManager := "" missingFields := Map() - dataSource := "" __New(container, themeObj, config, entityObj, entityManager) { this.entityObj := entityObj From 484b200f47d9715295b8e54fb6c2ce05ebc3fb35 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Wed, 14 Dec 2022 03:30:45 -0500 Subject: [PATCH 077/227] Add entity_lookup data type to WebServices module --- Lib/Shared/Modules/WebServices/WebServices.module.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/Shared/Modules/WebServices/WebServices.module.json b/Lib/Shared/Modules/WebServices/WebServices.module.json index ec745348..00854c63 100644 --- a/Lib/Shared/Modules/WebServices/WebServices.module.json +++ b/Lib/Shared/Modules/WebServices/WebServices.module.json @@ -66,6 +66,10 @@ "name": "Entity List", "description": "A listing of entities from a web service." }, + "web_services.data_types.entity_lookup": { + "name": "Entity Lookup", + "description": "Searches for the ID of a remote entity from the provided data." + }, "web_services.data_types.entity_data": { "name": "Entity Data", "description": "Data to be imported into an entity within the application." From 8cc332f2fe1b6917f6b54b9075fb2552df618723 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Wed, 14 Dec 2022 03:33:44 -0500 Subject: [PATCH 078/227] Lots of interconnected changes to replace DataSource and DSItem concepts with WebServices --- Launchpad.services.json | 4 - Lib/Launchpad/Entity/LauncherEntity.ahk | 99 +--- Lib/Launchpad/Entity/ManagedEntityBase.ahk | 39 +- Lib/Launchpad/Entity/ManagedGameEntity.ahk | 3 +- .../Entity/ManagedLauncherEntity.ahk | 1 - Lib/Launchpad/Entity/ManagedProcessEntity.ahk | 533 ++++++++++++++++++ Lib/Launchpad/Entity/PlatformEntity.ahk | 3 +- .../GamePlatform/GamePlatformBase.ahk | 60 +- Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk | 54 +- .../Gui/Form/LauncherCreateFormBase.ahk | 10 +- .../Gui/ManageWindow/DetectedGamesWindow.ahk | 4 +- Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk | 12 +- Lib/Launchpad/Includes.ahk | 1 + Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk | 5 - Lib/Shared/Includes.ahk | 9 - .../LaunchpadApi/DataSource/ApiDataSource.ahk | 121 ---- .../LaunchpadApiSubscriber.ahk | 234 +++++++- .../LaunchpadApi/LaunchpadApi.module.json | 19 +- .../WebServices/Entity/WebServiceEntity.ahk | 25 +- .../Entity/WebServiceProviderEntity.ahk | 4 +- .../WebServices/Gui/Form/FeedbackWindow.ahk | 2 +- .../WebServiceAdapterBase.ahk | 103 +++- .../WebServiceRequestBase.ahk | 4 + .../DataSource/DataSourceBase.ahk | 70 --- .../DataSourceItem/DSAssetFile.ahk | 3 - .../Volantis.App/DataSourceItem/DSFile.ahk | 7 - .../Volantis.App/DataSourceItem/DSJson.ahk | 17 - .../Volantis.App/DataSourceItem/DSListing.ahk | 7 - .../DataSourceItem/DataSourceItemBase.ahk | 53 -- .../Volantis.App/Entity/AppEntityBase.ahk | 204 ------- .../Volantis.App/Entity/BackupEntity.ahk | 13 +- Lib/Shared/Volantis.App/Entity/TaskEntity.ahk | 2 +- .../Volantis.App/Gui/Dialog/ErrorDialog.ahk | 32 +- .../Gui/Dialog/UpdateAvailableWindow.ahk | 8 +- .../Gui/EntityEditor/EntityEditorBase.ahk | 6 - .../Volantis.App/GuiControl/EntityControl.ahk | 4 +- .../ComponentManager/DataSourceManager.ahk | 61 -- .../Volantis.Entity/Entity/EntityBase.ahk | 71 ++- .../Entity/FieldableEntity.ahk | 23 +- .../EntityManager/EntityManagerBase.ahk | 26 + .../EntityStorage/EntityStorageBase.ahk | 2 +- 41 files changed, 1143 insertions(+), 815 deletions(-) create mode 100644 Lib/Launchpad/Entity/ManagedProcessEntity.ahk delete mode 100644 Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk delete mode 100644 Lib/Shared/Volantis.App/DataSource/DataSourceBase.ahk delete mode 100644 Lib/Shared/Volantis.App/DataSourceItem/DSAssetFile.ahk delete mode 100644 Lib/Shared/Volantis.App/DataSourceItem/DSFile.ahk delete mode 100644 Lib/Shared/Volantis.App/DataSourceItem/DSJson.ahk delete mode 100644 Lib/Shared/Volantis.App/DataSourceItem/DSListing.ahk delete mode 100644 Lib/Shared/Volantis.App/DataSourceItem/DataSourceItemBase.ahk delete mode 100644 Lib/Shared/Volantis.App/Entity/AppEntityBase.ahk delete mode 100644 Lib/Shared/Volantis.App/Service/ComponentManager/DataSourceManager.ahk diff --git a/Launchpad.services.json b/Launchpad.services.json index f4f23a8d..bf32f18e 100644 --- a/Launchpad.services.json +++ b/Launchpad.services.json @@ -131,10 +131,6 @@ "class": "BuilderManager", "arguments": ["@entity_manager.launcher", "@{}", "@manager.event", "@notifier"] }, - "manager.data_source": { - "class": "DataSourceManager", - "arguments": ["@{}", "@manager.event", "@notifier", "@@config.data_source_key"] - }, "state.app": { "class": "LaunchpadAppState", "arguments": ["@{App}", "@@state_path"] diff --git a/Lib/Launchpad/Entity/LauncherEntity.ahk b/Lib/Launchpad/Entity/LauncherEntity.ahk index 9477392c..ab663653 100644 --- a/Lib/Launchpad/Entity/LauncherEntity.ahk +++ b/Lib/Launchpad/Entity/LauncherEntity.ahk @@ -1,5 +1,4 @@ -class LauncherEntity extends AppEntityBase { - dataSourcePath := "games" +class LauncherEntity extends FieldableEntity { configPrefix := "Launcher" additionalManagedLauncherDefaults := Map() @@ -29,6 +28,11 @@ class LauncherEntity extends AppEntityBase { "weight", 80 ) + groups["advanced"] := Map( + "name", "Advanced", + "weight", 100 + ) + return groups } @@ -47,18 +51,12 @@ class LauncherEntity extends AppEntityBase { definitions["id"]["modes"]["wizard"]["formField"] := true definitions["id"]["modes"]["wizard"]["widget"] := "combo" - definitions["id"]["modes"]["wizard"]["selectOptionsCallback"] := ObjBindMethod(this, "ListKnownGames") + definitions["id"]["modes"]["wizard"]["selectOptionsCallback"] := ObjBindMethod(this, "ListEntities", false, true) definitions["id"]["modes"]["wizard"]["description"] := "Select an existing game from the API, or enter a custom game key to create your own." definitions["name"]["description"] := "You can change the display name of the game if it differs from the key." definitions["name"]["help"] := "The launcher filename will still be created using the key." - if (definitions.Has("DataSourceItemKey")) { - definitions["DataSourceItemKey"]["default"] := "" - definitions["DataSourceItemKey"]["description"] := "The key to use when looking this item up in its data source(s)." - definitions["DataSourceItemKey"]["help"] := "By default, this is the same as the main key." - } - definitions["Platform"] := Map( "type", "entity_reference", "entityType", "platform", @@ -295,6 +293,15 @@ class LauncherEntity extends AppEntityBase { "help", "If the Steam Overlay attaches within this time, and the Force option is not active, then the Launchpad Overlay will not be used." ) + definitions["AssetsDir"] := Map( + "type", "directory", + "description", "The directory where any required assets for this launcher will be saved.", + "default", this.app.Config["assets_dir"] . "\" . this.Id, + "group", "advanced", + "formField", false, + "modes", Map("simple", Map("formField", false)) + ) + return definitions } @@ -305,13 +312,6 @@ class LauncherEntity extends AppEntityBase { return (FileExist(this.GetLauncherFile(this.Id, checkSourceFile)) != "") } - ListKnownGames() { - return this.container - .Get("manager.data_source") - .GetDefaultDataSource() - .ReadListing("game-keys") - } - LauncherIsOutdated() { outdated := true @@ -384,71 +384,12 @@ class LauncherEntity extends AppEntityBase { this.app.State.SetLauncherConfigInfo(this.Id) } - DiscoverDataSourceItemKey() { - if (!this["DataSourceItemKey"]) { - dataSources := this.GetAllDataSources() - - for index, dataSource in dataSources { - platform := this["Platform"] ? this["Platform"]["id"] : "" - apiPath := "lookup/" this.Id - - if (platform) { - apiPath .= "/" . platform - } - - dsData := dataSource.ReadJson(apiPath) - - if (dsData != "" && dsData.Has("id") && dsData["id"]) { - this["DataSourceItemKey"] := dsData["id"] - break - } - } - } - - if (this["DataSourceItemKey"]) { - return this["DataSourceItemKey"] - } else { - return "" - } - } - IconFileExists() { - iconSrc := this["IconSrc"] != "" ? this["IconSrc"] : this.GetAssetPath(this.Id . ".ico") - return FileExist(iconSrc) - } - - MergeAdditionalDataSourceDefaults(defaults, dataSourceData) { - launcherType := this.DetectLauncherType(defaults, dataSourceData) - - checkType := (launcherType == "") ? "Default" : launcherType - if (dataSourceData.Has("Launchers") && dataSourceData["Launchers"].Has(checkType) && HasBase(dataSourceData["Launchers"][checkType], Map.Prototype)) { - this.additionalManagedLauncherDefaults := this.merger.Merge(dataSourceData["Launchers"][checkType], this.additionalManagedLauncherDefaults) - defaults := this.merger.Merge(defaults, dataSourceData["Launchers"][checkType]) - } - - defaults["ManagedLauncher"] := launcherType + iconSrc := (this["IconSrc"] != "") + ? this["IconSrc"] + : this["AssetsDir"] . "\" . this.Id . ".ico" - return defaults - } - - DetectLauncherType(defaults, dataSourceData := "") { - launcherType := "" - - if (this.UnmergedFieldData.Has("LauncherType")) { - launcherType := this.UnmergedFieldData["LauncherType"] - } else if (defaults.Has("LauncherType")) { - launcherType := defaults["LauncherType"] - } - - if (launcherType == "") { - launcherType := "Default" - } - - if (dataSourceData != "" && dataSourceData.Has("Launchers")) { - launcherType := this._dereferenceKey(launcherType, dataSourceData["Launchers"]) - } - - return launcherType + return FileExist(iconSrc) } _dereferenceKey(key, map) { diff --git a/Lib/Launchpad/Entity/ManagedEntityBase.ahk b/Lib/Launchpad/Entity/ManagedEntityBase.ahk index 99926db4..b7be2261 100644 --- a/Lib/Launchpad/Entity/ManagedEntityBase.ahk +++ b/Lib/Launchpad/Entity/ManagedEntityBase.ahk @@ -1,4 +1,4 @@ -class ManagedEntityBase extends AppEntityBase { +class ManagedEntityBase extends FieldableEntity { defaultType := "Default" defaultClass := "Default" @@ -54,7 +54,7 @@ class ManagedEntityBase extends AppEntityBase { "required", true, "storageKey", this.configPrefix . "Type", "widget", "select", - "selectOptionsCallback", ObjBindMethod(this, "ListEntityTypes"), + "selectOptionsCallback", ObjBindMethod(this, "ListEntities", false, true), "group", "general" ) @@ -316,10 +316,6 @@ class ManagedEntityBase extends AppEntityBase { return "" } - DiscoverDataSourceItemKey() { - return this["EntityType"] - } - AutoDetectValues(recurse := true) { detectedValues := super.AutoDetectValues(recurse) processId := "" @@ -347,31 +343,6 @@ class ManagedEntityBase extends AppEntityBase { return detectedValues } - ListEntityTypes() { - types := [] - dataSources := this.GetAllDataSources() - dsPath := this.GetDataSourceItemPath() - - for index, dataSource in dataSources { - for listingIndex, listingItem in dataSource.ReadListing(dsPath) { - exists := false - - for index, item in types { - if (item == listingItem) { - exists := true - break - } - } - - if (!exists) { - types.Push(listingItem) - } - } - } - - return types - } - ListRunTypes() { return [ "Command", @@ -428,12 +399,14 @@ class ManagedEntityBase extends AppEntityBase { } ShortcutFileExists() { - shortcutSrc := this["ShortcutSrc"] != "" ? this["ShortcutSrc"] : this.GetAssetPath(this.Id . ".lnk") + shortcutSrc := (this["ShortcutSrc"] != "") + ? this["ShortcutSrc"] + : this["AssetsDir"] . "\" . this.Id . ".lnk" exists := FileExist(shortcutSrc) if (!exists) { - shortcutSrc := this.GetAssetPath(this.Id . ".url") + shortcutSrc := this["AssetsDir"] . "\" . this.Id . ".url" exists := FileExist(shortcutSrc) } diff --git a/Lib/Launchpad/Entity/ManagedGameEntity.ahk b/Lib/Launchpad/Entity/ManagedGameEntity.ahk index 907991e6..235010cc 100644 --- a/Lib/Launchpad/Entity/ManagedGameEntity.ahk +++ b/Lib/Launchpad/Entity/ManagedGameEntity.ahk @@ -2,7 +2,6 @@ class ManagedGameEntity extends ManagedEntityBase { configPrefix := "Game" defaultType := "Default" defaultClass := "SimpleGame" - dataSourcePath := "game-types" BaseFieldDefinitions() { definitions := super.BaseFieldDefinitions() @@ -99,7 +98,7 @@ class ManagedGameEntity extends ManagedEntityBase { } if (this.ShouldDetectShortcutSrc(detectedValues)) { - basePath := this["AssetsDir"] . "\" . this.Id + basePath := this.ParentEntity["AssetsDir"] . "\" . this.Id shortcutSrc := "" if (FileExist(basePath . ".lnk")) { diff --git a/Lib/Launchpad/Entity/ManagedLauncherEntity.ahk b/Lib/Launchpad/Entity/ManagedLauncherEntity.ahk index c060cb91..12d5eb03 100644 --- a/Lib/Launchpad/Entity/ManagedLauncherEntity.ahk +++ b/Lib/Launchpad/Entity/ManagedLauncherEntity.ahk @@ -2,7 +2,6 @@ class ManagedLauncherEntity extends ManagedEntityBase { configPrefix := "Launcher" defaultType := "Default" defaultClass := "SimpleLauncher" - dataSourcePath := "launcher-types" BaseFieldDefinitions() { definitions := super.BaseFieldDefinitions() diff --git a/Lib/Launchpad/Entity/ManagedProcessEntity.ahk b/Lib/Launchpad/Entity/ManagedProcessEntity.ahk new file mode 100644 index 00000000..4ff6778c --- /dev/null +++ b/Lib/Launchpad/Entity/ManagedProcessEntity.ahk @@ -0,0 +1,533 @@ +class ManagedProcessEntity extends FieldableEntity { + defaultType := "Default" + defaultClass := "Default" + + DiscoverParentEntity(container, eventMgr, id, storageObj, idSanitizer) { + return container.Get("entity_manager.launcher")[id] + } + + GetDefaultFieldGroups() { + groups := super.GetDefaultFieldGroups() + + groups["locations"] := Map( + "name", "Locations", + "weight", 100 + ) + + groups["registry"] := Map( + "name", "Registry", + "weight", 125 + ) + + groups["process"] := Map( + "name", "Process", + "weight", 150 + ) + + return groups + } + + BaseFieldDefinitions() { + definitions := super.BaseFieldDefinitions() + + definitions["name"]["formField"] := false + + definitions["Launcher"] := Map( + "storageKey", "", + "type", "entity_reference", + "entityType", "launcher", + "required", true, + "formField", false, + "callbacks", Map( + "GetValue", ObjBindMethod(this, "GetId"), + "SetValue", ObjBindMethod(this, "SetId"), + "HasValue", ObjBindMethod(this, "HasId"), + "HasOverride", ObjBindMethod(this, "HasId"), + "IsEmpty", ObjBindMethod(this, "HasId", true), + "DeleteValue", "" + ) + ) + + definitions["EntityType"] := Map( + "default", this.defaultType, + "description", "The key of the managed type to load settings and defaults from.", + "required", true, + "storageKey", this.configPrefix . "Type", + "widget", "select", + "selectOptionsCallback", ObjBindMethod(this, "ListEntities", false, true), + "group", "general" + ) + + definitions["EntityClass"] := Map( + "default", this.defaultClass, + "description", "The name of the AHK class that will be used to control the managed entity.", + "formField", false, + "storageKey", this.configPrefix . "Class", + "required", true, + "group", "advanced", + "modes", Map( + "simple", Map("formField", false) + ), + ) + + definitions["SearchDirs"] := Map( + "type", "directory", + "mustExist", false, + "storageKey", this.configPrefix . "SearchDirs", + "default", [A_ProgramFiles], + "description", "Possible parent directories where the game's launcher might exist, to be used for auto-detection.", + "help", "These should be as specific as possible to reduce detection time.", + "multiple", true, + "group", "locations", + "modes", Map( + "simple", Map("formField", false) + ) + ) + + definitions["InstallDir"] := Map( + "type", "directory", + "mustExist", false, + "storageKey", this.configPrefix . "InstallDir", + "group", "locations", + "modes", Map( + "simple", Map("group", "general") + ), + "description", "Select the installation folder, or use default for auto-detection." + ) + + definitions["Exe"] := Map( + "type", "file", + "fileMask", "*.exe", + "mustExist", false, + "storageKey", this.configPrefix . "Exe", + "description", "This can be the full path on the system to the launcher's .exe file, or simply the name of the .exe file itself.", + "help", "If the .exe doesn't include the absolute path, auto-detection will be used by searching the DestinationDirs.", + "group", "locations", + "modes", Map( + "simple", Map("group", "general") + ) + ) + + ; Options include: + ; - Search (will search through each directory in SearchDirs until a match is found) + ; - BlizzardProductDb (will search Battle.net's product.db file if it can be located for the installation directory, and the file will be found from there + ; - Registry (will get a directory from the registry key specified by LocateRegKey and search for the file within it) + definitions["LocateMethod"] := Map( + "storageKey", this.configPrefix . "LocateMethod", + "default", "SearchDirs", + "description", "How to search for the .exe if it isn't a full path already", + "group", "general", + "modes", Map( + "simple", Map("formField", false) + ), + "widget", "select", + "selectOptionsCallback", ObjBindMethod(this, "ListLocateMethods"), + "help", "Search: Searches a list of possible directories (Defaulting to some common possibilities) for the .exe file and uses that directory`nRegistry: Looks for the provided registry key and uses its value as the install path if present`nBlizzardProductDb: Searches for LauncherSpecificId within the Blizzard product.db file if present" + ) + + definitions["WindowTitle"] := Map( + "storageKey", this.configPrefix . "WindowTitle", + "group", "process" + ) + + definitions["LocateRegView"] := Map( + "storageKey", this.configPrefix . "LocateRegView", + "default", 64, + "group", "registry", + "widget", "select", + "selectOptionsCallback", ObjBindMethod(this, "ListRegViews"), + "description", "The registry view to use when locating the install dir.", + "modes", Map( + "simple", Map("formField", false) + ) + ) + + definitions["LocateRegKey"] := Map( + "storageKey", this.configPrefix . "LocateRegKey", + "group", "registry", + "description", "The registry key to look up the install dir within.", + "help", "Path parts should be separated with backslashes and must start with one of: HKEY_LOCAL_MACHINE, HKEY_USERS, HKEY_CURRENT_USER, HKEY_CLASSES_ROOT, HKEY_CURRENT_CONFIG, or the abbreviation of one of those. To read from a remote registry, prefix the root path with two backslashes and the computer name.`n`nSimple example: HKLM\Path\To\Key`nRemote example: \\OTHERPC\HKLM\Path\To\Key", + "modes", Map( + "simple", Map("formField", false) + ) + ) + + definitions["LocateRegValue"] := Map( + "storageKey", this.configPrefix . "LocateRegValue", + "group", "registry", + "description", "The name of the registry value to look up within the specified key.", + "help", "Example: InstallPath", + "modes", Map( + "simple", Map("formField", false) + ) + ) + + definitions["LocateRegRemovePrefix"] := Map( + "storageKey", this.configPrefix . "LocateRegRemovePrefix", + "group", "registry", + "modes", Map( + "simple", Map("formField", false) + ) + ) + + definitions["LocateRegRemoveSuffix"] := Map( + "storageKey", this.configPrefix . "LocateRegRemoveSuffix", + "group", "registry", + "modes", Map( + "simple", Map("formField", false) + ) + ) + + definitions["LocateRegStripQuotes"] := Map( + "storageKey", this.configPrefix . "LocateRegStripQuotes", + "default", false, + "group", "registry", + "description", "Strip quotes from registry value", + "modes", Map( + "simple", Map("formField", false) + ) + ) + + definitions["LauncherSpecificId"] := Map( + "storageKey", this.configPrefix . "LauncherSpecificId", + "description", "If the item is known to the launcher by a specific ID, it should be stored here.", + "group", "general" + ) + + definitions["WorkingDir"] := Map( + "type", "directory", + "description", "The directory that the launcher should be run from.", + "help", "If not set, it will be run without setting an explicit working directory, which is usually sufficient.", + "storageKey", this.configPrefix . "WorkingDir", + "group", "locations", + "modes", Map( + "simple", Map("formField", false) + ) + ) + + ; - Shortcut (Run a shortcut file) + ; - Command (Run a command directly, the default if required) + definitions["RunType"] := Map( + "description", "Which method to use for launching this item.", + "help", "This is only needed for launchers that have to manage their own process.", + "storageKey", this.configPrefix . "RunType", + "default", "Command", + "group", "process", + "widget", "select", + "selectOptionsCallback", ObjBindMethod(this, "ListRunTypes") + ) + + definitions["UsesShortcut"] := Map( + "type", "boolean", + "description", "Whether a shortcut file will be used when starting the internally-managed game launcher", + "formField", false, + "storageKey", this.configPrefix . "UsesShortcut" + ) + + definitions["ReplaceProcess"] := Map( + "type", "boolean", + "description", "Kill and re-launch the game process immediately after it is detected.", + "help", "This can be used to force Launchpad to own the game process, but won't for for every game.", + "storageKey", this.configPrefix . "ReplaceProcess", + "default", false, + "group", "process" + ) + + ; - The filename of an existing shortcut (.url or .lnk file, or even another .exe) that will be used to run the game. + ; - The path of another shortcut file (.url or .lnk) on the system, which will be copied to the AssetsDir if it doesn't already exist + ; - The path of an .exe file on the system to which a shortcut will be created in AssetsDir if it doesn't already exist. Using this option + ; is usually not necessary, since you can run the .exe directly instead. + definitions["ShortcutSrc"] := Map( + "description", "The shortcut file used to launch the game launcher itself.", + "help", "This is typically only needed if the Shortcut LauncherRunType is selected.", + "storageKey", this.configPrefix . "ShortcutSrc", + "group", "locations", + "modes", Map( + "simple", Map("group", "general") + ) + ) + + ; - RunWait (the default, uses RunWait to both run a process and wait until it completes in one step. This is most efficient if it works.) + ; - Run (Uses Run, then watches for the game window and waits until the window opens (if needed) and then closes) + ; - Scheduled (Creates an immediate scheduled task that runs the game, then waits until the window opens (if needed) and then closes) + definitions["RunMethod"] := Map( + "description", "Which method to use to run the RunCmd", + "storageKey", this.configPrefix . "RunMethod", + "default", "Run", + "group", "process", + "widget", "select", + "selectOptionsCallback", ObjBindMethod(this, "ListRunMethods") + ) + + ; - "Exe" (Waits for the game's .exe process to start if it hasn't already, and then waits for it to stop again. This is the default if the game type is not RunWait) + ; - "Title" (Waits for the game's window title to open if it isn't already, and then waits for it to close again) + ; - "Class" (Wait's for the game's window class to open if it isn't already, and then waits for it to close again) + definitions["ProcessType"] := Map( + "description", "Which method to use to wait for the game to close.", + "help", "This is not needed if the GameRunType is RunWait", + "storageKey", this.configPrefix . "ProcessType", + "default", "Exe", + "group", "process", + "widget", "select", + "selectOptionsCallback", ObjBindMethod(this, "ListProcessTypes") + ) + + ; - Exe - This value will default to the GameExe unless overridden + ; - Title - This value will default to the game's Key unless overridden + ; - Class - This value should be set to the game's window class + definitions["ProcessId"] := Map( + "help", "This value's type is dependent on the ProcessType above. It can often be detected from other values, and is not needed if the GameRunType is RunWait.", + "storageKey", this.configPrefix . "ProcessId", + "group", "process", + "modes", Map( + "simple", Map("formField", false) + ) + ) + + definitions["ProcessTimeout"] := Map( + "description", "The number of seconds to wait before giving up when waiting for a process.", + "storageKey", this.configPrefix . "ProcessTimeout", + "default", 30, + "group", "process", + "modes", Map( + "simple", Map("formField", false) + ) + ) + + definitions["RunCmd"] := Map( + "description", "The command that will be used to run the game's launcher.", + "help", "Typically only used if LauncherRunType is Command.", + "storageKey", this.configPrefix . "RunCmd", + "group", "process" + ) + + return definitions + } + + GetData() { + if (!this.ParentEntity) { + throw EntityException("A parent entity is required on type " . Type(this)) + } + + return this.ParentEntity.GetData() + } + + _createEntityData() { + return "" + } + + AutoDetectValues(recurse := true) { + detectedValues := super.AutoDetectValues(recurse) + processId := "" + usesShortcut := false + + if (this.GetData().HasValue(this.configPrefix . "UsesShortcut")) { + usesShortcut := this.GetData().GetValue(this.configPrefix . "UsesShortcut") + } else { + usesShortcut := (this["RunType"] == "Shortcut" || this["ShortcutSrc"] != "" || this["RunCmd"] == "") + } + + detectedValues[this.configPrefix . "UsesShortcut"] := usesShortcut + detectedValues[this.configPrefix . "RunType"] := usesShortcut ? "Shortcut" : "Command" + detectedValues[this.configPrefix . "InstallDir"] := this.LocateInstallDir() ; This needs to run to expand exes without a dir + + if (this["ProcessType"] == "Exe") { + SplitPath(this["Exe"], &processId) + } else if (this["ProcessType"] == "Title") { + processId := this["WindowTitle"] ? this["WindowTitle"] : this.Id + } + + detectedValues[this.configPrefix . "ProcessId"] := processId + detectedValues[this.configPrefix . "WorkingDir"] := this["InstallDir"] + + return detectedValues + } + + ListRunTypes() { + return [ + "Command", + "Shortcut" + ] + } + + ListProcessTypes() { + return [ + "Exe", "Title", "Class" + ] + } + + ListRunMethods() { + return [ + "Run", "Scheduled", "RunWait" + ] + } + + ListLocateMethods() { + return [ + "Search", "Registry", "BlizzardProductDb" + ] + } + + ListRegViews() { + regViews := [ + "32" + ] + + if (A_Is64bitOS) { + regViews.Push("64") + } + + return regViews + } + + Validate() { + validateResult := super.Validate() + + if (((this["UsesShortcut"] && this["RunCmd"] == "") && this["ShortcutSrc"] == "") && !this.ShortcutFileExists()) { + validateResult["success"] := false + validateResult["invalidFields"].push("ShortcutSrc") + } + + if (this["ShortcutSrc"] == "" && this["RunCmd"] == "") { + validateResult["success"] := false + validateResult["invalidFields"].push("RunCmd") + } + + ; TODO: Perform more launcher and game type validation here + + return validateResult + } + + ShortcutFileExists() { + shortcutSrc := (this["ShortcutSrc"] != "") + ? this["ShortcutSrc"] + : this["AssetsDir"] . "\" . this.Id . ".lnk" + + exists := FileExist(shortcutSrc) + + if (!exists) { + shortcutSrc := this["AssetsDir"] . "\" . this.Id . ".url" + exists := FileExist(shortcutSrc) + } + + return exists + } + + LocateInstallDir() { + installDir := "" + + ; TODO: Add additional methods to detect the install dir + + if (this["LocateMethod"] == "BlizzardProductDb") { + blizzardDir := this.GetBlizzardProductDir() + + if (blizzardDir != "") { + installDir := blizzardDir + } + } + + return installDir + } + + LocateExe() { + return this.LocateFile(this["Exe"]) + } + + LocateFile(filePattern) { + filePath := "" + + if (filePattern != "") { + SplitPath(filePattern,,,,, &fileDrive) + + if (fileDrive != "") { + filePath := filePattern + } else { + searchDirs := [] + + if (this["InstallDir"] != "") { + searchDirs.Push(this["InstallDir"]) + } else if (this["LocateMethod"] == "SearchDirs") { + if (HasBase(this["SearchDirs"], Array.Prototype) && this["SearchDirs"].Length > 0) { + for index, dir in this["SearchDirs"] { + searchDirs.Push(dir) + } + } + } else if (this["LocateMethod"] == "Registry") { + regKey := this["LocateRegKey"] + + if (regKey != "") { + SetRegView(this["LocateRegView"]) + regDir := RegRead(this["LocateRegKey"], this["LocateRegValue"]) + SetRegView("Default") + + if (regDir != "") { + if (this["LocateRegStripQuotes"]) { + regDir := StrReplace(regDir, "`"", "") + } + + if (this["LocateRegRemovePrefix"] && SubStr(regDir, 1, StrLen(this["LocateRegRemovePrefix"])) == this["LocateRegRemovePrefix"]) { + regDir := SubStr(regDir, StrLen(this["LocateRegRemovePrefix"]) + 1) + } + + if (this["LocateRegRemoveSuffix"] && SubStr(regDir, 1, StrLen(this["LocateRegRemoveSuffix"])) == this["LocateRegRemoveSuffix"]) { + regDir := StrReplace(regDir, StrLen(this["LocateRegRemoveSuffix"]) + 1) + } + + searchDirs.Push(regDir) + } + } + } else if (this["LocateMethod"] == "BlizzardProductDb") { + blizzardDir := this.GetBlizzardProductDir() + + if (blizzardDir != "") { + searchDirs.Push(blizzardDir) + } + } + + filePath := this.LocateFileInSearchDirs(filePattern, searchDirs) + } + } + + return filePath + } + + LocateFileInSearchDirs(filePattern, searchDirs := "") { + path := "" + + if (searchDirs == "") { + searchDirs := this["SearchDirs"].Clone() + } + + if (!HasBase(searchDirs, Array.Prototype)) { + searchDirs := [searchDirs] + } + + for index, searchDir in searchDirs { + Loop Files, searchDir . "\" . filePattern, "R" { + path := A_LoopFileFullPath + break + } + + if (path != "") { + break + } + } + + return path + } + + GetBlizzardProductKey() { + return "bna" ; Default to the Battle.net client itself + } + + GetBlizzardProductDir() { + path := "" + productCode := this.GetBlizzardProductKey() + + if (productCode != "" && this.app.Services.Has("BlizzardProductDb")) { + path := this.app["BlizzardProductDb"].GetProductInstallPath(productCode) + } + + return path + } +} diff --git a/Lib/Launchpad/Entity/PlatformEntity.ahk b/Lib/Launchpad/Entity/PlatformEntity.ahk index 43a4a1b8..5eacd08a 100644 --- a/Lib/Launchpad/Entity/PlatformEntity.ahk +++ b/Lib/Launchpad/Entity/PlatformEntity.ahk @@ -1,7 +1,6 @@ -class PlatformEntity extends AppEntityBase { +class PlatformEntity extends FieldableEntity { platformObj := "" configPrefix := "" - dataSourcePath := "platforms" Platform { get => this.GetPlatform() diff --git a/Lib/Launchpad/GamePlatform/GamePlatformBase.ahk b/Lib/Launchpad/GamePlatform/GamePlatformBase.ahk index f9d674ef..0476cd6d 100644 --- a/Lib/Launchpad/GamePlatform/GamePlatformBase.ahk +++ b/Lib/Launchpad/GamePlatform/GamePlatformBase.ahk @@ -179,35 +179,53 @@ class GamePlatformBase { } DetermineMainExe(key, possibleExes) { - dataSource := this.app["manager.data_source"].GetDefaultDataSource() - dsData := this.GetDataSourceDefaults(dataSource, key) - mainExe := "" if (possibleExes.Length == 1) { mainExe := possibleExes[1] - } else if (possibleExes.Length > 1 && dsData.Has("GameExe")) { - for index, possibleExe in possibleExes { - SplitPath(possibleExe, &fileName) - - if (dsData["GameExe"] == fileName) { - mainExe := possibleExe - break + } else if (possibleExes.Length > 1) { + ; @todo move the API functionality into a module that depends on WebServices + if (this.app.Services.Has("entity_manager.web_service")) { + mgr := this.app["entity_manager.web_service"] + + if (mgr.Has("launchpad_api")) { + webService := mgr["launchpad_api"] + + resultData := webService.AdapterRequest( + Map("id", key), + Map( + "adapterType", "entity_data", + "entityType", "launcher" + ), + "read", + true + ) + + for key, data in resultData { + if ( + data + && HasBase(data, Map.Prototype) + && data.Has("defaults") + && data["defaults"] + && data["defaults"].Has("GameExe") + && data["defaults"]["GameExe"] + ) { + for index, possibleExe in possibleExes { + SplitPath(possibleExe, &fileName) + + if (data["defaults"]["GameExe"] == fileName) { + mainExe := possibleExe + break 2 + } + } + } + } } } - } - return mainExe - } - - GetDataSourceDefaults(dataSource, key) { - defaults := Map() - dsData := dataSource.ReadJson(key, "Games") - - if (dsData != "" && dsData.Has("data") && dsData["data"].Has("defaults")) { - defaults := this.merger.Merge(dsData["data"]["defaults"], defaults) + } - return defaults + return mainExe } } diff --git a/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk b/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk index 3513534a..518c04c5 100644 --- a/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk +++ b/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk @@ -2,7 +2,6 @@ detectedGameObj := "" newValues := Map() missingFields := Map() - dataSource := "" knownGames := "" launcherTypes := "" gameTypes := "" @@ -22,11 +21,54 @@ Create() { super.Create() - this.dataSource := this.app["manager.data_source"].GetDefaultDataSource() - this.knownPlatforms := this.dataSource.ReadListing("platforms") - this.knownGames := this.dataSource.ReadListing("game-keys") - this.launcherTypes := this.dataSource.ReadListing("launcher-types") - this.gameTypes := this.dataSource.ReadListing("game-types") + + this.knownPlatforms := [] + this.knownGames := [] + this.launcherTypes := [] + this.gameTypes := [] + + ; @todo replace this, or at least refactor it to live somewhere else + if (this.container.Has("entity_manager.web_service")) { + mgr := this.container["entity_manager.web_service"] + + if (mgr.Has("launchpad_api") && mgr["launchpad_api"]["Enabled"]) { + webService := mgr["launchpad_api"] + knownMap := Map( + "platform", "knownPlatforms", + "game", "knownGames", + "managed_launcher", "launcherTypes", + "managed_game", "gameTypes" + ) + + for entityTypeId, varName in knownMap { + results := webService.AdapterRequest("", Map( + "adapterType", "entity_list", + "entityType", entityTypeId + ), "read", true) + + if (results) { + for , idList in results { + if (idList) { + for , id in idList { + exists := false + + for , item in %varName% { + if (item == id) { + exists := true + break + } + } + + if (!exists) { + this.%varName%.Push(id) + } + } + } + } + } + } + } + } } GetTitle() { diff --git a/Lib/Launchpad/Gui/Form/LauncherCreateFormBase.ahk b/Lib/Launchpad/Gui/Form/LauncherCreateFormBase.ahk index dccde664..78d55ef9 100644 --- a/Lib/Launchpad/Gui/Form/LauncherCreateFormBase.ahk +++ b/Lib/Launchpad/Gui/Form/LauncherCreateFormBase.ahk @@ -1,10 +1,12 @@ class LauncherCreateFormBase extends FormGuiBase { knownGames := "" knownPlatforms := "" - dataSource := "" + launcherMgr := "" + platformMgr := "" __New(container, themeObj, config) { - this.dataSource := container.Get("manager.data_source").GetDefaultDataSource() + this.launcherMgr := container.Get("entity_manager.launcher") + this.platformMgr := container.Get("entity_manager.platform") super.__New(container, themeObj, config) } @@ -17,8 +19,8 @@ Create() { super.Create() - this.knownGames := this.dataSource.ReadListing("game-keys") - this.knownPlatforms := this.dataSource.ReadListing("platforms") + this.knownGames := this.launcherMgr.ListEntities(false, true) + this.knownPlatforms := this.platformMgr.ListEntities(false, true) } ProcessResult(result, submittedData := "") { diff --git a/Lib/Launchpad/Gui/ManageWindow/DetectedGamesWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/DetectedGamesWindow.ahk index 98f10890..6f8aa4ba 100644 --- a/Lib/Launchpad/Gui/ManageWindow/DetectedGamesWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/DetectedGamesWindow.ahk @@ -12,9 +12,7 @@ this.detectedGames := detectedGames this.state := container.Get("state.app") this.launcherManager := container.Get("entity_manager.launcher") - this.knownGames := container.Get("manager.data_source") - .GetDefaultDataSource() - .ReadListing("game-keys") + this.knownGames := this.launcherManager.ListEntities(false, true) super.__New(container, themeObj, config) } diff --git a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk index 81221664..8137e949 100644 --- a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk @@ -18,7 +18,7 @@ defaults["child"] := false defaults["title"] := container.GetApp().appName defaults["titleIsMenu"] := true - defaults["showStatusIndicator"] := !!(container.Get("config.app").Has("api_authentication") && container.Get("config.app")["api_authentication"]) + defaults["showStatusIndicator"] := container.Has("entity_manager.web_service") return defaults } @@ -131,7 +131,9 @@ } status := launcher.GetStatus() - apiStatus := launcher["DataSourceItemKey"] ? "Linked" : "Not linked" + + ; @todo Move the API data to an event in the LaunchpadApi module + apiStatus := (launcher.HasField["DataLookupKey"] && launcher["DataLookupKey"]) ? "Linked" : "Not linked" created := this.FormatDate(this.app.State.GetLauncherCreated(key)) updated := this.FormatDate(this.app.State.GetLauncherInfo("Config")["Timestamp"]) built := this.FormatDate(this.app.State.GetLauncherInfo("Build")["Timestamp"]) @@ -307,7 +309,9 @@ } status := launcher.GetStatus() - apiStatus := launcher["DataSourceItemKey"] ? "Linked" : "Not linked" + + ; @todo Move the API code to the LaunchpadApi module + apiStatus := (launcher.HasField("DataLookupKey") && launcher["DataLookupKey"]) ? "Linked" : "Not linked" created := this.FormatDate(this.app.State.GetLauncherCreated(key)) updated := this.FormatDate(this.app.State.GetLauncherInfo(key, "Config")["Timestamp"]) built := this.FormatDate(this.app.State.GetLauncherInfo(key, "Build")["Timestamp"]) @@ -450,7 +454,7 @@ } entity.SaveEntity() - entity.UpdateDataSourceDefaults() + entity.UpdateDefaults() this.UpdateListView() } } diff --git a/Lib/Launchpad/Includes.ahk b/Lib/Launchpad/Includes.ahk index 775a4966..cec1c809 100644 --- a/Lib/Launchpad/Includes.ahk +++ b/Lib/Launchpad/Includes.ahk @@ -26,6 +26,7 @@ #Include Entity\ManagedEntityBase.ahk #Include Entity\ManagedGameEntity.ahk #Include Entity\ManagedLauncherEntity.ahk +#Include Entity\ManagedProcessEntity.ahk #Include Entity\PlatformEntity.ahk #Include GamePlatform\BasicPlatform.ahk #Include GamePlatform\GamePlatformBase.ahk diff --git a/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk b/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk index 4b82e62d..6dffed31 100644 --- a/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk +++ b/Lib/LaunchpadBuilder/App/LaunchpadBuilder.ahk @@ -30,11 +30,6 @@ class LaunchpadBuilder extends AppBase { "arguments", [AppRef(), this.appDir . "\" . this.appName . ".ini"] ) - services["manager.data_source"] := Map( - "class", "DataSourceManager", - "arguments", [ContainerRef(), ServiceRef("manager.event"), ServiceRef("notifier"), ParameterRef("config.data_source_key")] - ) - services["FileHasher"] := "FileHasher" services["GitTagVersionIdentifier"] := Map( diff --git a/Lib/Shared/Includes.ahk b/Lib/Shared/Includes.ahk index 4f961270..d33ef65f 100644 --- a/Lib/Shared/Includes.ahk +++ b/Lib/Shared/Includes.ahk @@ -1,5 +1,4 @@ ; Automatically-generated file. Manual edits will be overwritten. -#Include Modules\LaunchpadApi\DataSource\ApiDataSource.ahk #Include Modules\LaunchpadApi\EventSubscriber\LaunchpadApiSubscriber.ahk #Include Modules\WebServices\Entity\WebServiceEntity.ahk #Include Modules\WebServices\Entity\WebServiceProviderEntity.ahk @@ -40,13 +39,6 @@ #Include Volantis.App\Config\AppConfig.ahk #Include Volantis.App\Container\ServiceComponentContainer.ahk #Include Volantis.App\Container\WindowContainer.ahk -#Include Volantis.App\DataSource\DataSourceBase.ahk -#Include Volantis.App\DataSourceItem\DataSourceItemBase.ahk -#Include Volantis.App\DataSourceItem\DSAssetFile.ahk -#Include Volantis.App\DataSourceItem\DSFile.ahk -#Include Volantis.App\DataSourceItem\DSJson.ahk -#Include Volantis.App\DataSourceItem\DSListing.ahk -#Include Volantis.App\Entity\AppEntityBase.ahk #Include Volantis.App\Entity\BackupEntity.ahk #Include Volantis.App\Entity\TaskEntity.ahk #Include Volantis.App\Event\AlterComponentsEvent.ahk @@ -106,7 +98,6 @@ #Include Volantis.App\Service\LoggerService.ahk #Include Volantis.App\Service\NotificationService.ahk #Include Volantis.App\Service\ComponentManager\CacheManager.ahk -#Include Volantis.App\Service\ComponentManager\DataSourceManager.ahk #Include Volantis.App\Service\ComponentManager\GuiManager.ahk #Include Volantis.App\Service\ComponentManager\InstallerManager.ahk #Include Volantis.App\Service\ComponentManager\ThemeManager.ahk diff --git a/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk b/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk deleted file mode 100644 index 1f84afbd..00000000 --- a/Lib/Shared/Modules/LaunchpadApi/DataSource/ApiDataSource.ahk +++ /dev/null @@ -1,121 +0,0 @@ -class ApiDataSource extends DataSourceBase { - endpointUrl := "" - app := "" - - __New(app, cacheManager, cacheName, endpointUrl) { - this.app := app - InvalidParameterException.CheckTypes("ApiDataSource", "endpointUrl", endpointUrl, "", "cacheManager", cacheManager, "CacheManager") - this.endpointUrl := endpointUrl - super.__New(cacheManager, cacheName) - } - - ItemExists(path) { - return super.ItemExists(path) || this.ItemExistsInApi(path) - } - - ItemExistsInApi(path) { - exists := (this.cache.ItemExists(path) && !this.cache.ItemNeedsUpdate(path)) - - if (!exists) { - request := this.SendHttpReq(path, "HEAD") - - exists := (request.GetStatusCode() == 200) - - if (!exists) { - this.cache.SetNotFound(path) - } - } - - return exists - } - - GetHttpReq(path, private := false) { - request := WinHttpReq(this.GetRemoteLocation(path)) - - if (private) { - request.requestHeaders["Cache-Control"] := "no-cache" - - if (this.app.Config["api_authentication"]) { - entityMgr := webService := this.app["entity_manager.web_service"] - - if (entityMgr.Has("launchpad_api") && entityMgr["launchpad_api"]["Enabled"]) { - webService := this.app["entity_manager.web_service"]["launchpad_api"] - webService["Provider"]["Authenticator"].AlterRequest(webService, request) - } - - } - } - - return request - } - - SendHttpReq(path, method := "GET", data := "", private := false) { - request := this.GetHttpReq(path, private) - returnCode := request.Send(method, data) - return request - } - - GetRemoteLocation(path) { - return this.endpointUrl . "/" . path - } - - RetrieveItem(path, private := false, maxCacheAge := "") { - if (maxCacheAge == "") { - maxCacheAge := this.maxCacheAge - } - - exists := (!private && this.cache.ItemExists(path) && !this.cache.ItemNeedsUpdate(path, maxCacheAge)) - - if (!exists) { - request := this.SendHttpReq(path, "GET", "", private) - - if (request.GetStatusCode() != 200) { - return "" - } - - responseBody := Trim(request.GetResponseData()) - - if (responseBody == "") { - return "" - } - - this.cache.WriteItem(path, responseBody) - } - - return this.cache.ItemExists(path) ? this.cache.ReadItem(path) : "" - } - - GetStatus() { - path := "status" - statusExpire := 5 ;60 - - status := Map("authenticated", false, "account", "", "photo", "") - - if (this.app.Config["api_authentication"]) { - entityMgr := webService := this.app["entity_manager.web_service"] - - if (entityMgr.Has("launchpad_api") && entityMgr["launchpad_api"]["Enabled"] && entityMgr["launchpad_api"]["Authenticated"]) { - statusResult := this.ReadItem(path, true) - - if (statusResult) { - status := JsonData().FromString(&statusResult) - - if (status.Has("email")) { - status["account"] := status["email"] - status.Delete("email") - } - } - } - } - - return status - } - - GetExt(path) { - - } - - Open() { - Run(this.endpointUrl) - } -} diff --git a/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk b/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk index 5d47081a..bc9da846 100644 --- a/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk +++ b/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk @@ -4,24 +4,248 @@ class LaunchpadApiSubscriber extends EventSubscriberBase { Events.APP_GET_RELEASE_INFO, [ ObjBindMethod(this, "GetReleaseInfo") ], + EntityEvents.ENTITY_DATA_LAYERS, [ + ObjBindMethod(this, "EntityDataLayers") + ], + EntityEvents.ENTITY_LAYER_SOURCES, [ + ObjBindMethod(this, "EntityLayerSources") + ], + EntityEvents.ENTITY_FIELD_GROUPS, [ + ObjBindMethod(this, "EntityFieldGroups") + ], + EntityEvents.ENTITY_FIELD_DEFINITIONS, [ + ObjBindMethod(this, "EntityFieldDefinitions") + ], + EntityEvents.ENTITY_DETECT_VALUES, [ + ObjBindMethod(this, "EntityDetectValues") + ], + EntityEvents.ENTITY_LIST_ENTITIES, [ + ObjBindMethod(this, "ListEntities") + ], ) } GetReleaseInfo(event, extra, eventName, hwnd) { releaseInfo := event.ReleaseInfo - if (!event.ReleaseInfo.Count && this.App.Version != "{{VERSION}}") { - webService := this.App["entity_manager.web_service"]["launchpad_api"] + if (!event.ReleaseInfo.Count && this.container.GetApp().Version != "{{VERSION}}") { + webService := this.container["entity_manager.web_service"]["launchpad_api"] if (webService["Enabled"]) { - releaseInfo := webService.AdapterRequest("", "release_info", "read", 1) + releaseInfo := webService.AdapterRequest("", "release_info") - if (releaseInfo && releaseInfo.Has("data")) { - for key, val in releaseInfo["data"] { + if (releaseInfo) { + for key, val in releaseInfo { event.ReleaseInfo[key] = val } } } } } + + EntityDataLayers(event, extra, eventName, hwnd) { + if (event.EntityTypeId == "web_service" || event.EntityTypeId == "web_service_provider") { + return + } + + webService := this.container["entity_manager.web_service"]["launchpad_api"] + + layers := event.Layers + + if (WebService["Enabled"]) { + entity := event.Entity + + adapters := webService.GetAdapters([ + "adapterType", "entity_data", + "entityType", event.EntityTypeId + ]) + + for key, adapter in adapters { + layerExists := false + layerKey := webService["id"] . "." . event.EntityTypeId . "." . key + + for index, layerName in layers { + if (layerName == layerKey) { + layerExists := true + break + } + } + + if (!layerExists) { + layers.Push(layerKey) + } + } + } + } + + EntityLayerSources(event, extra, eventName, hwnd) { + if (event.EntityTypeId == "web_service" || event.EntityTypeId == "web_service_provider") { + return + } + + webService := this.container["entity_manager.web_service"]["launchpad_api"] + + layerData := event.LayerSources + + if (WebService["Enabled"]) { + adapters := webService.GetAdapters([ + "adapterType", "entity_data", + "entityType", event.EntityTypeId + ]) + + for key, adapter in adapters { + layerKey := webService["id"] . "." . event.EntityTypeId . "." . key + + if (!layerData.Has(layerKey)) { + layerData[layerKey] := WebServiceAdapterLayerSource(adapter) + } + } + } + } + + EntityFieldGroups(event, extra, eventName, hwnd) { + if (event.EntityTypeId == "web_service" || event.EntityTypeId == "web_service_provider") { + return + } + + webService := this.container["entity_manager.web_service"]["launchpad_api"] + + if (WebService["Enabled"]) { + fieldGroups := event.FieldGroups + + if (!fieldGroups.Has("api")) { + adapters := webService.GetAdapters([ + "adapterType", "entity_data", + "entityType", event.EntityTypeId + ]) + + if (adapters.Count) { + fieldGroups["api"] := Map( + "name", "API", + "weight", 150 + ) + } + } + } + } + + EntityFieldDefinitions(event, extra, eventName, hwnd) { + if (event.EntityTypeId == "web_service" || event.EntityTypeId == "web_service_provider") { + return + } + + webService := this.container["entity_manager.web_service"]["launchpad_api"] + + if (WebService["Enabled"]) { + fieldDefinitions := event.FieldDefinitions + + adapters := webService.GetAdapters([ + "adapterType", "entity_data", + "entityType", event.EntityTypeId + ]) + + if (adapters.Count) { + fieldDefinitions["DataLookupKey"] := Map( + "description", "The key that is used to look up the entity's data from configured external data sources.", + "help", "It defaults to the key which is usually sufficient, but it can be overridden by setting this value.`n`nAddtionally, multiple copies of the same data source entity can exist by giving them different keys but using the same DataLookupKey", + "group", "api", + "processValue", false, + "modes", Map("simple", Map("formField", false)) + ) + } + } + } + + EntityDetectValues(event, extra, eventName, hwnd) { + if (event.EntityTypeId == "web_service" || event.EntityTypeId == "web_service_provider") { + return + } + + webService := this.container["entity_manager.web_service"]["launchpad_api"] + values := event.Values + entity := event.Entity + + if ( + webService["Enabled"] + && (!values.Has("DataLookupKey") || !values["DataLookupKey"]) + && entity.HasField("DataLookupKey") + && (!entity.RawData.Has["DataLookupKey"] || !entity.RawData["DataLookupKey"]) + ) { + result := "" + + if (event.EntityTypeId == "Launcher") { + platform := entity["Platform"] ? entity["Platform"]["id"] : "" + + result := webService.AdapterRequest( + Map("id", entity["id"], "platform", platform), + Map( + "adapterType", "entity_list", + "entityType", event.EntityTypeId + ) + ) + } else if (HasBase(entity, ManagedEntityBase.Prototype)) { + result := entity["EntityType"] + } else { + result := entity["id"] + } + + if (result) { + values["DataLookupKey"] := result + } + } + } + + ListEntities(event, extra, eventName, hwnd) { + if (event.EntityTypeId == "web_service" || event.EntityTypeId == "web_service_provider") { + return + } + + if (event.includeExtended) { + webService := this.container["entity_manager.web_service"]["launchpad_api"] + entityMgr := this.container["entity_manager." . event.EntityTypeId] + + managedIds := event.includeManaged + ? [] + : entityMgr.EntityQuery(EntityQuery.RESULT_TYPE_IDS).Execute() + + if (webService["Enabled"]) { + results := webService.AdapterRequest( + "", + Map( + "adapterType", "entity_list", + "entityType", event.EntityTypeId + ), + "read", + true + ) + + if (results && HasBase(results, Array.Prototype)) { + for index, id in results { + exists := false + + for , existingId in event.EntityList { + if (existingId == id) { + exists := true + break + } + } + + if (!exists && !event.includeManaged) { + for , managedId in managedIds { + if (managedId == id) { + exists := true + break + } + } + } + + if (!exists) { + event.EntityList.Push(id) + } + } + } + + } + } + } } diff --git a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json index ce2d9c31..78ac16ec 100644 --- a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json +++ b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json @@ -77,6 +77,7 @@ "web_services.adapters.launchpad_api.platform_data": { "dataType": "entity_data", "requestPath": "/game-platforms/{id}", + "dataSelector": "data.defaults", "entityType": "platform" }, "web_services.adapters.launchpad_api.game_type_list": { @@ -87,6 +88,7 @@ "web_services.adapters.launchpad_api.game_type_data": { "dataType": "entity_data", "requestPath": "/game-types/{id}", + "dataSelector": "data.defaults", "entityType": "managed_game" }, "web_services.adapters.launchpad_api.launcher_list": { @@ -97,6 +99,13 @@ "web_services.adapters.launchpad_api.launcher_data": { "dataType": "entity_data", "requestPath": "/games/{id}", + "dataSelector": "data.defaults", + "entityType": "launcher" + }, + "web_services.adapters.launchpad_api.launcher_lookup": { + "dataType": "entity_lookup", + "requestPath": "/lookup/{id}/{platform}", + "dataSelector": "id", "entityType": "launcher" }, "web_services.adapters.launchpad_api.launcher_type_list": { @@ -107,14 +116,11 @@ "web_services.adapters.launchpad_api.launcher_type_data": { "dataType": "entity_data", "requestPath": "/launcher-types/{id}", + "dataSelector": "data.defaults", "entityType": "managed_launcher" } }, "services": { - "data_source.launchpad_api": { - "class": "ApiDataSource", - "arguments": ["@{App}", "@manager.cache", "launchpad_api", "https://api.launchpad.games/v1"] - }, "cache_state.launchpad_api": { "class": "CacheState", "arguments": ["@{App}", "@@config.cache_dir", "API.json"] @@ -122,6 +128,11 @@ "cache.launchpad_api": { "class": "FileCache", "arguments": ["@{App}", "@cache_state.launchpad_api", "@@config.cache_dir", "API"] + }, + "event_subscriber.launchpad_api": { + "class": "LaunchpadApiSubscriber", + "arguments": ["@{}"], + "tags": ["event_subscriber"] } } } diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index 85a1b0ab..dd10622c 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -1,8 +1,7 @@ -class WebServiceEntity extends AppEntityBase { +class WebServiceEntity extends FieldableEntity { cacheObj := "" stateObj := "" persistentStateObj := "" - mergeDataFromApi := false statusIndicators := [] adapters := Map() adapterFactory := "" @@ -20,20 +19,19 @@ class WebServiceEntity extends AppEntityBase { set => this.SetAuthData(key, value) } - __New(app, id, entityTypeId, container, adapterFactory, cacheObj, stateObj, persistentStateObj, eventMgr, storageObj, idSanitizer, parentEntity := "") { + __New(id, entityTypeId, container, adapterFactory, cacheObj, stateObj, persistentStateObj, fieldFactory, widgetFactory, eventMgr, storageObj, idSanitizer, parentEntity := "") { this.cacheObj := cacheObj this.stateObj := stateObj this.persistentStateObj := persistentStateObj this.adapterFactory := adapterFactory - super.__New(app, id, entityTypeId, container, eventMgr, storageObj, idSanitizer, parentEntity) + super.__New(id, entityTypeId, container, fieldFactory, widgetFactory, eventMgr, storageObj, idSanitizer, parentEntity) } static Create(container, eventMgr, id, entityTypeId, storageObj, idSanitizer, parentEntity := "") { className := this.Prototype.__Class return %className%( - container.GetApp(), id, entityTypeId, container, @@ -41,6 +39,8 @@ class WebServiceEntity extends AppEntityBase { container.Get("cache.web_services"), container.Get("state.web_services_tmp"), container.Get("state.web_services"), + container.Get("entity_field_factory." . entityTypeId), + container.Get("entity_widget_factory." . entityTypeId), eventMgr, storageObj, idSanitizer, @@ -48,7 +48,7 @@ class WebServiceEntity extends AppEntityBase { ) } - AdapterRequest(params, adapterFilters, operation := "read", limit := false) { + AdapterRequest(params, adapterFilters, operation := "read", multiple := false) { if (!adapterFilters) { adapterFilters := Map() } @@ -64,9 +64,18 @@ class WebServiceEntity extends AppEntityBase { results := Map() for adapterKey, adapter in this.GetAdapters(adapterFilters, operation) { - results[adapterKey] := adapter.SendRequest(operation, params) + result := adapter.SendRequest(operation, params) - if (limit && results.Count >= limit) { + if (result) { + if (!multiple) { + results := result + break + } + + results[adapterKey] := result + } + + if (IsNumber(multiple) && results.Count >= multiple) { break } } diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceProviderEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceProviderEntity.ahk index c8b02d43..573120f3 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceProviderEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceProviderEntity.ahk @@ -1,6 +1,4 @@ -class WebServiceProviderEntity extends AppEntityBase { - mergeDataFromApi := false - +class WebServiceProviderEntity extends FieldableEntity { BaseFieldDefinitions() { definitions := super.BaseFieldDefinitions() diff --git a/Lib/Shared/Modules/WebServices/Gui/Form/FeedbackWindow.ahk b/Lib/Shared/Modules/WebServices/Gui/Form/FeedbackWindow.ahk index c508904b..fda1e07a 100644 --- a/Lib/Shared/Modules/WebServices/Gui/Form/FeedbackWindow.ahk +++ b/Lib/Shared/Modules/WebServices/Gui/Form/FeedbackWindow.ahk @@ -50,7 +50,7 @@ class FeedbackWindow extends DialogBox { body["version"] := appVersion body["feedback"] := this.guiObj["Feedback"].Text - results := webService.AdapterRequest(Map("data", body), "feedback_submission", "create") + results := webService.AdapterRequest(Map("data", body), "feedback_submission", "create", true) } for key, result in results { diff --git a/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk b/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk index f7002611..b34ea21d 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk @@ -5,6 +5,10 @@ class WebServiceAdapterBase { dataType := "" merger := "" operationTypes := ["create", "read", "update", "delete"] + + static ADAPTER_RESULT_DATA := "data" + static ADAPTER_RESULT_HTTP_STATUS := "httpStatus" + static ADAPTER_RESULT_SUCCESS := "success" __New(container, merger, webService, definition) { this.container := container @@ -53,7 +57,8 @@ class WebServiceAdapterBase { "deleteAllow", false, "deleteMethod", "PUT", "deleteAuth", true, - "dataMap", Map() + "dataMap", Map(), + "dataSelector", [] ) } @@ -93,13 +98,50 @@ class WebServiceAdapterBase { throw AppException("The 'create' operation is not allowed on this data adapter.") } - return this._request( + response := this._request( params, this.definition["createMethod"], data ? data : this._getData(params), this.definition["createAuth"], false - ).Send().IsSuccessful() + ).Send() + + return this._getResult( + params, + response, + this._getResultType(params, WebServiceAdapterBase.ADAPTER_RESULT_SUCCESS) + ) + } + + _getResultType(params, default) { + resultType := default + + if (params.Has("resultType") && params["resultType"]) { + resultType := params["resultType"] + } + + return resultType + } + + _getResult(params, response, resultType) { + result := "" + + if (resultType == WebServiceAdapterBase.ADAPTER_RESULT_DATA) { + if (response.IsSuccessful()) { + data := response.GetResponseBody() + + if (data) { + result := this._mapData(this._parseData(data, params), params) + } + + } + } else if (resultType == WebServiceAdapterBase.ADAPTER_RESULT_HTTP_STATUS) { + result := response.GetHttpStatusCode() + } else if (resultType == WebServiceAdapterBase.ADAPTER_RESULT_SUCCESS) { + result := response.IsSuccessful() + } + + return result } DataExists(params := "") { @@ -129,15 +171,11 @@ class WebServiceAdapterBase { this.definition["cacheResponse"] ).Send() - data := "" - - if (response.IsSuccessful()) { - data := response.GetResponseBody() - data := this._parseData(data, params) - this._mapData(data, params) - } - - return data + return this._getResult( + params, + response, + this._getResultType(params, WebServiceAdapterBase.ADAPTER_RESULT_DATA) + ) } UpdateData(data, params := "") { @@ -145,13 +183,19 @@ class WebServiceAdapterBase { throw AppException("The 'update' operation is not allowed on this data adapter.") } - return this._request( + response := this._request( params, this.definition["updateMethod"], data ? data : this._getData(params), this.definition["updateAuth"], false - ).Send().IsSuccessful() + ).Send() + + return this._getResult( + params, + response, + this._getResultType(params, WebServiceAdapterBase.ADAPTER_RESULT_SUCCESS) + ) } DeleteData(params := "") { @@ -159,13 +203,19 @@ class WebServiceAdapterBase { throw AppException("The 'delete' operation is not allowed on this data adapter.") } - return this._request( + response := this._request( params, this.definition["deleteMethod"], this._getData(params), this.definition["deleteAuth"], false - ).Send().IsSuccessful() + ).Send() + + return this._getResult( + params, + response, + this._getResultType(params, WebServiceAdapterBase.ADAPTER_RESULT_SUCCESS) + ) } _requestPath(params) { @@ -220,9 +270,26 @@ class WebServiceAdapterBase { } _parseData(data, params) { - if (data && this.dataType) { - dataType := this.dataType + if (data && this.definition["dataType"]) { + dataType := this.definition["dataType"] data := %dataType%().FromString(data) + + if (this.definition["dataSelector"]) { + dataSelector := this.definition["dataSelector"] + + if (Type(dataSelector) == "String") { + dataSelector := StrSplit(dataSelector, ".") + } + + for index, pathPart in dataSelector { + if (data.Has(pathPart)) { + data := data[pathPart] + } else { + data := "" + break + } + } + } } return data diff --git a/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk b/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk index 754981b0..8e7f56d8 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk @@ -132,6 +132,10 @@ class WebServiceRequestBase { authenticator.AlterRequest(this.webServiceEnt, httpReqObj) } + if (!this.cacheResponse) { + httpReqObj.requestHeaders["Cache-Control"] := "no-cache" + } + event := WebServiceRequestEvent(WebServicesEvents.WEB_SERVICES_HTTP_REQ_ALTER, this) this.eventMgr.DispatchEvent(event) diff --git a/Lib/Shared/Volantis.App/DataSource/DataSourceBase.ahk b/Lib/Shared/Volantis.App/DataSource/DataSourceBase.ahk deleted file mode 100644 index 91c1fdce..00000000 --- a/Lib/Shared/Volantis.App/DataSource/DataSourceBase.ahk +++ /dev/null @@ -1,70 +0,0 @@ -class DataSourceBase { - cache := "" - useCache := false - maxCacheAge := 86400 - - __New(cacheManager := "", cacheName := "") { - if (cacheManager != "" && cacheName != "") { - InvalidParameterException.CheckTypes("DataSourceBase", "cacheManager", cacheManager, "CacheManager") - this.useCache := true - this.cache := cacheManager[cacheName] - } - } - - ItemExists(path) { - return this.useCache ? this.cache.ItemExists(path) : false - } - - ReadItem(path, private := false, maxCacheAge := "") { - if (maxCacheAge == "") { - maxCacheAge := this.maxCacheAge - } - - item := "" - - if (this.ItemNeedsRetrieval(path)) { - item := this.RetrieveItem(path, private, maxCacheAge) - } else if (this.useCache) { - item := this.cache.ReadItem(path) - } - - return item - } - - ItemNeedsRetrieval(path) { - return (!this.useCache || this.cache.ItemNeedsUpdate(path)) - } - - RetrieveItem(path, private := false, maxCacheAge := "") { - return "" - } - - CopyItem(path, destination) { - if (this.ItemNeedsRetrieval(path)) { - this.RetrieveItem(path) - } - - return this.useCache ? this.cache.CopyItem(path, destination) : destination - } - - GetRemoteLocation(path) { - return path - } - - ReadListing(path) { - listingInstance := DSListing(path, this) - - listing := [] - - if (listingInstance.Exists()) { - listing := listingInstance.Read() - } - - return listing - } - - ReadJson(key, path := "") { - dsItem := DSJson(key, path, this) - return dsItem.Read() - } -} diff --git a/Lib/Shared/Volantis.App/DataSourceItem/DSAssetFile.ahk b/Lib/Shared/Volantis.App/DataSourceItem/DSAssetFile.ahk deleted file mode 100644 index a5165b1f..00000000 --- a/Lib/Shared/Volantis.App/DataSourceItem/DSAssetFile.ahk +++ /dev/null @@ -1,3 +0,0 @@ -class DSAssetFile extends DSFile { - allowRead := false -} diff --git a/Lib/Shared/Volantis.App/DataSourceItem/DSFile.ahk b/Lib/Shared/Volantis.App/DataSourceItem/DSFile.ahk deleted file mode 100644 index 187d931b..00000000 --- a/Lib/Shared/Volantis.App/DataSourceItem/DSFile.ahk +++ /dev/null @@ -1,7 +0,0 @@ -class DSFile extends DataSourceItemBase { - allowRead := true ; Some files are only meant to be copied - - Read() { - return this.allowRead ? super.Read() : "" - } -} diff --git a/Lib/Shared/Volantis.App/DataSourceItem/DSJson.ahk b/Lib/Shared/Volantis.App/DataSourceItem/DSJson.ahk deleted file mode 100644 index 991c41dc..00000000 --- a/Lib/Shared/Volantis.App/DataSourceItem/DSJson.ahk +++ /dev/null @@ -1,17 +0,0 @@ -class DSJson extends DSFile { - itemSuffix := "" - dataType := "Map" - - Read() { - content := super.Read() - dataType := this.dataType - obj := %dataType%() - - if (content) { - data := JsonData() - obj := data.FromString(&content) - } - - return obj - } -} diff --git a/Lib/Shared/Volantis.App/DataSourceItem/DSListing.ahk b/Lib/Shared/Volantis.App/DataSourceItem/DSListing.ahk deleted file mode 100644 index 1d48a12c..00000000 --- a/Lib/Shared/Volantis.App/DataSourceItem/DSListing.ahk +++ /dev/null @@ -1,7 +0,0 @@ -class DSListing extends DSJson { - dataType := "Array" - - __New(path, dataSourceKey := "") { - super.__New(path, "", dataSourceKey) - } -} diff --git a/Lib/Shared/Volantis.App/DataSourceItem/DataSourceItemBase.ahk b/Lib/Shared/Volantis.App/DataSourceItem/DataSourceItemBase.ahk deleted file mode 100644 index ef0ff3ea..00000000 --- a/Lib/Shared/Volantis.App/DataSourceItem/DataSourceItemBase.ahk +++ /dev/null @@ -1,53 +0,0 @@ -class DataSourceItemBase { - endpoint := "" - basePath := "" - itemSuffix := "" - path := "" - key := "" - - __New(key, path := "", dataSource := "") { - InvalidParameterException.CheckTypes("DataSourceItemBase", "key", key, "", "path", path, "") - InvalidParameterException.CheckEmpty("DataSourceItemBase", "key", key) - InvalidParameterException.CheckTypes("DataSourceItemBase", "dataSource", dataSource, "DataSourceBase") - - this.endpoint := dataSource - this.key := key - this.path := path - } - - GetPath(includeFilename := true) { - path := this.basePath - - if (path != "" && this.path != "") { - path .= "/" - } - - path .= this.path - - if (includeFilename) { - if (path) { - path .= "/" - } - - path .= this.key . this.itemSuffix - } - - return path - } - - GetRemoteLocation() { - return this.endpoint.GetRemoteLocation(this.GetPath()) - } - - Exists() { - return this.endpoint.ItemExists(this.GetPath()) - } - - Read() { - return this.endpoint.ReadItem(this.GetPath()) - } - - Copy(destination) { - return this.endpoint.CopyItem(this.GetPath(), destination) - } -} diff --git a/Lib/Shared/Volantis.App/Entity/AppEntityBase.ahk b/Lib/Shared/Volantis.App/Entity/AppEntityBase.ahk deleted file mode 100644 index b1be4fd7..00000000 --- a/Lib/Shared/Volantis.App/Entity/AppEntityBase.ahk +++ /dev/null @@ -1,204 +0,0 @@ -class AppEntityBase extends FieldableEntity { - app := "" - dataSourcePath := "" - existsInDataSource := false - mergeDataFromApi := true - - __New(app, id, entityTypeId, container, eventMgr, storageObj, idSanitizer, parentEntity := "") { - this.app := app - - super.__New(id, entityTypeId, container, eventMgr, storageObj, idSanitizer, parentEntity) - } - - static Create(container, eventMgr, id, entityTypeId, storageObj, idSanitizer, parentEntity := "") { - className := this.Prototype.__Class - - return %className%( - container.GetApp(), - id, - entityTypeId, - container, - eventMgr, - storageObj, - idSanitizer, - parentEntity - ) - } - - GetDefaultFieldGroups() { - groups := super.GetDefaultFieldGroups() - - groups["advanced"] := Map( - "name", "Advanced", - "weight", 100 - ) - - if (this.mergeDataFromApi) { - groups["api"] := Map( - "name", "API", - "weight", 150 - ) - } - - return groups - } - - BaseFieldDefinitions() { - definitions := super.BaseFieldDefinitions() - - if (this.mergeDataFromApi) { - definitions["DataSourceKeys"] := Map( - "description", "The data source keys to load defaults from, in order.", - "help", "The default data source is 'api' which connects to the default api endpoint (Which can be any HTTP location compatible with Launchpad's API format)", - "default", [this.app.Config["data_source_key"]], - "multiple", true, - "group", "api", - "processValue", false, - "modes", Map("simple", Map("formField", false)) - ) - - definitions["DataSourceItemKey"] := Map( - "description", "The key that is used to look up the entity's data from configured external data sources.", - "help", "It defaults to the key which is usually sufficient, but it can be overridden by setting this value.`n`nAddtionally, multiple copies of the same data source entity can exist by giving them different keys but using the same DataSourceKey", - "group", "api", - "processValue", false, - "modes", Map("simple", Map("formField", false)) - ) - } - - definitions["AssetsDir"] := Map( - "type", "directory", - "description", "The directory where any required assets for this entity will be saved.", - "default", this.app.Config["assets_dir"] . "\" . this.Id, - "group", "advanced", - "formField", false, - "modes", Map("simple", Map("formField", false)) - ) - - definitions["DependenciesDir"] := Map( - "type", "directory", - "description", "The directory where dependencies which have been installed for this entity can be accessed.", - "default", this.app.appDir . "\Vendor", - "group", "advanced", - "required", true, - "formField", false, - "modes", Map("simple", Map("formField", false)) - ) - - return definitions - } - - _getLayerNames() { - layerNames := super._getLayerNames() - layerNames.Push("ds") - - return layerNames - } - - _getLayerSources() { - layerSources := super._getLayerSources() - layerSources["ds"] := ObjBindMethod(this, "AggregateDataSourceDefaults") - - return layerSources - } - - UpdateDataSourceDefaults(recurse := true) { - ; @todo Move this to a module - this.GetData().UnloadLayer("ds") - - if (recurse) { - for key, child in this.GetReferencedEntities(true) { - child.UpdateDataSourceDefaults(recurse) - } - } - } - - AggregateDataSourceDefaults(includeParentData := true, includeChildData := true) { - defaults := Map() - - if (this.mergeDataFromApi) { - defaults := (this.parentEntity != "" && includeParentData) - ? this.parentEntity.AggregateDataSourceDefaults(includeParentData, false) - : defaults - - for index, dataSource in this.GetAllDataSources() { - defaults := this.merger.Merge(this.GetDataSourceDefaults(dataSource), defaults) - } - - if (includeChildData) { - for key, child in this.GetReferencedEntities(true) { - defaults := this.merger.Merge(child.AggregateDataSourceDefaults(false, includeChildData), defaults) - } - } - } - - return defaults - } - - GetAllDataSources() { - dataSources := Map() - - if (this.mergeDataFromApi && this.Has("DataSourceKeys", false)) { - dataSourceKeys := this["DataSourceKeys"] - - if (!HasBase(dataSourceKeys, Array.Prototype)) { - dataSourceKeys := [dataSourceKeys] - } - - for index, dataSourceKey in dataSourceKeys { - if (this.app["manager.data_source"].Has(dataSourceKey)) { - dataSource := this.app["manager.data_source"][dataSourceKey] - - if (dataSource) { - dataSources[dataSourceKey] := dataSource - } - } - } - } - - return dataSources - } - - GetDataSourceDefaults(dataSource) { - defaults := Map() - - if (this.mergeDataFromApi) { - itemKey := this.DiscoverDataSourceItemKey() - - if (itemKey) { - dsData := dataSource.ReadJson(itemKey, this.GetDataSourceItemPath()) - - if (dsData) { - this.existsInDataSource := true - - if (dsData.Has("data")) { - dsData := dsData["data"] - } - - if (dsData.Has("defaults")) { - defaults := this.merger.Merge(dsData["defaults"], defaults) - defaults := this.MergeAdditionalDataSourceDefaults(defaults, dsData) - } - } - } - } - - return defaults - } - - DiscoverDataSourceItemKey() { - return this.Id - } - - GetDataSourceItemPath() { - return this.dataSourcePath - } - - MergeAdditionalDataSourceDefaults(defaults, dataSourceData) { - return defaults - } - - GetAssetPath(filePath) { - return this["AssetsDir"] . "\" . filePath - } -} diff --git a/Lib/Shared/Volantis.App/Entity/BackupEntity.ahk b/Lib/Shared/Volantis.App/Entity/BackupEntity.ahk index c02ae818..9d2c32fa 100644 --- a/Lib/Shared/Volantis.App/Entity/BackupEntity.ahk +++ b/Lib/Shared/Volantis.App/Entity/BackupEntity.ahk @@ -1,11 +1,12 @@ -class BackupEntity extends AppEntityBase { +class BackupEntity extends FieldableEntity { backup := "" - __New(app, key, config, parentEntity := "", requiredConfigKeys := "") { - super.__New(app, key, config, parentEntity, requiredConfigKeys) - backupClass := config.Has("BackupClass") ? config["BackupClass"] : "FileBackup" + SetupEntity() { + super.SetupEntity() if (!this.backup) { + backupClass := this.config.Has("BackupClass") ? this.config["BackupClass"] : "FileBackup" + this.CreateBackupObject(backupClass) } } @@ -13,10 +14,6 @@ class BackupEntity extends AppEntityBase { BaseFieldDefinitions() { definitions := super.BaseFieldDefinitions() - if (definitions.Has("DataSourceKeys")) { - definitions["DataSourceKeys"]["default"] := [] - } - definitions["IsEditable"] := Map( "type", "boolean", "default", true diff --git a/Lib/Shared/Volantis.App/Entity/TaskEntity.ahk b/Lib/Shared/Volantis.App/Entity/TaskEntity.ahk index c8a5b9b7..3f294960 100644 --- a/Lib/Shared/Volantis.App/Entity/TaskEntity.ahk +++ b/Lib/Shared/Volantis.App/Entity/TaskEntity.ahk @@ -1,3 +1,3 @@ -class TaskEntity extends AppEntityBase { +class TaskEntity extends FieldableEntity { } diff --git a/Lib/Shared/Volantis.App/Gui/Dialog/ErrorDialog.ahk b/Lib/Shared/Volantis.App/Gui/Dialog/ErrorDialog.ahk index 566e0964..672baa1a 100644 --- a/Lib/Shared/Volantis.App/Gui/Dialog/ErrorDialog.ahk +++ b/Lib/Shared/Volantis.App/Gui/Dialog/ErrorDialog.ahk @@ -1,7 +1,6 @@ class ErrorDialog extends DialogBox { errorObj := "" notifierObj := "" - apiEndpoint := "" formShown := false formH := 0 guiH := 0 @@ -9,15 +8,6 @@ class ErrorDialog extends DialogBox { __New(container, themeObj, config, errorObj) { this.errorObj := errorObj this.notifierObj := container.Get("notifier").notifierObj - - if (container.Has("manager.data_source")) { - dsManager := container.Get("manager.data_source") - - if (dsManager.GetDefaultDataSource()) { - this.apiEndpoint := container.Get("manager.data_source").GetDefaultDataSource() - } - } - this.formShown := config.Has("submitError") ? config["submitError"] : false super.__New(container, themeObj, config) @@ -118,8 +108,14 @@ class ErrorDialog extends DialogBox { SendError() { global appVersion - if (this.apiEndpoint) { - endpoint := this.apiEndpoint.endpointUrl . "/submit-error" + ; @todo Move the API connection stuff into the LaunchpadApi module + + if ( + this.container.Has("entity_manager.web_service") + && this.container["entity_manager.web_service"].Has("launchpad_api") + && this.container["entity_manager.web_service"]["launchpad_api"]["Enabled"] + ) { + webService := this.container["entity_manager.web_service"]["launchpad_api"] body := Map() body["message"] := this.errorObj.Message @@ -132,12 +128,14 @@ class ErrorDialog extends DialogBox { body["version"] := appVersion ? appVersion : "" body["details"] := this.guiObj["ErrorDetails"].Text - request := WinHttpReq(endpoint) - response := request.Send("POST", body) - success := !!(request.GetStatusCode() == 200) + success := webService.AdapterRequest( + Map("data", body), + Map("adapterType", "error_submission"), + "create" + ) - notification := success ? "Successfully sent error to Volantis Development" : "Failed to send error to Volantis Development" - this.notifierObj.Notify(notification, "Error Sent", success ? "info" : "error") + notification := success ? "Successfully sent error details for further investigation" : "Failed to send error details" + this.notifierObj.Notify(notification, "Error Submission", success ? "info" : "error") } } } diff --git a/Lib/Shared/Volantis.App/Gui/Dialog/UpdateAvailableWindow.ahk b/Lib/Shared/Volantis.App/Gui/Dialog/UpdateAvailableWindow.ahk index 8e0a2a83..d0d8c4a1 100644 --- a/Lib/Shared/Volantis.App/Gui/Dialog/UpdateAvailableWindow.ahk +++ b/Lib/Shared/Volantis.App/Gui/Dialog/UpdateAvailableWindow.ahk @@ -21,9 +21,9 @@ class UpdateAvailableWindow extends FormGuiBase { super.Controls() this.guiObj.AddText("w" . this.windowSettings["contentWidth"] . " y+" . (this.margin*2), "Current version: " . appVersion) this.SetFont("normal", "Bold") - this.guiObj.AddText("w" . this.windowSettings["contentWidth"] . " y+" . (this.margin), "Latest version: " . this.releaseInfo["data"]["version"]) + this.guiObj.AddText("w" . this.windowSettings["contentWidth"] . " y+" . (this.margin), "Latest version: " . this.releaseInfo["version"]) this.SetFont() - this.guiObj.AddLink("w" . this.windowSettings["contentWidth"] . " y+" . (this.margin), 'View release notes') + this.guiObj.AddLink("w" . this.windowSettings["contentWidth"] . " y+" . (this.margin), 'View release notes') this.guiObj.AddText("w" . this.windowSettings["contentWidth"] . " y+" . (this.margin*2), "Would you like to update " . this.app.appName . " now?") } @@ -36,14 +36,14 @@ class UpdateAvailableWindow extends FormGuiBase { } ApplyUpdate() { - downloadUrl := this.releaseInfo["data"].Has("installer") ? this.releaseInfo["data"]["installer"] : "" + downloadUrl := this.releaseInfo.Has("installer") ? this.releaseInfo["installer"] : "" if (!DirExist(this.app.tmpDir . "\Installers")) { DirCreate(this.app.tmpDir . "\Installers") } if (downloadUrl) { - localFile := this.app.tmpDir . "\Installers\" . this.app.appName . "-" . this.releaseInfo["data"]["version"] . ".exe" + localFile := this.app.tmpDir . "\Installers\" . this.app.appName . "-" . this.releaseInfo["version"] . ".exe" FileDelete(this.app.tmpDir . "\Installers\" . this.app.appName . "-*") Download(downloadUrl, localFile) Run(localFile) diff --git a/Lib/Shared/Volantis.App/Gui/EntityEditor/EntityEditorBase.ahk b/Lib/Shared/Volantis.App/Gui/EntityEditor/EntityEditorBase.ahk index 467bd165..fd681036 100644 --- a/Lib/Shared/Volantis.App/Gui/EntityEditor/EntityEditorBase.ahk +++ b/Lib/Shared/Volantis.App/Gui/EntityEditor/EntityEditorBase.ahk @@ -9,7 +9,6 @@ class EntityEditorBase extends FormGuiBase { entityObj := "" missingFields := Map() - dataSource := "" entityFormFactory := "" entityForm := "" @@ -75,9 +74,4 @@ class EntityEditorBase extends FormGuiBase { AddEntityCtl(heading, fieldName, showDefaultCheckbox, params*) { return this.Add("EntityControl", "", heading, this.entityObj, fieldName, showDefaultCheckbox, params*) } - - Create() { - super.Create() - this.dataSource := this.app["manager.data_source"].GetDefaultDataSource() - } } diff --git a/Lib/Shared/Volantis.App/GuiControl/EntityControl.ahk b/Lib/Shared/Volantis.App/GuiControl/EntityControl.ahk index fb3688c8..7c159b01 100644 --- a/Lib/Shared/Volantis.App/GuiControl/EntityControl.ahk +++ b/Lib/Shared/Volantis.App/GuiControl/EntityControl.ahk @@ -66,7 +66,7 @@ class EntityControl extends GuiControlBase { this.widget.WriteValueToEntity() if (this.refreshDataOnChange && (!this.dependentFields || this.dependentFields.Length == 0)) { - this.entityObj.UpdateDataSourceDefaults() + this.entityObj.UpdateDefaults() } this.SetDependentFieldValues() @@ -117,7 +117,7 @@ class EntityControl extends GuiControlBase { SetDependentFieldValues() { if (this.dependentFields && this.dependentFields.Length > 0) { - this.entityObj.UpdateDataSourceDefaults() + this.entityObj.UpdateDefaults() for index, field in this.dependentFields { this.guiObj.guiObj[field].Value := this.entityObj.GetField(field).GetRawValue() diff --git a/Lib/Shared/Volantis.App/Service/ComponentManager/DataSourceManager.ahk b/Lib/Shared/Volantis.App/Service/ComponentManager/DataSourceManager.ahk deleted file mode 100644 index 2b688991..00000000 --- a/Lib/Shared/Volantis.App/Service/ComponentManager/DataSourceManager.ahk +++ /dev/null @@ -1,61 +0,0 @@ -class DataSourceManager extends ComponentManagerBase { - primaryKey := "" - - __New(container, eventMgr, notifierObj, primaryKey := "") { - if (primaryKey) { - this.primaryKey := primaryKey - } - - super.__New(container, "data_source.", eventMgr, notifierObj, DataSourceBase) - } - - GetDefaultDataSource() { - if (!this.primaryKey) { - throw ComponentException("There is no default data source set") - } - - if (!this.Has(this.primaryKey)) { - throw ComponentException("Primary data source key " . this.primaryKey . " does not exist") - } - - return this[this.primaryKey] - } - - GetDefaultComponentId() { - return this.primaryKey - } - - GetItem(key := "") { - if (key == "") { - key := this.primaryKey - } - - return super.GetItem(key) - } - - ReadListing(path, dataSourceKey := "") { - if (dataSourceKey == "") { - dataSourceKey := this.primaryKey - } - - if (!this.Has(dataSourceKey)) { - throw ComponentException("Component " . dataSourceKey . " does not exist in the data source manager") - } - - dataSource := this[dataSourceKey] - return dataSource.ReadListing(path) - } - - ReadJson(key, path := "", dataSourceKey := "") { - if (dataSourceKey == "") { - dataSourceKey := this.primaryKey - } - - if (!this.Has(dataSourceKey)) { - throw ComponentException("Component " . dataSourceKey . " does not exist in the data source manager") - } - - dataSource := this[dataSourceKey] - return dataSource.ReadJson(key, path) - } -} diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index c428f0b1..cc2562ea 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -3,6 +3,7 @@ class EntityBase { entityTypeIdVal := "" parentEntityObj := "" container := "" + app := "" eventMgr := "" dataObj := "" storageObj := "" @@ -37,7 +38,7 @@ class EntityBase { set => this.SetValue("name", value) } - UnmergedFieldData { + RawData { get => this.GetData().GetLayer(this.dataLayer) set => this.GetData().SetLayer(this.dataLayer, value) } @@ -57,6 +58,7 @@ class EntityBase { } __New(id, entityTypeId, container, eventMgr, storageObj, idSanitizer := "", autoLoad := true, parentEntity := "") { + this.app := container.GetApp() this.idSanitizer := idSanitizer if (this.sanitizeId && this.idSanitizer) { @@ -87,6 +89,20 @@ class EntityBase { } } + static Create(container, eventMgr, id, entityTypeId, storageObj, idSanitizer, parentEntity := "") { + className := this.Prototype.__Class + + return %className%( + id, + entityTypeId, + container, + eventMgr, + storageObj, + idSanitizer, + parentEntity + ) + } + _createEntityData() { this.dataObj := EntityData(this, this._getLayerNames(), this._getLayerSources()) } @@ -104,18 +120,14 @@ class EntityBase { ) } - static Create(container, eventMgr, id, entityTypeId, storageObj, idSanitizer, parentEntity := "") { - className := this.Prototype.__Class - - return %className%( - id, - entityTypeId, - container, - eventMgr, - storageObj, - idSanitizer, - parentEntity - ) + /** + * Get an array of all IDs + * + * List managed IDs and give modules a chance to add others. + */ + ListEntities(includeManaged := true, includeExtended := true) { + return this.container["entity_manager." . this.EntityTypeId] + .ListEntities(includeManaged, includeExtended) } DiscoverParentEntity(container, eventMgr, id, storageObj, idSanitizer) { @@ -139,10 +151,6 @@ class EntityBase { return this.GetData().GetMergedData(!raw) } - GetEntityTypeId() { - return this.entityTypeId - } - GetEntityType() { ; @todo Inject entity type manager service return this.container.Get("manager.entity_type")[this.EntityTypeId] @@ -224,6 +232,15 @@ class EntityBase { RestoreSnapshot(name, recurse := true) { this.GetData().RestoreSnapshot(name) + + if (recurse) { + for index, entityObj in this.GetReferencedEntities(true) { + if (entityObj.HasOwnDataStorage()) { + entityObj.GetData().RestoreSnapshot(name, recurse) + } + } + } + return this } @@ -275,7 +292,13 @@ class EntityBase { } } - return values + event := EntityDetectValuesEvent(EntityEvents.ENTITY_DETECT_VALUES, this.EntityTypeId, this, values) + this.eventMgr.DispatchEvent(event) + + event := EntityDetectValuesEvent(EntityEvents.ENTITY_DETECT_VALUES_ALTER, this.EntityTypeId, this, event.Values) + this.eventMgr.DispatchEvent(event) + + return event.Values } SaveEntity(recurse := true) { @@ -437,4 +460,16 @@ class EntityBase { return text } + + UpdateDefaults(recurse := true) { + if (this.HasOwnDataStorage()) { + this.GetData().UnloadAllLayers(false) + } + + if (recurse) { + for key, child in this.GetReferencedEntities(true) { + child.UpdateDefaults(recurse) + } + } + } } diff --git a/Lib/Shared/Volantis.Entity/Entity/FieldableEntity.ahk b/Lib/Shared/Volantis.Entity/Entity/FieldableEntity.ahk index 2776d249..e4e43d60 100644 --- a/Lib/Shared/Volantis.Entity/Entity/FieldableEntity.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/FieldableEntity.ahk @@ -11,12 +11,29 @@ class FieldableEntity extends EntityBase { get => this.GetFieldDefinitions() } - __New(id, entityTypeId, container, eventMgr, storageObj, idSanitizer := "", autoLoad := true) { - this.entityFieldFactory := container.Get("entity_field_factory." . entityTypeId) - this.entityWidgetFactory := container.Get("entity_widget_factory." . entityTypeId) + __New(id, entityTypeId, container, fieldFactory, widgetFactory, eventMgr, storageObj, idSanitizer := "", autoLoad := true) { + this.entityFieldFactory := fieldFactory + this.entityWidgetFactory := widgetFactory + super.__New(id, entityTypeId, container, eventMgr, storageObj, idSanitizer, autoLoad) } + static Create(container, eventMgr, id, entityTypeId, storageObj, idSanitizer, parentEntity := "") { + className := this.Prototype.__Class + + return %className%( + id, + entityTypeId, + container, + container.Get("entity_field_factory." . entityTypeId), + container.Get("entity_widget_factory." . entityTypeId), + eventMgr, + storageObj, + idSanitizer, + parentEntity + ) + } + GetDefaultFieldGroups() { return Map( "general", Map( diff --git a/Lib/Shared/Volantis.Entity/EntityManager/EntityManagerBase.ahk b/Lib/Shared/Volantis.Entity/EntityManager/EntityManagerBase.ahk index c84eacc2..00399489 100644 --- a/Lib/Shared/Volantis.Entity/EntityManager/EntityManagerBase.ahk +++ b/Lib/Shared/Volantis.Entity/EntityManager/EntityManagerBase.ahk @@ -94,4 +94,30 @@ class EntityManagerBase extends ComponentManagerBase { childManager.LoadComponents(reloadComponents) } } + + /** + * Get an array of all IDs + * + * List managed IDs and give modules a chance to add others. + */ + ListEntities(includeManaged := true, includeExtended := true) { + entities := includeManaged + ? this.EntityQuery(EntityQuery.RESULT_TYPE_IDS).Execute() + : [] + + if (includeExtended) { + event := EntityListEvent( + EntityEvents.ENTITY_LIST_ENTITIES, + this.entityTypeId, + entities, + includeManaged, + includeExtended + ) + this.eventMgr.DispatchEvent(event) + + entities := event.EntityList + } + + return entities + } } diff --git a/Lib/Shared/Volantis.Entity/EntityStorage/EntityStorageBase.ahk b/Lib/Shared/Volantis.Entity/EntityStorage/EntityStorageBase.ahk index b471ec6f..bd366064 100644 --- a/Lib/Shared/Volantis.Entity/EntityStorage/EntityStorageBase.ahk +++ b/Lib/Shared/Volantis.Entity/EntityStorage/EntityStorageBase.ahk @@ -64,7 +64,7 @@ class EntityStorageBase { _dereferenceData(idOrObj, data := "") { if (HasBase(idOrObj, EntityBase.Prototype) && !data) { - data := idOrObj.UnmergedFieldData + data := idOrObj.RawData } return data From ca26128c075abdc04a9b440ae4896765600ab350 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Wed, 14 Dec 2022 16:57:41 -0500 Subject: [PATCH 079/227] Rename DataLookupKey to LaunchpadApiRef --- Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk | 4 ++-- .../EventSubscriber/LaunchpadApiSubscriber.ahk | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk index 8137e949..d8833ccd 100644 --- a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk @@ -133,7 +133,7 @@ status := launcher.GetStatus() ; @todo Move the API data to an event in the LaunchpadApi module - apiStatus := (launcher.HasField["DataLookupKey"] && launcher["DataLookupKey"]) ? "Linked" : "Not linked" + apiStatus := (launcher.HasField["LaunchpadApiRef"] && launcher["LaunchpadApiRef"]) ? "Linked" : "Not linked" created := this.FormatDate(this.app.State.GetLauncherCreated(key)) updated := this.FormatDate(this.app.State.GetLauncherInfo("Config")["Timestamp"]) built := this.FormatDate(this.app.State.GetLauncherInfo("Build")["Timestamp"]) @@ -311,7 +311,7 @@ status := launcher.GetStatus() ; @todo Move the API code to the LaunchpadApi module - apiStatus := (launcher.HasField("DataLookupKey") && launcher["DataLookupKey"]) ? "Linked" : "Not linked" + apiStatus := (launcher.HasField("LaunchpadApiRef") && launcher["LaunchpadApiRef"]) ? "Linked" : "Not linked" created := this.FormatDate(this.app.State.GetLauncherCreated(key)) updated := this.FormatDate(this.app.State.GetLauncherInfo(key, "Config")["Timestamp"]) built := this.FormatDate(this.app.State.GetLauncherInfo(key, "Build")["Timestamp"]) diff --git a/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk b/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk index bc9da846..699cbb5d 100644 --- a/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk +++ b/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk @@ -145,9 +145,9 @@ class LaunchpadApiSubscriber extends EventSubscriberBase { ]) if (adapters.Count) { - fieldDefinitions["DataLookupKey"] := Map( + fieldDefinitions["LaunchpadApiRef"] := Map( "description", "The key that is used to look up the entity's data from configured external data sources.", - "help", "It defaults to the key which is usually sufficient, but it can be overridden by setting this value.`n`nAddtionally, multiple copies of the same data source entity can exist by giving them different keys but using the same DataLookupKey", + "help", "It defaults to the key which is usually sufficient, but it can be overridden by setting this value.`n`nAddtionally, multiple copies of the same data source entity can exist by giving them different keys but using the same LaunchpadApiRef", "group", "api", "processValue", false, "modes", Map("simple", Map("formField", false)) @@ -167,9 +167,9 @@ class LaunchpadApiSubscriber extends EventSubscriberBase { if ( webService["Enabled"] - && (!values.Has("DataLookupKey") || !values["DataLookupKey"]) - && entity.HasField("DataLookupKey") - && (!entity.RawData.Has["DataLookupKey"] || !entity.RawData["DataLookupKey"]) + && (!values.Has("LaunchpadApiRef") || !values["LaunchpadApiRef"]) + && entity.HasField("LaunchpadApiRef") + && (!entity.RawData.Has["LaunchpadApiRef"] || !entity.RawData["LaunchpadApiRef"]) ) { result := "" @@ -190,7 +190,7 @@ class LaunchpadApiSubscriber extends EventSubscriberBase { } if (result) { - values["DataLookupKey"] := result + values["LaunchpadApiRef"] := result } } } From e908cc9a515a3b8154879d219a963373b7db0c27 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Wed, 14 Dec 2022 16:58:48 -0500 Subject: [PATCH 080/227] Rename LauncherSpecificId to PlatformRef --- Lib/Launchpad/DetectedGame/DetectedGame.ahk | 12 ++++++------ Lib/Launchpad/Entity/ManagedEntityBase.ahk | 6 +++--- Lib/Launchpad/Entity/ManagedGameEntity.ahk | 2 +- Lib/Launchpad/Entity/ManagedProcessEntity.ahk | 6 +++--- Lib/Launchpad/GamePlatform/GamePlatformBase.ahk | 6 +++--- Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk | 6 +++--- .../Blizzard/GamePlatform/BlizzardPlatform.ahk | 6 +++--- .../Modules/Epic/GamePlatform/EpicPlatform.ahk | 4 ++-- .../Modules/Riot/GamePlatform/RiotPlatform.ahk | 2 +- .../Modules/Steam/GamePlatform/SteamPlatform.ahk | 4 ++-- Lib/LaunchpadLauncher/Game/BlizzardGame.ahk | 2 +- Lib/LaunchpadLauncher/Game/RiotGame.ahk | 2 +- 12 files changed, 29 insertions(+), 29 deletions(-) diff --git a/Lib/Launchpad/DetectedGame/DetectedGame.ahk b/Lib/Launchpad/DetectedGame/DetectedGame.ahk index 5707278f..68aa54b2 100644 --- a/Lib/Launchpad/DetectedGame/DetectedGame.ahk +++ b/Lib/Launchpad/DetectedGame/DetectedGame.ahk @@ -8,7 +8,7 @@ class DetectedGame { installDir := "" launcherInstallDir := "" exeName := "" - launcherSpecificId := "" + platformRef := "" possibleExeNames := [] keyMap := Map() ; @todo Move this to properties or config or allow it to be extended @@ -17,7 +17,7 @@ class DetectedGame { prioritySuffixes := ["-Win64-Shipping", "-Win32-Shipping"] filterExes := [] - __New(key, platform, launcherType, gameType := "Default", installDir := "", exeName := "", launcherSpecificId := "", possibleExeNames := "", displayName := "") { + __New(key, platform, launcherType, gameType := "Default", installDir := "", exeName := "", platformRef := "", possibleExeNames := "", displayName := "") { this.key := key this.displayName := displayName ? displayName : key this.platform := platform @@ -26,7 +26,7 @@ class DetectedGame { this.gameType := gameType this.installDir := installDir this.exeName := exeName - this.launcherSpecificId := launcherSpecificId + this.platformRef := platformRef if (possibleExeNames) { if (Type(possibleExeNames) == "String") { @@ -49,7 +49,7 @@ class DetectedGame { || this.installDir != launcher["ManagedGame"]["InstallDir"] || this.launcherInstallDir != launcher["ManagedLauncher"]["InstallDir"] || this.exeName != launcher["ManagedGame"]["Exe"] - || this.launcherSpecificId != launcher["ManagedGame"]["LauncherSpecificId"] + || this.platformRef != launcher["ManagedGame"]["PlatformRef"] ) { hasChanges := true } @@ -122,8 +122,8 @@ class DetectedGame { config["GameExe"] := this.exeName } - if (this.launcherSpecificId) { - config["GameLauncherSpecificId"] := this.launcherSpecificId + if (this.platformRef) { + config["GamePlatformRef"] := this.platformRef } entityObj := launcherManager.GetFactory().CreateEntity(this.key, config) diff --git a/Lib/Launchpad/Entity/ManagedEntityBase.ahk b/Lib/Launchpad/Entity/ManagedEntityBase.ahk index b7be2261..b84be410 100644 --- a/Lib/Launchpad/Entity/ManagedEntityBase.ahk +++ b/Lib/Launchpad/Entity/ManagedEntityBase.ahk @@ -122,7 +122,7 @@ class ManagedEntityBase extends FieldableEntity { ), "widget", "select", "selectOptionsCallback", ObjBindMethod(this, "ListLocateMethods"), - "help", "Search: Searches a list of possible directories (Defaulting to some common possibilities) for the .exe file and uses that directory`nRegistry: Looks for the provided registry key and uses its value as the install path if present`nBlizzardProductDb: Searches for LauncherSpecificId within the Blizzard product.db file if present" + "help", "Search: Searches a list of possible directories (Defaulting to some common possibilities) for the .exe file and uses that directory`nRegistry: Looks for the provided registry key and uses its value as the install path if present`nBlizzardProductDb: Searches for PlatformRef within the Blizzard product.db file if present" ) definitions["WindowTitle"] := Map( @@ -188,8 +188,8 @@ class ManagedEntityBase extends FieldableEntity { ) ) - definitions["LauncherSpecificId"] := Map( - "storageKey", this.configPrefix . "LauncherSpecificId", + definitions["PlatformRef"] := Map( + "storageKey", this.configPrefix . "PlatformRef", "description", "If the item is known to the launcher by a specific ID, it should be stored here.", "group", "general" ) diff --git a/Lib/Launchpad/Entity/ManagedGameEntity.ahk b/Lib/Launchpad/Entity/ManagedGameEntity.ahk index 235010cc..916ebe67 100644 --- a/Lib/Launchpad/Entity/ManagedGameEntity.ahk +++ b/Lib/Launchpad/Entity/ManagedGameEntity.ahk @@ -40,7 +40,7 @@ class ManagedGameEntity extends ManagedEntityBase { } GetBlizzardProductKey() { - productKey := this["LauncherSpecificId"] + productKey := this["PlatformRef"] if (this.HasConfigValue("BlizzardProductId", true, false)) { productKey := this.GetConfigValue("BlizzardProductId") diff --git a/Lib/Launchpad/Entity/ManagedProcessEntity.ahk b/Lib/Launchpad/Entity/ManagedProcessEntity.ahk index 4ff6778c..02cb087b 100644 --- a/Lib/Launchpad/Entity/ManagedProcessEntity.ahk +++ b/Lib/Launchpad/Entity/ManagedProcessEntity.ahk @@ -122,7 +122,7 @@ class ManagedProcessEntity extends FieldableEntity { ), "widget", "select", "selectOptionsCallback", ObjBindMethod(this, "ListLocateMethods"), - "help", "Search: Searches a list of possible directories (Defaulting to some common possibilities) for the .exe file and uses that directory`nRegistry: Looks for the provided registry key and uses its value as the install path if present`nBlizzardProductDb: Searches for LauncherSpecificId within the Blizzard product.db file if present" + "help", "Search: Searches a list of possible directories (Defaulting to some common possibilities) for the .exe file and uses that directory`nRegistry: Looks for the provided registry key and uses its value as the install path if present`nBlizzardProductDb: Searches for PlatformRef within the Blizzard product.db file if present" ) definitions["WindowTitle"] := Map( @@ -188,8 +188,8 @@ class ManagedProcessEntity extends FieldableEntity { ) ) - definitions["LauncherSpecificId"] := Map( - "storageKey", this.configPrefix . "LauncherSpecificId", + definitions["PlatformRef"] := Map( + "storageKey", this.configPrefix . "PlatformRef", "description", "If the item is known to the launcher by a specific ID, it should be stored here.", "group", "general" ) diff --git a/Lib/Launchpad/GamePlatform/GamePlatformBase.ahk b/Lib/Launchpad/GamePlatform/GamePlatformBase.ahk index 0476cd6d..a8dfc88b 100644 --- a/Lib/Launchpad/GamePlatform/GamePlatformBase.ahk +++ b/Lib/Launchpad/GamePlatform/GamePlatformBase.ahk @@ -138,7 +138,7 @@ class GamePlatformBase { return [] } - GetLauncherSpecificId(key) { + GetPlatformRef(key) { return key } @@ -164,8 +164,8 @@ class GamePlatformBase { locator := GameExeLocator(installDir) possibleExes := locator.Locate("") exeName := this.DetermineMainExe(key, possibleExes) - launcherSpecificId := this.GetLauncherSpecificId(key) - detectedGameObj := DetectedGame(key, this, this.launcherType, this.gameType, installDir, exeName, launcherSpecificId, possibleExes) + platformRef := this.GetPlatformRef(key) + detectedGameObj := DetectedGame(key, this, this.launcherType, this.gameType, installDir, exeName, platformRef, possibleExes) if (this.installDir) { detectedGameObj.launcherInstallDir := this["InstallDir"] diff --git a/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk b/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk index 518c04c5..6b8de59e 100644 --- a/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk +++ b/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk @@ -82,7 +82,7 @@ this.Add("SelectControl", "vGameType", "Game Type", this.detectedGameObj.gameType, this.gameTypes, "OnGameTypeChange", "This tells " . this.app.appName . " how to launch your game. Most games can use 'default', but launchers can support different game types.`n`nYou can customize the details of the game type after it is added.") this.Add("LocationBlock", "", "Install Dir", this.detectedGameObj.installDir, "InstallDir", "", true, "This is the directory that the game is installed in, if it could be detected.") this.Add("ComboBoxControl", "vExe", "Exe", this.detectedGameObj.exeName, this.detectedGameObj.possibleExeNames, "OnExeChange", "The main Exe, if detected, should be pre-selected. You may change it to be the name (or path) of another exe, or select another one of the detected .exe files from the list (if more than one was found).") - this.AddTextBlock("Launcher-Specific ID", "LauncherSpecificId", this.detectedGameObj.launcherSpecificId, "This is typically the ID which the game platform or launcher uses when referring to the game internally. Changing this value could cause issues with game launching.") + this.AddTextBlock("Launcher-Specific ID", "PlatformRef", this.detectedGameObj.platformRef, "This is typically the ID which the game platform or launcher uses when referring to the game internally. Changing this value could cause issues with game launching.") } AddTextBlock(heading, field, existingVal := "", helpText := "") { @@ -159,9 +159,9 @@ this.newValues["exeName"] := ctl.Text } - OnLauncherSpecificIdChange(ctl, info) { + OnPlatformRefChange(ctl, info) { this.guiObj.Submit(false) - this.newValues["launcherSpecificId"] := ctl.Text + this.newValues["platformRef"] := ctl.Text } ProcessResult(result, submittedData := "") { diff --git a/Lib/Launchpad/Modules/Blizzard/GamePlatform/BlizzardPlatform.ahk b/Lib/Launchpad/Modules/Blizzard/GamePlatform/BlizzardPlatform.ahk index 51c997e3..097d4a3e 100644 --- a/Lib/Launchpad/Modules/Blizzard/GamePlatform/BlizzardPlatform.ahk +++ b/Lib/Launchpad/Modules/Blizzard/GamePlatform/BlizzardPlatform.ahk @@ -28,16 +28,16 @@ class BlizzardPlatform extends RegistryLookupGamePlatformBase { games := [] for index, productData in productInstalls { - launcherSpecificId := productData["productCode"] + platformRef := productData["productCode"] - if (launcherSpecificId != "agent" && launcherSpecificId != "bna" && productData.Has("settings") && productData["settings"].Has("installPath")) { + if (platformRef != "agent" && platformRef != "bna" && productData.Has("settings") && productData["settings"].Has("installPath")) { installPath := productData["settings"]["installPath"] installPath := StrReplace(installPath, "/", "\") SplitPath(installPath, &key) locator := GameExeLocator(installPath) possibleExes := locator.Locate("") mainExe := this.DetermineMainExe(key, possibleExes) - games.Push(DetectedGame(key, this, this.launcherType, this.gameType, installPath, mainExe, launcherSpecificId, possibleExes)) + games.Push(DetectedGame(key, this, this.launcherType, this.gameType, installPath, mainExe, platformRef, possibleExes)) } } diff --git a/Lib/Launchpad/Modules/Epic/GamePlatform/EpicPlatform.ahk b/Lib/Launchpad/Modules/Epic/GamePlatform/EpicPlatform.ahk index b42c67bc..1cdb2845 100644 --- a/Lib/Launchpad/Modules/Epic/GamePlatform/EpicPlatform.ahk +++ b/Lib/Launchpad/Modules/Epic/GamePlatform/EpicPlatform.ahk @@ -57,13 +57,13 @@ class EpicPlatform extends RegistryLookupGamePlatformBase { displayName := obj.Has("DisplayName") ? obj["DisplayName"] : "" installDir := obj["InstallLocation"] - launcherSpecificId := obj["AppName"] + platformRef := obj["AppName"] ;exeName := obj["LaunchExecutable"] ;possibleExes := [obj["LaunchExecutable"]] locator := GameExeLocator(installDir) possibleExes := locator.Locate("") mainExe := this.DetermineMainExe(key, possibleExes) - games.Push(DetectedGame(key, this, this.launcherType, this.gameType, installDir, mainExe, launcherSpecificId, possibleExes, displayName)) + games.Push(DetectedGame(key, this, this.launcherType, this.gameType, installDir, mainExe, platformRef, possibleExes, displayName)) } } } diff --git a/Lib/Launchpad/Modules/Riot/GamePlatform/RiotPlatform.ahk b/Lib/Launchpad/Modules/Riot/GamePlatform/RiotPlatform.ahk index ef37ab9e..091b1bf5 100644 --- a/Lib/Launchpad/Modules/Riot/GamePlatform/RiotPlatform.ahk +++ b/Lib/Launchpad/Modules/Riot/GamePlatform/RiotPlatform.ahk @@ -66,7 +66,7 @@ class RiotPlatform extends RegistryLookupGamePlatformBase { return dirs } - GetLauncherSpecificId(key) { + GetPlatformRef(key) { if (key == "VALORANT") { key := "valorant" } else if (key == "Legends of Runeterra") { diff --git a/Lib/Launchpad/Modules/Steam/GamePlatform/SteamPlatform.ahk b/Lib/Launchpad/Modules/Steam/GamePlatform/SteamPlatform.ahk index 3d88dfad..644f96ff 100644 --- a/Lib/Launchpad/Modules/Steam/GamePlatform/SteamPlatform.ahk +++ b/Lib/Launchpad/Modules/Steam/GamePlatform/SteamPlatform.ahk @@ -64,7 +64,7 @@ class SteamPlatform extends RegistryLookupGamePlatformBase { if (IsObject(obj) && obj.Has("AppState")) { gameState := obj["AppState"] - launcherSpecificId := gameState["appid"] + platformRef := gameState["appid"] key := gameState["name"] installDir := dir . "\common\" . gameState["installdir"] installDir := StrReplace(installDir, "/", "\") @@ -76,7 +76,7 @@ class SteamPlatform extends RegistryLookupGamePlatformBase { } mainExe := this.DetermineMainExe(key, possibleExes) - games.Push(DetectedGame(key, this, this.launcherType, this.gameType, installDir, mainExe, launcherSpecificId, possibleExes)) + games.Push(DetectedGame(key, this, this.launcherType, this.gameType, installDir, mainExe, platformRef, possibleExes)) } } } diff --git a/Lib/LaunchpadLauncher/Game/BlizzardGame.ahk b/Lib/LaunchpadLauncher/Game/BlizzardGame.ahk index 5eafec37..c88ed59b 100644 --- a/Lib/LaunchpadLauncher/Game/BlizzardGame.ahk +++ b/Lib/LaunchpadLauncher/Game/BlizzardGame.ahk @@ -7,7 +7,7 @@ class BlizzardGame extends SimpleGame { launcherPath := this.app["Launcher"].config["LauncherInstallDir"] . "\" . this.app["Launcher"].config["LauncherExe"] if (launcherPath != "") { - gameKey := this.config["GameLauncherSpecificId"] + gameKey := this.config["GamePlatformRef"] launcherPath .= " --game=" . gameKey . " --gamepath=`"" . this.config["GameInstallDir"] . "`" --productcode=" . gameKey } diff --git a/Lib/LaunchpadLauncher/Game/RiotGame.ahk b/Lib/LaunchpadLauncher/Game/RiotGame.ahk index e4edda9b..1d63a6d2 100644 --- a/Lib/LaunchpadLauncher/Game/RiotGame.ahk +++ b/Lib/LaunchpadLauncher/Game/RiotGame.ahk @@ -3,7 +3,7 @@ launcherPath := "`"" . this.app["Launcher"].config["LauncherInstallDir"] . "\" . this.app["Launcher"].config["LauncherExe"] . "`"" if (launcherPath != "") { - gameKey := this.config["GameLauncherSpecificId"] + gameKey := this.config["GamePlatformRef"] launcherPath .= " --launch-product=" . gameKey . " --launch-patchline=live" } From cbd8a79e3b5309e508bf6ed7a436b5a92431bf35 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 02:50:02 -0500 Subject: [PATCH 081/227] Fix 0 array index in VersionChecker --- Lib/Shared/Volantis.Utility/VersionChecker/VersionChecker.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/Shared/Volantis.Utility/VersionChecker/VersionChecker.ahk b/Lib/Shared/Volantis.Utility/VersionChecker/VersionChecker.ahk index fdf689d8..fe00f734 100644 --- a/Lib/Shared/Volantis.Utility/VersionChecker/VersionChecker.ahk +++ b/Lib/Shared/Volantis.Utility/VersionChecker/VersionChecker.ahk @@ -77,7 +77,7 @@ class VersionChecker { } incrementIndex := versionArr.Length - 1 - } else if (versionArr[0] == "0") { + } else if (versionArr[1] == "0") { incrementIndex := versionArr.Length } From 85ffdca84d68ee31d351272a065490317813b08a Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 02:50:54 -0500 Subject: [PATCH 082/227] Throw an exception if unable to delete include file --- .../Volantis.Utility/IncludeWriter/IncludeWriterBase.ahk | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/Lib/Shared/Volantis.Utility/IncludeWriter/IncludeWriterBase.ahk b/Lib/Shared/Volantis.Utility/IncludeWriter/IncludeWriterBase.ahk index cb9e37da..aab7342d 100644 --- a/Lib/Shared/Volantis.Utility/IncludeWriter/IncludeWriterBase.ahk +++ b/Lib/Shared/Volantis.Utility/IncludeWriter/IncludeWriterBase.ahk @@ -25,7 +25,12 @@ class IncludeWriterBase { updated := this.FilesAreDifferent(this.tmpPath, this.outputPath) if (updated) { - FileDelete(this.outputPath) + try { + FileDelete(this.outputPath) + } catch Any { + throw AppException("Unable to delete file path " . this.outputPath) + } + } } From bd08d3dd2adb7c70b9a15d0eed9626531f796c91 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 02:51:49 -0500 Subject: [PATCH 083/227] Add NullEntityStorage class for use if you don't want any backend --- Lib/Shared/Includes.ahk | 1 + Lib/Shared/Volantis.Entity/EntityStorage/NullEntityStorage.ahk | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 Lib/Shared/Volantis.Entity/EntityStorage/NullEntityStorage.ahk diff --git a/Lib/Shared/Includes.ahk b/Lib/Shared/Includes.ahk index d33ef65f..e65b65cb 100644 --- a/Lib/Shared/Includes.ahk +++ b/Lib/Shared/Includes.ahk @@ -220,6 +220,7 @@ #Include Volantis.Entity\EntityManager\EntityManagerBase.ahk #Include Volantis.Entity\EntityStorage\ConfigEntityStorage.ahk #Include Volantis.Entity\EntityStorage\EntityStorageBase.ahk +#Include Volantis.Entity\EntityStorage\NullEntityStorage.ahk #Include Volantis.Entity\EntityType\BasicEntityType.ahk #Include Volantis.Entity\EntityType\EntityTypeBase.ahk #Include Volantis.Entity\Event\EntityDataProcessorsEvent.ahk diff --git a/Lib/Shared/Volantis.Entity/EntityStorage/NullEntityStorage.ahk b/Lib/Shared/Volantis.Entity/EntityStorage/NullEntityStorage.ahk new file mode 100644 index 00000000..38cb4c2b --- /dev/null +++ b/Lib/Shared/Volantis.Entity/EntityStorage/NullEntityStorage.ahk @@ -0,0 +1,3 @@ +class NullEntityStorage extends EntityStorageBase { + +} From 303f4054f584d0fe9c5e547296ee01588e0b6ef4 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 02:53:49 -0500 Subject: [PATCH 084/227] Update WebService module events --- Lib/Shared/Includes.ahk | 5 +++-- .../WebServicesEntityDataParamsEvent.ahk | 20 +++++++++++++++++++ ...tEvent.ahk => WebServicesRequestEvent.ahk} | 2 +- ...Event.ahk => WebServicesResponseEvent.ahk} | 0 .../WebServices/Events/WebServicesEvents.ahk | 13 ++++++------ 5 files changed, 31 insertions(+), 9 deletions(-) create mode 100644 Lib/Shared/Modules/WebServices/Event/WebServicesEntityDataParamsEvent.ahk rename Lib/Shared/Modules/WebServices/Event/{WebServiceRequestEvent.ahk => WebServicesRequestEvent.ahk} (83%) rename Lib/Shared/Modules/WebServices/Event/{WebServiceResponseEvent.ahk => WebServicesResponseEvent.ahk} (100%) diff --git a/Lib/Shared/Includes.ahk b/Lib/Shared/Includes.ahk index e65b65cb..041ac818 100644 --- a/Lib/Shared/Includes.ahk +++ b/Lib/Shared/Includes.ahk @@ -2,8 +2,9 @@ #Include Modules\LaunchpadApi\EventSubscriber\LaunchpadApiSubscriber.ahk #Include Modules\WebServices\Entity\WebServiceEntity.ahk #Include Modules\WebServices\Entity\WebServiceProviderEntity.ahk -#Include Modules\WebServices\Event\WebServiceRequestEvent.ahk -#Include Modules\WebServices\Event\WebServiceResponseEvent.ahk +#Include Modules\WebServices\Event\WebServicesEntityDataParamsEvent.ahk +#Include Modules\WebServices\Event\WebServicesRequestEvent.ahk +#Include Modules\WebServices\Event\WebServicesResponseEvent.ahk #Include Modules\WebServices\Events\WebServicesEvents.ahk #Include Modules\WebServices\EventSubscriber\WebServicesEventSubscriber.ahk #Include Modules\WebServices\Factory\WebServiceAdapterFactory.ahk diff --git a/Lib/Shared/Modules/WebServices/Event/WebServicesEntityDataParamsEvent.ahk b/Lib/Shared/Modules/WebServices/Event/WebServicesEntityDataParamsEvent.ahk new file mode 100644 index 00000000..ad6ba482 --- /dev/null +++ b/Lib/Shared/Modules/WebServices/Event/WebServicesEntityDataParamsEvent.ahk @@ -0,0 +1,20 @@ +class WebServicesEntityDataParamsEvent extends EntityEvent { + _webService := "" + _params := "" + + __New(eventName, entityTypeId, entityObj, webService, params) { + this._webService := webService + this._params := params + + super.__New(eventName, entityTypeId, entityObj) + } + + WebService { + get => this._webService + } + + Params { + get => this._params + set => this._params := value + } +} diff --git a/Lib/Shared/Modules/WebServices/Event/WebServiceRequestEvent.ahk b/Lib/Shared/Modules/WebServices/Event/WebServicesRequestEvent.ahk similarity index 83% rename from Lib/Shared/Modules/WebServices/Event/WebServiceRequestEvent.ahk rename to Lib/Shared/Modules/WebServices/Event/WebServicesRequestEvent.ahk index 1b8ba941..d52a8c42 100644 --- a/Lib/Shared/Modules/WebServices/Event/WebServiceRequestEvent.ahk +++ b/Lib/Shared/Modules/WebServices/Event/WebServicesRequestEvent.ahk @@ -1,4 +1,4 @@ -class WebServiceRequestEvent extends EventBase { +class WebServicesRequestEvent extends EventBase { _requestObj := "" __New(eventName, requestObj) { diff --git a/Lib/Shared/Modules/WebServices/Event/WebServiceResponseEvent.ahk b/Lib/Shared/Modules/WebServices/Event/WebServicesResponseEvent.ahk similarity index 100% rename from Lib/Shared/Modules/WebServices/Event/WebServiceResponseEvent.ahk rename to Lib/Shared/Modules/WebServices/Event/WebServicesResponseEvent.ahk diff --git a/Lib/Shared/Modules/WebServices/Events/WebServicesEvents.ahk b/Lib/Shared/Modules/WebServices/Events/WebServicesEvents.ahk index 787d5a1a..bde6b479 100644 --- a/Lib/Shared/Modules/WebServices/Events/WebServicesEvents.ahk +++ b/Lib/Shared/Modules/WebServices/Events/WebServicesEvents.ahk @@ -1,7 +1,8 @@ class WebServicesEvents { - static WEB_SERVICES_HTTP_REQ_ALTER := 0x4200 - static WEB_SERVICES_REQUEST_PRESEND := 0x4210 - static WEB_SERVICES_CACHED_RESPONSE_CREATED := 0x4215 - static WEB_SERVICES_HTTP_RESPONSE_CREATED := 0x4217 - static WEB_SERVICES_RESPONSE_ALTER := 0x4220 -} \ No newline at end of file + static HTTP_REQ_ALTER := 0x4200 + static REQUEST_PRESEND := 0x4210 + static CACHED_RESPONSE_CREATED := 0x4215 + static HTTP_RESPONSE_CREATED := 0x4217 + static RESPONSE_ALTER := 0x4220 + static ENTITY_DATA_PARAMS := 0x4225 +} From e0c2b7ed6c622a247dc09b94ce17955c1c3606e2 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 02:54:27 -0500 Subject: [PATCH 085/227] Add an availability_check web service data type --- Lib/Shared/Modules/WebServices/WebServices.module.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Lib/Shared/Modules/WebServices/WebServices.module.json b/Lib/Shared/Modules/WebServices/WebServices.module.json index 00854c63..c00967b0 100644 --- a/Lib/Shared/Modules/WebServices/WebServices.module.json +++ b/Lib/Shared/Modules/WebServices/WebServices.module.json @@ -74,6 +74,10 @@ "name": "Entity Data", "description": "Data to be imported into an entity within the application." }, + "web_services.data_types.availability_check": { + "name": "Availability check", + "description": "Simply checks for any response from the web service." + }, "web_services.adapter_types.json": { "class": "JsonWebServiceAdapter" }, From b7e6f7cdb2bfe13c4e9013aff0b94897f670984b Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 02:55:18 -0500 Subject: [PATCH 086/227] Return this from EntityStorageLayerSource DeleteData method --- .../Volantis.Entity/LayerSource/EntityStorageLayerSource.ahk | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/Shared/Volantis.Entity/LayerSource/EntityStorageLayerSource.ahk b/Lib/Shared/Volantis.Entity/LayerSource/EntityStorageLayerSource.ahk index b58e5bb9..ec182c25 100644 --- a/Lib/Shared/Volantis.Entity/LayerSource/EntityStorageLayerSource.ahk +++ b/Lib/Shared/Volantis.Entity/LayerSource/EntityStorageLayerSource.ahk @@ -24,5 +24,7 @@ class EntityStorageLayerSource extends LayerSourceBase { if (this.HasData()) { this.storageObj.DeleteData(this.storageId) } + + return this } } From a91c24c57672462d90fced85ab743fdb8babcc0c Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 02:57:44 -0500 Subject: [PATCH 087/227] Simple language update --- Lib/LaunchpadLauncher/Launcher/LauncherBase.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/LaunchpadLauncher/Launcher/LauncherBase.ahk b/Lib/LaunchpadLauncher/Launcher/LauncherBase.ahk index 21754d91..900a7adb 100644 --- a/Lib/LaunchpadLauncher/Launcher/LauncherBase.ahk +++ b/Lib/LaunchpadLauncher/Launcher/LauncherBase.ahk @@ -189,7 +189,7 @@ class LauncherBase { } LaunchGameAction() { - this.Log("Calling managed game's RunGame action") + this.Log("Calling game process's RunGame action") return this.game.RunGame(this.progress) } From 94d327e08a6b9ff1208a5f457869bb5c760c55b4 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 02:59:13 -0500 Subject: [PATCH 088/227] Define a WebServiceAdapter service --- Lib/Shared/Includes.ahk | 1 + .../WebServiceAdapterManager.ahk | 287 ++++++++++++++++++ .../WebServices/WebServices.module.json | 4 + 3 files changed, 292 insertions(+) create mode 100644 Lib/Shared/Modules/WebServices/ComponentManager/WebServiceAdapterManager.ahk diff --git a/Lib/Shared/Includes.ahk b/Lib/Shared/Includes.ahk index 041ac818..c5d4ec96 100644 --- a/Lib/Shared/Includes.ahk +++ b/Lib/Shared/Includes.ahk @@ -1,5 +1,6 @@ ; Automatically-generated file. Manual edits will be overwritten. #Include Modules\LaunchpadApi\EventSubscriber\LaunchpadApiSubscriber.ahk +#Include Modules\WebServices\ComponentManager\WebServiceAdapterManager.ahk #Include Modules\WebServices\Entity\WebServiceEntity.ahk #Include Modules\WebServices\Entity\WebServiceProviderEntity.ahk #Include Modules\WebServices\Event\WebServicesEntityDataParamsEvent.ahk diff --git a/Lib/Shared/Modules/WebServices/ComponentManager/WebServiceAdapterManager.ahk b/Lib/Shared/Modules/WebServices/ComponentManager/WebServiceAdapterManager.ahk new file mode 100644 index 00000000..b4e81fd3 --- /dev/null +++ b/Lib/Shared/Modules/WebServices/ComponentManager/WebServiceAdapterManager.ahk @@ -0,0 +1,287 @@ +class WebServiceAdapterManager { + container := "" + parameterPrefix := "" + adapterFactory := "" + entityTypeMgr := "" + eventMgr := "" + adapters := Map() + + __New(container, parameterPrefix, adapterFactory, entityTypeMgr, eventMgr) { + this.container := container + this.parameterPrefix := parameterPrefix + this.adapterFactory := adapterFactory + this.entityTypeMgr := entityTypeMgr + this.eventMgr := eventMgr + } + + AdapterRequest(params, filters, operation := "read", multiple := false, webService := "") { + if (!params) { + params := Map() + } + + if (!filters) { + filters := Map() + } + + if (Type(filters) == "String") { + filters := Map("dataType", filters) + } + + results := Map() + + for adapterKey, adapter in this.GetAdapters(filters, operation, 0, webService) { + result := adapter.SendRequest(operation, params) + + if (result) { + if (!multiple) { + results := result + + break + } + + results[adapterKey] := result + } + + if (IsNumber(multiple) && results.Count >= multiple) { + break + } + } + + return results + } + + HasAdapters(filters := "", operation := "", webService := "") { + return !!(this.GetAdapterIds(filters, operation, 1, webService).Length) + } + + GetAdapters(filters := "", operation := "", limit := 0, webService := "") { + adapterIds := this.GetAdapterIds(filters, operation, limit, webService) + + adapters := Map() + + for , adapterId in adapterIds { + adapters[adapterId] := this.GetAdapter(adapterId) + } + + return adapters + } + + GetAdapter(id) { + adapter := "" + + if (this.adapters.Has(id)) { + adapter := this.adapters[id] + } + + if (!adapter && InStr(id, ".")) { + idParts := StrSplit(id, ".") + webServiceId := idParts[1] + adapterKey := idParts[2] + + webService := this.entityTypeMgr.GetManager("web_service")[webServiceId] + + if (webService["Enabled"]) { + param := this.parameterPrefix . webService["Provider"]["id"] . "." . adapterKey + + if (this.container.HasParameter(param)) { + adapter := this.adapterFactory.CreateWebServiceAdapter(webService, this.container.GetParameter(param)) + this.adapters[id] := adapter + } + } + } + + return adapter + } + + HasAdapter(id) { + exists := this.adapters.Has(id) + + if (!exists) { + idParts := StrSplit(id, ".") + webServiceId := idParts[1] + adapterKey := idParts[2] + webService := this.entityTypeMgr.GetManager("web_service")[webServiceId] + param := this.parameterPrefix . webService["Provider"]["id"] . "." . id + exists := this.container.HasParameter(param) + } + + return exists + } + + GetAdapterIds(filters := "", operation := "", limit := 0, webService := "") { + if (!filters) { + filters := Map() + } + + if (Type(filters) == "String") { + filters := Map("dataType", filters) + } + + adapterIds := [] + weights := this._getFilterWeights(filters) + + for webServiceId, webService in this._getWebServicesForOperation(webService) { + providerId := webService["Provider"]["id"] + paramKey := "web_services.adapters." . providerId + + if (this.container.HasParameter(paramKey)) { + adapterData := this.container.GetParameter(this.parameterPrefix . providerId) + + for weightIndex, weight in weights { + filters["weight"] := weight + + for key, definition in adapterData { + adapterId := webServiceId . "." . key + adapter := this.GetAdapter(adapterId) + definition := adapter.definition + include := (!operation || adapter.SupportsOperation(operation)) + + if (include) { + for filterKey, filterVal in filters { + if (!definition.Has(filterKey)) { + include := false + + break + } + + include := this._filterValue(definition[filterKey], filterVal) + + if (!include) { + break + } + } + } + + if (include) { + adapterIds.Push(adapterId) + + if (limit && adapterIds.Length >= limit) { + break 2 + } + } + } + } + } + } + + return adapterIds + } + + _getWebServicesForOperation(webService) { + webServices := "" + + if (webService) { + webServices := Type(webService == "String") ? Map(webService["id"], webService) : webService + } else { + webServices := this.entityTypeMgr.GetManager("web_service") + .EntityQuery(EntityQuery.RESULT_TYPE_ENTITIES) + .Condition(IsTrueCondition(), "Enabled") + .Execute() + } + + return webServices + } + + _getFilterWeights(filters) { + weights := filters.Has("weight") + ? filters["weight"] + : "" + + if (!weights) { + weights := [] + + startingWeight := -10 + maxWeight := 10 + + Loop (maxWeight - startingWeight) { + weights.Push(startingWeight + A_Index - 1) + } + } + + if (Type(weights) == "String") { + weights := [weights] + } + + return weights + } + + _filterArrayValues(definitionArray, filterArray) { + include := !filterArray || !!definitionArray + + if (include) { + if (Type(filterArray) == "String") { + filterArray := [filterArray] + } + + if (Type(definitionArray) == "String") { + definitionArray := [definitionArray] + } + + for , val in filterArray { + definitionHasVal := false + + for , definitionVal in definitionArray { + definitionHasVal := this._filterValue(definitionVal, val) + + if (definitionVal == val) { + definitionHasVal := true + + break + } + } + + if (!definitionHasVal) { + include := false + + break + } + } + } + + return include + } + + _filterMapValues(definitionMap, filterMap) { + include := !filterMap || !!definitionMap + + if (include) { + if (Type(filterMap) == "String") { + filterMap := [filterMap] + } + + if (Type(definitionMap) == "String") { + definitionMap := [definitionMap] + } + + for key, val in filterMap { + exists := definitionMap.Has(key) + + if (exists) { + exists := this._filterValue(definitionMap[key], val) + } + + if (!exists) { + include := false + + break + } + } + } + + return include + } + + _filterValue(definitionVal, filterVal) { + include := false + + if (Type(filterVal) == "Array") { + include := this._filterArrayValues(definitionVal, filterVal) + } else if (Type(filterVal) == "Map") { + include := this._filterMapValues(definitionVal, filterVal) + } else { + include := (definitionVal == filterVal) + } + + return include + } +} diff --git a/Lib/Shared/Modules/WebServices/WebServices.module.json b/Lib/Shared/Modules/WebServices/WebServices.module.json index c00967b0..99634be6 100644 --- a/Lib/Shared/Modules/WebServices/WebServices.module.json +++ b/Lib/Shared/Modules/WebServices/WebServices.module.json @@ -114,6 +114,10 @@ "web_services.adapter_factory": { "class": "WebServiceAdapterFactory", "arguments": ["@{}"] + }, + "web_services.adapter_manager": { + "class": "WebServiceAdapterManager", + "arguments": ["@{}", "web_services.adapters.", "@web_services.adapter_factory", "@manager.entity_type", "@manager.event"] } } } From 284c38f20fef15f7fb860e68cb165d27d4742807 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:07:25 -0500 Subject: [PATCH 089/227] Add an option to store child entity data inside the parent's entity storage data object --- Lib/Shared/Includes.ahk | 1 + .../LayeredData/LayeredDataBase.ahk | 50 +++++++++++++++++ .../Volantis.Entity/Entity/EntityBase.ahk | 42 +++++++++++--- .../Factory/EntityTypeFactory.ahk | 1 + .../LayerSource/ParentEntityLayerSource.ahk | 55 +++++++++++++++++++ 5 files changed, 141 insertions(+), 8 deletions(-) create mode 100644 Lib/Shared/Volantis.Entity/LayerSource/ParentEntityLayerSource.ahk diff --git a/Lib/Shared/Includes.ahk b/Lib/Shared/Includes.ahk index c5d4ec96..a96a19ed 100644 --- a/Lib/Shared/Includes.ahk +++ b/Lib/Shared/Includes.ahk @@ -245,6 +245,7 @@ #Include Volantis.Entity\Factory\EntityTypeFactory.ahk #Include Volantis.Entity\LayeredData\EntityData.ahk #Include Volantis.Entity\LayerSource\EntityStorageLayerSource.ahk +#Include Volantis.Entity\LayerSource\ParentEntityLayerSource.ahk #Include Volantis.Entity\Query\EntityQuery.ahk #Include Volantis.Entity\Validator\BasicValidator.ahk #Include Volantis.Entity\Validator\ValidatorBase.ahk diff --git a/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk b/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk index 423b9889..b1a18acd 100644 --- a/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk +++ b/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk @@ -25,6 +25,8 @@ class LayeredDataBase { cloner := "" userLayers := ["data"] loadingLayers := Map() + extraDataLayer := "data" + extraDataKey := "extra" static NO_VALUE := ":NO_VAL:" @@ -255,6 +257,54 @@ class LayeredDataBase { } } + GetExtraData(key := "") { + extraData := this.GetValue(this.extraDataKey, false, this.extraDataLayer, Map()) + + if (key) { + extraData := extraData.Has(key) ? extraData[key] : Map() + } + + return extraData + } + + SetExtraData(value, key := "") { + if (key) { + extraData := this.GetExtraData() + extraData[key] := value + value := extraData + } + + this.SetValue(this.extraDataKey, value, this.extraDataLayer) + + return this + } + + HasExtraData(key := "") { + hasData := this.HasValue(this.extraDataKey, this.extraDataLayer, false) + + if (hasData && key) { + extraData := this.GetExtraData() + hasData := extraData.Has(key) + } + + return hasData + } + + DeleteExtraData(key := "") { + if (key) { + extraData := this.GetExtraData() + + if (extraData.Has(key)) { + extraData.Delete(key) + this.SetExtraData(extraData) + } + } else { + this.DeleteValue(this.extraDataKey, this.extraDataLayer) + } + + return this + } + /** key: The key to retrieve diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index cc2562ea..12c624e0 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -461,15 +461,41 @@ class EntityBase { return text } - UpdateDefaults(recurse := true) { - if (this.HasOwnDataStorage()) { - this.GetData().UnloadAllLayers(false) - } + GetAllChildEntityData() { + return this.GetData().GetExtraData() + } - if (recurse) { - for key, child in this.GetReferencedEntities(true) { - child.UpdateDefaults(recurse) - } + GetChildEntityData(entityTypeId, entityId) { + dataKey := entityTypeId . "." . entityId + + childData := this.GetData().GetExtraData(dataKey) + + return childData ? childData : Map() + } + + SetChildEntityData(entityTypeId, entityId, data) { + dataKey := entityTypeId . "." . entityId + + if (!data) { + data := Map() } + + this.GetData().SetExtraData(dataKey, data) + + return this + } + + HasChildEntityData(entityTypeId, entityId) { + dataKey := entityTypeId . "." . entityId + + return this.GetData().HasExtraData(dataKey) + } + + DeleteChildEntityData(entityTypeId, entityId) { + dataKey := entityTypeId . "." . entityId + + this.GetData().DeleteExtraData(dataKey) + + return this } } diff --git a/Lib/Shared/Volantis.Entity/Factory/EntityTypeFactory.ahk b/Lib/Shared/Volantis.Entity/Factory/EntityTypeFactory.ahk index 8383c8fa..fe637a5b 100644 --- a/Lib/Shared/Volantis.Entity/Factory/EntityTypeFactory.ahk +++ b/Lib/Shared/Volantis.Entity/Factory/EntityTypeFactory.ahk @@ -30,6 +30,7 @@ class EntityTypeFactory { "event_manager", "manager.event", "notifier", "notifier", "parent_entity_type", "", + "parent_entity_storage", false, "default_icon", "cube-outline", "icon_field", "IconSrc", "allow_view", false, diff --git a/Lib/Shared/Volantis.Entity/LayerSource/ParentEntityLayerSource.ahk b/Lib/Shared/Volantis.Entity/LayerSource/ParentEntityLayerSource.ahk new file mode 100644 index 00000000..dfa685aa --- /dev/null +++ b/Lib/Shared/Volantis.Entity/LayerSource/ParentEntityLayerSource.ahk @@ -0,0 +1,55 @@ +class ParentEntityLayerSource extends LayerSourceBase { + entityObj := "" + + __New(entityObj) { + this.entityObj := entityObj + } + + SaveData(data := "") { + this._validateParentEntity() + + this.entityObj.ParentEntity + .SetChildEntityData(this.entityObj.EntityTypeId, this.entityObj.Id, data) + + return this + } + + LoadData() { + this._validateParentEntity() + + return this.entityObj.ParentEntity + .GetChildEntityData(this.entityObj.EntityTypeId, this.entityObj.Id) + } + + HasData() { + this._validateParentEntity() + + return this.entityObj.ParentEntity + .HasChildEntityData(this.entityObj.EntityTypeId, this.entityObj.Id) + } + + DeleteData() { + this._validateParentEntity() + + this.entityObj.ParentEntity + .DeleteChildEntityData(this.entityObj.EntityTypeId, this.entityObj.Id) + + return this + } + + _validateParentEntity() { + if (!this.entityObj.ParentEntity) { + throw AppException("Parent entity not set.") + } + + if (!HasBase(this.entityObj.ParentEntity, EntityBase.Prototype)) { + throw AppException("Parent entity is not an entity.") + } + + parentData := this.entityObj.ParentEntity.GetData() + + if (!parentData) { + throw AppException("Parent entity data is not set.") + } + } +} From 17ce18ad694a602987360d20c0386849264d5717 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:08:33 -0500 Subject: [PATCH 090/227] Add an event that can be fired to determine an entity's parent entity --- Lib/Shared/Includes.ahk | 1 + .../Event/EntityParentEvent.ahk | 28 +++++++++++++++++++ .../Volantis.Entity/Events/EntityEvents.ahk | 1 + 3 files changed, 30 insertions(+) create mode 100644 Lib/Shared/Volantis.Entity/Event/EntityParentEvent.ahk diff --git a/Lib/Shared/Includes.ahk b/Lib/Shared/Includes.ahk index a96a19ed..faff2db3 100644 --- a/Lib/Shared/Includes.ahk +++ b/Lib/Shared/Includes.ahk @@ -233,6 +233,7 @@ #Include Volantis.Entity\Event\EntityLayersEvent.ahk #Include Volantis.Entity\Event\EntityLayerSourcesEvent.ahk #Include Volantis.Entity\Event\EntityListEvent.ahk +#Include Volantis.Entity\Event\EntityParentEvent.ahk #Include Volantis.Entity\Event\EntityReferenceEvent.ahk #Include Volantis.Entity\Event\EntityRefreshEvent.ahk #Include Volantis.Entity\Event\EntityValidateEvent.ahk diff --git a/Lib/Shared/Volantis.Entity/Event/EntityParentEvent.ahk b/Lib/Shared/Volantis.Entity/Event/EntityParentEvent.ahk new file mode 100644 index 00000000..c95ed335 --- /dev/null +++ b/Lib/Shared/Volantis.Entity/Event/EntityParentEvent.ahk @@ -0,0 +1,28 @@ +class EntityParentEvent extends EntityEvent { + _parentEntity := "" + _parentEntityTypeId := "" + _parentEntityId := "" + + __New(eventName, entityTypeId, entityObj, parentEntity := "", parentEntityTypeId := "", parentEntityId := "") { + this._parentEntity := parentEntity + this._parentEntityTypeId := parentEntityTypeId + this._parentEntityId := parentEntityId + + super.__New(eventName, entityTypeId, entityObj) + } + + ParentEntity { + get => this._parentEntity + set => this._parentEntity := value + } + + ParentEntityTypeId { + get => this._parentEntityTypeId + set => this._parentEntityTypeId := value + } + + ParentEntityId { + get => this._parentEntityId + set => this._parentEntityId := value + } +} diff --git a/Lib/Shared/Volantis.Entity/Events/EntityEvents.ahk b/Lib/Shared/Volantis.Entity/Events/EntityEvents.ahk index 3e0a883b..2d2ed47a 100644 --- a/Lib/Shared/Volantis.Entity/Events/EntityEvents.ahk +++ b/Lib/Shared/Volantis.Entity/Events/EntityEvents.ahk @@ -24,4 +24,5 @@ class EntityEvents { static ENTITY_FIELD_GROUPS_ALTER := 0x4092 static ENTITY_REFERENCE_ENTITY_SAVED := 0x4095 static ENTITY_LIST_ENTITIES := 0x4098 + static ENTITY_DISCOVER_PARENT := 0x4100 } From 78b14b415d444b24b7a243a1d65fd5caf7667a21 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:09:22 -0500 Subject: [PATCH 091/227] Refactor EntityData so that passing in layer names and layer sources is optional, and the events are handled in existing overridden functions instead of new ones --- .../LayeredData/EntityData.ahk | 60 +++++++++++-------- 1 file changed, 35 insertions(+), 25 deletions(-) diff --git a/Lib/Shared/Volantis.Entity/LayeredData/EntityData.ahk b/Lib/Shared/Volantis.Entity/LayeredData/EntityData.ahk index 3086142b..26addd79 100644 --- a/Lib/Shared/Volantis.Entity/LayeredData/EntityData.ahk +++ b/Lib/Shared/Volantis.Entity/LayeredData/EntityData.ahk @@ -3,20 +3,47 @@ class EntityData extends LayeredDataBase { entity := "" eventMgr := "" - __New(entity, layerNames, layerSources) { + __New(entity, layerNames := "", layerSources := "") { this.entityTypeId := entity.EntityTypeId this.entity := entity this.eventMgr := entity.eventMgr super.__New( entity.cloner, - this._createProcessors(), - this._collectLayerNames(layerNames), - this._collectSources(layerSources) + this._createProcessors(), + layerNames, + layerSources ) } - _collectSources(layerSources) { + InitializeLayers(layerNames) { + if (!layerNames) { + layerNames := [] + } + + this._appendLayerNames(["defaults"], layerNames) + + event := EntityLayersEvent(EntityEvents.ENTITY_DATA_LAYERS, this.entityTypeId, this.entity, layerNames) + this.eventMgr.DispatchEvent(event) + + layerNames := event.Layers + this._appendLayerNames(["auto", "data"], layerNames) + + event := EntityLayersEvent(EntityEvents.ENTITY_DATA_LAYERS_ALTER, this.entityTypeId, this.entity, layerNames) + this.eventMgr.DispatchEvent(event) + + layerNames := event.Layers + layers := Map() + + for index, layerName in layerNames { + this.layerPriority.Push(layerName) + layers[layerName] := Map() + } + + this.SetLayers(layers) + } + + SetLayerSources(layerSources) { if (!layerSources.Has("defaults")) { layerSources["defaults"] := ObjBindMethod(this.entity, "InitializeDefaults") } @@ -27,7 +54,9 @@ class EntityData extends LayeredDataBase { event := EntityLayerSourcesEvent(EntityEvents.ENTITY_LAYER_SOURCES_ALTER, this.entityTypeId, this.entity, event.LayerSources) this.eventMgr.DispatchEvent(event) - return event.LayerSources + for key, source in event.LayerSources { + this.SetLayerSource(key, source) + } } _createProcessors() { @@ -45,25 +74,6 @@ class EntityData extends LayeredDataBase { return event.Processors } - _collectLayerNames(layerNames) { - if (!layerNames) { - layerNames := [] - } - - this._appendLayerNames(["defaults"], layerNames) - - event := EntityLayersEvent(EntityEvents.ENTITY_DATA_LAYERS, this.entityTypeId, this.entity, layerNames) - this.eventMgr.DispatchEvent(event) - - layerNames := event.Layers - this._appendLayerNames(["auto", "data"], layerNames) - - event := EntityLayersEvent(EntityEvents.ENTITY_DATA_LAYERS_ALTER, this.entityTypeId, this.entity, layerNames) - this.eventMgr.DispatchEvent(event) - - return event.Layers - } - _appendLayerNames(namesToAppend, existingNames) { for index, name in namesToAppend { exists := false From b12a217b8c57b9814da28526da371d07ca817884 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:14:38 -0500 Subject: [PATCH 092/227] Fix entity constructors (Except Managed entities because they will be committed next --- .../WebServices/Entity/WebServiceEntity.ahk | 100 ++---------------- .../Volantis.Entity/Entity/EntityBase.ahk | 10 +- .../Entity/FieldableEntity.ahk | 10 +- .../Volantis.Entity/Factory/EntityFactory.ahk | 2 +- 4 files changed, 20 insertions(+), 102 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index dd10622c..411e4f2a 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -19,23 +19,21 @@ class WebServiceEntity extends FieldableEntity { set => this.SetAuthData(key, value) } - __New(id, entityTypeId, container, adapterFactory, cacheObj, stateObj, persistentStateObj, fieldFactory, widgetFactory, eventMgr, storageObj, idSanitizer, parentEntity := "") { + __New(id, entityTypeId, container, cacheObj, stateObj, persistentStateObj, fieldFactory, widgetFactory, eventMgr, storageObj, idSanitizer, autoLoad := true, parentEntity := "", parentEntityStorage := false) { this.cacheObj := cacheObj this.stateObj := stateObj this.persistentStateObj := persistentStateObj - this.adapterFactory := adapterFactory - super.__New(id, entityTypeId, container, fieldFactory, widgetFactory, eventMgr, storageObj, idSanitizer, parentEntity) + super.__New(id, entityTypeId, container, fieldFactory, widgetFactory, eventMgr, storageObj, idSanitizer, autoLoad, parentEntity, parentEntityStorage) } - static Create(container, eventMgr, id, entityTypeId, storageObj, idSanitizer, parentEntity := "") { + static Create(container, eventMgr, id, entityTypeId, storageObj, idSanitizer, autoLoad := true, parentEntity := "", parentEntityStorage := false) { className := this.Prototype.__Class return %className%( id, entityTypeId, container, - container.Get("web_services.adapter_factory"), container.Get("cache.web_services"), container.Get("state.web_services_tmp"), container.Get("state.web_services"), @@ -44,98 +42,12 @@ class WebServiceEntity extends FieldableEntity { eventMgr, storageObj, idSanitizer, - parentEntity + autoLoad, + parentEntity, + parentEntityStorage ) } - AdapterRequest(params, adapterFilters, operation := "read", multiple := false) { - if (!adapterFilters) { - adapterFilters := Map() - } - - if (!adapterFilters) { - adapterFilters := Map() - } - - if (Type(adapterFilters) == "String") { - adapterFilters := Map("adapterType", adapterFilters) - } - - results := Map() - - for adapterKey, adapter in this.GetAdapters(adapterFilters, operation) { - result := adapter.SendRequest(operation, params) - - if (result) { - if (!multiple) { - results := result - break - } - - results[adapterKey] := result - } - - if (IsNumber(multiple) && results.Count >= multiple) { - break - } - } - - return results - } - - GetAdapters(filters := "", operation := "") { - if (!filters) { - filters := Map() - } - - adapterData := this.container.GetParameter("web_services.adapters." . this["Provider"]["id"]) - - adapters := Map() - - for key, definition in adapterData { - adapter := this.GetAdapter(key) - definition := adapter.definition - include := true - - for filterKey, filterVal in filters { - if (!definition.Has(filterKey) || definition[filterKey] != filterVal) { - include := false - - break - } - } - - if (include && operation) { - include := adapter.SupportsOperation(operation) - } - - if (include) { - adapters[key] := adapter - } - } - - return adapters - } - - GetAdapter(key) { - adapter := "" - - if (this.adapters.Has(key)) { - adapter := this.adapters[key] - } - - if (!adapter) { - param := "web_services.adapters." . this["Provider"]["id"] - - if (this.container.HasParameter(param)) { - adapter := this.adapterFactory.CreateWebServiceAdapter(this, this.container.GetParameter(param)) - this.adapters[key] := adapter - } - } - - return adapter - } - BaseFieldDefinitions() { definitions := super.BaseFieldDefinitions() diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index 12c624e0..da91df35 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -2,6 +2,7 @@ class EntityBase { idVal := "" entityTypeIdVal := "" parentEntityObj := "" + parentEntityStorage := false container := "" app := "" eventMgr := "" @@ -57,7 +58,7 @@ class EntityBase { return this.GetAllValues().__Enum(numberOfVars) } - __New(id, entityTypeId, container, eventMgr, storageObj, idSanitizer := "", autoLoad := true, parentEntity := "") { + __New(id, entityTypeId, container, eventMgr, storageObj, idSanitizer := "", autoLoad := true, parentEntity := "", parentEntityStorage := false) { this.app := container.GetApp() this.idSanitizer := idSanitizer @@ -72,6 +73,7 @@ class EntityBase { this.storageObj := storageObj this.merger := container.Get("merger.list") this.cloner := container.Get("cloner.list") + this.parentEntityStorage := parentEntityStorage if (!parentEntity) { parentEntity := this.DiscoverParentEntity(container, eventMgr, id, storageObj, idSanitizer) @@ -89,7 +91,7 @@ class EntityBase { } } - static Create(container, eventMgr, id, entityTypeId, storageObj, idSanitizer, parentEntity := "") { + static Create(container, eventMgr, id, entityTypeId, storageObj, idSanitizer, autoLoad := true, parentEntity := "", parentEntityStorage := false) { className := this.Prototype.__Class return %className%( @@ -99,7 +101,9 @@ class EntityBase { eventMgr, storageObj, idSanitizer, - parentEntity + autoLoad, + parentEntity, + parentEntityStorage ) } diff --git a/Lib/Shared/Volantis.Entity/Entity/FieldableEntity.ahk b/Lib/Shared/Volantis.Entity/Entity/FieldableEntity.ahk index e4e43d60..d39183b0 100644 --- a/Lib/Shared/Volantis.Entity/Entity/FieldableEntity.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/FieldableEntity.ahk @@ -11,14 +11,14 @@ class FieldableEntity extends EntityBase { get => this.GetFieldDefinitions() } - __New(id, entityTypeId, container, fieldFactory, widgetFactory, eventMgr, storageObj, idSanitizer := "", autoLoad := true) { + __New(id, entityTypeId, container, fieldFactory, widgetFactory, eventMgr, storageObj, idSanitizer := "", autoLoad := true, parentEntity := "", parentEntityStorage := false) { this.entityFieldFactory := fieldFactory this.entityWidgetFactory := widgetFactory - super.__New(id, entityTypeId, container, eventMgr, storageObj, idSanitizer, autoLoad) + super.__New(id, entityTypeId, container, eventMgr, storageObj, idSanitizer, autoLoad, parentEntity, parentEntityStorage) } - static Create(container, eventMgr, id, entityTypeId, storageObj, idSanitizer, parentEntity := "") { + static Create(container, eventMgr, id, entityTypeId, storageObj, idSanitizer, autoLoad := true, parentEntity := "", parentEntityStorage := false) { className := this.Prototype.__Class return %className%( @@ -30,7 +30,9 @@ class FieldableEntity extends EntityBase { eventMgr, storageObj, idSanitizer, - parentEntity + autoLoad, + parentEntity, + parentEntityStorage ) } diff --git a/Lib/Shared/Volantis.Entity/Factory/EntityFactory.ahk b/Lib/Shared/Volantis.Entity/Factory/EntityFactory.ahk index 19af31bc..7d45933b 100644 --- a/Lib/Shared/Volantis.Entity/Factory/EntityFactory.ahk +++ b/Lib/Shared/Volantis.Entity/Factory/EntityFactory.ahk @@ -110,6 +110,6 @@ class EntityFactory { throw EntityException("Unable to create entity '" . id . "' of type '" . entityTypeObj . "' in EntityFactory") } - return %entityTypeObj%.Create(this.container, this.eventMgr, id, this.entityTypeId, this.storageObj, this.idSanitizer, parentEntity) + return %entityTypeObj%.Create(this.container, this.eventMgr, id, this.entityTypeId, this.storageObj, this.idSanitizer, true, parentEntity, this.definition["parent_entity_storage"]) } } From 760826dd01791d14973568a4c47c3b52da0e18c7 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:21:41 -0500 Subject: [PATCH 093/227] Change ManagedEntityBase into LaunchProcessEntity, ManagedLauncherEntity into LauncherProcessEntity, and ManagedGameEntity into GameProcessEntity --- Launchpad.services.json | 8 +- .../Builder/BuildFile/GameAhkFile.ahk | 4 +- .../Builder/BuildFile/ShortcutFile.ahk | 4 +- Lib/Launchpad/Builder/BuilderBase.ahk | 2 +- Lib/Launchpad/DetectedGame/DetectedGame.ahk | 32 +- ...edGameEntity.ahk => GameProcessEntity.ahk} | 38 +- ...EntityBase.ahk => LaunchProcessEntity.ahk} | 134 ++--- Lib/Launchpad/Entity/LauncherEntity.ahk | 16 +- ...erEntity.ahk => LauncherProcessEntity.ahk} | 32 +- Lib/Launchpad/Entity/ManagedProcessEntity.ahk | 533 ------------------ Lib/Launchpad/Includes.ahk | 7 +- .../App/LaunchpadLauncher.ahk | 4 +- 12 files changed, 111 insertions(+), 703 deletions(-) rename Lib/Launchpad/Entity/{ManagedGameEntity.ahk => GameProcessEntity.ahk} (72%) rename Lib/Launchpad/Entity/{ManagedEntityBase.ahk => LaunchProcessEntity.ahk} (81%) rename Lib/Launchpad/Entity/{ManagedLauncherEntity.ahk => LauncherProcessEntity.ahk} (80%) delete mode 100644 Lib/Launchpad/Entity/ManagedProcessEntity.ahk diff --git a/Launchpad.services.json b/Launchpad.services.json index bf32f18e..59a88e39 100644 --- a/Launchpad.services.json +++ b/Launchpad.services.json @@ -42,18 +42,18 @@ "allow_delete": true, "manager_gui": "MainWindow" }, - "entity_type.managed_game": { + "entity_type.game_process": { "name_singular": "Managed Game", "name_plural": "Managed Games", - "entity_class": "ManagedGameEntity", + "entity_class": "GameProcessEntity", "storage_config_storage_parent_key": "Games", "storage_config_path_parameter": "config.launcher_file", "parent_entity_type": "launcher" }, - "entity_type.managed_launcher": { + "entity_type.launcher_process": { "name_singular": "Managed Launcher", "name_plural": "Managed Launchers", - "entity_class": "ManagedLauncherEntity", + "entity_class": "LauncherProcessEntity", "storage_config_storage_parent_key": "Games", "storage_config_path_parameter": "config.launcher_file", "parent_entity_type": "launcher" diff --git a/Lib/Launchpad/Builder/BuildFile/GameAhkFile.ahk b/Lib/Launchpad/Builder/BuildFile/GameAhkFile.ahk index 230f51e6..d1cbe8cd 100644 --- a/Lib/Launchpad/Builder/BuildFile/GameAhkFile.ahk +++ b/Lib/Launchpad/Builder/BuildFile/GameAhkFile.ahk @@ -14,9 +14,9 @@ class GameAhkFile extends ComposableBuildFile { "launcherName", this.launcherEntityObj.Id . " - Launchpad", "appVersion", appVersion, "appDir", this.appDir, - "gameConfig", this.launcherEntityObj["ManagedGame"].FieldData, + "gameConfig", this.launcherEntityObj["GameProcess"].FieldData, "launchpadLauncherConfig", this.launcherEntityObj.FieldData, - "launcherConfig", this.launcherEntityObj["ManagedLauncher"].FieldData, + "launcherConfig", this.launcherEntityObj["LauncherProcess"].FieldData, "launcherId", this.launcherEntityObj.Id, "themesDir", this.launcherEntityObj["ThemesDir"], "resourcesDir", this.launcherEntityObj["ResourcesDir"], diff --git a/Lib/Launchpad/Builder/BuildFile/ShortcutFile.ahk b/Lib/Launchpad/Builder/BuildFile/ShortcutFile.ahk index 6f61ab18..d434a996 100644 --- a/Lib/Launchpad/Builder/BuildFile/ShortcutFile.ahk +++ b/Lib/Launchpad/Builder/BuildFile/ShortcutFile.ahk @@ -6,14 +6,14 @@ class ShortcutFile extends CopyableBuildFile { if (destPath == "") { ext := ".lnk" - if (launcherEntityObj["ManagedGame"]["ShortcutSrc"] != "" && SubStr(launcherEntityObj["ManagedGame"]["ShortcutSrc"], -4) == ".url") { + if (launcherEntityObj["GameProcess"]["ShortcutSrc"] != "" && SubStr(launcherEntityObj["GameProcess"]["ShortcutSrc"], -4) == ".url") { ext := ".url" } destPath := launcherEntityObj["AssetsDir"] . "\" . launcherEntityObj.Id . ext } - super.__New(launcherEntityObj, launcherEntityObj["ManagedGame"]["ShortcutSrc"], destPath) + super.__New(launcherEntityObj, launcherEntityObj["GameProcess"]["ShortcutSrc"], destPath) } Locate() { diff --git a/Lib/Launchpad/Builder/BuilderBase.ahk b/Lib/Launchpad/Builder/BuilderBase.ahk index b9609894..d4f77787 100644 --- a/Lib/Launchpad/Builder/BuilderBase.ahk +++ b/Lib/Launchpad/Builder/BuilderBase.ahk @@ -32,7 +32,7 @@ class BuilderBase { } NeedsShortcutFile(launcherEntityObj) { - return (launcherEntityObj["ManagedGame"]["UsesShortcut"]) + return (launcherEntityObj["GameProcess"]["UsesShortcut"]) } BuildAction(launcherEntityObj, launcherDir, assetsDir) { diff --git a/Lib/Launchpad/DetectedGame/DetectedGame.ahk b/Lib/Launchpad/DetectedGame/DetectedGame.ahk index 68aa54b2..0e6a966e 100644 --- a/Lib/Launchpad/DetectedGame/DetectedGame.ahk +++ b/Lib/Launchpad/DetectedGame/DetectedGame.ahk @@ -44,12 +44,10 @@ class DetectedGame { if ( this.displayName != launcher["name"] - || this.launcherType != launcher["ManagedLauncher"].EntityTypeId - || this.gameType != launcher["ManagedGame"].EntityTypeId - || this.installDir != launcher["ManagedGame"]["InstallDir"] - || this.launcherInstallDir != launcher["ManagedLauncher"]["InstallDir"] - || this.exeName != launcher["ManagedGame"]["Exe"] - || this.platformRef != launcher["ManagedGame"]["PlatformRef"] + || this.installDir != launcher["GameProcess"]["InstallDir"] + || this.launcherInstallDir != launcher["LauncherProcess"]["InstallDir"] + || this.exeName != launcher["GameProcess"]["Exe"] + || this.platformRef != launcher["GameProcess"]["PlatformRef"] ) { hasChanges := true } @@ -70,27 +68,17 @@ class DetectedGame { modified := true } - if (this.launcherType && launcher["ManagedLauncher"].EntityTypeId != this.launcherType) { - launcher["ManagedLauncher"].EntityType := this.launcherType - modified := true - } - - if (this.gameType && launcher["ManagedGame"].EntityTypeId != this.gameType) { - launcher["ManagedGame"].EntityType := this.gameType - modified := true - } - - if (this.launcherInstallDir && launcher["ManagedLauncher"]["InstallDir"] != this.launcherInstallDir) { - launcher["ManagedLauncher"]["InstallDir"] := this.launcherInstallDir + if (this.launcherInstallDir && launcher["LauncherProcess"]["InstallDir"] != this.launcherInstallDir) { + launcher["LauncherProcess"]["InstallDir"] := this.launcherInstallDir } - if (this.installDir && launcher["ManagedGame"]["InstallDir"] != this.installDir) { - launcher["ManagedGame"]["InstallDir"] := this.installDir + if (this.installDir && launcher["GameProcess"]["InstallDir"] != this.installDir) { + launcher["GameProcess"]["InstallDir"] := this.installDir modified := true } - if (this.exeName && launcher["ManagedGame"]["Exe"] != this.exeName) { - launcher["ManagedGame"]["Exe"] := this.exeName + if (this.exeName && launcher["GameProcess"]["Exe"] != this.exeName) { + launcher["GameProcess"]["Exe"] := this.exeName modified := true } diff --git a/Lib/Launchpad/Entity/ManagedGameEntity.ahk b/Lib/Launchpad/Entity/GameProcessEntity.ahk similarity index 72% rename from Lib/Launchpad/Entity/ManagedGameEntity.ahk rename to Lib/Launchpad/Entity/GameProcessEntity.ahk index 916ebe67..da93710b 100644 --- a/Lib/Launchpad/Entity/ManagedGameEntity.ahk +++ b/Lib/Launchpad/Entity/GameProcessEntity.ahk @@ -1,6 +1,4 @@ -class ManagedGameEntity extends ManagedEntityBase { - configPrefix := "Game" - defaultType := "Default" +class GameProcessEntity extends LaunchProcessEntity { defaultClass := "SimpleGame" BaseFieldDefinitions() { @@ -9,7 +7,7 @@ class ManagedGameEntity extends ManagedEntityBase { definitions["HasLoadingWindow"] := Map( "type", "boolean", "description", "Whether or not the game has a loading window to watch for.", - "storageKey", this.configPrefix . "HasLoadingWindow", + "storageKey", "HasLoadingWindow", "default", false ) @@ -19,7 +17,7 @@ class ManagedGameEntity extends ManagedEntityBase { definitions["LoadingWindowProcessType"] := Map( "description", "Which method to use to wait for the game's loading window to open.", "help", "This lets Launchpad know when the game is loading. Only used if a LoadingWindowProcessId is set", - "storageKey", this.configPrefix . "LoadingWindowProcessType", + "storageKey", "LoadingWindowProcessType", "default", "Exe", "widget", "select", "selectOptionsCallback", ObjBindMethod(this, "ListProcessTypes") @@ -29,7 +27,7 @@ class ManagedGameEntity extends ManagedEntityBase { ; - Title - This value will default to the game's Key unless overridden ; - Class - This value should be set to the game's window class definitions["LoadingWindowProcessId"] := Map( - "storageKey", this.configPrefix . "LoadingWindowProcessId", + "storageKey", "LoadingWindowProcessId", "help", "This value's type is dependent on the GameProcessType above. It can often be detected from other values, and is not needed if the GameRunType is RunWait.", "modes", Map( "simple", Map("formField", false) @@ -39,16 +37,6 @@ class ManagedGameEntity extends ManagedEntityBase { return definitions } - GetBlizzardProductKey() { - productKey := this["PlatformRef"] - - if (this.HasConfigValue("BlizzardProductId", true, false)) { - productKey := this.GetConfigValue("BlizzardProductId") - } - - return productKey - } - ShouldDetectShortcutSrc(extraConfig) { detectShortcut := false @@ -77,24 +65,24 @@ class ManagedGameEntity extends ManagedEntityBase { return detectShortcut } - AutoDetectValues(recurse := true) { - detectedValues := super.AutoDetectValues(recurse) - exeKey := this.configPrefix . "Exe" + AutoDetectValues() { + detectedValues := super.AutoDetectValues() + exeKey := "Exe" if (!detectedValues.Has(exeKey)) { detectedValues[exeKey] := this["Exe"] ? this["Exe"] : this.Id . ".exe" } - if (!detectedValues.Has(this.configPrefix . "ProcessId") || !detectedValues[this.configPrefix . "ProcessId"]) { - detectedValues[this.configPrefix . "ProcessId"] := detectedValues[exeKey] + if (!detectedValues.Has("ProcessId") || !detectedValues["ProcessId"]) { + detectedValues["ProcessId"] := detectedValues[exeKey] } - if (detectedValues.Has(this.configPrefix . "ProcessType")) { - detectedValues[this.configPrefix . "LoadingWindowProcessType"] := detectedValues[this.configPrefix . "ProcessType"] + if (detectedValues.Has("ProcessType")) { + detectedValues["LoadingWindowProcessType"] := detectedValues["ProcessType"] } if (!this["LoadingWindowProcessId"]) { - detectedValues[this.configPrefix . "LoadingWindowProcessId"] := detectedValues[exeKey] + detectedValues["LoadingWindowProcessId"] := detectedValues[exeKey] } if (this.ShouldDetectShortcutSrc(detectedValues)) { @@ -110,7 +98,7 @@ class ManagedGameEntity extends ManagedEntityBase { } if (shortcutSrc != "") { - detectedValues[this.configPrefix . "ShortcutSrc"] := shortcutSrc + detectedValues["ShortcutSrc"] := shortcutSrc } } diff --git a/Lib/Launchpad/Entity/ManagedEntityBase.ahk b/Lib/Launchpad/Entity/LaunchProcessEntity.ahk similarity index 81% rename from Lib/Launchpad/Entity/ManagedEntityBase.ahk rename to Lib/Launchpad/Entity/LaunchProcessEntity.ahk index b84be410..d28b726c 100644 --- a/Lib/Launchpad/Entity/ManagedEntityBase.ahk +++ b/Lib/Launchpad/Entity/LaunchProcessEntity.ahk @@ -1,9 +1,12 @@ -class ManagedEntityBase extends FieldableEntity { - defaultType := "Default" +class LaunchProcessEntity extends FieldableEntity { defaultClass := "Default" - DiscoverParentEntity(container, eventMgr, id, storageObj, idSanitizer) { - return container.Get("entity_manager.launcher")[id] + DiscoverParentEntity(container, eventMgr, id, storageObj, idSanitizer, parentEntity := "") { + ; TODO fix circular reference occurring + + return parentEntity + ? parentEntity + : container.Get("entity_manager.launcher")[id] } GetDefaultFieldGroups() { @@ -48,21 +51,10 @@ class ManagedEntityBase extends FieldableEntity { ) ) - definitions["EntityType"] := Map( - "default", this.defaultType, - "description", "The key of the managed type to load settings and defaults from.", - "required", true, - "storageKey", this.configPrefix . "Type", - "widget", "select", - "selectOptionsCallback", ObjBindMethod(this, "ListEntities", false, true), - "group", "general" - ) - - definitions["EntityClass"] := Map( + definitions["ProcessClass"] := Map( "default", this.defaultClass, - "description", "The name of the AHK class that will be used to control the managed entity.", + "description", "The name of the AHK class that will be used to control the process.", "formField", false, - "storageKey", this.configPrefix . "Class", "required", true, "group", "advanced", "modes", Map( @@ -73,11 +65,10 @@ class ManagedEntityBase extends FieldableEntity { definitions["SearchDirs"] := Map( "type", "directory", "mustExist", false, - "storageKey", this.configPrefix . "SearchDirs", "default", [A_ProgramFiles], "description", "Possible parent directories where the game's launcher might exist, to be used for auto-detection.", "help", "These should be as specific as possible to reduce detection time.", - "multiple", true, + "cardinality", 1, ; Change to another number once widgets for multiple values are worked out "group", "locations", "modes", Map( "simple", Map("formField", false) @@ -87,7 +78,6 @@ class ManagedEntityBase extends FieldableEntity { definitions["InstallDir"] := Map( "type", "directory", "mustExist", false, - "storageKey", this.configPrefix . "InstallDir", "group", "locations", "modes", Map( "simple", Map("group", "general") @@ -99,7 +89,6 @@ class ManagedEntityBase extends FieldableEntity { "type", "file", "fileMask", "*.exe", "mustExist", false, - "storageKey", this.configPrefix . "Exe", "description", "This can be the full path on the system to the launcher's .exe file, or simply the name of the .exe file itself.", "help", "If the .exe doesn't include the absolute path, auto-detection will be used by searching the DestinationDirs.", "group", "locations", @@ -113,7 +102,6 @@ class ManagedEntityBase extends FieldableEntity { ; - BlizzardProductDb (will search Battle.net's product.db file if it can be located for the installation directory, and the file will be found from there ; - Registry (will get a directory from the registry key specified by LocateRegKey and search for the file within it) definitions["LocateMethod"] := Map( - "storageKey", this.configPrefix . "LocateMethod", "default", "SearchDirs", "description", "How to search for the .exe if it isn't a full path already", "group", "general", @@ -126,12 +114,10 @@ class ManagedEntityBase extends FieldableEntity { ) definitions["WindowTitle"] := Map( - "storageKey", this.configPrefix . "WindowTitle", "group", "process" ) definitions["LocateRegView"] := Map( - "storageKey", this.configPrefix . "LocateRegView", "default", 64, "group", "registry", "widget", "select", @@ -143,7 +129,7 @@ class ManagedEntityBase extends FieldableEntity { ) definitions["LocateRegKey"] := Map( - "storageKey", this.configPrefix . "LocateRegKey", + "title", "Registry Locator - Key", "group", "registry", "description", "The registry key to look up the install dir within.", "help", "Path parts should be separated with backslashes and must start with one of: HKEY_LOCAL_MACHINE, HKEY_USERS, HKEY_CURRENT_USER, HKEY_CLASSES_ROOT, HKEY_CURRENT_CONFIG, or the abbreviation of one of those. To read from a remote registry, prefix the root path with two backslashes and the computer name.`n`nSimple example: HKLM\Path\To\Key`nRemote example: \\OTHERPC\HKLM\Path\To\Key", @@ -153,7 +139,7 @@ class ManagedEntityBase extends FieldableEntity { ) definitions["LocateRegValue"] := Map( - "storageKey", this.configPrefix . "LocateRegValue", + "title", "Registry Locator - Value", "group", "registry", "description", "The name of the registry value to look up within the specified key.", "help", "Example: InstallPath", @@ -163,7 +149,7 @@ class ManagedEntityBase extends FieldableEntity { ) definitions["LocateRegRemovePrefix"] := Map( - "storageKey", this.configPrefix . "LocateRegRemovePrefix", + "title", "Registry Locator - Remove Prefix", "group", "registry", "modes", Map( "simple", Map("formField", false) @@ -171,7 +157,7 @@ class ManagedEntityBase extends FieldableEntity { ) definitions["LocateRegRemoveSuffix"] := Map( - "storageKey", this.configPrefix . "LocateRegRemoveSuffix", + "title", "Registry Locator - Remove Suffix", "group", "registry", "modes", Map( "simple", Map("formField", false) @@ -179,7 +165,7 @@ class ManagedEntityBase extends FieldableEntity { ) definitions["LocateRegStripQuotes"] := Map( - "storageKey", this.configPrefix . "LocateRegStripQuotes", + "title", "Registry Locator - Strip Quotes", "default", false, "group", "registry", "description", "Strip quotes from registry value", @@ -189,16 +175,16 @@ class ManagedEntityBase extends FieldableEntity { ) definitions["PlatformRef"] := Map( - "storageKey", this.configPrefix . "PlatformRef", + "title", "Platform Reference", "description", "If the item is known to the launcher by a specific ID, it should be stored here.", "group", "general" ) definitions["WorkingDir"] := Map( + "title", "Working Directory", "type", "directory", "description", "The directory that the launcher should be run from.", "help", "If not set, it will be run without setting an explicit working directory, which is usually sufficient.", - "storageKey", this.configPrefix . "WorkingDir", "group", "locations", "modes", Map( "simple", Map("formField", false) @@ -210,7 +196,6 @@ class ManagedEntityBase extends FieldableEntity { definitions["RunType"] := Map( "description", "Which method to use for launching this item.", "help", "This is only needed for launchers that have to manage their own process.", - "storageKey", this.configPrefix . "RunType", "default", "Command", "group", "process", "widget", "select", @@ -220,15 +205,13 @@ class ManagedEntityBase extends FieldableEntity { definitions["UsesShortcut"] := Map( "type", "boolean", "description", "Whether a shortcut file will be used when starting the internally-managed game launcher", - "formField", false, - "storageKey", this.configPrefix . "UsesShortcut" + "formField", false ) definitions["ReplaceProcess"] := Map( "type", "boolean", "description", "Kill and re-launch the game process immediately after it is detected.", "help", "This can be used to force Launchpad to own the game process, but won't for for every game.", - "storageKey", this.configPrefix . "ReplaceProcess", "default", false, "group", "process" ) @@ -238,9 +221,9 @@ class ManagedEntityBase extends FieldableEntity { ; - The path of an .exe file on the system to which a shortcut will be created in AssetsDir if it doesn't already exist. Using this option ; is usually not necessary, since you can run the .exe directly instead. definitions["ShortcutSrc"] := Map( + "title", "Shortcut Source", "description", "The shortcut file used to launch the game launcher itself.", "help", "This is typically only needed if the Shortcut LauncherRunType is selected.", - "storageKey", this.configPrefix . "ShortcutSrc", "group", "locations", "modes", Map( "simple", Map("group", "general") @@ -252,7 +235,6 @@ class ManagedEntityBase extends FieldableEntity { ; - Scheduled (Creates an immediate scheduled task that runs the game, then waits until the window opens (if needed) and then closes) definitions["RunMethod"] := Map( "description", "Which method to use to run the RunCmd", - "storageKey", this.configPrefix . "RunMethod", "default", "Run", "group", "process", "widget", "select", @@ -265,7 +247,6 @@ class ManagedEntityBase extends FieldableEntity { definitions["ProcessType"] := Map( "description", "Which method to use to wait for the game to close.", "help", "This is not needed if the GameRunType is RunWait", - "storageKey", this.configPrefix . "ProcessType", "default", "Exe", "group", "process", "widget", "select", @@ -277,7 +258,6 @@ class ManagedEntityBase extends FieldableEntity { ; - Class - This value should be set to the game's window class definitions["ProcessId"] := Map( "help", "This value's type is dependent on the ProcessType above. It can often be detected from other values, and is not needed if the GameRunType is RunWait.", - "storageKey", this.configPrefix . "ProcessId", "group", "process", "modes", Map( "simple", Map("formField", false) @@ -286,7 +266,6 @@ class ManagedEntityBase extends FieldableEntity { definitions["ProcessTimeout"] := Map( "description", "The number of seconds to wait before giving up when waiting for a process.", - "storageKey", this.configPrefix . "ProcessTimeout", "default", 30, "group", "process", "modes", Map( @@ -295,41 +274,26 @@ class ManagedEntityBase extends FieldableEntity { ) definitions["RunCmd"] := Map( + "title", "Run Command", "description", "The command that will be used to run the game's launcher.", "help", "Typically only used if LauncherRunType is Command.", - "storageKey", this.configPrefix . "RunCmd", "group", "process" ) return definitions } - GetData() { - if (!this.ParentEntity) { - throw EntityException("A parent entity is required on type " . Type(this)) - } - - return this.ParentEntity.GetData() - } - - _createEntityData() { - return "" - } - - AutoDetectValues(recurse := true) { - detectedValues := super.AutoDetectValues(recurse) + AutoDetectValues() { + detectedValues := super.AutoDetectValues() processId := "" - usesShortcut := false - if (this.GetData().HasValue(this.configPrefix . "UsesShortcut")) { - usesShortcut := this.GetData().GetValue(this.configPrefix . "UsesShortcut") - } else { - usesShortcut := (this["RunType"] == "Shortcut" || this["ShortcutSrc"] != "" || this["RunCmd"] == "") - } + usesShortcut := (this.GetData().HasValue("UsesShortcut")) + ? this.GetData().GetValue("UsesShortcut") + : (this["RunType"] == "Shortcut" || this["ShortcutSrc"] != "" || this["RunCmd"] == "") - detectedValues[this.configPrefix . "UsesShortcut"] := usesShortcut - detectedValues[this.configPrefix . "RunType"] := usesShortcut ? "Shortcut" : "Command" - detectedValues[this.configPrefix . "InstallDir"] := this.LocateInstallDir() ; This needs to run to expand exes without a dir + detectedValues["UsesShortcut"] := usesShortcut + detectedValues["RunType"] := usesShortcut ? "Shortcut" : "Command" + detectedValues["InstallDir"] := this.LocateInstallDir() ; This needs to run to expand exes without a dir if (this["ProcessType"] == "Exe") { SplitPath(this["Exe"], &processId) @@ -337,8 +301,8 @@ class ManagedEntityBase extends FieldableEntity { processId := this["WindowTitle"] ? this["WindowTitle"] : this.Id } - detectedValues[this.configPrefix . "ProcessId"] := processId - detectedValues[this.configPrefix . "WorkingDir"] := this["InstallDir"] + detectedValues["ProcessId"] := processId + detectedValues["WorkingDir"] := this["InstallDir"] return detectedValues } @@ -352,19 +316,25 @@ class ManagedEntityBase extends FieldableEntity { ListProcessTypes() { return [ - "Exe", "Title", "Class" + "Exe", + "Title", + "Class" ] } ListRunMethods() { return [ - "Run", "Scheduled", "RunWait" + "Run", + "Scheduled", + "RunWait" ] } ListLocateMethods() { return [ - "Search", "Registry", "BlizzardProductDb" + "Search", + "Registry", + "BlizzardProductDb" ; TODO Move this to the Blizzard module ] } @@ -393,7 +363,7 @@ class ManagedEntityBase extends FieldableEntity { validateResult["invalidFields"].push("RunCmd") } - ; TODO: Perform more launcher and game type validation here + ; TODO: Perform more validation here return validateResult } @@ -416,8 +386,7 @@ class ManagedEntityBase extends FieldableEntity { LocateInstallDir() { installDir := "" - ; TODO: Add additional methods to detect the install dir - + ; TODO Move BlizzardProductDb method to an event handled by the Blizzard module if (this["LocateMethod"] == "BlizzardProductDb") { blizzardDir := this.GetBlizzardProductDir() @@ -426,6 +395,8 @@ class ManagedEntityBase extends FieldableEntity { } } + ; TODO: Add additional methods to detect the install dir + return installDir } @@ -477,6 +448,7 @@ class ManagedEntityBase extends FieldableEntity { } } } else if (this["LocateMethod"] == "BlizzardProductDb") { + ; TODO Move BlizzardProductDb method to an event handled by the Blizzard module blizzardDir := this.GetBlizzardProductDir() if (blizzardDir != "") { @@ -516,18 +488,14 @@ class ManagedEntityBase extends FieldableEntity { return path } - GetBlizzardProductKey() { - return "bna" ; Default to the Battle.net client itself - } - + ; TODO Move this method to the Blizzard module and call it from an eevent GetBlizzardProductDir() { - path := "" - productCode := this.GetBlizzardProductKey() + productCode (HasBase(this, GameProcessEntity.Prototype)) + ? productCode := this["PlatformRef"] + : "bna" ; Default to the Battle.net client itself - if (productCode != "" && this.app.Services.Has("BlizzardProductDb")) { - path := this.app["BlizzardProductDb"].GetProductInstallPath(productCode) - } - - return path + return (productCode != "" && this.app.Services.Has("BlizzardProductDb")) + ? this.app["BlizzardProductDb"].GetProductInstallPath(productCode) + : "" } } diff --git a/Lib/Launchpad/Entity/LauncherEntity.ahk b/Lib/Launchpad/Entity/LauncherEntity.ahk index ab663653..947504ae 100644 --- a/Lib/Launchpad/Entity/LauncherEntity.ahk +++ b/Lib/Launchpad/Entity/LauncherEntity.ahk @@ -66,10 +66,10 @@ class LauncherEntity extends FieldableEntity { "showDefaultCheckbox", false ) - definitions["ManagedLauncher"] := Map( + definitions["LauncherProcess"] := Map( "type", "entity_reference", "required", true, - "entityType", "managed_launcher", + "entityType", "launcher_process", "child", true, "weight", -25, "widget", "entity_select", @@ -85,13 +85,13 @@ class LauncherEntity extends FieldableEntity { ), "default", this.idVal, "showDefaultCheckbox", false, - "valueType", EntityFieldBase.VALUE_TYPE_DEFAULT + "storeEntityData", true ) - definitions["ManagedGame"] := Map( + definitions["GameProcess"] := Map( "type", "entity_reference", "required", true, - "entityType", "managed_game", + "entityType", "game_process", "child", true, "weight", -20, "widget", "entity_select", @@ -107,7 +107,7 @@ class LauncherEntity extends FieldableEntity { ), "default", this.idVal, "showDefaultCheckbox", false, - "valueType", EntityFieldBase.VALUE_TYPE_DEFAULT + "storeEntityData", true ) definitions["DestinationDir"] := Map( @@ -408,8 +408,8 @@ class LauncherEntity extends FieldableEntity { if (FileExist(checkPath)) { detectedValues["IconSrc"] := checkPath - } else if (this.Has("ManagedGame", false) && this["ManagedGame"].Has("Exe", false)) { - detectedValues["IconSrc"] := this["ManagedGame"].LocateExe() + } else if (this.Has("GameProcess", false) && this["GameProcess"].Has("Exe", false)) { + detectedValues["IconSrc"] := this["GameProcess"].LocateExe() } else { theme := this.container.Get("manager.theme").GetComponent() detectedValues["IconSrc"] := theme.GetIconPath("game") diff --git a/Lib/Launchpad/Entity/ManagedLauncherEntity.ahk b/Lib/Launchpad/Entity/LauncherProcessEntity.ahk similarity index 80% rename from Lib/Launchpad/Entity/ManagedLauncherEntity.ahk rename to Lib/Launchpad/Entity/LauncherProcessEntity.ahk index 12d5eb03..ee21fcc1 100644 --- a/Lib/Launchpad/Entity/ManagedLauncherEntity.ahk +++ b/Lib/Launchpad/Entity/LauncherProcessEntity.ahk @@ -1,15 +1,13 @@ -class ManagedLauncherEntity extends ManagedEntityBase { - configPrefix := "Launcher" - defaultType := "Default" +class LauncherProcessEntity extends LaunchProcessEntity { defaultClass := "SimpleLauncher" BaseFieldDefinitions() { definitions := super.BaseFieldDefinitions() - definitions["ManagedGame"] := Map( + definitions["GameProcess"] := Map( "type", "entity_reference", "required", true, - "entityType", "managed_game", + "entityType", "game_process", "child", false, "formField", false, "editable", false @@ -17,14 +15,14 @@ class ManagedLauncherEntity extends ManagedEntityBase { definitions["CloseBeforeRun"] := Map( "type", "boolean", - "storageKey", this.configPrefix . "CloseBeforeRun", + "storageKey", "CloseBeforeRun", "default", false, "description", "whether or not the launcher should be closed (if it is running) before starting the game" ) definitions["CloseAfterRun"] := Map( "type", "boolean", - "storageKey", this.configPrefix . "CloseAfterRun", + "storageKey", "CloseAfterRun", "default", false, "description", "Indicates whether the launcher should be closed after the game stops" ) @@ -33,7 +31,7 @@ class ManagedLauncherEntity extends ManagedEntityBase { "type", "time_offset", "timeUnits", "s", "min", 0, - "storageKey", this.configPrefix . "ClosePreDelay", + "storageKey", "ClosePreDelay", "default", 0, "required", true, "group", "advanced", @@ -47,7 +45,7 @@ class ManagedLauncherEntity extends ManagedEntityBase { "type", "time_offset", "timeUnits", "s", "min", 0, - "storageKey", this.configPrefix . "ClosePostDelay", + "storageKey", "ClosePostDelay", "default", 0, "required", true, "group", "advanced", @@ -64,7 +62,7 @@ class ManagedLauncherEntity extends ManagedEntityBase { ; - "AutoPolite" - Automatically attempt to politely close the launcher, or fail if it can't be closed politely ; - "AutoKill" - Automatically attempts to end the launcher process, forcefully if needed definitions["CloseMethod"] := Map( - "storageKey", this.configPrefix . "CloseMethod", + "storageKey", "CloseMethod", "default", "Prompt", "description", "How to attempt to close the launcher if running", "widget", "select", @@ -76,7 +74,7 @@ class ManagedLauncherEntity extends ManagedEntityBase { "type", "time_offset", "timeUnits", "s", "min", 0, - "storageKey", this.configPrefix . "RecheckDelay", + "storageKey", "RecheckDelay", "default", 10, "required", true, "group", "advanced", @@ -90,7 +88,7 @@ class ManagedLauncherEntity extends ManagedEntityBase { "type", "time_offset", "timeUnits", "s", "min", 0, - "storageKey", this.configPrefix . "WaitTimeout", + "storageKey", "WaitTimeout", "default", 30, "required", true, "group", "advanced", @@ -104,11 +102,11 @@ class ManagedLauncherEntity extends ManagedEntityBase { "type", "time_offset", "timeUnits", "ms", "min", 0, - "storageKey", this.configPrefix . "KillPreDelay", + "storageKey", "KillPreDelay", "default", 10, "required", true, "group", "advanced", - "description", "If killing a managed launcher forcefully, ending the process will be delayed by this number of seconds.", + "description", "If killing a launch process forcefully, ending the process will be delayed by this number of seconds.", "modes", Map( "simple", Map("formField", false) ) @@ -118,11 +116,11 @@ class ManagedLauncherEntity extends ManagedEntityBase { "type", "time_offset", "timeUnits", "ms", "min", 0, - "storageKey", this.configPrefix . "KillPostDelay", + "storageKey", "KillPostDelay", "default", 5, "required", true, "group", "advanced", - "description", "If killing a managed launcher forcefully, the launcher will wait this number of seconds after trying to end the process before reporting success.", + "description", "If killing a launch process forcefully, the launcher will wait this number of seconds after trying to end the process before reporting success.", "modes", Map( "simple", Map("formField", false) ) @@ -132,7 +130,7 @@ class ManagedLauncherEntity extends ManagedEntityBase { "type", "time_offset", "timeUnits", "s", "min", 0, - "storageKey", this.configPrefix . "PoliteCloseWait", + "storageKey", "PoliteCloseWait", "required", true, "default", 10, "group", "advanced", diff --git a/Lib/Launchpad/Entity/ManagedProcessEntity.ahk b/Lib/Launchpad/Entity/ManagedProcessEntity.ahk deleted file mode 100644 index 02cb087b..00000000 --- a/Lib/Launchpad/Entity/ManagedProcessEntity.ahk +++ /dev/null @@ -1,533 +0,0 @@ -class ManagedProcessEntity extends FieldableEntity { - defaultType := "Default" - defaultClass := "Default" - - DiscoverParentEntity(container, eventMgr, id, storageObj, idSanitizer) { - return container.Get("entity_manager.launcher")[id] - } - - GetDefaultFieldGroups() { - groups := super.GetDefaultFieldGroups() - - groups["locations"] := Map( - "name", "Locations", - "weight", 100 - ) - - groups["registry"] := Map( - "name", "Registry", - "weight", 125 - ) - - groups["process"] := Map( - "name", "Process", - "weight", 150 - ) - - return groups - } - - BaseFieldDefinitions() { - definitions := super.BaseFieldDefinitions() - - definitions["name"]["formField"] := false - - definitions["Launcher"] := Map( - "storageKey", "", - "type", "entity_reference", - "entityType", "launcher", - "required", true, - "formField", false, - "callbacks", Map( - "GetValue", ObjBindMethod(this, "GetId"), - "SetValue", ObjBindMethod(this, "SetId"), - "HasValue", ObjBindMethod(this, "HasId"), - "HasOverride", ObjBindMethod(this, "HasId"), - "IsEmpty", ObjBindMethod(this, "HasId", true), - "DeleteValue", "" - ) - ) - - definitions["EntityType"] := Map( - "default", this.defaultType, - "description", "The key of the managed type to load settings and defaults from.", - "required", true, - "storageKey", this.configPrefix . "Type", - "widget", "select", - "selectOptionsCallback", ObjBindMethod(this, "ListEntities", false, true), - "group", "general" - ) - - definitions["EntityClass"] := Map( - "default", this.defaultClass, - "description", "The name of the AHK class that will be used to control the managed entity.", - "formField", false, - "storageKey", this.configPrefix . "Class", - "required", true, - "group", "advanced", - "modes", Map( - "simple", Map("formField", false) - ), - ) - - definitions["SearchDirs"] := Map( - "type", "directory", - "mustExist", false, - "storageKey", this.configPrefix . "SearchDirs", - "default", [A_ProgramFiles], - "description", "Possible parent directories where the game's launcher might exist, to be used for auto-detection.", - "help", "These should be as specific as possible to reduce detection time.", - "multiple", true, - "group", "locations", - "modes", Map( - "simple", Map("formField", false) - ) - ) - - definitions["InstallDir"] := Map( - "type", "directory", - "mustExist", false, - "storageKey", this.configPrefix . "InstallDir", - "group", "locations", - "modes", Map( - "simple", Map("group", "general") - ), - "description", "Select the installation folder, or use default for auto-detection." - ) - - definitions["Exe"] := Map( - "type", "file", - "fileMask", "*.exe", - "mustExist", false, - "storageKey", this.configPrefix . "Exe", - "description", "This can be the full path on the system to the launcher's .exe file, or simply the name of the .exe file itself.", - "help", "If the .exe doesn't include the absolute path, auto-detection will be used by searching the DestinationDirs.", - "group", "locations", - "modes", Map( - "simple", Map("group", "general") - ) - ) - - ; Options include: - ; - Search (will search through each directory in SearchDirs until a match is found) - ; - BlizzardProductDb (will search Battle.net's product.db file if it can be located for the installation directory, and the file will be found from there - ; - Registry (will get a directory from the registry key specified by LocateRegKey and search for the file within it) - definitions["LocateMethod"] := Map( - "storageKey", this.configPrefix . "LocateMethod", - "default", "SearchDirs", - "description", "How to search for the .exe if it isn't a full path already", - "group", "general", - "modes", Map( - "simple", Map("formField", false) - ), - "widget", "select", - "selectOptionsCallback", ObjBindMethod(this, "ListLocateMethods"), - "help", "Search: Searches a list of possible directories (Defaulting to some common possibilities) for the .exe file and uses that directory`nRegistry: Looks for the provided registry key and uses its value as the install path if present`nBlizzardProductDb: Searches for PlatformRef within the Blizzard product.db file if present" - ) - - definitions["WindowTitle"] := Map( - "storageKey", this.configPrefix . "WindowTitle", - "group", "process" - ) - - definitions["LocateRegView"] := Map( - "storageKey", this.configPrefix . "LocateRegView", - "default", 64, - "group", "registry", - "widget", "select", - "selectOptionsCallback", ObjBindMethod(this, "ListRegViews"), - "description", "The registry view to use when locating the install dir.", - "modes", Map( - "simple", Map("formField", false) - ) - ) - - definitions["LocateRegKey"] := Map( - "storageKey", this.configPrefix . "LocateRegKey", - "group", "registry", - "description", "The registry key to look up the install dir within.", - "help", "Path parts should be separated with backslashes and must start with one of: HKEY_LOCAL_MACHINE, HKEY_USERS, HKEY_CURRENT_USER, HKEY_CLASSES_ROOT, HKEY_CURRENT_CONFIG, or the abbreviation of one of those. To read from a remote registry, prefix the root path with two backslashes and the computer name.`n`nSimple example: HKLM\Path\To\Key`nRemote example: \\OTHERPC\HKLM\Path\To\Key", - "modes", Map( - "simple", Map("formField", false) - ) - ) - - definitions["LocateRegValue"] := Map( - "storageKey", this.configPrefix . "LocateRegValue", - "group", "registry", - "description", "The name of the registry value to look up within the specified key.", - "help", "Example: InstallPath", - "modes", Map( - "simple", Map("formField", false) - ) - ) - - definitions["LocateRegRemovePrefix"] := Map( - "storageKey", this.configPrefix . "LocateRegRemovePrefix", - "group", "registry", - "modes", Map( - "simple", Map("formField", false) - ) - ) - - definitions["LocateRegRemoveSuffix"] := Map( - "storageKey", this.configPrefix . "LocateRegRemoveSuffix", - "group", "registry", - "modes", Map( - "simple", Map("formField", false) - ) - ) - - definitions["LocateRegStripQuotes"] := Map( - "storageKey", this.configPrefix . "LocateRegStripQuotes", - "default", false, - "group", "registry", - "description", "Strip quotes from registry value", - "modes", Map( - "simple", Map("formField", false) - ) - ) - - definitions["PlatformRef"] := Map( - "storageKey", this.configPrefix . "PlatformRef", - "description", "If the item is known to the launcher by a specific ID, it should be stored here.", - "group", "general" - ) - - definitions["WorkingDir"] := Map( - "type", "directory", - "description", "The directory that the launcher should be run from.", - "help", "If not set, it will be run without setting an explicit working directory, which is usually sufficient.", - "storageKey", this.configPrefix . "WorkingDir", - "group", "locations", - "modes", Map( - "simple", Map("formField", false) - ) - ) - - ; - Shortcut (Run a shortcut file) - ; - Command (Run a command directly, the default if required) - definitions["RunType"] := Map( - "description", "Which method to use for launching this item.", - "help", "This is only needed for launchers that have to manage their own process.", - "storageKey", this.configPrefix . "RunType", - "default", "Command", - "group", "process", - "widget", "select", - "selectOptionsCallback", ObjBindMethod(this, "ListRunTypes") - ) - - definitions["UsesShortcut"] := Map( - "type", "boolean", - "description", "Whether a shortcut file will be used when starting the internally-managed game launcher", - "formField", false, - "storageKey", this.configPrefix . "UsesShortcut" - ) - - definitions["ReplaceProcess"] := Map( - "type", "boolean", - "description", "Kill and re-launch the game process immediately after it is detected.", - "help", "This can be used to force Launchpad to own the game process, but won't for for every game.", - "storageKey", this.configPrefix . "ReplaceProcess", - "default", false, - "group", "process" - ) - - ; - The filename of an existing shortcut (.url or .lnk file, or even another .exe) that will be used to run the game. - ; - The path of another shortcut file (.url or .lnk) on the system, which will be copied to the AssetsDir if it doesn't already exist - ; - The path of an .exe file on the system to which a shortcut will be created in AssetsDir if it doesn't already exist. Using this option - ; is usually not necessary, since you can run the .exe directly instead. - definitions["ShortcutSrc"] := Map( - "description", "The shortcut file used to launch the game launcher itself.", - "help", "This is typically only needed if the Shortcut LauncherRunType is selected.", - "storageKey", this.configPrefix . "ShortcutSrc", - "group", "locations", - "modes", Map( - "simple", Map("group", "general") - ) - ) - - ; - RunWait (the default, uses RunWait to both run a process and wait until it completes in one step. This is most efficient if it works.) - ; - Run (Uses Run, then watches for the game window and waits until the window opens (if needed) and then closes) - ; - Scheduled (Creates an immediate scheduled task that runs the game, then waits until the window opens (if needed) and then closes) - definitions["RunMethod"] := Map( - "description", "Which method to use to run the RunCmd", - "storageKey", this.configPrefix . "RunMethod", - "default", "Run", - "group", "process", - "widget", "select", - "selectOptionsCallback", ObjBindMethod(this, "ListRunMethods") - ) - - ; - "Exe" (Waits for the game's .exe process to start if it hasn't already, and then waits for it to stop again. This is the default if the game type is not RunWait) - ; - "Title" (Waits for the game's window title to open if it isn't already, and then waits for it to close again) - ; - "Class" (Wait's for the game's window class to open if it isn't already, and then waits for it to close again) - definitions["ProcessType"] := Map( - "description", "Which method to use to wait for the game to close.", - "help", "This is not needed if the GameRunType is RunWait", - "storageKey", this.configPrefix . "ProcessType", - "default", "Exe", - "group", "process", - "widget", "select", - "selectOptionsCallback", ObjBindMethod(this, "ListProcessTypes") - ) - - ; - Exe - This value will default to the GameExe unless overridden - ; - Title - This value will default to the game's Key unless overridden - ; - Class - This value should be set to the game's window class - definitions["ProcessId"] := Map( - "help", "This value's type is dependent on the ProcessType above. It can often be detected from other values, and is not needed if the GameRunType is RunWait.", - "storageKey", this.configPrefix . "ProcessId", - "group", "process", - "modes", Map( - "simple", Map("formField", false) - ) - ) - - definitions["ProcessTimeout"] := Map( - "description", "The number of seconds to wait before giving up when waiting for a process.", - "storageKey", this.configPrefix . "ProcessTimeout", - "default", 30, - "group", "process", - "modes", Map( - "simple", Map("formField", false) - ) - ) - - definitions["RunCmd"] := Map( - "description", "The command that will be used to run the game's launcher.", - "help", "Typically only used if LauncherRunType is Command.", - "storageKey", this.configPrefix . "RunCmd", - "group", "process" - ) - - return definitions - } - - GetData() { - if (!this.ParentEntity) { - throw EntityException("A parent entity is required on type " . Type(this)) - } - - return this.ParentEntity.GetData() - } - - _createEntityData() { - return "" - } - - AutoDetectValues(recurse := true) { - detectedValues := super.AutoDetectValues(recurse) - processId := "" - usesShortcut := false - - if (this.GetData().HasValue(this.configPrefix . "UsesShortcut")) { - usesShortcut := this.GetData().GetValue(this.configPrefix . "UsesShortcut") - } else { - usesShortcut := (this["RunType"] == "Shortcut" || this["ShortcutSrc"] != "" || this["RunCmd"] == "") - } - - detectedValues[this.configPrefix . "UsesShortcut"] := usesShortcut - detectedValues[this.configPrefix . "RunType"] := usesShortcut ? "Shortcut" : "Command" - detectedValues[this.configPrefix . "InstallDir"] := this.LocateInstallDir() ; This needs to run to expand exes without a dir - - if (this["ProcessType"] == "Exe") { - SplitPath(this["Exe"], &processId) - } else if (this["ProcessType"] == "Title") { - processId := this["WindowTitle"] ? this["WindowTitle"] : this.Id - } - - detectedValues[this.configPrefix . "ProcessId"] := processId - detectedValues[this.configPrefix . "WorkingDir"] := this["InstallDir"] - - return detectedValues - } - - ListRunTypes() { - return [ - "Command", - "Shortcut" - ] - } - - ListProcessTypes() { - return [ - "Exe", "Title", "Class" - ] - } - - ListRunMethods() { - return [ - "Run", "Scheduled", "RunWait" - ] - } - - ListLocateMethods() { - return [ - "Search", "Registry", "BlizzardProductDb" - ] - } - - ListRegViews() { - regViews := [ - "32" - ] - - if (A_Is64bitOS) { - regViews.Push("64") - } - - return regViews - } - - Validate() { - validateResult := super.Validate() - - if (((this["UsesShortcut"] && this["RunCmd"] == "") && this["ShortcutSrc"] == "") && !this.ShortcutFileExists()) { - validateResult["success"] := false - validateResult["invalidFields"].push("ShortcutSrc") - } - - if (this["ShortcutSrc"] == "" && this["RunCmd"] == "") { - validateResult["success"] := false - validateResult["invalidFields"].push("RunCmd") - } - - ; TODO: Perform more launcher and game type validation here - - return validateResult - } - - ShortcutFileExists() { - shortcutSrc := (this["ShortcutSrc"] != "") - ? this["ShortcutSrc"] - : this["AssetsDir"] . "\" . this.Id . ".lnk" - - exists := FileExist(shortcutSrc) - - if (!exists) { - shortcutSrc := this["AssetsDir"] . "\" . this.Id . ".url" - exists := FileExist(shortcutSrc) - } - - return exists - } - - LocateInstallDir() { - installDir := "" - - ; TODO: Add additional methods to detect the install dir - - if (this["LocateMethod"] == "BlizzardProductDb") { - blizzardDir := this.GetBlizzardProductDir() - - if (blizzardDir != "") { - installDir := blizzardDir - } - } - - return installDir - } - - LocateExe() { - return this.LocateFile(this["Exe"]) - } - - LocateFile(filePattern) { - filePath := "" - - if (filePattern != "") { - SplitPath(filePattern,,,,, &fileDrive) - - if (fileDrive != "") { - filePath := filePattern - } else { - searchDirs := [] - - if (this["InstallDir"] != "") { - searchDirs.Push(this["InstallDir"]) - } else if (this["LocateMethod"] == "SearchDirs") { - if (HasBase(this["SearchDirs"], Array.Prototype) && this["SearchDirs"].Length > 0) { - for index, dir in this["SearchDirs"] { - searchDirs.Push(dir) - } - } - } else if (this["LocateMethod"] == "Registry") { - regKey := this["LocateRegKey"] - - if (regKey != "") { - SetRegView(this["LocateRegView"]) - regDir := RegRead(this["LocateRegKey"], this["LocateRegValue"]) - SetRegView("Default") - - if (regDir != "") { - if (this["LocateRegStripQuotes"]) { - regDir := StrReplace(regDir, "`"", "") - } - - if (this["LocateRegRemovePrefix"] && SubStr(regDir, 1, StrLen(this["LocateRegRemovePrefix"])) == this["LocateRegRemovePrefix"]) { - regDir := SubStr(regDir, StrLen(this["LocateRegRemovePrefix"]) + 1) - } - - if (this["LocateRegRemoveSuffix"] && SubStr(regDir, 1, StrLen(this["LocateRegRemoveSuffix"])) == this["LocateRegRemoveSuffix"]) { - regDir := StrReplace(regDir, StrLen(this["LocateRegRemoveSuffix"]) + 1) - } - - searchDirs.Push(regDir) - } - } - } else if (this["LocateMethod"] == "BlizzardProductDb") { - blizzardDir := this.GetBlizzardProductDir() - - if (blizzardDir != "") { - searchDirs.Push(blizzardDir) - } - } - - filePath := this.LocateFileInSearchDirs(filePattern, searchDirs) - } - } - - return filePath - } - - LocateFileInSearchDirs(filePattern, searchDirs := "") { - path := "" - - if (searchDirs == "") { - searchDirs := this["SearchDirs"].Clone() - } - - if (!HasBase(searchDirs, Array.Prototype)) { - searchDirs := [searchDirs] - } - - for index, searchDir in searchDirs { - Loop Files, searchDir . "\" . filePattern, "R" { - path := A_LoopFileFullPath - break - } - - if (path != "") { - break - } - } - - return path - } - - GetBlizzardProductKey() { - return "bna" ; Default to the Battle.net client itself - } - - GetBlizzardProductDir() { - path := "" - productCode := this.GetBlizzardProductKey() - - if (productCode != "" && this.app.Services.Has("BlizzardProductDb")) { - path := this.app["BlizzardProductDb"].GetProductInstallPath(productCode) - } - - return path - } -} diff --git a/Lib/Launchpad/Includes.ahk b/Lib/Launchpad/Includes.ahk index cec1c809..cd69a6ec 100644 --- a/Lib/Launchpad/Includes.ahk +++ b/Lib/Launchpad/Includes.ahk @@ -22,11 +22,10 @@ #Include Config\PlatformsConfig.ahk #Include ConfigMigrator\LaunchpadIniMigrator.ahk #Include DetectedGame\DetectedGame.ahk +#Include Entity\GameProcessEntity.ahk #Include Entity\LauncherEntity.ahk -#Include Entity\ManagedEntityBase.ahk -#Include Entity\ManagedGameEntity.ahk -#Include Entity\ManagedLauncherEntity.ahk -#Include Entity\ManagedProcessEntity.ahk +#Include Entity\LauncherProcessEntity.ahk +#Include Entity\LaunchProcessEntity.ahk #Include Entity\PlatformEntity.ahk #Include GamePlatform\BasicPlatform.ahk #Include GamePlatform\GamePlatformBase.ahk diff --git a/Lib/LaunchpadLauncher/App/LaunchpadLauncher.ahk b/Lib/LaunchpadLauncher/App/LaunchpadLauncher.ahk index 747feefa..7bc3a131 100644 --- a/Lib/LaunchpadLauncher/App/LaunchpadLauncher.ahk +++ b/Lib/LaunchpadLauncher/App/LaunchpadLauncher.ahk @@ -39,7 +39,7 @@ class LaunchpadLauncher extends AppBase { ) services["Game"] := Map( - "class", config["gameConfig"]["GameClass"], + "class", config["gameConfig"]["ProcessClass"], "arguments", [ AppRef(), ParameterRef("launcher_key"), @@ -48,7 +48,7 @@ class LaunchpadLauncher extends AppBase { ) services["Launcher"] := Map( - "class", config["launcherConfig"]["LauncherClass"], + "class", config["launcherConfig"]["ProcessClass"], "arguments", [ ParameterRef("launcher_key"), ServiceRef("manager.gui"), From c92e332856a20bf4fda33455f0717e6f1027bce2 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:23:06 -0500 Subject: [PATCH 094/227] Update field types to support cardinality options better cardinality of 1 means a single value will always be returned, while any other option will return an array --- .../EntityField/BooleanEntityField.ahk | 55 ++++++++-- .../EntityField/EntityFieldBase.ahk | 91 +++++++++++++--- .../EntityField/EntityReferenceField.ahk | 102 ++++++++++++++---- .../EntityField/ServiceReferenceField.ahk | 67 ++++++++---- 4 files changed, 254 insertions(+), 61 deletions(-) diff --git a/Lib/Shared/Volantis.Entity/EntityField/BooleanEntityField.ahk b/Lib/Shared/Volantis.Entity/EntityField/BooleanEntityField.ahk index b3dad413..91d37e01 100644 --- a/Lib/Shared/Volantis.Entity/EntityField/BooleanEntityField.ahk +++ b/Lib/Shared/Volantis.Entity/EntityField/BooleanEntityField.ahk @@ -2,20 +2,61 @@ class BooleanEntityField extends EntityFieldBase { DefinitionDefaults(fieldDefinition) { defaults := super.DefinitionDefaults(fieldDefinition) defaults["widget"] := "checkbox" + defaults["default"] := false return defaults } - GetValue() { - isTrue := StrLower(super.GetValue()) + GetValue(index := "") { + value := super.GetValue(index) - if (isTrue == "true" || isTrue == "false") { - isTrue := (isTrue == "true") + if (!HasBase(value, Array.Prototype)) { + value := [value] } - return !!(isTrue) + newValues := [] + + for singleIndex, singleValue in value { + isTrue := StrLower(singleValue) + + if (isTrue == "true" || isTrue == "false") { + isTrue := (isTrue == "true") + } + + newValues.Push(!!isTrue) + } + + value := newValues + + if (!value.Length) { + value.Push("") + } + + if (index && !value.Has(index)) { + throw AppException("Index out of range") + } + + if (index) { + return value[index] + } else if (this.multiple) { + return value + } else { + return value[1] + } } - SetValue(value) { - super.SetValue(!!(value)) + SetValue(value, index := "") { + if (index || !this.multiple || !HasBase(value, Array.Prototype)) { + value := !!value + } else { + newValues := [] + + for singleIndex, singleValue in value { + newValues.Push(!!singleValue) + } + + value := newValues + } + + super.SetValue(value, index) } } diff --git a/Lib/Shared/Volantis.Entity/EntityField/EntityFieldBase.ahk b/Lib/Shared/Volantis.Entity/EntityField/EntityFieldBase.ahk index a13646a4..c4856c10 100644 --- a/Lib/Shared/Volantis.Entity/EntityField/EntityFieldBase.ahk +++ b/Lib/Shared/Volantis.Entity/EntityField/EntityFieldBase.ahk @@ -10,6 +10,7 @@ class EntityFieldBase { userLayer := "data" cloner := "" merger := "" + multiple := false needsEntityRefresh := false static VALUE_TYPE_DATA := "data" @@ -19,6 +20,10 @@ class EntityFieldBase { get => this.fieldDefinition set => this.fieldDefinition := value } + + IsMultiple { + get => this.multiple + } __New(fieldTypeId, entityObj, container, eventMgr, dataObj, fieldKey, fieldDefinition) { this.fieldTypeId := fieldTypeId @@ -31,6 +36,11 @@ class EntityFieldBase { this.merger := container.Get("merger.list") this.Definition := ParameterBag(this.DefinitionDefaults(fieldDefinition)) this.Definition.Add(fieldDefinition) + this.multiple := (this.Definition["cardinality"] == 0 || this.Definition["cardinality"] > 1) + + if (this.multiple && this.Definition["default"] && !HasBase(this.Definition["default"], Array.Prototype)) { + this.Definition["default"] := [this.Definition["default"]] + } } static Create(container, entityTypeId, entityObj, dataObj, fieldId, definition) { @@ -98,9 +108,8 @@ class EntityFieldBase { "formField", true, "group", "general", "help", "", - "limit", false, + "cardinality", 1, "modes", Map(), - "multiple", false, "processValue", false, "refreshEntityOnChange", false, "required", false, @@ -139,15 +148,49 @@ class EntityFieldBase { return result } - GetValue() { - return this.GetRawValue() + GetValue(index := "") { + return this.GetRawValue(index) } - GetRawValue() { - return this._callback("GetValue") + GetRawValue(index := "") { + value := this._callback("GetValue") + + if (this.multiple && !HasBase(value, Array.Prototype)) { + value := [value] + } + + if (this.multiple && index && !value.Has(index)) { + throw AppException("Index " . index . " does not exist in field " . this.fieldKey . ".") + } + + if (this.multiple && index) { + value := value[index] + } + + return value } - SetValue(value) { + SetValue(value, index := "") { + if (index && this.multiple) { + existingValues := this.GetRawValue() + + if (existingValues.Length < (index + 1)) { + throw AppException("Index to set is too high, there are only " . existingValues.Length . " values in field " . this.fieldKey . ".") + } + + if (existingValues.Length < index) { + existingValues.Push(value) + } else { + existingValues[index] := value + } + + value := existingValues + } + + if (this.multiple && !HasBase(value, Array.Prototype)) { + value := [value] + } + this._callback("SetValue", value) this.RefreshEntity() return this @@ -175,9 +218,19 @@ class EntityFieldBase { } Validate(value) { - return this - .CreateValidator(this.GetValidators(value)) - .Validate(value) + if (!HasBase(value, Array.Prototype)) { + value := [value] + } + + results := [] + + validator := this.CreateValidator(this.GetValidators(value)) + + for index, singleValue in value { + results.Push(validator.Validate(singleValue)) + } + + return this.multiple ? results : results[1] } /** @@ -187,11 +240,9 @@ class EntityFieldBase { _parseLayer(layer := "", allowAll := true) { if (!layer) { layer := this.Definition["dataLayer"] + } else if (layer == "*" && !allowAll) { + throw EntityException("Cannot pass wildcard for this layer value.") } else if (layer == "*") { - if (!allowAll) { - throw EntityException("Cannot pass wildcard for this layer value.") - } - layer := "" } @@ -234,6 +285,14 @@ class EntityFieldBase { allowEmpty ) + if (this.multiple && HasBase(val, Array.Prototype)) { + if (val.Length) { + val := (val[1] != "") + } else { + val := false + } + } + if (negate) { val := !val } @@ -244,6 +303,10 @@ class EntityFieldBase { _hasDefaultValue(allowEmpty := true, negate := false) { hasValue := allowEmpty ? true : !!(this.Definition["default"]) + if (hasValue && !allowEmpty && this.multiple && HasBase(this.Definition["default"], Array.Prototype)) { + hasValue := !!(this.Definition["default"][1]) + } + if (negate) { hasValue := !hasValue } diff --git a/Lib/Shared/Volantis.Entity/EntityField/EntityReferenceField.ahk b/Lib/Shared/Volantis.Entity/EntityField/EntityReferenceField.ahk index 6591eedc..af424717 100644 --- a/Lib/Shared/Volantis.Entity/EntityField/EntityReferenceField.ahk +++ b/Lib/Shared/Volantis.Entity/EntityField/EntityReferenceField.ahk @@ -1,4 +1,4 @@ -class EntityReferenceField extends ServiceReferenceField { +class EntityReferenceField extends EntityFieldBase { managerObj := "" DefinitionDefaults(fieldDefinition) { @@ -8,12 +8,14 @@ class EntityReferenceField extends ServiceReferenceField { throw EntityException("Entity reference fields require an entityType mapping.") } - managerObj := this._entityManager(entityTypeId) defaults := super.DefinitionDefaults(fieldDefinition) - defaults["servicePrefix"] := managerObj.GetServicePrefix() - defaults["entityType"] := managerObj.entityTypeId + defaults["entityType"] := entityTypeId + defaults["widget"] := "select" defaults["child"] := false + defaults["storeEntityData"] := false + defaults["selectOptionsCallback"] := ObjBindMethod(this, "GetEntitySelectOptions") + defaults["selectConditions"] := [] return defaults } @@ -32,34 +34,98 @@ class EntityReferenceField extends ServiceReferenceField { return validators } + GetValue(index := "") { + value := super.GetValue(index) + + if (!HasBase(value, Array.Prototype)) { + value := [value] + } + + entities := [] + entityManager := this._entityManager() + + for entityIndex, entityId in value { + if (!entityId) { + entities.Push("") + } else if (entityManager.Has(entityId)) { + entities.Push(entityManager[entityId]) + } else { + throw AppException("Entity with ID '" . entityId . "' does not exist.") + } + } + + if (!this.multiple || index) { + value := entities.Length ? entities[1] : "" + } else { + value := entities + } + + return value + } + + SetValue(value, index := "") { + if (!HasBase(value, Array.Prototype)) { + value := [value] + } + + newValues := [] + + for singleIndex, singleValue in value { + if (HasBase(singleValue, EntityBase.Prototype)) { + newValues.Push(singleValue.Id) + } else if (Type(singleValue) == "String") { + newValues.Push(singleValue) + } else { + throw AppException("Invalid entity reference data.") + } + } + + value := newValues + + if (!this.multiple || index) { + value := value.Length ? value[1] : "" + } + + super.SetValue(value, index) + return this + } + + GetEntitySelectOptions() { + options := this._getSelectQuery().Execute() + + if (!this.Definition["required"]) { + options.InsertAt(1, "") + } + + return options + } + _entityManager(entityTypeId := "") { if (!this.managerObj) { if (!entityTypeId) { entityTypeId := this.Definition["entityType"] } - this.managerObj := this.container.Get("entity_manager." . entityTypeId) + this.managerObj := this.container["entity_manager." . entityTypeId] } return this.managerObj } - _getService(entityId) { - if (!this.Definition["entityType"]) { - throw AppException("Entity type of reference field is not specified") - } + _getSelectQuery() { + query := this._entityManager().EntityQuery(EntityQuery.RESULT_TYPE_IDS) + conditions := this.Definition["selectConditions"] - entityObj := "" + if (conditions) { + if (Type(conditions) != "Array") { + conditions := [conditions] + } - if (entityId) { - entityObj := this._entityManager()[entityId] - entityObj.LoadEntity() + for index, condition in conditions { + query.Condition(condition) + } } - return entityObj - } - - _getSelectQuery() { - return this._entityManager().EntityQuery(EntityQuery.RESULT_TYPE_IDS) + return query } } diff --git a/Lib/Shared/Volantis.Entity/EntityField/ServiceReferenceField.ahk b/Lib/Shared/Volantis.Entity/EntityField/ServiceReferenceField.ahk index 10ed2747..b0c21423 100644 --- a/Lib/Shared/Volantis.Entity/EntityField/ServiceReferenceField.ahk +++ b/Lib/Shared/Volantis.Entity/EntityField/ServiceReferenceField.ahk @@ -1,6 +1,13 @@ class ServiceReferenceField extends EntityFieldBase { - ReferencedObject { - get => this.GetValue() + DefinitionDefaults(fieldDefinition) { + defaults := super.DefinitionDefaults(fieldDefinition) + + defaults["servicePrefix"] := "" + defaults["widget"] := "select" + defaults["selectOptionsCallback"] := ObjBindMethod(this, "GetServiceSelectOptions") + defaults["selectConditions"] := [] + + return defaults } GetValidators(value) { @@ -13,30 +20,34 @@ class ServiceReferenceField extends EntityFieldBase { return validators } - DefinitionDefaults(fieldDefinition) { - defaults := super.DefinitionDefaults(fieldDefinition) + GetValue(index := "") { + value := super.GetValue(index) - defaults["servicePrefix"] := "" - defaults["widget"] := "select" - defaults["selectOptionsCallback"] := ObjBindMethod(this, "GetEntitySelectOptions") - defaults["selectConditions"] := [] - - return defaults - } + if (!HasBase(value, Array.Prototype)) { + value := [value] + } - GetValue() { - serviceObj := "" - serviceId := super.GetValue() + newValues := [] - if (serviceId ) { - if (Type(serviceId) != "String") { - serviceObj := serviceId + for singleIndex, singleValue in value { + if (Type(singleValue) != "String") { + serviceObj := singleValue } else { - serviceObj := this._getService(serviceId) + serviceObj := this._getService(singleValue) + } + + if (serviceObj) { + newValues.Push(serviceObj) } } - return serviceObj + value := newValues + + if (!this.multiple || index) { + value := value.Length ? value[1] : "" + } + + return value } _getService(serviceId) { @@ -53,8 +64,20 @@ class ServiceReferenceField extends EntityFieldBase { return serviceObj } - SetValue(value) { - super.SetValue(this._getServiceId(value)) + SetValue(value, index := "") { + if (index || !this.multiple || !HasBase(value, Array.Prototype)) { + value := this._getServiceId(singleValue) + } else { + newValues := [] + + for singleIndex, singleValue in value { + newValues[singleIndex] = this._getServiceId(singleValue) + } + + value := newValues + } + + super.SetValue(value, index) } _getServiceId(value) { @@ -69,7 +92,7 @@ class ServiceReferenceField extends EntityFieldBase { return this.container.Query(this.Definition["servicePrefix"], ContainerQuery.RESULT_TYPE_NAMES, false, true) } - GetEntitySelectOptions() { + GetServiceSelectOptions() { query := this._getSelectQuery() conditions := this.Definition["selectConditions"] From fd110bee4d5a69f4c4f47d2376d75ed232576d7a Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:23:46 -0500 Subject: [PATCH 095/227] Return this from set functions --- Lib/Shared/Volantis.Entity/EntityStorage/EntityStorageBase.ahk | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/Shared/Volantis.Entity/EntityStorage/EntityStorageBase.ahk b/Lib/Shared/Volantis.Entity/EntityStorage/EntityStorageBase.ahk index bd366064..ebf04287 100644 --- a/Lib/Shared/Volantis.Entity/EntityStorage/EntityStorageBase.ahk +++ b/Lib/Shared/Volantis.Entity/EntityStorage/EntityStorageBase.ahk @@ -24,6 +24,7 @@ class EntityStorageBase { id := this._dereferenceId(idOrObj) data := this._dereferenceData(idOrObj, data) this._saveEntityData(id, data) + return this } _saveEntityData(id, data) { @@ -48,6 +49,7 @@ class EntityStorageBase { DeleteData(idOrObj) { this._deleteEntityData(this._dereferenceId(idOrObj)) + return this } _dereferenceId(idOrObj) { From 25804aaeecba67978845a98dd68fe7d5bf494b98 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:24:59 -0500 Subject: [PATCH 096/227] Fire the new EntityParentEvent from EntityBase to determine the parent --- .../Volantis.Entity/Entity/EntityBase.ahk | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index da91df35..81a7dfd2 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -134,18 +134,29 @@ class EntityBase { .ListEntities(includeManaged, includeExtended) } - DiscoverParentEntity(container, eventMgr, id, storageObj, idSanitizer) { - return "" + DiscoverParentEntity(container, eventMgr, id, storageObj, idSanitizer, parentEntity := "") { + event := EntityParentEvent(EntityEvents.ENTITY_DISCOVER_PARENT, this.entityTypeId, this, parentEntity) + this.eventMgr.DispatchEvent(event) + + if (event.ParentEntity) { + this.parentEntityObj := event.ParentEntity + } else if (event.ParentEntityId) { + this.parentEntityId := event.ParentEntityId + this.parentEntityMgr := event.ParentEntityManager + ? event.ParentEntityManager + : container.Get("entity_manager." . event.ParentEntityTypeId) + + } + + this.parentEntityObj := event.ParentEntity + + return event.ParentEntity } GetParentEntity() { return this.parentEntityObj } - SetParentEntity(parentEntity) { - this.parentEntityObj := parentEntity - } - SetupEntity() { event := EntityEvent(EntityEvents.ENTITY_PREPARE, this.entityTypeId, this) this.eventMgr.DispatchEvent(event) From ad8e56d80045dd4202387d75de8c4292ebdfd6f5 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:27:15 -0500 Subject: [PATCH 097/227] Only create EntityDat automatically if dataLoaded = false, and set it to true afterward --- Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index 81a7dfd2..3351b1ea 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -11,8 +11,8 @@ class EntityBase { idSanitizer := "" sanitizeId := true loaded := false - merger := "" dataLayer := "data" + dataLoaded := false cloner := "" Id { @@ -108,7 +108,11 @@ class EntityBase { } _createEntityData() { - this.dataObj := EntityData(this, this._getLayerNames(), this._getLayerSources()) + if (!this.dataLoaded) { + this.dataObj := EntityData(this, this._getLayerNames(), this._getLayerSources()) + } + + this.dataLoaded := true } _getLayerNames() { From 7215fa3cc425fc345ab08fdb4eee69c4c9150cdf Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:29:22 -0500 Subject: [PATCH 098/227] Remove ConfigPrefix and update additionalManagedLauncherDefaults to additionalLauncherProcessDefaults --- Lib/Launchpad/Entity/LauncherEntity.ahk | 3 +-- Lib/Launchpad/Entity/PlatformEntity.ahk | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/Launchpad/Entity/LauncherEntity.ahk b/Lib/Launchpad/Entity/LauncherEntity.ahk index 947504ae..e75c0d05 100644 --- a/Lib/Launchpad/Entity/LauncherEntity.ahk +++ b/Lib/Launchpad/Entity/LauncherEntity.ahk @@ -1,6 +1,5 @@ class LauncherEntity extends FieldableEntity { - configPrefix := "Launcher" - additionalManagedLauncherDefaults := Map() + additionalLauncherProcessDefaults := Map() IsBuilt { get => this.LauncherExists(false) diff --git a/Lib/Launchpad/Entity/PlatformEntity.ahk b/Lib/Launchpad/Entity/PlatformEntity.ahk index 5eacd08a..7e2aef0e 100644 --- a/Lib/Launchpad/Entity/PlatformEntity.ahk +++ b/Lib/Launchpad/Entity/PlatformEntity.ahk @@ -1,6 +1,5 @@ class PlatformEntity extends FieldableEntity { platformObj := "" - configPrefix := "" Platform { get => this.GetPlatform() From bf7e213be757715568fcc2443a65f9a7ff305843 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:30:22 -0500 Subject: [PATCH 099/227] Don't pass LauncherType or GameType in during detected game creation since it's handled by the platform now --- Lib/Launchpad/DetectedGame/DetectedGame.ahk | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/Launchpad/DetectedGame/DetectedGame.ahk b/Lib/Launchpad/DetectedGame/DetectedGame.ahk index 0e6a966e..e92e87a7 100644 --- a/Lib/Launchpad/DetectedGame/DetectedGame.ahk +++ b/Lib/Launchpad/DetectedGame/DetectedGame.ahk @@ -89,9 +89,7 @@ class DetectedGame { CreateLauncher(launcherManager) { config := Map( - "Platform", this.platform.key, - "LauncherType", this.launcherType, - "GameType", this.gameType + "Platform", this.platform.key ) if (this.displayName && this.displayName != this.key) { From d4aa4817663b6ab04cb8dfa0e1045d2a471ee959 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:32:14 -0500 Subject: [PATCH 100/227] Remove recursive options from AutoDetectValues and InitializeDefaults for better data separation --- Lib/Launchpad/Entity/LauncherEntity.ahk | 4 ++-- Lib/Launchpad/Entity/PlatformEntity.ahk | 4 ++-- Lib/Shared/Volantis.App/Entity/BackupEntity.ahk | 4 ++-- Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk | 16 ++-------------- .../Volantis.Entity/Entity/FieldableEntity.ahk | 4 ++-- 5 files changed, 10 insertions(+), 22 deletions(-) diff --git a/Lib/Launchpad/Entity/LauncherEntity.ahk b/Lib/Launchpad/Entity/LauncherEntity.ahk index e75c0d05..f94596de 100644 --- a/Lib/Launchpad/Entity/LauncherEntity.ahk +++ b/Lib/Launchpad/Entity/LauncherEntity.ahk @@ -399,8 +399,8 @@ class LauncherEntity extends FieldableEntity { return key } - AutoDetectValues(recurse := true) { - detectedValues := super.AutoDetectValues(recurse) + AutoDetectValues() { + detectedValues := super.AutoDetectValues() if (!detectedValues.Has("IconSrc")) { checkPath := this["AssetsDir"] . "\" . this.Id . ".ico" diff --git a/Lib/Launchpad/Entity/PlatformEntity.ahk b/Lib/Launchpad/Entity/PlatformEntity.ahk index 7e2aef0e..77060a1a 100644 --- a/Lib/Launchpad/Entity/PlatformEntity.ahk +++ b/Lib/Launchpad/Entity/PlatformEntity.ahk @@ -134,8 +134,8 @@ class PlatformEntity extends FieldableEntity { } } - AutoDetectValues(recurse := true) { - detectedValues := super.AutoDetectValues(recurse) + AutoDetectValues() { + detectedValues := super.AutoDetectValues() detectedValues["IsInstalled"] := this.Platform.IsInstalled() detectedValues["InstalledVersion"] := this.Platform.GetInstalledVersion() detectedValues["InstallDir"] := this.Platform.GetInstallDir() diff --git a/Lib/Shared/Volantis.App/Entity/BackupEntity.ahk b/Lib/Shared/Volantis.App/Entity/BackupEntity.ahk index 9d2c32fa..9dfc0cf0 100644 --- a/Lib/Shared/Volantis.App/Entity/BackupEntity.ahk +++ b/Lib/Shared/Volantis.App/Entity/BackupEntity.ahk @@ -93,12 +93,12 @@ class BackupEntity extends FieldableEntity { this.CreateBackupObject() } - AutoDetectValues(recurse := true) { + AutoDetectValues() { if (!this.backup) { this.CreateBackupObject() } - detectedValues := super.AutoDetectValues(recurse) + detectedValues := super.AutoDetectValues() return detectedValues } diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index 3351b1ea..c7876fc5 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -175,17 +175,11 @@ class EntityBase { return this.container.Get("manager.entity_type")[this.EntityTypeId] } - InitializeDefaults(recurse := true) { + InitializeDefaults() { defaults := Map( "name", this.Id ) - if (recurse) { - for key, referencedEntity in this.GetReferencedEntities(true) { - this.merger.Merge(defaults, referencedEntity.InitializeDefaults()) - } - } - return defaults } @@ -302,15 +296,9 @@ class EntityBase { this.eventMgr.DispatchEvent(event) } - AutoDetectValues(recurse := true) { + AutoDetectValues() { values := Map() - if (recurse) { - for key, referencedEntity in this.GetReferencedEntities(true) { - this.merger.Merge(values, referencedEntity.AutoDetectValues(recurse)) - } - } - event := EntityDetectValuesEvent(EntityEvents.ENTITY_DETECT_VALUES, this.EntityTypeId, this, values) this.eventMgr.DispatchEvent(event) diff --git a/Lib/Shared/Volantis.Entity/Entity/FieldableEntity.ahk b/Lib/Shared/Volantis.Entity/Entity/FieldableEntity.ahk index d39183b0..38709289 100644 --- a/Lib/Shared/Volantis.Entity/Entity/FieldableEntity.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/FieldableEntity.ahk @@ -254,8 +254,8 @@ class FieldableEntity extends EntityBase { } } - InitializeDefaults(recurse := true) { - defaults := super.InitializeDefaults(recurse) + InitializeDefaults() { + defaults := super.InitializeDefaults() for key, fieldObj in this.GetFields() { defaults[fieldObj.Definition["storageKey"]] := fieldObj.Definition["default"] From 1eae1875ef4328d003db3a5ff48e255aeff523fa Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:33:53 -0500 Subject: [PATCH 101/227] Rename recursive option to recurse in DiffChanges method --- Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index c7876fc5..6f13e21b 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -376,14 +376,14 @@ class EntityBase { return !!(changes.GetAdded().Count || changes.GetModified().Count || changes.GetDeleted().Count) } - DiffChanges(recursive := true) { + DiffChanges(recurse := true) { diff := this.GetData().DiffChanges("original", this.dataLayer) - if (recursive) { + if (recurse) { diffs := [diff] for index, referencedEntity in this.GetReferencedEntities(true) { - diffs.Push(referencedEntity.DiffChanges(recursive)) + diffs.Push(referencedEntity.DiffChanges(recurse)) } diff := DiffResult.Combine(diffs) From a7f0d9d53d4f2d2d2fafa1aa6665b767e5ed5b55 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:34:20 -0500 Subject: [PATCH 102/227] Make layerNames and layerSources optional in LayeredDataBase --- Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk b/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk index b1a18acd..8d88908b 100644 --- a/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk +++ b/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk @@ -30,7 +30,7 @@ class LayeredDataBase { static NO_VALUE := ":NO_VAL:" - __New(cloner, processors, layerNames, layerSources) { + __New(cloner, processors, layerNames := "", layerSources := "") { this.cloner := cloner if (processors) { From 0399fb1d10d6ec24406d35c61e747e198491cb27 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:35:00 -0500 Subject: [PATCH 103/227] Define parent entity storage on game_process and launcher_process entity types --- Launchpad.services.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Launchpad.services.json b/Launchpad.services.json index 59a88e39..feedbddb 100644 --- a/Launchpad.services.json +++ b/Launchpad.services.json @@ -48,7 +48,8 @@ "entity_class": "GameProcessEntity", "storage_config_storage_parent_key": "Games", "storage_config_path_parameter": "config.launcher_file", - "parent_entity_type": "launcher" + "parent_entity_type": "launcher", + "parent_entity_storage": true }, "entity_type.launcher_process": { "name_singular": "Managed Launcher", From f2f7b05f30af641d21bec451dedd305d2d0da758 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:35:13 -0500 Subject: [PATCH 104/227] Missing change from last commit --- Launchpad.services.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Launchpad.services.json b/Launchpad.services.json index feedbddb..7df3dbb0 100644 --- a/Launchpad.services.json +++ b/Launchpad.services.json @@ -57,7 +57,8 @@ "entity_class": "LauncherProcessEntity", "storage_config_storage_parent_key": "Games", "storage_config_path_parameter": "config.launcher_file", - "parent_entity_type": "launcher" + "parent_entity_type": "launcher", + "parent_entity_storage": true }, "entity_type.platform": { "name_singular": "Platform", From 90b452b64956c0d2926f12c3ba9fe9348b98ed05 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:35:42 -0500 Subject: [PATCH 105/227] Temporarily add single cardinality options to some Launcher fields until widgets support multiple values --- Lib/Launchpad/Entity/LauncherEntity.ahk | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Lib/Launchpad/Entity/LauncherEntity.ahk b/Lib/Launchpad/Entity/LauncherEntity.ahk index f94596de..b5d4c554 100644 --- a/Lib/Launchpad/Entity/LauncherEntity.ahk +++ b/Lib/Launchpad/Entity/LauncherEntity.ahk @@ -186,7 +186,7 @@ class LauncherEntity extends FieldableEntity { definitions["RunBefore"] := Map( "type", "entity_reference", "entityType", "task", - "multiple", true, + "cardinality", 1, ; Change to another number once widgets for multiple values are worked out "group", "tasks", "modes", Map( "simple", Map("formField", false) @@ -198,7 +198,7 @@ class LauncherEntity extends FieldableEntity { definitions["CloseBefore"] := Map( "type", "entity_reference", "entityType", "task", - "multiple", true, + "cardinality", 1, ; Change to another number once widgets for multiple values are worked out "group", "tasks", "modes", Map( "simple", Map("formField", false) @@ -210,7 +210,7 @@ class LauncherEntity extends FieldableEntity { definitions["RunAfter"] := Map( "type", "entity_reference", "entityType", "task", - "multiple", true, + "cardinality", 1, ; Change to another number once widgets for multiple values are worked out "group", "tasks", "modes", Map( "simple", Map("formField", false) @@ -222,7 +222,7 @@ class LauncherEntity extends FieldableEntity { definitions["CloseAfter"] := Map( "type", "entity_reference", "entityType", "task", - "multiple", true, + "cardinality", 1, ; Change to another number once widgets for multiple values are worked out "group", "tasks", "modes", Map( "simple", Map("formField", false) From 4ed87e1de52da94f25f824ed1299863c3b033ea4 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:36:11 -0500 Subject: [PATCH 106/227] Always set cache object in CachedWebServiceResponseBase --- .../WebServiceResponse/CachedWebServiceResponse.ahk | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/WebServiceResponse/CachedWebServiceResponse.ahk b/Lib/Shared/Modules/WebServices/WebServiceResponse/CachedWebServiceResponse.ahk index a237cfb8..551b7efa 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceResponse/CachedWebServiceResponse.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceResponse/CachedWebServiceResponse.ahk @@ -2,9 +2,7 @@ class CachedWebServiceResponse extends WebServiceResponseBase { cacheObj := "" __New(webServiceEnt, webServiceReq) { - if (webServiceEnt.Has("Cache", false)) { - this.cacheObj := webServiceEnt.cacheObj - } + this.cacheObj := webServiceEnt.cacheObj super.__New(webServiceEnt, webServiceReq) } From 3444470665f1b961bc54a3a6634766e58bf75bd2 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:36:24 -0500 Subject: [PATCH 107/227] Simplify event dispatch call --- Lib/Shared/Volantis.App/App/AppBase.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/Shared/Volantis.App/App/AppBase.ahk b/Lib/Shared/Volantis.App/App/AppBase.ahk index 07f03fa2..307ceb47 100644 --- a/Lib/Shared/Volantis.App/App/AppBase.ahk +++ b/Lib/Shared/Volantis.App/App/AppBase.ahk @@ -956,7 +956,7 @@ class AppBase { updateAvailable := false event := ReleaseInfoEvent(Events.APP_GET_RELEASE_INFO, this) - this["manager.event"].DispatchEvent(event) + this.Dispatch(event) releaseInfo := event.ReleaseInfo if ( From ee45219dd1aca103a2015574b946f83cd8b662a7 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:37:00 -0500 Subject: [PATCH 108/227] Change how CloneLayers works in LayeredDataBase so it only clones user layers by default --- .../LayeredData/LayeredDataBase.ahk | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk b/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk index 8d88908b..6575e28e 100644 --- a/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk +++ b/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk @@ -536,8 +536,22 @@ class LayeredDataBase { return data } + GetUserLayers() { + layerNames := this.userLayers + + layers := Map() + + for index, layerName in layerNames { + layers[layerName] := this.GetLayer(layerName) + } + + return layers + } + CloneLayers(layers := "") { if (layers == "") { + layers := this.GetUserLayers() + } else if (layers == "*") { this.LoadAllLayers() layers := this.layers } else if (Type(layers) == "String") { @@ -550,8 +564,10 @@ class LayeredDataBase { cloned := Map() - for key, layer in layers { - cloned[key] := this.CloneData(layer) + if (layers) { + for key, layer in layers { + cloned[key] := this.CloneData(layer) + } } return cloned From 079fc79968496253d441e834c05a9f6bf9cba74f Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:37:47 -0500 Subject: [PATCH 109/227] Update GamePlatformBase to make a web service call to look up a game's main Exe --- .../GamePlatform/GamePlatformBase.ahk | 61 +++++++++---------- 1 file changed, 28 insertions(+), 33 deletions(-) diff --git a/Lib/Launchpad/GamePlatform/GamePlatformBase.ahk b/Lib/Launchpad/GamePlatform/GamePlatformBase.ahk index a8dfc88b..7c25316b 100644 --- a/Lib/Launchpad/GamePlatform/GamePlatformBase.ahk +++ b/Lib/Launchpad/GamePlatform/GamePlatformBase.ahk @@ -184,39 +184,34 @@ class GamePlatformBase { if (possibleExes.Length == 1) { mainExe := possibleExes[1] } else if (possibleExes.Length > 1) { - ; @todo move the API functionality into a module that depends on WebServices - if (this.app.Services.Has("entity_manager.web_service")) { - mgr := this.app["entity_manager.web_service"] - - if (mgr.Has("launchpad_api")) { - webService := mgr["launchpad_api"] - - resultData := webService.AdapterRequest( - Map("id", key), - Map( - "adapterType", "entity_data", - "entityType", "launcher" - ), - "read", - true - ) - - for key, data in resultData { - if ( - data - && HasBase(data, Map.Prototype) - && data.Has("defaults") - && data["defaults"] - && data["defaults"].Has("GameExe") - && data["defaults"]["GameExe"] - ) { - for index, possibleExe in possibleExes { - SplitPath(possibleExe, &fileName) - - if (data["defaults"]["GameExe"] == fileName) { - mainExe := possibleExe - break 2 - } + ; @todo move the API functionality into an event in an event in the WebServicesEventSubscriber + if (this.app.Services.Has("web_services.adapter_manager")) { + resultData := this.app["web_services.adapter_manager"].AdapterRequest( + Map("id", key), + Map( + "dataType", "entity_data", + "entityType", "launcher", + "tags", "defaults" + ), + "read", + true + ) + + for key, data in resultData { + if ( + data + && HasBase(data, Map.Prototype) + && data.Has("defaults") + && data["defaults"] + && data["defaults"].Has("GameExe") + && data["defaults"]["GameExe"] + ) { + for index, possibleExe in possibleExes { + SplitPath(possibleExe, &fileName) + + if (data["defaults"]["GameExe"] == fileName) { + mainExe := possibleExe + break 2 } } } From f0b027489c51149745dad1bcf6f293380bc0c7a2 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:41:56 -0500 Subject: [PATCH 110/227] Update the error dialog and feedback window to send data via WebServices if it's installed --- .../WebServices/Gui/Form/FeedbackWindow.ahk | 62 ++++++++++--------- .../Volantis.App/Gui/Dialog/ErrorDialog.ahk | 26 +++++--- 2 files changed, 52 insertions(+), 36 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/Gui/Form/FeedbackWindow.ahk b/Lib/Shared/Modules/WebServices/Gui/Form/FeedbackWindow.ahk index fda1e07a..b790b0dc 100644 --- a/Lib/Shared/Modules/WebServices/Gui/Form/FeedbackWindow.ahk +++ b/Lib/Shared/Modules/WebServices/Gui/Form/FeedbackWindow.ahk @@ -36,44 +36,50 @@ class FeedbackWindow extends DialogBox { SendFeedback() { global appVersion - webServiceId := "launchpad_api" - entityMgr := this.container["entity_manager.web_service"] - - results := Map() - success := false - - if (entityMgr.Has(webServiceId) && entityMgr[webServiceId]["Enabled"]) { - webService := entityMgr[webServiceId] + filters := "feedback_submission" + operation := "create" + if ( + this.container.Has("web_services.adapter_manager") + && this.container["web_services.adapter_manager"].HasAdapters(filters, operation) + ) { body := Map() body["email"] := this.guiObj["Email"].Text body["version"] := appVersion body["feedback"] := this.guiObj["Feedback"].Text - results := webService.AdapterRequest(Map("data", body), "feedback_submission", "create", true) - } + results := this.container["web_services.adapter_manager"].AdapterRequest( + Map("data", body), + filters, + operation, + true + ) + + success := false + + for adapterId, adapterResult in results { + if (adapterResult) { + success := true - for key, result in results { - if (result) { - success := true - break + break + } } - } - message := "" + message := "" - if (success) { - message := "Successfully sent feedback" - } else if (results.Count) { - message := "Failed to send feedback" - } else { - message := "No feedback adapters are enabled" - } + if (success) { + message := "Successfully sent feedback" + } else if (results.Count) { + message := "Failed to send feedback" + } else { + message := "No feedback adapters are enabled" + } - this.notifierObj.Notify( - message, - "Feedback Submission", - success ? "info" : "error" - ) + this.notifierObj.Notify( + message, + "Feedback Submission", + success ? "info" : "error" + ) + } } } diff --git a/Lib/Shared/Volantis.App/Gui/Dialog/ErrorDialog.ahk b/Lib/Shared/Volantis.App/Gui/Dialog/ErrorDialog.ahk index 672baa1a..a97a6e87 100644 --- a/Lib/Shared/Volantis.App/Gui/Dialog/ErrorDialog.ahk +++ b/Lib/Shared/Volantis.App/Gui/Dialog/ErrorDialog.ahk @@ -109,14 +109,13 @@ class ErrorDialog extends DialogBox { global appVersion ; @todo Move the API connection stuff into the LaunchpadApi module + filters := "error_submission" + operation := "create" if ( - this.container.Has("entity_manager.web_service") - && this.container["entity_manager.web_service"].Has("launchpad_api") - && this.container["entity_manager.web_service"]["launchpad_api"]["Enabled"] + this.container.Has("web_services.adapter_manager") + && this.container["web_services.adapter_manager"].HasAdapters(filters, operation) ) { - webService := this.container["entity_manager.web_service"]["launchpad_api"] - body := Map() body["message"] := this.errorObj.Message body["what"] := this.errorObj.What @@ -128,12 +127,23 @@ class ErrorDialog extends DialogBox { body["version"] := appVersion ? appVersion : "" body["details"] := this.guiObj["ErrorDetails"].Text - success := webService.AdapterRequest( + results := this.container["web_services.adapter_manager"].AdapterRequest( Map("data", body), - Map("adapterType", "error_submission"), - "create" + filters, + operation, + true ) + success := false + + for adapterId, adapterResult in results { + if (adapterResult) { + success := true + + break + } + } + notification := success ? "Successfully sent error details for further investigation" : "Failed to send error details" this.notifierObj.Notify(notification, "Error Submission", success ? "info" : "error") } From 35aefe2492491033f6378889185c1a8290f5d556 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:49:11 -0500 Subject: [PATCH 111/227] Remove LauncherType and GameType handling from DetectedGameEditor --- Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk b/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk index 6b8de59e..2e53e2a3 100644 --- a/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk +++ b/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk @@ -1,10 +1,8 @@ -class DetectedGameEditor extends FormGuiBase { +class DetectedGameEditor extends FormGuiBase { detectedGameObj := "" newValues := Map() missingFields := Map() knownGames := "" - launcherTypes := "" - gameTypes := "" __New(container, themeObj, config, detectedGameObj) { this.detectedGameObj := detectedGameObj @@ -77,9 +75,7 @@ Controls() { super.Controls() - this.Add("ComboBoxControl", "vId", "Id", this.detectedGameObj.Id, this.knownGames, "OnIdChange", "You can change the detected game key here, which will become the name of your launcher. Your existing launchers, and all launchers known about via the API, can be selected to match this game up with one of those items.") - this.Add("SelectControl", "vLauncherType", "Launcher Type", this.detectedGameObj.launcherType, this.launcherTypes, "OnLauncherTypeChange", "This tells " . this.app.appName . " how to interact with any launcher your game might require. If your game's launcher isn't listed, or your game doesn't have a launcher, start with `"Default`".`n`nYou can customize the details of the launcher type after it is added.") - this.Add("SelectControl", "vGameType", "Game Type", this.detectedGameObj.gameType, this.gameTypes, "OnGameTypeChange", "This tells " . this.app.appName . " how to launch your game. Most games can use 'default', but launchers can support different game types.`n`nYou can customize the details of the game type after it is added.") + this.Add("ComboBoxControl", "vId", "Id", this.detectedGameObj.key, this.knownGames, "OnIdChange", "You can change the detected game key here, which will become the name of your launcher. Your existing launchers, and all launchers known about via the API, can be selected to match this game up with one of those items.") this.Add("LocationBlock", "", "Install Dir", this.detectedGameObj.installDir, "InstallDir", "", true, "This is the directory that the game is installed in, if it could be detected.") this.Add("ComboBoxControl", "vExe", "Exe", this.detectedGameObj.exeName, this.detectedGameObj.possibleExeNames, "OnExeChange", "The main Exe, if detected, should be pre-selected. You may change it to be the name (or path) of another exe, or select another one of the detected .exe files from the list (if more than one was found).") this.AddTextBlock("Launcher-Specific ID", "PlatformRef", this.detectedGameObj.platformRef, "This is typically the ID which the game platform or launcher uses when referring to the game internally. Changing this value could cause issues with game launching.") @@ -103,16 +99,6 @@ this.newValues["key"] := ctl.Text } - OnLauncherTypeChange(ctl, info) { - this.guiObj.Submit(false) - this.newValues["launcherType"] := ctl.Text - } - - OnGameTypeChange(ctl, info) { - this.guiObj.Submit(false) - this.newValues["gameType"] := ctl.Text - } - GetValue(key) { val := this.detectedGameObj.%key% From 75c3d7d5be7e53e333774ac51e11fe95d63e90bc Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:56:11 -0500 Subject: [PATCH 112/227] Subscribe to events to automatically make API requests on behalf of entities based on registered adapters --- .../WebServicesEventSubscriber.ahk | 279 ++++++++++++++++++ 1 file changed, 279 insertions(+) diff --git a/Lib/Shared/Modules/WebServices/EventSubscriber/WebServicesEventSubscriber.ahk b/Lib/Shared/Modules/WebServices/EventSubscriber/WebServicesEventSubscriber.ahk index 0924015c..97023350 100644 --- a/Lib/Shared/Modules/WebServices/EventSubscriber/WebServicesEventSubscriber.ahk +++ b/Lib/Shared/Modules/WebServices/EventSubscriber/WebServicesEventSubscriber.ahk @@ -1,4 +1,12 @@ class WebServicesEventSubscriber extends EventSubscriberBase { + adapterMgr := "" + + __New(container, adapterMgr) { + this.adapterMgr := adapterMgr + + super.__New(container) + } + GetEventSubscribers() { return Map( Events.APP_POST_STARTUP, [ @@ -9,6 +17,27 @@ class WebServicesEventSubscriber extends EventSubscriberBase { ], Events.APP_MENU_PROCESS_RESULT, [ ObjBindMethod(this, "OnMenuProcessResult") + ], + EntityEvents.ENTITY_DATA_LAYERS, [ + ObjBindMethod(this, "EntityDataLayers") + ], + EntityEvents.ENTITY_LAYER_SOURCES, [ + ObjBindMethod(this, "EntityLayerSources") + ], + Events.APP_GET_RELEASE_INFO, [ + ObjBindMethod(this, "GetReleaseInfo") + ], + EntityEvents.ENTITY_FIELD_GROUPS, [ + ObjBindMethod(this, "EntityFieldGroups") + ], + EntityEvents.ENTITY_FIELD_DEFINITIONS, [ + ObjBindMethod(this, "EntityFieldDefinitions") + ], + EntityEvents.ENTITY_DETECT_VALUES, [ + ObjBindMethod(this, "EntityDetectValues") + ], + EntityEvents.ENTITY_LIST_ENTITIES, [ + ObjBindMethod(this, "ListEntities") ] ) } @@ -40,4 +69,254 @@ class WebServicesEventSubscriber extends EventSubscriberBase { } } } + + _getEntityLayerKey(webService, adapterId) { + return webService["id"] . "." . adapterId + } + + EntityDataLayers(event, extra, eventName, hwnd) { + if (HasProp(event.Entity, "isWebServiceEntity") && event.Entity.isWebServiceEntity) { + return + } + + webServices := this.container["entity_manager.web_service"] + .EntityQuery(EntityQuery.RESULT_TYPE_ENTITIES) + .Condition(IsTrueCondition(), "Enabled") + .Execute() + + for webServiceId, webService in webServices { + adapterIds := this.adapterMgr.GetAdapterIds(Map( + "dataType", "entity_data", + "entityType", event.EntityTypeId, + "tags", "defaults" + ), "", 0, webService) + + for , adapterId in adapterIds { + adapter := this.adapterMgr.GetAdapter(adapterId) + layerExists := false + layerKey := this._getEntityLayerKey(webService, adapterId) + + for , existingLayerKey in event.Layers { + if (existingLayerKey == layerKey) { + layerExists := true + + break + } + } + + if (!layerExists) { + event.Layers.Push(layerKey) + } + } + } + } + + EntityLayerSources(event, extra, eventName, hwnd) { + if (HasProp(event.Entity, "isWebServiceEntity") && event.Entity.isWebServiceEntity) { + return + } + + webServices := this.container["entity_manager.web_service"] + .EntityQuery(EntityQuery.RESULT_TYPE_ENTITIES) + .Condition(IsTrueCondition(), "Enabled") + .Execute() + + for webServiceId, webService in webServices { + adapters := this.adapterMgr.GetAdapters(Map( + "dataType", "entity_data", + "entityType", event.EntityTypeId + ), "", 0, webService) + + paramsEvent := WebServicesEntityDataParamsEvent( + WebServicesEvents.ENTITY_DATA_PARAMS, + event.EntityTypeId, + event.Entity, + webService, + Map("id", event.Entity.Id) + ) + this.container["manager.event"].DispatchEvent(paramsEvent) + + for key, adapter in adapters { + layerKey := this._getEntityLayerKey(webService, key) + + if (!event.LayerSources.Has(layerKey)) { + event.LayerSources[layerKey] := WebServiceAdapterLayerSource(adapter, paramsEvent.Params) + } + } + } + } + + GetReleaseInfo(event, extra, eventName, hwnd) { + if (!event.ReleaseInfo.Count && this.container.GetApp().Version != "{{VERSION}}") { + releaseInfo := this.adapterMgr.AdapterRequest("", "release_info") + + if (releaseInfo && releaseInfo.Count) { + event.ReleaseInfo := releaseInfo + } + } + } + + EntityFieldGroups(event, extra, eventName, hwnd) { + if (HasProp(event.Entity, "isWebServiceEntity") && event.Entity.isWebServiceEntity) { + return + } + + if (!event.FieldGroups.Has("web_services")) { + webServices := this.container["entity_manager.web_service"] + .EntityQuery(EntityQuery.RESULT_TYPE_ENTITIES) + .Condition(IsTrueCondition(), "Enabled") + .Execute() + + addGroup := false + + for key, webService in webServices { + filters := Map( + "dataType", "entity_data", + "entityType", event.EntityTypeId + ) + operation := "read" + + if (this.adapterMgr.HasAdapters(filters, operation, webService)) { + addGroup := true + + break + } + } + + if (addGroup) { + event.FieldGroups["web_services"] := Map( + "name", "Web Services", + "weight", 100 + ) + } + } + } + + EntityFieldDefinitions(event, extra, eventName, hwnd) { + if (HasProp(event.Entity, "isWebServiceEntity") && event.Entity.isWebServiceEntity) { + return + } + + webServices := this.container["entity_manager.web_service"] + .EntityQuery(EntityQuery.RESULT_TYPE_ENTITIES) + .Condition(IsTrueCondition(), "Enabled") + .Execute() + + for key, webService in webServices { + filters := Map( + "dataType", "entity_data", + "entityType", event.EntityTypeId + ) + operation := "read" + + if (this.adapterMgr.HasAdapters(filters, operation, webService)) { + event.FieldDefinitions["web_service_" . webService["id"] . "_ref"] := Map( + "title", webService["name"] . " Reference", + "description", "The key that is used to look up the entity's data from the " . webService["name"] . " web service.", + "help", "It defaults to the entity ID, but it can be overridden by setting this value.`n`nAddtionally, multiple copies of the same entity can exist by giving them different IDs but using the same " . webService["name"] . " reference.", + "group", "web_services", + "processValue", false, + "modes", Map("simple", Map("formField", false)) + ) + + break + } + } + } + + EntityDetectValues(event, extra, eventName, hwnd) { + if (HasProp(event.Entity, "isWebServiceEntity") && event.Entity.isWebServiceEntity) { + return + } + + webServices := this.container["entity_manager.web_service"] + .EntityQuery(EntityQuery.RESULT_TYPE_ENTITIES) + .Condition(IsTrueCondition(), "Enabled") + .Execute() + + for key, webService in webServices { + fieldId := "web_service_" . webService["id"] . "_ref" + filters := Map( + "dataType", "entity_data", + "entityType", event.EntityTypeId + ) + + if ( + this.adapterMgr.HasAdapters(filters, "read", webService) + && (!event.Values.Has(fieldId) || !event.Values[fieldId]) + && event.Entity.HasField(fieldId) + && (!event.Entity.RawData.Has(fieldId) || !event.Entity.RawData[fieldId]) + ) { + paramsEvent := WebServicesEntityDataParamsEvent( + WebServicesEvents.ENTITY_DATA_PARAMS, + event.EntityTypeId, + event.Entity, + webService, + Map("id", event.Entity.Id) + ) + this.container["manager.event"].DispatchEvent(paramsEvent) + + result := this.adapterMgr.AdapterRequest( + paramsEvent.Params, + Map( + "dataType", "entity_lookup", + "entityType", event.EntityTypeId + ), + "read", + false, + webService + ) + + if (!result) { + result := event.Entity["id"] + } + + event.Values[fieldId] := result + } + } + } + + ListEntities(event, extra, eventName, hwnd) { + if (event.EntityTypeId == "web_service" || event.EntityTypeId == "web_service_provider") { + return + } + + if (event.includeExtended) { + entityMgr := this.container["entity_manager." . event.EntityTypeId] + results := this.adapterMgr.AdapterRequest("", Map( + "dataType", "entity_list", + "entityType", event.EntityTypeId + ), "read", true) + + if (results && HasBase(results, Array.Prototype)) { + managedIds := event.includeManaged + ? [] + : entityMgr.EntityQuery(EntityQuery.RESULT_TYPE_IDS).Execute() + + for index, id in results { + exists := false + + for , existingId in event.EntityList { + if (existingId == id) { + exists := true + break + } + } + + if (!exists && !event.includeManaged) { + for , managedId in managedIds { + if (managedId == id) { + exists := true + break + } + } + } + + if (!exists) { + event.EntityList.Push(id) + } + } + } + } + } } From 23c386583256bc8d3af54bc087e6f275d7b41a6a Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:56:42 -0500 Subject: [PATCH 113/227] Add a recurse option, defaulting to false, when calling DeleteEntity --- Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index 6f13e21b..af504744 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -349,11 +349,17 @@ class EntityBase { } } - DeleteEntity() { + DeleteEntity(recurse := false) { if (this.storageObj.HasData(this.GetStorageId())) { event := EntityEvent(EntityEvents.ENTITY_PREDELETE, this.entityTypeId, this) this.eventMgr.DispatchEvent(event) + if (recurse) { + for index, entityObj in this.ChildEntities { + entityObj.DeleteEntity(recurse) + } + } + this.storageObj.DeleteData(this.GetStorageId()) event := EntityEvent(EntityEvents.ENTITY_DELETED, this.entityTypeId, this) From d63f348a900f826e685ae73d287d2f85ce7b8344 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:57:36 -0500 Subject: [PATCH 114/227] Move SaveData and CreateSnapshot to below the recursive saving loop in SaveEntity --- Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index af504744..f4a242c4 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -317,15 +317,15 @@ class EntityBase { event := EntityEvent(EntityEvents.ENTITY_PRESAVE, this.entityTypeId, this) this.eventMgr.DispatchEvent(event) - - this.GetData().SaveData() - this.CreateSnapshot("original") if (recurse) { for index, entityObj in this.GetReferencedEntities(true) { entityObj.SaveEntity(recurse) } } + + this.GetData().SaveData() + this.CreateSnapshot("original") if (alreadyExists) { event := EntityEvent(EntityEvents.ENTITY_UPDATED, this.entityTypeId, this) From 23999223360f3f18e98331f89b51e8a825777c50 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 03:59:46 -0500 Subject: [PATCH 115/227] Remove duplicate event subscriber definitions from LaunchpadApiSubscriber, and update field in MainWindow to match WebService's field name --- Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk | 4 +- .../LaunchpadApiSubscriber.ahk | 249 +----------------- 2 files changed, 14 insertions(+), 239 deletions(-) diff --git a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk index d8833ccd..3f897337 100644 --- a/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk +++ b/Lib/Launchpad/Gui/ManageWindow/MainWindow.ahk @@ -133,7 +133,7 @@ status := launcher.GetStatus() ; @todo Move the API data to an event in the LaunchpadApi module - apiStatus := (launcher.HasField["LaunchpadApiRef"] && launcher["LaunchpadApiRef"]) ? "Linked" : "Not linked" + apiStatus := (launcher.HasField["web_service_launchpad_api_ref"] && launcher["web_service_launchpad_api_ref"]) ? "Linked" : "Not linked" created := this.FormatDate(this.app.State.GetLauncherCreated(key)) updated := this.FormatDate(this.app.State.GetLauncherInfo("Config")["Timestamp"]) built := this.FormatDate(this.app.State.GetLauncherInfo("Build")["Timestamp"]) @@ -311,7 +311,7 @@ status := launcher.GetStatus() ; @todo Move the API code to the LaunchpadApi module - apiStatus := (launcher.HasField("LaunchpadApiRef") && launcher["LaunchpadApiRef"]) ? "Linked" : "Not linked" + apiStatus := (launcher.HasField("web_service_launchpad_api_ref") && launcher["web_service_launchpad_api_ref"]) ? "Linked" : "Not linked" created := this.FormatDate(this.app.State.GetLauncherCreated(key)) updated := this.FormatDate(this.app.State.GetLauncherInfo(key, "Config")["Timestamp"]) built := this.FormatDate(this.app.State.GetLauncherInfo(key, "Build")["Timestamp"]) diff --git a/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk b/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk index 699cbb5d..8b864ab7 100644 --- a/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk +++ b/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk @@ -1,250 +1,25 @@ class LaunchpadApiSubscriber extends EventSubscriberBase { GetEventSubscribers() { return Map( - Events.APP_GET_RELEASE_INFO, [ - ObjBindMethod(this, "GetReleaseInfo") - ], - EntityEvents.ENTITY_DATA_LAYERS, [ - ObjBindMethod(this, "EntityDataLayers") - ], - EntityEvents.ENTITY_LAYER_SOURCES, [ - ObjBindMethod(this, "EntityLayerSources") - ], - EntityEvents.ENTITY_FIELD_GROUPS, [ - ObjBindMethod(this, "EntityFieldGroups") - ], - EntityEvents.ENTITY_FIELD_DEFINITIONS, [ - ObjBindMethod(this, "EntityFieldDefinitions") - ], - EntityEvents.ENTITY_DETECT_VALUES, [ - ObjBindMethod(this, "EntityDetectValues") - ], - EntityEvents.ENTITY_LIST_ENTITIES, [ - ObjBindMethod(this, "ListEntities") - ], + WebServicesEvents.ENTITY_DATA_PARAMS, [ + ObjBindMethod(this, "EntityDataParams") + ] ) } - GetReleaseInfo(event, extra, eventName, hwnd) { - releaseInfo := event.ReleaseInfo - - if (!event.ReleaseInfo.Count && this.container.GetApp().Version != "{{VERSION}}") { - webService := this.container["entity_manager.web_service"]["launchpad_api"] - - if (webService["Enabled"]) { - releaseInfo := webService.AdapterRequest("", "release_info") - - if (releaseInfo) { - for key, val in releaseInfo { - event.ReleaseInfo[key] = val - } - } - } - } - } - - EntityDataLayers(event, extra, eventName, hwnd) { - if (event.EntityTypeId == "web_service" || event.EntityTypeId == "web_service_provider") { - return - } - - webService := this.container["entity_manager.web_service"]["launchpad_api"] - - layers := event.Layers - - if (WebService["Enabled"]) { - entity := event.Entity - - adapters := webService.GetAdapters([ - "adapterType", "entity_data", - "entityType", event.EntityTypeId - ]) - - for key, adapter in adapters { - layerExists := false - layerKey := webService["id"] . "." . event.EntityTypeId . "." . key - - for index, layerName in layers { - if (layerName == layerKey) { - layerExists := true - break - } - } - - if (!layerExists) { - layers.Push(layerKey) - } - } - } - } - - EntityLayerSources(event, extra, eventName, hwnd) { - if (event.EntityTypeId == "web_service" || event.EntityTypeId == "web_service_provider") { - return - } - - webService := this.container["entity_manager.web_service"]["launchpad_api"] - - layerData := event.LayerSources - - if (WebService["Enabled"]) { - adapters := webService.GetAdapters([ - "adapterType", "entity_data", - "entityType", event.EntityTypeId - ]) - - for key, adapter in adapters { - layerKey := webService["id"] . "." . event.EntityTypeId . "." . key - - if (!layerData.Has(layerKey)) { - layerData[layerKey] := WebServiceAdapterLayerSource(adapter) - } - } - } - } - - EntityFieldGroups(event, extra, eventName, hwnd) { - if (event.EntityTypeId == "web_service" || event.EntityTypeId == "web_service_provider") { - return - } - - webService := this.container["entity_manager.web_service"]["launchpad_api"] - - if (WebService["Enabled"]) { - fieldGroups := event.FieldGroups - - if (!fieldGroups.Has("api")) { - adapters := webService.GetAdapters([ - "adapterType", "entity_data", - "entityType", event.EntityTypeId - ]) - - if (adapters.Count) { - fieldGroups["api"] := Map( - "name", "API", - "weight", 150 - ) - } - } - } - } - - EntityFieldDefinitions(event, extra, eventName, hwnd) { - if (event.EntityTypeId == "web_service" || event.EntityTypeId == "web_service_provider") { + EntityDataParams(event, extra, eventName, hwnd) { + if (HasProp(event.Entity, "isWebServiceEntity") && event.Entity.isWebServiceEntity) { return } - webService := this.container["entity_manager.web_service"]["launchpad_api"] - - if (WebService["Enabled"]) { - fieldDefinitions := event.FieldDefinitions - - adapters := webService.GetAdapters([ - "adapterType", "entity_data", - "entityType", event.EntityTypeId - ]) - - if (adapters.Count) { - fieldDefinitions["LaunchpadApiRef"] := Map( - "description", "The key that is used to look up the entity's data from configured external data sources.", - "help", "It defaults to the key which is usually sufficient, but it can be overridden by setting this value.`n`nAddtionally, multiple copies of the same data source entity can exist by giving them different keys but using the same LaunchpadApiRef", - "group", "api", - "processValue", false, - "modes", Map("simple", Map("formField", false)) - ) - } - } - } - - EntityDetectValues(event, extra, eventName, hwnd) { - if (event.EntityTypeId == "web_service" || event.EntityTypeId == "web_service_provider") { - return - } - - webService := this.container["entity_manager.web_service"]["launchpad_api"] - values := event.Values - entity := event.Entity - - if ( - webService["Enabled"] - && (!values.Has("LaunchpadApiRef") || !values["LaunchpadApiRef"]) - && entity.HasField("LaunchpadApiRef") - && (!entity.RawData.Has["LaunchpadApiRef"] || !entity.RawData["LaunchpadApiRef"]) - ) { - result := "" - - if (event.EntityTypeId == "Launcher") { - platform := entity["Platform"] ? entity["Platform"]["id"] : "" - - result := webService.AdapterRequest( - Map("id", entity["id"], "platform", platform), - Map( - "adapterType", "entity_list", - "entityType", event.EntityTypeId - ) - ) - } else if (HasBase(entity, ManagedEntityBase.Prototype)) { - result := entity["EntityType"] - } else { - result := entity["id"] - } - - if (result) { - values["LaunchpadApiRef"] := result - } - } - } - - ListEntities(event, extra, eventName, hwnd) { - if (event.EntityTypeId == "web_service" || event.EntityTypeId == "web_service_provider") { - return - } - - if (event.includeExtended) { - webService := this.container["entity_manager.web_service"]["launchpad_api"] - entityMgr := this.container["entity_manager." . event.EntityTypeId] - - managedIds := event.includeManaged - ? [] - : entityMgr.EntityQuery(EntityQuery.RESULT_TYPE_IDS).Execute() - - if (webService["Enabled"]) { - results := webService.AdapterRequest( - "", - Map( - "adapterType", "entity_list", - "entityType", event.EntityTypeId - ), - "read", - true - ) - - if (results && HasBase(results, Array.Prototype)) { - for index, id in results { - exists := false - - for , existingId in event.EntityList { - if (existingId == id) { - exists := true - break - } - } - - if (!exists && !event.includeManaged) { - for , managedId in managedIds { - if (managedId == id) { - exists := true - break - } - } - } + test := "here" - if (!exists) { - event.EntityList.Push(id) - } - } - } - + ; TODO figure out how to access these values while the data layers are still being loaded + if (event.WebService["id"] == "launchpad_api") { + if (HasBase(event.Entity, LauncherEntity.Prototype)) { + event.Params["platformId"] := "Blizzard" ;event.Entity["Platform"]["id"] + } else if (HasBase(event.Entity, LaunchProcessEntity.Prototype)) { + event.Params["platformId"] := "Blizzard" ;event.Entity.ParentEntity["Platform"]["id"] } } } From af33820ab859ea6cf74465ac29147e4b6df8b6db Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:01:12 -0500 Subject: [PATCH 116/227] Add an additional check to ensure that the referenced Platform exists before accessing it in WebServiceEntity --- Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index 411e4f2a..f2b2ffc9 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -107,7 +107,7 @@ class WebServiceEntity extends FieldableEntity { IsAuthenticated() { isAuthenticated := false - if (this["Provider"]["SupportsAuthentication"]) { + if (this["Provider"] && this["Provider"]["SupportsAuthentication"]) { isAuthenticated := this["Provider"]["Authenticator"].IsAuthenticated(this) } @@ -115,13 +115,13 @@ class WebServiceEntity extends FieldableEntity { } Login() { - if (this["Provider"]["SupportsAuthentication"]) { + if (this["Provider"] && this["Provider"]["SupportsAuthentication"]) { this["Provider"]["Authenticator"].Login(this) } } Logout() { - if (this["Provider"]["SupportsAuthentication"]) { + if (this["Provider"] && this["Provider"]["SupportsAuthentication"]) { this["Provider"]["Authenticator"].Logout(this) } } From fd74dd6bc2c2f2411bee958efdcbf052e3974f69 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:02:46 -0500 Subject: [PATCH 117/227] Fix adapter definitions and organize parameters in LaunchpadApi.module.json --- .../LaunchpadApi/LaunchpadApi.module.json | 180 ++++++++++-------- 1 file changed, 103 insertions(+), 77 deletions(-) diff --git a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json index 78ac16ec..3bb1307d 100644 --- a/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json +++ b/Lib/Shared/Modules/LaunchpadApi/LaunchpadApi.module.json @@ -16,21 +16,6 @@ }, "parameters": { "app.supports_update_check": true, - "web_services.providers.launchpad_api": { - "name": "Launchpad API", - "EndpointUrl": "https://api.launchpad.games/v1", - "AuthenticationEndpointUrl": "https://securetoken.googleapis.com/v1", - "AuthenticationRefreshPath": "token", - "IconSrc": "logo", - "SupportsAuthentication": true, - "Authenticator": "jwt", - "AppKey": "AIzaSyCbwzOWJjTft77P96dV5VB3dAx9TjdDowQ", - "LoginWindow": "LaunchpadLoginWindow" - }, - "web_services.services.launchpad_api": { - "name": "Launchpad API", - "Provider": "launchpad_api" - }, "web_services.adapters.launchpad_api.account_info": { "dataType": "account_info", "requestPath": "/status", @@ -40,84 +25,125 @@ "email": "account" } }, - "web_services.adapters.launchpad_api.error_submission": { - "dataType": "error_submission", - "requestPath": "/error-submissions/{submission}", - "cacheResponse": false, - "readAllow": false, - "createAllow": true, - "createAuth": false + "web_services.adapters.launchpad_api.hello": { + "dataType": "availability_check", + "requestPath": "/hello", + "cacheResponse": false }, - "web_services.adapters.launchpad_api.feedback_submission": { - "dataType": "feedback_submission", - "requestPath": "/feedback-submissions/{submission}", - "cacheResponse": false, - "readAllow": false, - "createAllow": true, - "createAuth": false - }, - "web_services.adapters.launchpad_api.game_submission": { - "dataType": "game_submission", - "requestPath": "/game-submissions/{submission}", - "cacheResponse": false, - "readAllow": false, - "createAllow": true, - "createAuth": false - }, - "web_services.adapters.launchpad_api.release_info": { - "dataType": "release_info", - "requestPath": "/release-info/{tag}", - "cacheMaxAge": 1800 - }, - "web_services.adapters.launchpad_api.platform_list": { - "dataType": "entity_list", - "requestPath": "/game-platforms", - "entityType": "platform" - }, - "web_services.adapters.launchpad_api.platform_data": { + "web_services.adapters.launchpad_api.game_process_launcher_defaults": { "dataType": "entity_data", - "requestPath": "/game-platforms/{id}", - "dataSelector": "data.defaults", - "entityType": "platform" - }, - "web_services.adapters.launchpad_api.game_type_list": { - "dataType": "entity_list", - "requestPath": "/game-types", - "entityType": "managed_game" + "requestPath": "/games/{id}", + "dataSelector": "gameProcessDefaults", + "entityType": "game_process", + "requiredParams": ["id"], + "tags": ["defaults"], + "weight": 5 }, - "web_services.adapters.launchpad_api.game_type_data": { + "web_services.adapters.launchpad_api.game_process_platform_defaults": { "dataType": "entity_data", - "requestPath": "/game-types/{id}", - "dataSelector": "data.defaults", - "entityType": "managed_game" + "requestPath": "/platforms/{platformId}", + "dataSelector": "data.gameProcessDefaults", + "entityType": "game_process", + "requiredParams": ["platformId"], + "tags": ["defaults"], + "weight": 2 + }, + "web_services.adapters.launchpad_api.launcher_defaults": { + "dataType": "entity_data", + "requestPath": "/games/{id}", + "dataSelector": "defaults", + "entityType": "launcher", + "requiredParams": ["id"], + "tags": ["defaults"], + "weight": 5 }, "web_services.adapters.launchpad_api.launcher_list": { "dataType": "entity_list", - "requestPath": "/games", - "entityType": "launcher" - }, - "web_services.adapters.launchpad_api.launcher_data": { - "dataType": "entity_data", - "requestPath": "/games/{id}", - "dataSelector": "data.defaults", + "requestPath": "/game-keys", "entityType": "launcher" }, "web_services.adapters.launchpad_api.launcher_lookup": { "dataType": "entity_lookup", - "requestPath": "/lookup/{id}/{platform}", + "requestPath": "/lookup/{id}/{platformId}", "dataSelector": "id", - "entityType": "launcher" + "entityType": "launcher", + "requiredParams": ["id"] }, - "web_services.adapters.launchpad_api.launcher_type_list": { - "dataType": "entity_list", - "requestPath": "/launcher-types", - "entityType": "managed_launcher" + "web_services.adapters.launchpad_api.launcher_platform_defaults": { + "dataType": "entity_data", + "requestPath": "/platforms/{platformId}", + "dataSelector": "data.launcherDefaults", + "entityType": "launcher", + "requiredParams": ["platformId"], + "tags": ["defaults"], + "weight": 2 + }, + "web_services.adapters.launchpad_api.launcher_process_launcher_defaults": { + "dataType": "entity_data", + "requestPath": "/games/{id}", + "dataSelector": "launcherProcessDefaults", + "entityType": "launcher_process", + "requiredParams": ["id"], + "tags": ["defaults"], + "weight": 5 }, - "web_services.adapters.launchpad_api.launcher_type_data": { + "web_services.adapters.launchpad_api.launcher_process_platform_defaults": { + "dataType": "entity_data", + "requestPath": "/platforms/{platformId}", + "dataSelector": "data.launcherProcessDefaults", + "entityType": "launcher_process", + "requiredParams": ["platformId"], + "tags": ["defaults"], + "weight": 2 + }, + "web_services.adapters.launchpad_api.platform_defaults": { "dataType": "entity_data", - "requestPath": "/launcher-types/{id}", + "requestPath": "/platforms/{id}", "dataSelector": "data.defaults", - "entityType": "managed_launcher" + "entityType": "platform", + "requiredParams": ["id"], + "tags": ["defaults"] + }, + "web_services.adapters.launchpad_api.platform_list": { + "dataType": "entity_list", + "requestPath": "/platforms", + "entityType": "platform" + }, + "web_services.adapters.launchpad_api.release_info": { + "dataType": "release_info", + "requestPath": "/release-info/{version}", + "cacheMaxAge": 1800 + }, + "web_services.adapters.launchpad_api.submit_error": { + "dataType": "error_submission", + "requestPath": "/submit-error", + "cacheResponse": false, + "readAllow": false, + "createAllow": true, + "createAuth": false + }, + "web_services.adapters.launchpad_api.submit_feedback": { + "dataType": "feedback_submission", + "requestPath": "/submit-feedback", + "cacheResponse": false, + "readAllow": false, + "createAllow": true, + "createAuth": false + }, + "web_services.providers.launchpad_api": { + "name": "Launchpad API", + "EndpointUrl": "https://api.launchpad.games/v1", + "AuthenticationEndpointUrl": "https://securetoken.googleapis.com/v1", + "AuthenticationRefreshPath": "token", + "IconSrc": "logo", + "SupportsAuthentication": true, + "Authenticator": "jwt", + "AppKey": "AIzaSyCbwzOWJjTft77P96dV5VB3dAx9TjdDowQ", + "LoginWindow": "LaunchpadLoginWindow" + }, + "web_services.services.launchpad_api": { + "name": "Launchpad API", + "Provider": "launchpad_api" } }, "services": { From 8a0ff45e00e57262f30f8a23cf6909aafb1f9ffa Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:05:03 -0500 Subject: [PATCH 118/227] Add ReferencedEntities, ChildEntities, and ChildEntityData properties to EntityBase --- .../Volantis.Entity/Entity/EntityBase.ahk | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index f4a242c4..314b6c5f 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -31,7 +31,7 @@ class EntityBase { } FieldData { - Get => this.GetData().GetMergedData() + get => this.GetData().GetMergedData() } Name { @@ -46,7 +46,18 @@ class EntityBase { ParentEntity { get => this.GetParentEntity() - set => this.SetParentEntity(value) + } + + ReferencedEntities { + get => this.GetReferencedEntities(false) + } + + ChildEntities { + get => this.GetReferencedEntities(true) + } + + ChildEntityData { + get => this.GetAllChildEntityData() } __Item[key := ""] { @@ -287,7 +298,7 @@ class EntityBase { this.GetData().UnloadAllLayers(reloadUserData) if (recurse) { - for index, entityObj in this.GetReferencedEntities(true) { + for index, entityObj in this.ChildEntities { entityObj.RefreshEntityData(recurse, reloadUserData) } } @@ -319,7 +330,7 @@ class EntityBase { this.eventMgr.DispatchEvent(event) if (recurse) { - for index, entityObj in this.GetReferencedEntities(true) { + for index, entityObj in this.ChildEntities { entityObj.SaveEntity(recurse) } } @@ -388,7 +399,7 @@ class EntityBase { if (recurse) { diffs := [diff] - for index, referencedEntity in this.GetReferencedEntities(true) { + for index, referencedEntity in this.ChildEntities { diffs.Push(referencedEntity.DiffChanges(recurse)) } From 2b74d9407402ca12be4827af704362e47e4c64e1 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:05:56 -0500 Subject: [PATCH 119/227] Update handling of parent entity discover in EntityBase --- Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index 314b6c5f..c0b94f4b 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -2,6 +2,8 @@ class EntityBase { idVal := "" entityTypeIdVal := "" parentEntityObj := "" + parentEntityTypeId := "" + parentEntityId := "" parentEntityStorage := false container := "" app := "" @@ -86,13 +88,11 @@ class EntityBase { this.cloner := container.Get("cloner.list") this.parentEntityStorage := parentEntityStorage - if (!parentEntity) { - parentEntity := this.DiscoverParentEntity(container, eventMgr, id, storageObj, idSanitizer) + if (!parentEntity && this.parentEntityObj) { + parentEntity := this.parentEntityObj } - if (parentEntity) { - this.SetParentEntity(parentEntity) - } + this.DiscoverParentEntity(container, eventMgr, id, storageObj, idSanitizer, parentEntity) this._createEntityData() this.SetupEntity() @@ -156,6 +156,7 @@ class EntityBase { if (event.ParentEntity) { this.parentEntityObj := event.ParentEntity } else if (event.ParentEntityId) { + this.parentEntityTypeId := event.ParentEntityTypeId this.parentEntityId := event.ParentEntityId this.parentEntityMgr := event.ParentEntityManager ? event.ParentEntityManager From 744038e77708b46791f70a4cb960380638f0aff7 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:06:17 -0500 Subject: [PATCH 120/227] Add missing merger attribute --- Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk | 1 + 1 file changed, 1 insertion(+) diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index c0b94f4b..98e4a1db 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -15,6 +15,7 @@ class EntityBase { loaded := false dataLayer := "data" dataLoaded := false + merger := "" cloner := "" Id { From 4dd1034f51fb48081441c1f13cb8a894bb9a988e Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:07:01 -0500 Subject: [PATCH 121/227] Refactor entity loading slightly, and use a "loading" parameter to try and prevent recursive loops --- .../Volantis.Entity/Entity/EntityBase.ahk | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index 98e4a1db..2ada223d 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -12,6 +12,7 @@ class EntityBase { storageObj := "" idSanitizer := "" sanitizeId := true + loading := false loaded := false dataLayer := "data" dataLoaded := false @@ -99,7 +100,7 @@ class EntityBase { this.SetupEntity() if (autoLoad) { - this.LoadEntity(false, true) + this.LoadEntity() } } @@ -275,24 +276,28 @@ class EntityBase { } LoadEntity(reload := false, recurse := false) { - loaded := false + if (this.loading) { + throw AppException("Attempting to load entity with a circular reference.") + } - if (!this.loaded || reload) { - this.RefreshEntityData(true) + if (!this.loading && this.dataLoaded && (!this.loaded || reload)) { + this.loading := true + this.RefreshEntityData(recurse) this.CreateSnapshot("original") this.loaded := true loaded := true - } + this.loading := false - if (recurse) { - for index, entityObj in this.GetReferencedEntities(true) { - entityObj.LoadEntity(reload, recurse) + if (recurse) { + for index, entityObj in this.ChildEntities { + entityObj.LoadEntity(reload, recurse) + } } - } - if (loaded) { - event := EntityEvent(EntityEvents.ENTITY_LOADED, this.entityTypeId, this) - this.eventMgr.DispatchEvent(event) + if (loaded) { + event := EntityEvent(EntityEvents.ENTITY_LOADED, this.entityTypeId, this) + this.eventMgr.DispatchEvent(event) + } } } From 04e7f67c50704d1dffae463bf5dc246b1511dd19 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:07:39 -0500 Subject: [PATCH 122/227] Update recursive handling of snapshots in EntityBase --- .../Volantis.Entity/Entity/EntityBase.ahk | 24 +++++++------------ 1 file changed, 8 insertions(+), 16 deletions(-) diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index 2ada223d..fc73bbe1 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -239,32 +239,24 @@ class EntityBase { return this.GetData().DeleteValue(key, this.dataLayer) } - CreateSnapshot(name, recurse := true) { - this.GetData().CreateSnapshot(name) - + CreateSnapshot(name, recurse := false) { if (recurse) { - for index, entityObj in this.GetReferencedEntities(true) { - if (entityObj.HasOwnDataStorage()) { - entityObj.GetData().CreateSnapshot(name, recurse) - } + for index, entityObj in this.ChildEntities { + entityObj.GetData().CreateSnapshot(name, recurse) } } - return this - } + this.GetData().CreateSnapshot(name) - HasOwnDataStorage() { - return this.dataObj + return this } - RestoreSnapshot(name, recurse := true) { + RestoreSnapshot(name, recurse := false) { this.GetData().RestoreSnapshot(name) if (recurse) { - for index, entityObj in this.GetReferencedEntities(true) { - if (entityObj.HasOwnDataStorage()) { - entityObj.GetData().RestoreSnapshot(name, recurse) - } + for index, entityObj in this.ChildEntities { + entityObj.GetData().RestoreSnapshot(name, recurse) } } From b026b19981dfed70a73fedc18b0014a6e5f63dc4 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:08:53 -0500 Subject: [PATCH 123/227] Update _getLayerSources in EntityBase to use parent entity storage when configured --- Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index fc73bbe1..17e3456d 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -134,10 +134,14 @@ class EntityBase { } _getLayerSources() { + layerSource := this.parentEntityStorage + ? ParentEntityLayerSource(this) + : EntityStorageLayerSource(this.storageObj, this.GetStorageId()) + return Map( "defaults", ObjBindMethod(this, "InitializeDefaults"), "auto", ObjBindMethod(this, "AutoDetectValues"), - "data", EntityStorageLayerSource(this.storageObj, this.GetStorageId()) + "data", layerSource ) } From cb23292844a7d700001a6bb135853ba953d9ccb6 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:09:37 -0500 Subject: [PATCH 124/227] Add UpdateDefaults method to EntityBase --- Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index 17e3456d..8ba83fa1 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -488,6 +488,16 @@ class EntityBase { return text } + UpdateDefaults(recurse := false) { + if (recurse) { + for key, child in this.ChildEntities { + child.UpdateDefaults(recurse) + } + } + + this.GetData().UnloadAllLayers(false) + } + GetAllChildEntityData() { return this.GetData().GetExtraData() } From 124c6cb098195012f96f2434248c13a6f866a76c Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:11:01 -0500 Subject: [PATCH 125/227] Fix event class name and event name references --- .../WebServices/Event/WebServicesResponseEvent.ahk | 2 +- .../WebServiceRequest/WebServiceRequestBase.ahk | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/Event/WebServicesResponseEvent.ahk b/Lib/Shared/Modules/WebServices/Event/WebServicesResponseEvent.ahk index c2d5773d..80010b44 100644 --- a/Lib/Shared/Modules/WebServices/Event/WebServicesResponseEvent.ahk +++ b/Lib/Shared/Modules/WebServices/Event/WebServicesResponseEvent.ahk @@ -1,4 +1,4 @@ -class WebServiceResponseEvent extends EventBase { +class WebServicesResponseEvent extends EventBase { _requestObj := "" _responseObj := "" diff --git a/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk b/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk index 8e7f56d8..e89cfce5 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceRequest/WebServiceRequestBase.ahk @@ -136,10 +136,10 @@ class WebServiceRequestBase { httpReqObj.requestHeaders["Cache-Control"] := "no-cache" } - event := WebServiceRequestEvent(WebServicesEvents.WEB_SERVICES_HTTP_REQ_ALTER, this) + event := WebServicesRequestEvent(WebServicesEvents.HTTP_REQ_ALTER, this) this.eventMgr.DispatchEvent(event) - event := WebServiceRequestEvent(WebServicesEvents.WEB_SERVICES_REQUEST_PRESEND, this) + event := WebServicesRequestEvent(WebServicesEvents.REQUEST_PRESEND, this) this.eventMgr.DispatchEvent(event) httpReqObj.Send(this.GetMethod(), this.GetData()) @@ -147,7 +147,7 @@ class WebServiceRequestBase { this._cacheResponse() } - event := WebServiceResponseEvent(WebServicesEvents.WEB_SERVICES_RESPONSE_ALTER, this, this.responseObj) + event := WebServicesResponseEvent(WebServicesEvents.RESPONSE_ALTER, this, this.responseObj) this.eventMgr.DispatchEvent(event) this.responseObj := event.Response @@ -165,7 +165,7 @@ class WebServiceRequestBase { _createCachedResponse() { response := CachedWebServiceResponse(this.webServiceEnt, this) - event := WebServiceResponseEvent(WebServicesEvents.WEB_SERVICES_CACHED_RESPONSE_CREATED, this, response) + event := WebServicesResponseEvent(WebServicesEvents.CACHED_RESPONSE_CREATED, this, response) this.eventMgr.DispatchEvent(event) return event.Response @@ -174,7 +174,7 @@ class WebServiceRequestBase { _createHttpReqResponse() { response := HttpReqWebServiceResponse(this.webServiceEnt, this) - event := WebServiceResponseEvent(WebServicesEvents.WEB_SERVICES_HTTP_RESPONSE_CREATED, this, response) + event := WebServicesResponseEvent(WebServicesEvents.HTTP_RESPONSE_CREATED, this, response) this.eventMgr.DispatchEvent(event) return event.Response From 322f1901f0386c22e4e7458325e860f25e337b62 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:11:22 -0500 Subject: [PATCH 126/227] Add missing argument to event_subscriber.web_services --- Lib/Shared/Modules/WebServices/WebServices.module.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/Shared/Modules/WebServices/WebServices.module.json b/Lib/Shared/Modules/WebServices/WebServices.module.json index 99634be6..1f19e86b 100644 --- a/Lib/Shared/Modules/WebServices/WebServices.module.json +++ b/Lib/Shared/Modules/WebServices/WebServices.module.json @@ -96,7 +96,7 @@ }, "event_subscriber.web_services": { "class": "WebServicesEventSubscriber", - "arguments": ["@{}"], + "arguments": ["@{}", "@web_services.adapter_manager"], "tags": ["event_subscriber"] }, "state.web_services": { From a6bbd3eeb8a49475a072e05a4a723f7d7f8a08cf Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:12:29 -0500 Subject: [PATCH 127/227] Change dataType property to dataClass on adapters --- .../WebServiceAdapter/FileWebServiceAdapter.ahk | 2 +- .../WebServiceAdapter/JsonWebServiceAdapter.ahk | 2 +- .../WebServiceAdapter/WebServiceAdapterBase.ahk | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/WebServiceAdapter/FileWebServiceAdapter.ahk b/Lib/Shared/Modules/WebServices/WebServiceAdapter/FileWebServiceAdapter.ahk index 22d44841..e922134a 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceAdapter/FileWebServiceAdapter.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceAdapter/FileWebServiceAdapter.ahk @@ -1,5 +1,5 @@ class FileWebServiceAdapter extends WebServiceAdapterBase { - dataType := "" + dataClass := "" ; @todo Implement file downloading } diff --git a/Lib/Shared/Modules/WebServices/WebServiceAdapter/JsonWebServiceAdapter.ahk b/Lib/Shared/Modules/WebServices/WebServiceAdapter/JsonWebServiceAdapter.ahk index 7c61899b..5f63b104 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceAdapter/JsonWebServiceAdapter.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceAdapter/JsonWebServiceAdapter.ahk @@ -1,3 +1,3 @@ class JsonWebServiceAdapter extends WebServiceAdapterBase { - dataType := "JsonData" + dataClass := "JsonData" } \ No newline at end of file diff --git a/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk b/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk index b34ea21d..d963e6a8 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk @@ -2,7 +2,7 @@ class WebServiceAdapterBase { container := "" webService := "" definition := "" - dataType := "" + dataClass := "" merger := "" operationTypes := ["create", "read", "update", "delete"] @@ -270,9 +270,9 @@ class WebServiceAdapterBase { } _parseData(data, params) { - if (data && this.definition["dataType"]) { - dataType := this.definition["dataType"] - data := %dataType%().FromString(data) + if (data && this.dataClass) { + dataClass := this.dataClass + data := %dataClass%().FromString(&data) if (this.definition["dataSelector"]) { dataSelector := this.definition["dataSelector"] From 9e3a09e9605620fea12902f1fd2d7a82b4ab32e2 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:13:30 -0500 Subject: [PATCH 128/227] Enhance WebServiceAdapterBase code --- .../WebServiceAdapterBase.ahk | 55 +++++++++++++++++-- 1 file changed, 49 insertions(+), 6 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk b/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk index d963e6a8..8e30611e 100644 --- a/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk +++ b/Lib/Shared/Modules/WebServices/WebServiceAdapter/WebServiceAdapterBase.ahk @@ -58,15 +58,23 @@ class WebServiceAdapterBase { "deleteMethod", "PUT", "deleteAuth", true, "dataMap", Map(), - "dataSelector", [] + "dataSelector", "", + "weight", 0, + "entityType", "", + "tags", [], + "requiredParams", [], ) } SupportsOperation(operation) { supported := false - if (this.operationTypes.Contains(operation)) { - supported := this.definition[operation + "Allow"] + for index, operationType in this.operationTypes { + if (operation == operationType) { + supported := true + + break + } } return supported @@ -240,12 +248,47 @@ class WebServiceAdapterBase { return requestPath } - _request(params, method, data, cacheResponse) { + _validateParams(params) { + if (!params) { + params := Map() + } + + valid := true + requiredParams := this.definition["requiredParams"] + + if (requiredParams) { + if (Type(requiredParams) == "String") { + requiredParams := [requiredParams] + } + + for , requiredParam in requiredParams { + if (!params.Has(requiredParam) || !params[requiredParam]) { + valid := false + + break + } + } + } + + return valid + } + + _request(params, method, data, useAuthentication, cacheResponse) { + if (!this._validateParams(params)) { + throw AppException("The data adapter request was called with invalid or missing parameters.") + } + + requestPath := this.definition["requestPath"] + + for key, value in params { + + } + return this.webService.Request( - this.definition["requestPath"], + this._requestPath(params), method, data, - this.definition["useAuthentication"], + useAuthentication, cacheResponse ) } From e0512087c15973444f6082523b05cf96285b0e70 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:13:49 -0500 Subject: [PATCH 129/227] Use web service calls in DetectedGameEditor and refactor a bit of the code --- Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk | 61 ++++++++----------- 1 file changed, 26 insertions(+), 35 deletions(-) diff --git a/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk b/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk index 2e53e2a3..3aee2ba9 100644 --- a/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk +++ b/Lib/Launchpad/Gui/Form/DetectedGameEditor.ahk @@ -1,4 +1,4 @@ -class DetectedGameEditor extends FormGuiBase { +class DetectedGameEditor extends FormGuiBase { detectedGameObj := "" newValues := Map() missingFields := Map() @@ -22,44 +22,35 @@ class DetectedGameEditor extends FormGuiBase { this.knownPlatforms := [] this.knownGames := [] - this.launcherTypes := [] - this.gameTypes := [] ; @todo replace this, or at least refactor it to live somewhere else - if (this.container.Has("entity_manager.web_service")) { - mgr := this.container["entity_manager.web_service"] - - if (mgr.Has("launchpad_api") && mgr["launchpad_api"]["Enabled"]) { - webService := mgr["launchpad_api"] - knownMap := Map( - "platform", "knownPlatforms", - "game", "knownGames", - "managed_launcher", "launcherTypes", - "managed_game", "gameTypes" - ) - - for entityTypeId, varName in knownMap { - results := webService.AdapterRequest("", Map( - "adapterType", "entity_list", - "entityType", entityTypeId - ), "read", true) - - if (results) { - for , idList in results { - if (idList) { - for , id in idList { - exists := false - - for , item in %varName% { - if (item == id) { - exists := true - break - } + if (this.container.Has("web_services.adapter_manager")) { + knownMap := Map( + "platform", "knownPlatforms", + "launcher", "knownGames", + ) + + for entityTypeId, varName in knownMap { + results := this.container["web_services.adapter_manager"].AdapterRequest("", Map( + "dataType", "entity_list", + "entityType", entityTypeId + ), "read", true) + + if (results) { + for , idList in results { + if (idList) { + for , id in idList { + exists := false + + for , item in %varName% { + if (item == id) { + exists := true + break } + } - if (!exists) { - this.%varName%.Push(id) - } + if (!exists) { + this.%varName%.Push(id) } } } From 6a27ffd3bebd3118aca985749bcbd3d2951e0eaa Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:14:42 -0500 Subject: [PATCH 130/227] Add isWebServiceEntity to web service entities, and remove old adapters and adapterFactory --- Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk | 3 +-- .../Modules/WebServices/Entity/WebServiceProviderEntity.ahk | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index f2b2ffc9..6c46d5c2 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -3,8 +3,7 @@ class WebServiceEntity extends FieldableEntity { stateObj := "" persistentStateObj := "" statusIndicators := [] - adapters := Map() - adapterFactory := "" + isWebServiceEntity := true Authenticated { get => this.IsAuthenticated() diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceProviderEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceProviderEntity.ahk index 573120f3..5c54ae3a 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceProviderEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceProviderEntity.ahk @@ -1,4 +1,6 @@ class WebServiceProviderEntity extends FieldableEntity { + isWebServiceEntity := true + BaseFieldDefinitions() { definitions := super.BaseFieldDefinitions() From 1cef906acdfdc2b5e10b87f42829005684998cd3 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:15:10 -0500 Subject: [PATCH 131/227] Add CacheResponses and DefaultResponseCacheExpiration fields to WebServiceEntity --- .../Modules/WebServices/Entity/WebServiceEntity.ahk | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index 6c46d5c2..cdc2a7a9 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -86,6 +86,18 @@ class WebServiceEntity extends FieldableEntity { "default", (this.idVal == "launchpad_api") ) + definitions["CacheResponses"] := Map( + "type", "boolean", + "required", false, + "default", true + ) + + definitions["DefaultResponseCacheExpiration"] := Map( + "type", "string", + "required", false, + "default", 3600 + ) + return definitions } From bd64d5b2628724e85a7ae26cf6800d2e5245716b Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:24:10 -0500 Subject: [PATCH 132/227] Rename new web service fields to ResponseCache and ResponseCacheDefaultExpireSeconds --- Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index cdc2a7a9..a17a8ad9 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -86,13 +86,14 @@ class WebServiceEntity extends FieldableEntity { "default", (this.idVal == "launchpad_api") ) - definitions["CacheResponses"] := Map( + definitions["ResponseCache"] := Map( "type", "boolean", "required", false, "default", true ) - definitions["DefaultResponseCacheExpiration"] := Map( + definitions["ResponseCacheDefaultExpireSeconds"] := Map( + "title", "Response Cache - Default Expiration (seconds)" "type", "string", "required", false, "default", 3600 From 9ca8909acea70868d59f6af48d28f564f2437c7c Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:24:34 -0500 Subject: [PATCH 133/227] Missing comma --- Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk index a17a8ad9..d0caf51b 100644 --- a/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk +++ b/Lib/Shared/Modules/WebServices/Entity/WebServiceEntity.ahk @@ -93,7 +93,7 @@ class WebServiceEntity extends FieldableEntity { ) definitions["ResponseCacheDefaultExpireSeconds"] := Map( - "title", "Response Cache - Default Expiration (seconds)" + "title", "Response Cache - Default Expiration (seconds)", "type", "string", "required", false, "default", 3600 From 8b90b24ec79f1a3c04703cfb780756519a053cbf Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:35:33 -0500 Subject: [PATCH 134/227] Fix parent entity assignment --- Lib/Launchpad/Entity/LaunchProcessEntity.ahk | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/Launchpad/Entity/LaunchProcessEntity.ahk b/Lib/Launchpad/Entity/LaunchProcessEntity.ahk index d28b726c..a102f336 100644 --- a/Lib/Launchpad/Entity/LaunchProcessEntity.ahk +++ b/Lib/Launchpad/Entity/LaunchProcessEntity.ahk @@ -2,9 +2,7 @@ class LaunchProcessEntity extends FieldableEntity { defaultClass := "Default" DiscoverParentEntity(container, eventMgr, id, storageObj, idSanitizer, parentEntity := "") { - ; TODO fix circular reference occurring - - return parentEntity + this.parentEntityObj := parentEntity ? parentEntity : container.Get("entity_manager.launcher")[id] } From 5f02482a272d5da024bfae189b0a10242af0ec0a Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:35:42 -0500 Subject: [PATCH 135/227] Remove test code --- .../LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk | 2 -- 1 file changed, 2 deletions(-) diff --git a/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk b/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk index 8b864ab7..1d349909 100644 --- a/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk +++ b/Lib/Shared/Modules/LaunchpadApi/EventSubscriber/LaunchpadApiSubscriber.ahk @@ -12,8 +12,6 @@ class LaunchpadApiSubscriber extends EventSubscriberBase { return } - test := "here" - ; TODO figure out how to access these values while the data layers are still being loaded if (event.WebService["id"] == "launchpad_api") { if (HasBase(event.Entity, LauncherEntity.Prototype)) { From 113b1677a7af98e224ef2da7d2d0a87fd595b1d6 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:50:40 -0500 Subject: [PATCH 136/227] Change extra key in data storage from "extra" to "_extra" --- Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk b/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk index 6575e28e..60a4095d 100644 --- a/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk +++ b/Lib/Shared/Volantis.Data/LayeredData/LayeredDataBase.ahk @@ -26,7 +26,7 @@ class LayeredDataBase { userLayers := ["data"] loadingLayers := Map() extraDataLayer := "data" - extraDataKey := "extra" + extraDataKey := "_extra" static NO_VALUE := ":NO_VAL:" From e8d9c08f2317db0c3ace49389891cb68d0140b09 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 04:50:49 -0500 Subject: [PATCH 137/227] Fix child entity data storage --- Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk index 8ba83fa1..079af972 100644 --- a/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk +++ b/Lib/Shared/Volantis.Entity/Entity/EntityBase.ahk @@ -517,7 +517,7 @@ class EntityBase { data := Map() } - this.GetData().SetExtraData(dataKey, data) + this.GetData().SetExtraData(data, dataKey) return this } From 0e7c3d674b49084145703597c1e6bd0d50923e63 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 05:03:22 -0500 Subject: [PATCH 138/227] Fix web service ref discovery in the case of the wrong data type being received --- .../EventSubscriber/WebServicesEventSubscriber.ahk | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Lib/Shared/Modules/WebServices/EventSubscriber/WebServicesEventSubscriber.ahk b/Lib/Shared/Modules/WebServices/EventSubscriber/WebServicesEventSubscriber.ahk index 97023350..3e7a0598 100644 --- a/Lib/Shared/Modules/WebServices/EventSubscriber/WebServicesEventSubscriber.ahk +++ b/Lib/Shared/Modules/WebServices/EventSubscriber/WebServicesEventSubscriber.ahk @@ -267,8 +267,8 @@ class WebServicesEventSubscriber extends EventSubscriberBase { webService ) - if (!result) { - result := event.Entity["id"] + if (!result || Type(result) != "String") { + result := "" ; TODO decide whether to default to blank or the entity ID } event.Values[fieldId] := result From 34fed831f3f39f4568e69016091bf12edc34e6cc Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 16:22:59 -0500 Subject: [PATCH 139/227] Create a devcontainer that can be used to generate linux builds --- .devcontainer/Dockerfile | 82 ++++ .devcontainer/devcontainer.json | 38 ++ .../library-scripts/common-debian.sh | 454 ++++++++++++++++++ .npmignore | 6 + 4 files changed, 580 insertions(+) create mode 100644 .devcontainer/Dockerfile create mode 100644 .devcontainer/devcontainer.json create mode 100644 .devcontainer/library-scripts/common-debian.sh create mode 100644 .npmignore diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile new file mode 100644 index 00000000..9374f389 --- /dev/null +++ b/.devcontainer/Dockerfile @@ -0,0 +1,82 @@ +# Update VARIANT in devcontainer.json to pick a Dart version +ARG VARIANT=2 +FROM dart:${VARIANT} + +# [Option] Install zsh +ARG INSTALL_ZSH="true" +# [Option] Upgrade OS packages to their latest versions +ARG UPGRADE_PACKAGES="true" + +# Install needed packages and setup non-root user. Use a separate RUN statement to add your own dependencies. +ARG USERNAME=vscode +ARG USER_HOME=/home/${USERNAME} +ARG USER_UID=1000 +ARG USER_GID=$USER_UID +COPY library-scripts/*.sh /tmp/library-scripts/ +RUN apt-get update && /bin/bash /tmp/library-scripts/common-debian.sh "${INSTALL_ZSH}" "${USERNAME}" "${USER_UID}" "${USER_GID}" "${UPGRADE_PACKAGES}" "true" "true" + +# Add bin location to path +ENV PUB_CACHE="/usr/local/share/pub-cache" +ENV PATH="${PUB_CACHE}/bin:${PATH}" +RUN if ! cat /etc/group | grep -e "^pub-cache:" > /dev/null 2>&1; then groupadd -r pub-cache; fi \ + && usermod -a -G pub-cache ${USERNAME} \ + && umask 0002 \ + && mkdir -p ${PUB_CACHE} \ + && chown :pub-cache ${PUB_CACHE} \ + && sed -i -e "s/export PATH=/export PATH=\/usr\/local\/share\/pub-cache:/" /etc/profile.d/00-restore-env.sh \ + # + # Fix incorrect privs if present on directories - https://github.com/dart-lang/dart-docker/issues/62, https://github.com/dart-lang/sdk/issues/47093 + && chmod 755 "$DART_SDK" "$DART_SDK/bin" + +# Install additional OS packages. +RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ + && apt-get -y install --no-install-recommends \ + openjdk-11-jdk-headless wget curl git xz-utils zip unzip \ + clang cmake ninja-build pkg-config libgtk-3-dev +# Clean up packages +RUN apt-get autoremove -y && apt-get clean -y && rm -rf /var/lib/apt/lists/* /tmp/library-scripts + +# Switch to non-root user +USER ${USERNAME} +WORKDIR ${USER_HOME} + +# +# Android SDK +# https://developer.android.com/studio#downloads +# ENV ANDROID_SDK_TOOLS_VERSION=8512546 +# ENV ANDROID_PLATFORM_VERSION=33 +# ENV ANDROID_BUILD_TOOLS_VERSION=33.0.0 +# ENV ANDROID_HOME=~/android-sdk-linux +# ENV ANDROID_SDK_ROOT="$ANDROID_HOME" +# ENV PATH=${PATH}:${ANDROID_HOME}/cmdline-tools/cmdline-tools/bin:${ANDROID_HOME}/platform-tools:${ANDROID_HOME}/emulator + +# RUN curl -C - --output android-sdk-tools.zip https://dl.google.com/android/repository/commandlinetools-linux-${ANDROID_SDK_TOOLS_VERSION}_latest.zip \ +# && mkdir -p ${ANDROID_HOME}/ \ +# && unzip -q android-sdk-tools.zip -d ${ANDROID_HOME}/cmdline-tools/ \ +# && rm android-sdk-tools.zip \ +# && yes | sdkmanager --licenses \ +# && touch $HOME/.android/repositories.cfg \ +# && sdkmanager platform-tools \ +# && sdkmanager emulator \ +# && sdkmanager "platforms;android-${ANDROID_PLATFORM_VERSION}" "build-tools;$ANDROID_BUILD_TOOLS_VERSION" \ +# && sdkmanager --install "cmdline-tools;latest" +# # create emulator android +# RUN sdkmanager "system-images;android-${ANDROID_PLATFORM_VERSION};google_apis;x86_64" \ +# && avdmanager create avd -n Android${ANDROID_PLATFORM_VERSION} -k "system-images;android-${ANDROID_PLATFORM_VERSION};google_apis;x86_64" + +# +# Flutter SDK +# https://flutter.dev/docs/development/tools/sdk/releases?tab=linux +ENV FLUTTER_CHANNEL="stable" +ENV FLUTTER_VERSION="3.3.10" +# Make sure to use the needed channel and version for this. +ENV FLUTTER_HOME=${USER_HOME}/flutter +ENV PATH=${PATH}:${FLUTTER_HOME}/bin + +RUN curl -C - --output flutter.tar.xz https://storage.googleapis.com/flutter_infra_release/releases/${FLUTTER_CHANNEL}/linux/flutter_linux_${FLUTTER_VERSION}-${FLUTTER_CHANNEL}.tar.xz \ + && tar -xf flutter.tar.xz -C ~ \ + && rm flutter.tar.xz \ + && flutter config --android-sdk "${ANDROID_SDK_ROOT}" \ + && yes | flutter doctor --android-licenses \ + && flutter config --no-analytics \ + && flutter update-packages diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 00000000..a570d3b7 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,38 @@ +{ + "name": "Dart (Community)", + "build": { + "dockerfile": "Dockerfile", + // Update VARIANT to pick a Dart version + "args": { "VARIANT": "2" } + }, + + // Configure tool-specific properties. + "customizations": { + // Configure properties specific to VS Code. + "vscode": { + // Add the IDs of extensions you want installed when the container is created. + "extensions": [ + "dart-code.dart-code", + "dart-code.flutter" + ] + } + }, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "uname -a", + // "postCreateCommand": "flutter create test_project", + + // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. + "remoteUser": "vscode", + "features": { + "ghcr.io/devcontainers/features/common-utils:2": {}, + "ghcr.io/devcontainers-contrib/features/cue-asdf:1": {}, + "ghcr.io/devcontainers-contrib/features/svu-asdf:1": {}, + "ghcr.io/eitsupi/devcontainer-features/go-task:1": {}, + "ghcr.io/dhoeric/features/google-cloud-cli:1": {}, + "ghcr.io/warrenbuckley/codespace-features/sqlite:1": {} + } +} diff --git a/.devcontainer/library-scripts/common-debian.sh b/.devcontainer/library-scripts/common-debian.sh new file mode 100644 index 00000000..efdca351 --- /dev/null +++ b/.devcontainer/library-scripts/common-debian.sh @@ -0,0 +1,454 @@ +#!/usr/bin/env bash +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- +# +# Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/common.md +# Maintainer: The VS Code and Codespaces Teams +# +# Syntax: ./common-debian.sh [install zsh flag] [username] [user UID] [user GID] [upgrade packages flag] [install Oh My Zsh! flag] [Add non-free packages] + +set -e + +INSTALL_ZSH=${1:-"true"} +USERNAME=${2:-"automatic"} +USER_UID=${3:-"automatic"} +USER_GID=${4:-"automatic"} +UPGRADE_PACKAGES=${5:-"true"} +INSTALL_OH_MYS=${6:-"true"} +ADD_NON_FREE_PACKAGES=${7:-"false"} +SCRIPT_DIR="$(cd $(dirname "${BASH_SOURCE[0]}") && pwd)" +MARKER_FILE="/usr/local/etc/vscode-dev-containers/common" + +if [ "$(id -u)" -ne 0 ]; then + echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.' + exit 1 +fi + +# Ensure that login shells get the correct path if the user updated the PATH using ENV. +rm -f /etc/profile.d/00-restore-env.sh +echo "export PATH=${PATH//$(sh -lc 'echo $PATH')/\$PATH}" > /etc/profile.d/00-restore-env.sh +chmod +x /etc/profile.d/00-restore-env.sh + +# If in automatic mode, determine if a user already exists, if not use vscode +if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then + USERNAME="" + POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)") + for CURRENT_USER in ${POSSIBLE_USERS[@]}; do + if id -u ${CURRENT_USER} > /dev/null 2>&1; then + USERNAME=${CURRENT_USER} + break + fi + done + if [ "${USERNAME}" = "" ]; then + USERNAME=vscode + fi +elif [ "${USERNAME}" = "none" ]; then + USERNAME=root + USER_UID=0 + USER_GID=0 +fi + +# Load markers to see which steps have already run +if [ -f "${MARKER_FILE}" ]; then + echo "Marker file found:" + cat "${MARKER_FILE}" + source "${MARKER_FILE}" +fi + +# Ensure apt is in non-interactive to avoid prompts +export DEBIAN_FRONTEND=noninteractive + +# Function to call apt-get if needed +apt_get_update_if_needed() +{ + if [ ! -d "/var/lib/apt/lists" ] || [ "$(ls /var/lib/apt/lists/ | wc -l)" = "0" ]; then + echo "Running apt-get update..." + apt-get update + else + echo "Skipping apt-get update." + fi +} + +# Run install apt-utils to avoid debconf warning then verify presence of other common developer tools and dependencies +if [ "${PACKAGES_ALREADY_INSTALLED}" != "true" ]; then + + package_list="apt-utils \ + openssh-client \ + gnupg2 \ + dirmngr \ + iproute2 \ + procps \ + lsof \ + htop \ + net-tools \ + psmisc \ + curl \ + wget \ + rsync \ + ca-certificates \ + unzip \ + zip \ + nano \ + vim-tiny \ + less \ + jq \ + lsb-release \ + apt-transport-https \ + dialog \ + libc6 \ + libgcc1 \ + libkrb5-3 \ + libgssapi-krb5-2 \ + libicu[0-9][0-9] \ + liblttng-ust[0-9] \ + libstdc++6 \ + zlib1g \ + locales \ + sudo \ + ncdu \ + man-db \ + strace \ + manpages \ + manpages-dev \ + init-system-helpers" + + # Needed for adding manpages-posix and manpages-posix-dev which are non-free packages in Debian + if [ "${ADD_NON_FREE_PACKAGES}" = "true" ]; then + # Bring in variables from /etc/os-release like VERSION_CODENAME + . /etc/os-release + sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb-src http:\/\/(deb|httredir)\.debian\.org\/debian ${VERSION_CODENAME} main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME} main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list + sed -i -E "s/deb-src http:\/\/(deb|httpredir)\.debian\.org\/debian ${VERSION_CODENAME}-updates main/deb http:\/\/\1\.debian\.org\/debian ${VERSION_CODENAME}-updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}\/updates main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main/deb http:\/\/deb\.debian\.org\/debian ${VERSION_CODENAME}-backports main contrib non-free/" /etc/apt/sources.list + # Handle bullseye location for security https://www.debian.org/releases/bullseye/amd64/release-notes/ch-information.en.html + sed -i "s/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list + sed -i "s/deb-src http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main/deb http:\/\/security\.debian\.org\/debian-security ${VERSION_CODENAME}-security main contrib non-free/" /etc/apt/sources.list + echo "Running apt-get update..." + apt-get update + package_list="${package_list} manpages-posix manpages-posix-dev" + else + apt_get_update_if_needed + fi + + # Install libssl1.1 if available + if [[ ! -z $(apt-cache --names-only search ^libssl1.1$) ]]; then + package_list="${package_list} libssl1.1" + fi + + # Install appropriate version of libssl1.0.x if available + libssl_package=$(dpkg-query -f '${db:Status-Abbrev}\t${binary:Package}\n' -W 'libssl1\.0\.?' 2>&1 || echo '') + if [ "$(echo "$LIlibssl_packageBSSL" | grep -o 'libssl1\.0\.[0-9]:' | uniq | sort | wc -l)" -eq 0 ]; then + if [[ ! -z $(apt-cache --names-only search ^libssl1.0.2$) ]]; then + # Debian 9 + package_list="${package_list} libssl1.0.2" + elif [[ ! -z $(apt-cache --names-only search ^libssl1.0.0$) ]]; then + # Ubuntu 18.04, 16.04, earlier + package_list="${package_list} libssl1.0.0" + fi + fi + + echo "Packages to verify are installed: ${package_list}" + apt-get -y install --no-install-recommends ${package_list} 2> >( grep -v 'debconf: delaying package configuration, since apt-utils is not installed' >&2 ) + + # Install git if not already installed (may be more recent than distro version) + if ! type git > /dev/null 2>&1; then + apt-get -y install --no-install-recommends git + fi + + PACKAGES_ALREADY_INSTALLED="true" +fi + +# Get to latest versions of all packages +if [ "${UPGRADE_PACKAGES}" = "true" ]; then + apt_get_update_if_needed + apt-get -y upgrade --no-install-recommends + apt-get autoremove -y +fi + +# Ensure at least the en_US.UTF-8 UTF-8 locale is available. +# Common need for both applications and things like the agnoster ZSH theme. +if [ "${LOCALE_ALREADY_SET}" != "true" ] && ! grep -o -E '^\s*en_US.UTF-8\s+UTF-8' /etc/locale.gen > /dev/null; then + echo "en_US.UTF-8 UTF-8" >> /etc/locale.gen + locale-gen + LOCALE_ALREADY_SET="true" +fi + +# Create or update a non-root user to match UID/GID. +group_name="${USERNAME}" +if id -u ${USERNAME} > /dev/null 2>&1; then + # User exists, update if needed + if [ "${USER_GID}" != "automatic" ] && [ "$USER_GID" != "$(id -g $USERNAME)" ]; then + group_name="$(id -gn $USERNAME)" + groupmod --gid $USER_GID ${group_name} + usermod --gid $USER_GID $USERNAME + fi + if [ "${USER_UID}" != "automatic" ] && [ "$USER_UID" != "$(id -u $USERNAME)" ]; then + usermod --uid $USER_UID $USERNAME + fi +else + # Create user + if [ "${USER_GID}" = "automatic" ]; then + groupadd $USERNAME + else + groupadd --gid $USER_GID $USERNAME + fi + if [ "${USER_UID}" = "automatic" ]; then + useradd -s /bin/bash --gid $USERNAME -m $USERNAME + else + useradd -s /bin/bash --uid $USER_UID --gid $USERNAME -m $USERNAME + fi +fi + +# Add sudo support for non-root user +if [ "${USERNAME}" != "root" ] && [ "${EXISTING_NON_ROOT_USER}" != "${USERNAME}" ]; then + echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME + chmod 0440 /etc/sudoers.d/$USERNAME + EXISTING_NON_ROOT_USER="${USERNAME}" +fi + +# ** Shell customization section ** +if [ "${USERNAME}" = "root" ]; then + user_rc_path="/root" +else + user_rc_path="/home/${USERNAME}" +fi + +# Restore user .bashrc defaults from skeleton file if it doesn't exist or is empty +if [ ! -f "${user_rc_path}/.bashrc" ] || [ ! -s "${user_rc_path}/.bashrc" ] ; then + cp /etc/skel/.bashrc "${user_rc_path}/.bashrc" +fi + +# Restore user .profile defaults from skeleton file if it doesn't exist or is empty +if [ ! -f "${user_rc_path}/.profile" ] || [ ! -s "${user_rc_path}/.profile" ] ; then + cp /etc/skel/.profile "${user_rc_path}/.profile" +fi + +# .bashrc/.zshrc snippet +rc_snippet="$(cat << 'EOF' + +if [ -z "${USER}" ]; then export USER=$(whoami); fi +if [[ "${PATH}" != *"$HOME/.local/bin"* ]]; then export PATH="${PATH}:$HOME/.local/bin"; fi + +# Display optional first run image specific notice if configured and terminal is interactive +if [ -t 1 ] && [[ "${TERM_PROGRAM}" = "vscode" || "${TERM_PROGRAM}" = "codespaces" ]] && [ ! -f "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed" ]; then + if [ -f "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" ]; then + cat "/usr/local/etc/vscode-dev-containers/first-run-notice.txt" + elif [ -f "/workspaces/.codespaces/shared/first-run-notice.txt" ]; then + cat "/workspaces/.codespaces/shared/first-run-notice.txt" + fi + mkdir -p "$HOME/.config/vscode-dev-containers" + # Mark first run notice as displayed after 10s to avoid problems with fast terminal refreshes hiding it + ((sleep 10s; touch "$HOME/.config/vscode-dev-containers/first-run-notice-already-displayed") &) +fi + +# Set the default git editor if not already set +if [ -z "$(git config --get core.editor)" ] && [ -z "${GIT_EDITOR}" ]; then + if [ "${TERM_PROGRAM}" = "vscode" ]; then + if [[ -n $(command -v code-insiders) && -z $(command -v code) ]]; then + export GIT_EDITOR="code-insiders --wait" + else + export GIT_EDITOR="code --wait" + fi + fi +fi + +EOF +)" + +# code shim, it fallbacks to code-insiders if code is not available +cat << 'EOF' > /usr/local/bin/code +#!/bin/sh + +get_in_path_except_current() { + which -a "$1" | grep -A1 "$0" | grep -v "$0" +} + +code="$(get_in_path_except_current code)" + +if [ -n "$code" ]; then + exec "$code" "$@" +elif [ "$(command -v code-insiders)" ]; then + exec code-insiders "$@" +else + echo "code or code-insiders is not installed" >&2 + exit 127 +fi +EOF +chmod +x /usr/local/bin/code + +# systemctl shim - tells people to use 'service' if systemd is not running +cat << 'EOF' > /usr/local/bin/systemctl +#!/bin/sh +set -e +if [ -d "/run/systemd/system" ]; then + exec /bin/systemctl "$@" +else + echo '\n"systemd" is not running in this container due to its overhead.\nUse the "service" command to start services instead. e.g.: \n\nservice --status-all' +fi +EOF +chmod +x /usr/local/bin/systemctl + +# Codespaces bash and OMZ themes - partly inspired by https://github.com/ohmyzsh/ohmyzsh/blob/master/themes/robbyrussell.zsh-theme +codespaces_bash="$(cat \ +<<'EOF' + +# Codespaces bash prompt theme +__bash_prompt() { + local userpart='`export XIT=$? \ + && [ ! -z "${GITHUB_USER}" ] && echo -n "\[\033[0;32m\]@${GITHUB_USER} " || echo -n "\[\033[0;32m\]\u " \ + && [ "$XIT" -ne "0" ] && echo -n "\[\033[1;31m\]➜" || echo -n "\[\033[0m\]➜"`' + local gitbranch='`\ + if [ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ]; then \ + export BRANCH=$(git symbolic-ref --short HEAD 2>/dev/null || git rev-parse --short HEAD 2>/dev/null); \ + if [ "${BRANCH}" != "" ]; then \ + echo -n "\[\033[0;36m\](\[\033[1;31m\]${BRANCH}" \ + && if git ls-files --error-unmatch -m --directory --no-empty-directory -o --exclude-standard ":/*" > /dev/null 2>&1; then \ + echo -n " \[\033[1;33m\]✗"; \ + fi \ + && echo -n "\[\033[0;36m\]) "; \ + fi; \ + fi`' + local lightblue='\[\033[1;34m\]' + local removecolor='\[\033[0m\]' + PS1="${userpart} ${lightblue}\w ${gitbranch}${removecolor}\$ " + unset -f __bash_prompt +} +__bash_prompt + +EOF +)" + +codespaces_zsh="$(cat \ +<<'EOF' +# Codespaces zsh prompt theme +__zsh_prompt() { + local prompt_username + if [ ! -z "${GITHUB_USER}" ]; then + prompt_username="@${GITHUB_USER}" + else + prompt_username="%n" + fi + PROMPT="%{$fg[green]%}${prompt_username} %(?:%{$reset_color%}➜ :%{$fg_bold[red]%}➜ )" # User/exit code arrow + PROMPT+='%{$fg_bold[blue]%}%(5~|%-1~/…/%3~|%4~)%{$reset_color%} ' # cwd + PROMPT+='$([ "$(git config --get codespaces-theme.hide-status 2>/dev/null)" != 1 ] && git_prompt_info)' # Git status + PROMPT+='%{$fg[white]%}$ %{$reset_color%}' + unset -f __zsh_prompt +} +ZSH_THEME_GIT_PROMPT_PREFIX="%{$fg_bold[cyan]%}(%{$fg_bold[red]%}" +ZSH_THEME_GIT_PROMPT_SUFFIX="%{$reset_color%} " +ZSH_THEME_GIT_PROMPT_DIRTY=" %{$fg_bold[yellow]%}✗%{$fg_bold[cyan]%})" +ZSH_THEME_GIT_PROMPT_CLEAN="%{$fg_bold[cyan]%})" +__zsh_prompt + +EOF +)" + +# Add RC snippet and custom bash prompt +if [ "${RC_SNIPPET_ALREADY_ADDED}" != "true" ]; then + echo "${rc_snippet}" >> /etc/bash.bashrc + echo "${codespaces_bash}" >> "${user_rc_path}/.bashrc" + echo 'export PROMPT_DIRTRIM=4' >> "${user_rc_path}/.bashrc" + if [ "${USERNAME}" != "root" ]; then + echo "${codespaces_bash}" >> "/root/.bashrc" + echo 'export PROMPT_DIRTRIM=4' >> "/root/.bashrc" + fi + chown ${USERNAME}:${group_name} "${user_rc_path}/.bashrc" + RC_SNIPPET_ALREADY_ADDED="true" +fi + +# Optionally install and configure zsh and Oh My Zsh! +if [ "${INSTALL_ZSH}" = "true" ]; then + if ! type zsh > /dev/null 2>&1; then + apt_get_update_if_needed + apt-get install -y zsh + fi + if [ "${ZSH_ALREADY_INSTALLED}" != "true" ]; then + echo "${rc_snippet}" >> /etc/zsh/zshrc + ZSH_ALREADY_INSTALLED="true" + fi + + # Adapted, simplified inline Oh My Zsh! install steps that adds, defaults to a codespaces theme. + # See https://github.com/ohmyzsh/ohmyzsh/blob/master/tools/install.sh for official script. + oh_my_install_dir="${user_rc_path}/.oh-my-zsh" + if [ ! -d "${oh_my_install_dir}" ] && [ "${INSTALL_OH_MYS}" = "true" ]; then + template_path="${oh_my_install_dir}/templates/zshrc.zsh-template" + user_rc_file="${user_rc_path}/.zshrc" + umask g-w,o-w + mkdir -p ${oh_my_install_dir} + git clone --depth=1 \ + -c core.eol=lf \ + -c core.autocrlf=false \ + -c fsck.zeroPaddedFilemode=ignore \ + -c fetch.fsck.zeroPaddedFilemode=ignore \ + -c receive.fsck.zeroPaddedFilemode=ignore \ + "https://github.com/ohmyzsh/ohmyzsh" "${oh_my_install_dir}" 2>&1 + echo -e "$(cat "${template_path}")\nDISABLE_AUTO_UPDATE=true\nDISABLE_UPDATE_PROMPT=true" > ${user_rc_file} + sed -i -e 's/ZSH_THEME=.*/ZSH_THEME="codespaces"/g' ${user_rc_file} + + mkdir -p ${oh_my_install_dir}/custom/themes + echo "${codespaces_zsh}" > "${oh_my_install_dir}/custom/themes/codespaces.zsh-theme" + # Shrink git while still enabling updates + cd "${oh_my_install_dir}" + git repack -a -d -f --depth=1 --window=1 + # Copy to non-root user if one is specified + if [ "${USERNAME}" != "root" ]; then + cp -rf "${user_rc_file}" "${oh_my_install_dir}" /root + chown -R ${USERNAME}:${group_name} "${user_rc_path}" + fi + fi +fi + +# Persist image metadata info, script if meta.env found in same directory +meta_info_script="$(cat << 'EOF' +#!/bin/sh +. /usr/local/etc/vscode-dev-containers/meta.env + +# Minimal output +if [ "$1" = "version" ] || [ "$1" = "image-version" ]; then + echo "${VERSION}" + exit 0 +elif [ "$1" = "release" ]; then + echo "${GIT_REPOSITORY_RELEASE}" + exit 0 +elif [ "$1" = "content" ] || [ "$1" = "content-url" ] || [ "$1" = "contents" ] || [ "$1" = "contents-url" ]; then + echo "${CONTENTS_URL}" + exit 0 +fi + +#Full output +echo +echo "Development container image information" +echo +if [ ! -z "${VERSION}" ]; then echo "- Image version: ${VERSION}"; fi +if [ ! -z "${DEFINITION_ID}" ]; then echo "- Definition ID: ${DEFINITION_ID}"; fi +if [ ! -z "${VARIANT}" ]; then echo "- Variant: ${VARIANT}"; fi +if [ ! -z "${GIT_REPOSITORY}" ]; then echo "- Source code repository: ${GIT_REPOSITORY}"; fi +if [ ! -z "${GIT_REPOSITORY_RELEASE}" ]; then echo "- Source code release/branch: ${GIT_REPOSITORY_RELEASE}"; fi +if [ ! -z "${BUILD_TIMESTAMP}" ]; then echo "- Timestamp: ${BUILD_TIMESTAMP}"; fi +if [ ! -z "${CONTENTS_URL}" ]; then echo && echo "More info: ${CONTENTS_URL}"; fi +echo +EOF +)" +if [ -f "${SCRIPT_DIR}/meta.env" ]; then + mkdir -p /usr/local/etc/vscode-dev-containers/ + cp -f "${SCRIPT_DIR}/meta.env" /usr/local/etc/vscode-dev-containers/meta.env + echo "${meta_info_script}" > /usr/local/bin/devcontainer-info + chmod +x /usr/local/bin/devcontainer-info +fi + +# Write marker file +mkdir -p "$(dirname "${MARKER_FILE}")" +echo -e "\ + PACKAGES_ALREADY_INSTALLED=${PACKAGES_ALREADY_INSTALLED}\n\ + LOCALE_ALREADY_SET=${LOCALE_ALREADY_SET}\n\ + EXISTING_NON_ROOT_USER=${EXISTING_NON_ROOT_USER}\n\ + RC_SNIPPET_ALREADY_ADDED=${RC_SNIPPET_ALREADY_ADDED}\n\ + ZSH_ALREADY_INSTALLED=${ZSH_ALREADY_INSTALLED}" > "${MARKER_FILE}" + +echo "Done!" diff --git a/.npmignore b/.npmignore new file mode 100644 index 00000000..e1c966ce --- /dev/null +++ b/.npmignore @@ -0,0 +1,6 @@ +README.md +test-project +definition-manifest.json +.devcontainer/library-scripts/README.md +.vscode +.npmignore From 9970c41b6b0de2c6017f3cf810b02b056748e05c Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 16:25:20 -0500 Subject: [PATCH 140/227] Add launch configuration for testing the flutter app --- .vscode/launch.json | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/.vscode/launch.json b/.vscode/launch.json index 57ded091..ff2049ef 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -31,6 +31,29 @@ "console": "internalConsole", "stopAtEntry": false, "internalConsoleOptions": "openOnSessionStart" + }, + { + "name": "Launchpad (app)", + "cwd": "launchpad_app", + "program": "lib/main.dart", + "request": "launch", + "type": "dart" + }, + { + "name": "Launchpad (app profile mode)", + "cwd": "launchpad_app", + "program": "lib/main.dart", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "Launchpad (app release mode)", + "cwd": "launchpad_app", + "program": "lib/main.dart", + "request": "launch", + "type": "dart", + "flutterMode": "release" } ] } From 1616be7272b2ab49467172007f0ae6b6a89f7630 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 16:28:26 -0500 Subject: [PATCH 141/227] Platform files for launchpad_app --- launchpad_app/.gitignore | 45 ++ launchpad_app/.metadata | 45 ++ launchpad_app/analysis_options.yaml | 1 + launchpad_app/macos/.gitignore | 7 + .../macos/Flutter/Flutter-Debug.xcconfig | 2 + .../macos/Flutter/Flutter-Release.xcconfig | 2 + .../Flutter/GeneratedPluginRegistrant.swift | 26 + launchpad_app/macos/Podfile | 40 + launchpad_app/macos/Podfile.lock | 34 + .../macos/Runner.xcodeproj/project.pbxproj | 632 +++++++++++++++ .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 87 ++ .../contents.xcworkspacedata | 10 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + launchpad_app/macos/Runner/AppDelegate.swift | 9 + .../AppIcon.appiconset/Contents.json | 68 ++ .../AppIcon.appiconset/app_icon_1024.png | Bin 0 -> 46993 bytes .../AppIcon.appiconset/app_icon_128.png | Bin 0 -> 3276 bytes .../AppIcon.appiconset/app_icon_16.png | Bin 0 -> 1429 bytes .../AppIcon.appiconset/app_icon_256.png | Bin 0 -> 5933 bytes .../AppIcon.appiconset/app_icon_32.png | Bin 0 -> 1243 bytes .../AppIcon.appiconset/app_icon_512.png | Bin 0 -> 14800 bytes .../AppIcon.appiconset/app_icon_64.png | Bin 0 -> 1874 bytes .../macos/Runner/Base.lproj/MainMenu.xib | 343 ++++++++ .../macos/Runner/Configs/AppInfo.xcconfig | 14 + .../macos/Runner/Configs/Debug.xcconfig | 2 + .../macos/Runner/Configs/Release.xcconfig | 2 + .../macos/Runner/Configs/Warnings.xcconfig | 13 + .../macos/Runner/DebugProfile.entitlements | 12 + launchpad_app/macos/Runner/Info.plist | 32 + .../macos/Runner/MainFlutterWindow.swift | 15 + .../macos/Runner/Release.entitlements | 8 + launchpad_app/pubspec.lock | 740 ++++++++++++++++++ launchpad_app/pubspec.yaml | 50 ++ launchpad_app/web/assets/flutter_logo.png | Bin 0 -> 7048 bytes launchpad_app/web/favicon.png | Bin 0 -> 917 bytes launchpad_app/web/icons/Icon-192.png | Bin 0 -> 5292 bytes launchpad_app/web/icons/Icon-512.png | Bin 0 -> 8252 bytes launchpad_app/web/icons/Icon-maskable-192.png | Bin 0 -> 5594 bytes launchpad_app/web/icons/Icon-maskable-512.png | Bin 0 -> 20998 bytes launchpad_app/web/index.html | 51 ++ launchpad_app/web/manifest.json | 23 + launchpad_app/web/splash/img/dark-1x.png | Bin 0 -> 4659 bytes launchpad_app/web/splash/img/dark-2x.png | Bin 0 -> 12757 bytes launchpad_app/web/splash/img/dark-3x.png | Bin 0 -> 8441 bytes launchpad_app/web/splash/img/light-1x.png | Bin 0 -> 4659 bytes launchpad_app/web/splash/img/light-2x.png | Bin 0 -> 12757 bytes launchpad_app/web/splash/img/light-3x.png | Bin 0 -> 8441 bytes launchpad_app/web/splash/style.css | 43 + launchpad_app/windows/.gitignore | 17 + launchpad_app/windows/CMakeLists.txt | 95 +++ launchpad_app/windows/flutter/CMakeLists.txt | 103 +++ .../flutter/generated_plugin_registrant.cc | 29 + .../flutter/generated_plugin_registrant.h | 15 + .../windows/flutter/generated_plugins.cmake | 29 + launchpad_app/windows/runner/CMakeLists.txt | 17 + launchpad_app/windows/runner/Runner.rc | 121 +++ .../windows/runner/flutter_window.cpp | 61 ++ launchpad_app/windows/runner/flutter_window.h | 33 + launchpad_app/windows/runner/main.cpp | 43 + launchpad_app/windows/runner/resource.h | 16 + .../windows/runner/resources/app_icon.ico | Bin 0 -> 33772 bytes .../windows/runner/runner.exe.manifest | 20 + launchpad_app/windows/runner/utils.cpp | 64 ++ launchpad_app/windows/runner/utils.h | 19 + launchpad_app/windows/runner/win32_window.cpp | 245 ++++++ launchpad_app/windows/runner/win32_window.h | 98 +++ 67 files changed, 3397 insertions(+) create mode 100644 launchpad_app/.gitignore create mode 100644 launchpad_app/.metadata create mode 100644 launchpad_app/analysis_options.yaml create mode 100644 launchpad_app/macos/.gitignore create mode 100644 launchpad_app/macos/Flutter/Flutter-Debug.xcconfig create mode 100644 launchpad_app/macos/Flutter/Flutter-Release.xcconfig create mode 100644 launchpad_app/macos/Flutter/GeneratedPluginRegistrant.swift create mode 100644 launchpad_app/macos/Podfile create mode 100644 launchpad_app/macos/Podfile.lock create mode 100644 launchpad_app/macos/Runner.xcodeproj/project.pbxproj create mode 100644 launchpad_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 launchpad_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme create mode 100644 launchpad_app/macos/Runner.xcworkspace/contents.xcworkspacedata create mode 100644 launchpad_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 launchpad_app/macos/Runner/AppDelegate.swift create mode 100644 launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png create mode 100644 launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png create mode 100644 launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png create mode 100644 launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png create mode 100644 launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png create mode 100644 launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png create mode 100644 launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png create mode 100644 launchpad_app/macos/Runner/Base.lproj/MainMenu.xib create mode 100644 launchpad_app/macos/Runner/Configs/AppInfo.xcconfig create mode 100644 launchpad_app/macos/Runner/Configs/Debug.xcconfig create mode 100644 launchpad_app/macos/Runner/Configs/Release.xcconfig create mode 100644 launchpad_app/macos/Runner/Configs/Warnings.xcconfig create mode 100644 launchpad_app/macos/Runner/DebugProfile.entitlements create mode 100644 launchpad_app/macos/Runner/Info.plist create mode 100644 launchpad_app/macos/Runner/MainFlutterWindow.swift create mode 100644 launchpad_app/macos/Runner/Release.entitlements create mode 100644 launchpad_app/pubspec.lock create mode 100644 launchpad_app/pubspec.yaml create mode 100644 launchpad_app/web/assets/flutter_logo.png create mode 100644 launchpad_app/web/favicon.png create mode 100644 launchpad_app/web/icons/Icon-192.png create mode 100644 launchpad_app/web/icons/Icon-512.png create mode 100644 launchpad_app/web/icons/Icon-maskable-192.png create mode 100644 launchpad_app/web/icons/Icon-maskable-512.png create mode 100644 launchpad_app/web/index.html create mode 100644 launchpad_app/web/manifest.json create mode 100644 launchpad_app/web/splash/img/dark-1x.png create mode 100644 launchpad_app/web/splash/img/dark-2x.png create mode 100644 launchpad_app/web/splash/img/dark-3x.png create mode 100644 launchpad_app/web/splash/img/light-1x.png create mode 100644 launchpad_app/web/splash/img/light-2x.png create mode 100644 launchpad_app/web/splash/img/light-3x.png create mode 100644 launchpad_app/web/splash/style.css create mode 100644 launchpad_app/windows/.gitignore create mode 100644 launchpad_app/windows/CMakeLists.txt create mode 100644 launchpad_app/windows/flutter/CMakeLists.txt create mode 100644 launchpad_app/windows/flutter/generated_plugin_registrant.cc create mode 100644 launchpad_app/windows/flutter/generated_plugin_registrant.h create mode 100644 launchpad_app/windows/flutter/generated_plugins.cmake create mode 100644 launchpad_app/windows/runner/CMakeLists.txt create mode 100644 launchpad_app/windows/runner/Runner.rc create mode 100644 launchpad_app/windows/runner/flutter_window.cpp create mode 100644 launchpad_app/windows/runner/flutter_window.h create mode 100644 launchpad_app/windows/runner/main.cpp create mode 100644 launchpad_app/windows/runner/resource.h create mode 100644 launchpad_app/windows/runner/resources/app_icon.ico create mode 100644 launchpad_app/windows/runner/runner.exe.manifest create mode 100644 launchpad_app/windows/runner/utils.cpp create mode 100644 launchpad_app/windows/runner/utils.h create mode 100644 launchpad_app/windows/runner/win32_window.cpp create mode 100644 launchpad_app/windows/runner/win32_window.h diff --git a/launchpad_app/.gitignore b/launchpad_app/.gitignore new file mode 100644 index 00000000..f29eff8c --- /dev/null +++ b/launchpad_app/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.packages +.pub-cache/ +.pub/ +/build/ + +# Web related +lib/generated_plugin_registrant.dart + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +/android/ +ios/ +linux/ \ No newline at end of file diff --git a/launchpad_app/.metadata b/launchpad_app/.metadata new file mode 100644 index 00000000..262ceed0 --- /dev/null +++ b/launchpad_app/.metadata @@ -0,0 +1,45 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: 135454af32477f815a7525073027a3ff9eff1bfd + channel: stable + +project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: android + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: ios + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: linux + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: macos + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: web + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + - platform: windows + create_revision: 135454af32477f815a7525073027a3ff9eff1bfd + base_revision: 135454af32477f815a7525073027a3ff9eff1bfd + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/launchpad_app/analysis_options.yaml b/launchpad_app/analysis_options.yaml new file mode 100644 index 00000000..a3be6b82 --- /dev/null +++ b/launchpad_app/analysis_options.yaml @@ -0,0 +1 @@ +include: package:flutter_lints/flutter.yaml \ No newline at end of file diff --git a/launchpad_app/macos/.gitignore b/launchpad_app/macos/.gitignore new file mode 100644 index 00000000..746adbb6 --- /dev/null +++ b/launchpad_app/macos/.gitignore @@ -0,0 +1,7 @@ +# Flutter-related +**/Flutter/ephemeral/ +**/Pods/ + +# Xcode-related +**/dgph +**/xcuserdata/ diff --git a/launchpad_app/macos/Flutter/Flutter-Debug.xcconfig b/launchpad_app/macos/Flutter/Flutter-Debug.xcconfig new file mode 100644 index 00000000..4b81f9b2 --- /dev/null +++ b/launchpad_app/macos/Flutter/Flutter-Debug.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/launchpad_app/macos/Flutter/Flutter-Release.xcconfig b/launchpad_app/macos/Flutter/Flutter-Release.xcconfig new file mode 100644 index 00000000..5caa9d15 --- /dev/null +++ b/launchpad_app/macos/Flutter/Flutter-Release.xcconfig @@ -0,0 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" +#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/launchpad_app/macos/Flutter/GeneratedPluginRegistrant.swift b/launchpad_app/macos/Flutter/GeneratedPluginRegistrant.swift new file mode 100644 index 00000000..a66cfdc8 --- /dev/null +++ b/launchpad_app/macos/Flutter/GeneratedPluginRegistrant.swift @@ -0,0 +1,26 @@ +// +// Generated file. Do not edit. +// + +import FlutterMacOS +import Foundation + +import device_info_plus +import flutter_acrylic +import network_info_plus +import package_info_plus +import path_provider_macos +import screen_retriever +import url_launcher_macos +import window_manager + +func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + FlutterAcrylicPlugin.register(with: registry.registrar(forPlugin: "FlutterAcrylicPlugin")) + NetworkInfoPlusPlugin.register(with: registry.registrar(forPlugin: "NetworkInfoPlusPlugin")) + FLTPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlusPlugin")) + PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) + ScreenRetrieverPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverPlugin")) + UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) + WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) +} diff --git a/launchpad_app/macos/Podfile b/launchpad_app/macos/Podfile new file mode 100644 index 00000000..dade8dfa --- /dev/null +++ b/launchpad_app/macos/Podfile @@ -0,0 +1,40 @@ +platform :osx, '10.11' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + use_modular_headers! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/launchpad_app/macos/Podfile.lock b/launchpad_app/macos/Podfile.lock new file mode 100644 index 00000000..d41cc784 --- /dev/null +++ b/launchpad_app/macos/Podfile.lock @@ -0,0 +1,34 @@ +PODS: + - flutter_acrylic (0.1.0): + - FlutterMacOS + - FlutterMacOS (1.0.0) + - url_launcher_macos (0.0.1): + - FlutterMacOS + - window_manager (0.2.0): + - FlutterMacOS + +DEPENDENCIES: + - flutter_acrylic (from `Flutter/ephemeral/.symlinks/plugins/flutter_acrylic/macos`) + - FlutterMacOS (from `Flutter/ephemeral`) + - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) + - window_manager (from `Flutter/ephemeral/.symlinks/plugins/window_manager/macos`) + +EXTERNAL SOURCES: + flutter_acrylic: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_acrylic/macos + FlutterMacOS: + :path: Flutter/ephemeral + url_launcher_macos: + :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos + window_manager: + :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos + +SPEC CHECKSUMS: + flutter_acrylic: c3df24ae52ab6597197837ce59ef2a8542640c17 + FlutterMacOS: 57701585bf7de1b3fc2bb61f6378d73bbdea8424 + url_launcher_macos: 45af3d61de06997666568a7149c1be98b41c95d4 + window_manager: 3a1844359a6295ab1e47659b1a777e36773cd6e8 + +PODFILE CHECKSUM: 6eac6b3292e5142cfc23bdeb71848a40ec51c14c + +COCOAPODS: 1.11.3 diff --git a/launchpad_app/macos/Runner.xcodeproj/project.pbxproj b/launchpad_app/macos/Runner.xcodeproj/project.pbxproj new file mode 100644 index 00000000..32056d99 --- /dev/null +++ b/launchpad_app/macos/Runner.xcodeproj/project.pbxproj @@ -0,0 +1,632 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 51; + objects = { + +/* Begin PBXAggregateTarget section */ + 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { + isa = PBXAggregateTarget; + buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; + buildPhases = ( + 33CC111E2044C6BF0003C045 /* ShellScript */, + ); + dependencies = ( + ); + name = "Flutter Assemble"; + productName = FLX; + }; +/* End PBXAggregateTarget section */ + +/* Begin PBXBuildFile section */ + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 907C05D991C99ABFF709B8FA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 14616010BF15918C171DC4C0 /* Pods_Runner.framework */; }; +/* End PBXBuildFile section */ + +/* Begin PBXContainerItemProxy section */ + 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 33CC10E52044A3C60003C045 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 33CC111A2044C6BA0003C045; + remoteInfo = FLX; + }; +/* End PBXContainerItemProxy section */ + +/* Begin PBXCopyFilesBuildPhase section */ + 33CC110E2044A8840003C045 /* Bundle Framework */ = { + isa = PBXCopyFilesBuildPhase; + buildActionMask = 2147483647; + dstPath = ""; + dstSubfolderSpec = 10; + files = ( + ); + name = "Bundle Framework"; + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXCopyFilesBuildPhase section */ + +/* Begin PBXFileReference section */ + 14616010BF15918C171DC4C0 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 196E76E7997C7B95D18DC878 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; + 33CC10ED2044A3C60003C045 /* example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = example.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; + 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; + 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; + 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; + 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; + 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 479877830A4E470E6B020281 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + AEA6E30DABBB9A9AA880D2FF /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 33CC10EA2044A3C60003C045 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 907C05D991C99ABFF709B8FA /* Pods_Runner.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 33BA886A226E78AF003329D5 /* Configs */ = { + isa = PBXGroup; + children = ( + 33E5194F232828860026EE4D /* AppInfo.xcconfig */, + 9740EEB21CF90195004384FC /* Debug.xcconfig */, + 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, + 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, + ); + path = Configs; + sourceTree = ""; + }; + 33CC10E42044A3C60003C045 = { + isa = PBXGroup; + children = ( + 33FAB671232836740065AC1E /* Runner */, + 33CEB47122A05771004F2AC0 /* Flutter */, + 33CC10EE2044A3C60003C045 /* Products */, + D73912EC22F37F3D000D13A0 /* Frameworks */, + F2335EBCB83188AA4E7F8B88 /* Pods */, + ); + sourceTree = ""; + }; + 33CC10EE2044A3C60003C045 /* Products */ = { + isa = PBXGroup; + children = ( + 33CC10ED2044A3C60003C045 /* example.app */, + ); + name = Products; + sourceTree = ""; + }; + 33CC11242044D66E0003C045 /* Resources */ = { + isa = PBXGroup; + children = ( + 33CC10F22044A3C60003C045 /* Assets.xcassets */, + 33CC10F42044A3C60003C045 /* MainMenu.xib */, + 33CC10F72044A3C60003C045 /* Info.plist */, + ); + name = Resources; + path = ..; + sourceTree = ""; + }; + 33CEB47122A05771004F2AC0 /* Flutter */ = { + isa = PBXGroup; + children = ( + 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, + 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, + 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, + 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, + ); + path = Flutter; + sourceTree = ""; + }; + 33FAB671232836740065AC1E /* Runner */ = { + isa = PBXGroup; + children = ( + 33CC10F02044A3C60003C045 /* AppDelegate.swift */, + 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, + 33E51913231747F40026EE4D /* DebugProfile.entitlements */, + 33E51914231749380026EE4D /* Release.entitlements */, + 33CC11242044D66E0003C045 /* Resources */, + 33BA886A226E78AF003329D5 /* Configs */, + ); + path = Runner; + sourceTree = ""; + }; + D73912EC22F37F3D000D13A0 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 14616010BF15918C171DC4C0 /* Pods_Runner.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; + F2335EBCB83188AA4E7F8B88 /* Pods */ = { + isa = PBXGroup; + children = ( + 196E76E7997C7B95D18DC878 /* Pods-Runner.debug.xcconfig */, + AEA6E30DABBB9A9AA880D2FF /* Pods-Runner.release.xcconfig */, + 479877830A4E470E6B020281 /* Pods-Runner.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 33CC10EC2044A3C60003C045 /* Runner */ = { + isa = PBXNativeTarget; + buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; + buildPhases = ( + 3FA56BC156B9E0FF51A1A919 /* [CP] Check Pods Manifest.lock */, + 33CC10E92044A3C60003C045 /* Sources */, + 33CC10EA2044A3C60003C045 /* Frameworks */, + 33CC10EB2044A3C60003C045 /* Resources */, + 33CC110E2044A8840003C045 /* Bundle Framework */, + 3399D490228B24CF009A79C7 /* ShellScript */, + 3ED2E9284E8AAA09A67D95C2 /* [CP] Embed Pods Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 33CC11202044C79F0003C045 /* PBXTargetDependency */, + ); + name = Runner; + productName = Runner; + productReference = 33CC10ED2044A3C60003C045 /* example.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 33CC10E52044A3C60003C045 /* Project object */ = { + isa = PBXProject; + attributes = { + LastSwiftUpdateCheck = 0920; + LastUpgradeCheck = 1300; + ORGANIZATIONNAME = ""; + TargetAttributes = { + 33CC10EC2044A3C60003C045 = { + CreatedOnToolsVersion = 9.2; + LastSwiftMigration = 1100; + ProvisioningStyle = Automatic; + SystemCapabilities = { + com.apple.Sandbox = { + enabled = 1; + }; + }; + }; + 33CC111A2044C6BA0003C045 = { + CreatedOnToolsVersion = 9.2; + ProvisioningStyle = Manual; + }; + }; + }; + buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; + compatibilityVersion = "Xcode 9.3"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 33CC10E42044A3C60003C045; + productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 33CC10EC2044A3C60003C045 /* Runner */, + 33CC111A2044C6BA0003C045 /* Flutter Assemble */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 33CC10EB2044A3C60003C045 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, + 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 3399D490228B24CF009A79C7 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; + }; + 33CC111E2044C6BF0003C045 /* ShellScript */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + Flutter/ephemeral/FlutterInputs.xcfilelist, + ); + inputPaths = ( + Flutter/ephemeral/tripwire, + ); + outputFileListPaths = ( + Flutter/ephemeral/FlutterOutputs.xcfilelist, + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + }; + 3ED2E9284E8AAA09A67D95C2 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 3FA56BC156B9E0FF51A1A919 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 33CC10E92044A3C60003C045 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, + 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, + 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin PBXTargetDependency section */ + 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; + targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + +/* Begin PBXVariantGroup section */ + 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { + isa = PBXVariantGroup; + children = ( + 33CC10F52044A3C60003C045 /* Base */, + ); + name = MainMenu.xib; + path = Runner; + sourceTree = ""; + }; +/* End PBXVariantGroup section */ + +/* Begin XCBuildConfiguration section */ + 338D0CE9231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Profile; + }; + 338D0CEA231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Profile; + }; + 338D0CEB231458BD00FA5F75 /* Profile */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Profile; + }; + 33CC10F92044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = macosx; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 33CC10FA2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; + CLANG_CXX_LIBRARY = "libc++"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CODE_SIGN_IDENTITY = "-"; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + GCC_C_LANGUAGE_STANDARD = gnu11; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + MACOSX_DEPLOYMENT_TARGET = 10.11; + MTL_ENABLE_DEBUG_INFO = NO; + SDKROOT = macosx; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; + }; + name = Release; + }; + 33CC10FC2044A3C60003C045 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + }; + name = Debug; + }; + 33CC10FD2044A3C60003C045 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; + CODE_SIGN_STYLE = Automatic; + COMBINE_HIDPI_IMAGES = YES; + INFOPLIST_FILE = Runner/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/../Frameworks", + ); + PROVISIONING_PROFILE_SPECIFIER = ""; + SWIFT_VERSION = 5.0; + }; + name = Release; + }; + 33CC111C2044C6BA0003C045 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Manual; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Debug; + }; + 33CC111D2044C6BA0003C045 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + PRODUCT_NAME = "$(TARGET_NAME)"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10F92044A3C60003C045 /* Debug */, + 33CC10FA2044A3C60003C045 /* Release */, + 338D0CE9231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC10FC2044A3C60003C045 /* Debug */, + 33CC10FD2044A3C60003C045 /* Release */, + 338D0CEA231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 33CC111C2044C6BA0003C045 /* Debug */, + 33CC111D2044C6BA0003C045 /* Release */, + 338D0CEB231458BD00FA5F75 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 33CC10E52044A3C60003C045 /* Project object */; +} diff --git a/launchpad_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/launchpad_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/launchpad_app/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/launchpad_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/launchpad_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme new file mode 100644 index 00000000..fb7259e1 --- /dev/null +++ b/launchpad_app/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launchpad_app/macos/Runner.xcworkspace/contents.xcworkspacedata b/launchpad_app/macos/Runner.xcworkspace/contents.xcworkspacedata new file mode 100644 index 00000000..21a3cc14 --- /dev/null +++ b/launchpad_app/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,10 @@ + + + + + + + diff --git a/launchpad_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/launchpad_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 00000000..18d98100 --- /dev/null +++ b/launchpad_app/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/launchpad_app/macos/Runner/AppDelegate.swift b/launchpad_app/macos/Runner/AppDelegate.swift new file mode 100644 index 00000000..d53ef643 --- /dev/null +++ b/launchpad_app/macos/Runner/AppDelegate.swift @@ -0,0 +1,9 @@ +import Cocoa +import FlutterMacOS + +@NSApplicationMain +class AppDelegate: FlutterAppDelegate { + override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { + return true + } +} diff --git a/launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 00000000..a2ec33f1 --- /dev/null +++ b/launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,68 @@ +{ + "images" : [ + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_16.png", + "scale" : "1x" + }, + { + "size" : "16x16", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "2x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_32.png", + "scale" : "1x" + }, + { + "size" : "32x32", + "idiom" : "mac", + "filename" : "app_icon_64.png", + "scale" : "2x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_128.png", + "scale" : "1x" + }, + { + "size" : "128x128", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "2x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_256.png", + "scale" : "1x" + }, + { + "size" : "256x256", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "2x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_512.png", + "scale" : "1x" + }, + { + "size" : "512x512", + "idiom" : "mac", + "filename" : "app_icon_1024.png", + "scale" : "2x" + } + ], + "info" : { + "version" : 1, + "author" : "xcode" + } +} diff --git a/launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 0000000000000000000000000000000000000000..3c4935a7ca84f0976aca34b7f2895d65fb94d1ea GIT binary patch literal 46993 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 literal 0 HcmV?d00001 diff --git a/launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png new file mode 100644 index 0000000000000000000000000000000000000000..ed4cc16421680a50164ba74381b4b35ceaa0ccfc GIT binary patch literal 3276 zcmZ`*X*|?x8~)E?#xi3t91%vcMKbnsIy2_j%QE2ziLq8HEtbf{7%?Q-9a%z_Y^9`> zEHh*&vUG%uWkg7pKTS-`$veH@-Vg8ZdG7oAJ@<88AMX3Z{d}TU-4*=KI1-hF6u>DKF2moPt09c{` zfN3rO$X+gJI&oA$AbgKoTL8PiPI1eFOhHBDvW+$&oPl1s$+O5y3$30Jx9nC_?fg%8Om)@;^P;Ee~8ibejUNlSR{FL7-+ zCzU}3UT98m{kYI^@`mgCOJ))+D#erb#$UWt&((j-5*t1id2Zak{`aS^W*K5^gM02# zUAhZn-JAUK>i+SNuFbWWd*7n1^!}>7qZ1CqCl*T+WoAy&z9pm~0AUt1cCV24f z3M@&G~UKrjVHa zjcE@a`2;M>eV&ocly&W3h{`Kt`1Fpp?_h~9!Uj5>0eXw@$opV(@!pixIux}s5pvEqF5$OEMG0;c zAfMxC(-;nx_`}8!F?OqK19MeaswOomKeifCG-!9PiHSU$yamJhcjXiq)-}9`M<&Au|H!nKY(0`^x16f205i2i;E%(4!?0lLq0sH_%)Wzij)B{HZxYWRl3DLaN5`)L zx=x=|^RA?d*TRCwF%`zN6wn_1C4n;lZG(9kT;2Uhl&2jQYtC1TbwQlP^BZHY!MoHm zjQ9)uu_K)ObgvvPb}!SIXFCtN!-%sBQe{6NU=&AtZJS%}eE$i}FIll!r>~b$6gt)V z7x>OFE}YetHPc-tWeu!P@qIWb@Z$bd!*!*udxwO6&gJ)q24$RSU^2Mb%-_`dR2`nW z)}7_4=iR`Tp$TPfd+uieo)8B}Q9#?Szmy!`gcROB@NIehK|?!3`r^1>av?}e<$Qo` zo{Qn#X4ktRy<-+f#c@vILAm;*sfS}r(3rl+{op?Hx|~DU#qsDcQDTvP*!c>h*nXU6 zR=Un;i9D!LcnC(AQ$lTUv^pgv4Z`T@vRP3{&xb^drmjvOruIBJ%3rQAFLl7d9_S64 zN-Uv?R`EzkbYIo)af7_M=X$2p`!u?nr?XqQ_*F-@@(V zFbNeVEzbr;i2fefJ@Gir3-s`syC93he_krL1eb;r(}0yUkuEK34aYvC@(yGi`*oq? zw5g_abg=`5Fdh1Z+clSv*N*Jifmh&3Ghm0A=^s4be*z5N!i^FzLiShgkrkwsHfMjf z*7&-G@W>p6En#dk<^s@G?$7gi_l)y7k`ZY=?ThvvVKL~kM{ehG7-q6=#%Q8F&VsB* zeW^I zUq+tV(~D&Ii_=gn-2QbF3;Fx#%ajjgO05lfF8#kIllzHc=P}a3$S_XsuZI0?0__%O zjiL!@(C0$Nr+r$>bHk(_oc!BUz;)>Xm!s*C!32m1W<*z$^&xRwa+AaAG= z9t4X~7UJht1-z88yEKjJ68HSze5|nKKF9(Chw`{OoG{eG0mo`^93gaJmAP_i_jF8a z({|&fX70PXVE(#wb11j&g4f{_n>)wUYIY#vo>Rit(J=`A-NYYowTnl(N6&9XKIV(G z1aD!>hY!RCd^Sy#GL^0IgYF~)b-lczn+X}+eaa)%FFw41P#f8n2fm9=-4j7}ULi@Z zm=H8~9;)ShkOUAitb!1fvv%;2Q+o)<;_YA1O=??ie>JmIiTy6g+1B-1#A(NAr$JNL znVhfBc8=aoz&yqgrN|{VlpAniZVM?>0%bwB6>}S1n_OURps$}g1t%)YmCA6+5)W#B z=G^KX>C7x|X|$~;K;cc2x8RGO2{{zmjPFrfkr6AVEeW2$J9*~H-4~G&}~b+Pb}JJdODU|$n1<7GPa_>l>;{NmA^y_eXTiv z)T61teOA9Q$_5GEA_ox`1gjz>3lT2b?YY_0UJayin z64qq|Nb7^UhikaEz3M8BKhNDhLIf};)NMeS8(8?3U$ThSMIh0HG;;CW$lAp0db@s0 zu&jbmCCLGE*NktXVfP3NB;MQ>p?;*$-|htv>R`#4>OG<$_n)YvUN7bwzbWEsxAGF~ zn0Vfs?Dn4}Vd|Cf5T-#a52Knf0f*#2D4Lq>-Su4g`$q={+5L$Ta|N8yfZ}rgQm;&b z0A4?$Hg5UkzI)29=>XSzdH4wH8B@_KE{mSc>e3{yGbeiBY_+?^t_a#2^*x_AmN&J$ zf9@<5N15~ty+uwrz0g5k$sL9*mKQazK2h19UW~#H_X83ap-GAGf#8Q5b8n@B8N2HvTiZu&Mg+xhthyG3#0uIny33r?t&kzBuyI$igd`%RIcO8{s$$R3+Z zt{ENUO)pqm_&<(vPf*$q1FvC}W&G)HQOJd%x4PbxogX2a4eW-%KqA5+x#x`g)fN&@ zLjG8|!rCj3y0%N)NkbJVJgDu5tOdMWS|y|Tsb)Z04-oAVZ%Mb311P}}SG#!q_ffMV z@*L#25zW6Ho?-x~8pKw4u9X)qFI7TRC)LlEL6oQ9#!*0k{=p?Vf_^?4YR(M z`uD+8&I-M*`sz5af#gd$8rr|oRMVgeI~soPKB{Q{FwV-FW)>BlS?inI8girWs=mo5b18{#~CJz!miCgQYU>KtCPt()StN;x)c2P3bMVB$o(QUh z$cRQlo_?#k`7A{Tw z!~_YKSd(%1dBM+KE!5I2)ZZsGz|`+*fB*n}yxtKVyx14Ba#1H&(%P{RubhEf9thF1v;3|2E37{m+a>GbI`Jdw*pGcA%L+*Q#&*YQOJ$_%U#(BDn``;rKxi&&)LfRxIZ*98z8UWRslDo@Xu)QVh}rB>bKwe@Bjzwg%m$hd zG)gFMgHZlPxGcm3paLLb44yHI|Ag0wdp!_yD5R<|B29Ui~27`?vfy#ktk_KyHWMDA42{J=Uq-o}i z*%kZ@45mQ-Rw?0?K+z{&5KFc}xc5Q%1PFAbL_xCmpj?JNAm>L6SjrCMpiK}5LG0ZE zO>_%)r1c48n{Iv*t(u1=&kH zeO=ifbFy+6aSK)V_5t;NKhE#$Iz=+Oii|KDJ}W>g}0%`Svgra*tnS6TRU4iTH*e=dj~I` zym|EM*}I1?pT2#3`oZ(|3I-Y$DkeHMN=8~%YSR?;>=X?(Emci*ZIz9+t<|S1>hE8$ zVa1LmTh{DZv}x6@Wz!a}+qZDz%AHHMuHCzM^XlEpr!QPzf9QzkS_0!&1MPx*ICxe}RFdTH+c}l9E`G zYL#4+3Zxi}3=A!G4S>ir#L(2r)WFKnP}jiR%D`ZOPH`@ZhTQy=%(P0}8ZH)|z6jL7 N;OXk;vd$@?2>?>Ex^Vyi literal 0 HcmV?d00001 diff --git a/launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png new file mode 100644 index 0000000000000000000000000000000000000000..bcbf36df2f2aaaa0a63c7dabc94e600184229d0d GIT binary patch literal 5933 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 f$KZHO*s&vt|9^w-6P?|#0pRK8NSwWJ?9znhg z3{`3j3=J&|48MRv4KElNN(~qoUL`OvSj}Ky5HFasE6@fg!ItFh?!xdN1Q+aGJ{c&& zS>O>_%)r1c48n{Iv*t(u1=&kHeO=ifbFy+6aSK)V_AxLppYn8Z42d|rc6w}vOsL55 z`t&mC&y2@JTEyg!eDiFX^k#CC!jq%>erB=yHqUP0XcDOTw6ko}L zX;EmMrq(fKk*eygEuA616;0)>@A{TK|55PV@70 z$OfzS*(VJxQev3J?yY?O=ul(v`fp}?u9z`JK3ugibK>)DyCwImZOF4d{xK%%Ks1*} zv$oa)9anR%lXIBUqYnhLmT>VOzHfNP?ZwJNZ!5$s9M08RynIvaXw>@G^T9@r9^KH1 zVy??F&uuk)bH9Y4pQY!hP58i_H6 znl-NcuCpLV6ZWU;4C zu@9exF&OZi`Bovq_m%T+WhU2kvkz@^_LpycBvqm3bMpLw8X-Or5sL>0AKE1$(k_L=_Zc=CUq#=x1-QZf)G7nHu@fmsQ1eN_N3+nTEz`4HI4Z6uVlE zJH+X&det8JU?tO?upcM4Z=cV!JV;yF>FfL5Q$M|W_2Z!P`S=}Wzp|_1^#d%e?_H`> zV@%vA$+bFVqhw9`U;TfP|5|PD{||OiYdor8P*i??|NJcb%kzT_73*7WE?Ua5hAnR2 z=7WE=PhTlJ#ZeRznjTUb;`E(wkMZrj4e|Hilz-mK>9cZHQY**5TUPw~u}k;u73KI}xAx!0m-)GVia|x^d3p~s_9gh83jA&Ra<8rM%`>U3x69t&NzbwWY}7Ar?)FK#IZ0z|d0H0EkRO w3{9;}4Xg|ebq&m|3=9_N6z8I7$jwj5OsmAL;bP(Gi$Dzwp00i_>zopr02+f8CIA2c literal 0 HcmV?d00001 diff --git a/launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/launchpad_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png new file mode 100644 index 0000000000000000000000000000000000000000..e71a726136a47ed24125c7efc79d68a4a01961b4 GIT binary patch literal 14800 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 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launchpad_app/macos/Runner/Configs/AppInfo.xcconfig b/launchpad_app/macos/Runner/Configs/AppInfo.xcconfig new file mode 100644 index 00000000..8b42559e --- /dev/null +++ b/launchpad_app/macos/Runner/Configs/AppInfo.xcconfig @@ -0,0 +1,14 @@ +// Application-level settings for the Runner target. +// +// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the +// future. If not, the values below would default to using the project name when this becomes a +// 'flutter create' template. + +// The application's name. By default this is also the title of the Flutter window. +PRODUCT_NAME = example + +// The application's bundle identifier +PRODUCT_BUNDLE_IDENTIFIER = com.example.example + +// The copyright displayed in application information +PRODUCT_COPYRIGHT = Copyright © 2022 com.example. All rights reserved. diff --git a/launchpad_app/macos/Runner/Configs/Debug.xcconfig b/launchpad_app/macos/Runner/Configs/Debug.xcconfig new file mode 100644 index 00000000..36b0fd94 --- /dev/null +++ b/launchpad_app/macos/Runner/Configs/Debug.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Debug.xcconfig" +#include "Warnings.xcconfig" diff --git a/launchpad_app/macos/Runner/Configs/Release.xcconfig b/launchpad_app/macos/Runner/Configs/Release.xcconfig new file mode 100644 index 00000000..dff4f495 --- /dev/null +++ b/launchpad_app/macos/Runner/Configs/Release.xcconfig @@ -0,0 +1,2 @@ +#include "../../Flutter/Flutter-Release.xcconfig" +#include "Warnings.xcconfig" diff --git a/launchpad_app/macos/Runner/Configs/Warnings.xcconfig b/launchpad_app/macos/Runner/Configs/Warnings.xcconfig new file mode 100644 index 00000000..42bcbf47 --- /dev/null +++ b/launchpad_app/macos/Runner/Configs/Warnings.xcconfig @@ -0,0 +1,13 @@ +WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings +GCC_WARN_UNDECLARED_SELECTOR = YES +CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES +CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE +CLANG_WARN__DUPLICATE_METHOD_MATCH = YES +CLANG_WARN_PRAGMA_PACK = YES +CLANG_WARN_STRICT_PROTOTYPES = YES +CLANG_WARN_COMMA = YES +GCC_WARN_STRICT_SELECTOR_MATCH = YES +CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES +CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES +GCC_WARN_SHADOW = YES +CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/launchpad_app/macos/Runner/DebugProfile.entitlements b/launchpad_app/macos/Runner/DebugProfile.entitlements new file mode 100644 index 00000000..dddb8a30 --- /dev/null +++ b/launchpad_app/macos/Runner/DebugProfile.entitlements @@ -0,0 +1,12 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.cs.allow-jit + + com.apple.security.network.server + + + diff --git a/launchpad_app/macos/Runner/Info.plist b/launchpad_app/macos/Runner/Info.plist new file mode 100644 index 00000000..4789daa6 --- /dev/null +++ b/launchpad_app/macos/Runner/Info.plist @@ -0,0 +1,32 @@ + + + + + CFBundleDevelopmentRegion + $(DEVELOPMENT_LANGUAGE) + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL + CFBundleShortVersionString + $(FLUTTER_BUILD_NAME) + CFBundleVersion + $(FLUTTER_BUILD_NUMBER) + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + NSHumanReadableCopyright + $(PRODUCT_COPYRIGHT) + NSMainNibFile + MainMenu + NSPrincipalClass + NSApplication + + diff --git a/launchpad_app/macos/Runner/MainFlutterWindow.swift b/launchpad_app/macos/Runner/MainFlutterWindow.swift new file mode 100644 index 00000000..2722837e --- /dev/null +++ b/launchpad_app/macos/Runner/MainFlutterWindow.swift @@ -0,0 +1,15 @@ +import Cocoa +import FlutterMacOS + +class MainFlutterWindow: NSWindow { + override func awakeFromNib() { + let flutterViewController = FlutterViewController.init() + let windowFrame = self.frame + self.contentViewController = flutterViewController + self.setFrame(windowFrame, display: true) + + RegisterGeneratedPlugins(registry: flutterViewController) + + super.awakeFromNib() + } +} diff --git a/launchpad_app/macos/Runner/Release.entitlements b/launchpad_app/macos/Runner/Release.entitlements new file mode 100644 index 00000000..852fa1a4 --- /dev/null +++ b/launchpad_app/macos/Runner/Release.entitlements @@ -0,0 +1,8 @@ + + + + + com.apple.security.app-sandbox + + + diff --git a/launchpad_app/pubspec.lock b/launchpad_app/pubspec.lock new file mode 100644 index 00000000..c1e8799f --- /dev/null +++ b/launchpad_app/pubspec.lock @@ -0,0 +1,740 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _discoveryapis_commons: + dependency: transitive + description: + name: _discoveryapis_commons + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + url: "https://pub.dartlang.org" + source: hosted + version: "50.0.0" + analyzer: + dependency: transitive + description: + name: analyzer + url: "https://pub.dartlang.org" + source: hosted + version: "5.2.0" + analyzer_plugin: + dependency: transitive + description: + name: analyzer_plugin + url: "https://pub.dartlang.org" + source: hosted + version: "0.11.2" + ansicolor: + dependency: transitive + description: + name: ansicolor + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + archive: + dependency: transitive + description: + name: archive + url: "https://pub.dartlang.org" + source: hosted + version: "3.3.5" + args: + dependency: transitive + description: + name: args + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.1" + async: + dependency: transitive + description: + name: async + url: "https://pub.dartlang.org" + source: hosted + version: "2.9.0" + auto_size_text: + dependency: "direct main" + description: + name: auto_size_text + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + characters: + dependency: "direct main" + description: + name: characters + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + charcode: + dependency: transitive + description: + name: charcode + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + clipboard: + dependency: "direct main" + description: + name: clipboard + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.3" + clock: + dependency: transitive + description: + name: clock + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + collection: + dependency: transitive + description: + name: collection + url: "https://pub.dartlang.org" + source: hosted + version: "1.16.0" + convert: + dependency: transitive + description: + name: convert + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" + crypto: + dependency: transitive + description: + name: crypto + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + csslib: + dependency: transitive + description: + name: csslib + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.2" + dart_code_metrics: + dependency: "direct main" + description: + name: dart_code_metrics + url: "https://pub.dartlang.org" + source: hosted + version: "5.2.1" + dart_style: + dependency: transitive + description: + name: dart_style + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.4" + dbus: + dependency: transitive + description: + name: dbus + url: "https://pub.dartlang.org" + source: hosted + version: "0.7.8" + device_info_plus: + dependency: "direct main" + description: + name: device_info_plus + url: "https://pub.dartlang.org" + source: hosted + version: "8.0.0" + device_info_plus_platform_interface: + dependency: transitive + description: + name: device_info_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "7.0.0" + drift: + dependency: "direct main" + description: + name: drift + url: "https://pub.dartlang.org" + source: hosted + version: "2.3.0" + email_validator: + dependency: "direct main" + description: + name: email_validator + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + fake_async: + dependency: transitive + description: + name: fake_async + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.4" + fluent_ui: + dependency: "direct main" + description: + name: fluent_ui + url: "https://pub.dartlang.org" + source: hosted + version: "4.1.2" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_acrylic: + dependency: "direct main" + description: + name: flutter_acrylic + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0+2" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_markdown: + dependency: "direct main" + description: + name: flutter_markdown + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.10+5" + flutter_native_splash: + dependency: "direct main" + description: + name: flutter_native_splash + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.16" + flutter_syntax_view: + dependency: "direct main" + description: + path: "." + ref: HEAD + resolved-ref: "8b0fb4944fc36f691e40fdc944d08b7bc16973ea" + url: "https://github.com/YehudaKremer/flutter_syntax_view.git" + source: git + version: "4.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + glob: + dependency: transitive + description: + name: glob + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.1" + googleapis: + dependency: "direct main" + description: + name: googleapis + url: "https://pub.dartlang.org" + source: hosted + version: "9.2.0" + html: + dependency: transitive + description: + name: html + url: "https://pub.dartlang.org" + source: hosted + version: "0.15.1" + http: + dependency: "direct main" + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.13.4" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.1" + image: + dependency: transitive + description: + name: image + url: "https://pub.dartlang.org" + source: hosted + version: "3.2.2" + intl: + dependency: transitive + description: + name: intl + url: "https://pub.dartlang.org" + source: hosted + version: "0.17.0" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" + json_annotation: + dependency: transitive + description: + name: json_annotation + url: "https://pub.dartlang.org" + source: hosted + version: "4.7.0" + lints: + dependency: transitive + description: + name: lints + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + markdown: + dependency: transitive + description: + name: markdown + url: "https://pub.dartlang.org" + source: hosted + version: "5.0.0" + matcher: + dependency: transitive + description: + name: matcher + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.12" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.5" + meta: + dependency: transitive + description: + name: meta + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.0" + nested: + dependency: transitive + description: + name: nested + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" + network_info_plus: + dependency: "direct main" + description: + name: network_info_plus + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + network_info_plus_platform_interface: + dependency: transitive + description: + name: network_info_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.3" + nm: + dependency: transitive + description: + name: nm + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.0" + package_config: + dependency: transitive + description: + name: package_config + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + package_info_plus: + dependency: "direct main" + description: + name: package_info_plus + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.2" + package_info_plus_platform_interface: + dependency: transitive + description: + name: package_info_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + path: + dependency: transitive + description: + name: path + url: "https://pub.dartlang.org" + source: hosted + version: "1.8.2" + path_provider: + dependency: "direct main" + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.22" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.7" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + petitparser: + dependency: transitive + description: + name: petitparser + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.0" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + pointycastle: + dependency: transitive + description: + name: pointycastle + url: "https://pub.dartlang.org" + source: hosted + version: "3.6.2" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" + provider: + dependency: "direct main" + description: + name: provider + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.3" + pub_semver: + dependency: transitive + description: + name: pub_semver + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + pub_updater: + dependency: transitive + description: + name: pub_updater + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.3" + recase: + dependency: transitive + description: + name: recase + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.0" + screen_retriever: + dependency: transitive + description: + name: screen_retriever + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.2" + scroll_pos: + dependency: transitive + description: + name: scroll_pos + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.0" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + source_span: + dependency: transitive + description: + name: source_span + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.0" + sqlite3: + dependency: transitive + description: + name: sqlite3 + url: "https://pub.dartlang.org" + source: hosted + version: "1.9.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + url: "https://pub.dartlang.org" + source: hosted + version: "1.10.0" + stream_channel: + dependency: transitive + description: + name: stream_channel + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + string_scanner: + dependency: transitive + description: + name: string_scanner + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.1" + system_theme: + dependency: "direct main" + description: + name: system_theme + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" + system_theme_web: + dependency: transitive + description: + name: system_theme_web + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.2" + term_glyph: + dependency: transitive + description: + name: term_glyph + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.12" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + universal_io: + dependency: transitive + description: + name: universal_io + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.4" + url_launcher: + dependency: "direct main" + description: + name: url_launcher + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.5" + url_launcher_android: + dependency: transitive + description: + name: url_launcher_android + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.13" + url_launcher_ios: + dependency: transitive + description: + name: url_launcher_ios + url: "https://pub.dartlang.org" + source: hosted + version: "6.0.13" + url_launcher_linux: + dependency: transitive + description: + name: url_launcher_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + url_launcher_macos: + dependency: transitive + description: + name: url_launcher_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + url_launcher_platform_interface: + dependency: transitive + description: + name: url_launcher_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.0" + url_launcher_web: + dependency: transitive + description: + name: url_launcher_web + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + url_launcher_windows: + dependency: transitive + description: + name: url_launcher_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + url_strategy: + dependency: "direct main" + description: + name: url_strategy + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0" + vector_math: + dependency: transitive + description: + name: vector_math + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.2" + watcher: + dependency: transitive + description: + name: watcher + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" + win32: + dependency: "direct main" + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.3" + window_manager: + dependency: "direct main" + description: + name: window_manager + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.7" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+2" + xml: + dependency: transitive + description: + name: xml + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.0" + yaml: + dependency: transitive + description: + name: yaml + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.1" +sdks: + dart: ">=2.18.0 <3.0.0" + flutter: ">=3.0.0" diff --git a/launchpad_app/pubspec.yaml b/launchpad_app/pubspec.yaml new file mode 100644 index 00000000..ea3a7731 --- /dev/null +++ b/launchpad_app/pubspec.yaml @@ -0,0 +1,50 @@ +name: example +description: Showcase App for Fluent UI package for Flutter + +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +version: 1.0.0+1 + +environment: + sdk: '>=2.17.0 <3.0.0' + +dependencies: + flutter: + sdk: flutter + fluent_ui: ^4.1.2 + provider: ^6.0.3 + system_theme: ^2.0.0 + window_manager: ^0.2.7 + flutter_acrylic: ^1.0.0+2 + url_strategy: ^0.2.0 + url_launcher: ^6.1.5 + clipboard: ^0.1.3 + email_validator: ^2.0.1 + http: ^0.13.4 + flutter_markdown: ^0.6.10+5 + flutter_syntax_view: + git: + url: https://github.com/YehudaKremer/flutter_syntax_view.git + dart_code_metrics: ^5.2.1 + path_provider: ^2.0.11 + auto_size_text: ^3.0.0 + device_info_plus: ^8.0.0 + package_info_plus: ^3.0.2 + googleapis: ^9.2.0 + win32: ^3.1.3 + drift: ^2.3.0 + characters: ^1.2.1 + network_info_plus: ^3.0.1 + flutter_native_splash: ^2.2.16 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^1.0.4 + +flutter: + uses-material-design: true + +flutter_native_splash: + color: "#42a5f5" + image: web/assets/flutter_logo.png \ No newline at end of file diff --git a/launchpad_app/web/assets/flutter_logo.png b/launchpad_app/web/assets/flutter_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..933cc3d1aaab53770e9833f391aa20dc54668590 GIT binary patch literal 7048 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 literal 0 HcmV?d00001 diff --git a/launchpad_app/web/icons/Icon-192.png b/launchpad_app/web/icons/Icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..b749bfef07473333cf1dd31e9eed89862a5d52aa GIT binary patch literal 5292 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 literal 0 HcmV?d00001 diff --git a/launchpad_app/web/icons/Icon-512.png b/launchpad_app/web/icons/Icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..88cfd48dff1169879ba46840804b412fe02fefd6 GIT binary patch literal 8252 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 literal 0 HcmV?d00001 diff --git a/launchpad_app/web/icons/Icon-maskable-192.png b/launchpad_app/web/icons/Icon-maskable-192.png new file mode 100644 index 0000000000000000000000000000000000000000..eb9b4d76e525556d5d89141648c724331630325d GIT binary patch literal 5594 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 X>c!)3PgKfL&lX^$Z>Cpu&6)6jvi^Z! literal 0 HcmV?d00001 diff --git a/launchpad_app/web/icons/Icon-maskable-512.png b/launchpad_app/web/icons/Icon-maskable-512.png new file mode 100644 index 0000000000000000000000000000000000000000..d69c56691fbdb0b7efa65097c7cc1edac12a6d3e GIT binary patch literal 20998 zcmeFZ_gj-)&^4Nb2tlbLMU<{!p(#yjqEe+=0IA_oih%ScH9@5#MNp&}Y#;;(h=A0@ zh7{>lT2MkSQ344eAvrhici!td|HJuyvJm#Y_w1Q9Yu3!26dNlO-oxUDK_C#XnW^Co z5C{VN6#{~B0)K2j7}*1Xq(Nqemv23A-6&=ZpEijkVnSwVGqLv40?n0=p;k3-U5e5+ z+z3>aS`u9DS=!wg8ROu?X4TFoW6CFLL&{GzoVT)ldhLekLM|+j3tIxRd|*5=c{=s&*vfPdBr(Fyj(v@%eQj1Soy7m4^@VRl1~@-PV7y+c!xz$8436WBn$t{=}mEdK#k`aystimGgI{(IBx$!pAwFoE9Y`^t^;> zKAD)C(Dl^s%`?q5$P|fZf8Xymrtu^Pv(7D`rn>Z-w$Ahs!z9!94WNVxrJuXfHAaxg zC6s@|Z1$7R$(!#t%Jb{{s6(Y?NoQXDYq)!}X@jKPhe`{9KQ@sAU8y-5`xt?S9$jKH zoi}6m5PcG*^{kjvt+kwPpyQzVg4o)a>;LK`aaN2x4@itBD3Aq?yWTM20VRn1rrd+2 zKO=P0rMjEGq_UqpMa`~7B|p?xAN1SCoCp}QxAv8O`jLJ5CVh@umR%c%i^)6!o+~`F zaalSTQcl5iwOLC&H)efzd{8(88mo`GI(56T<(&p7>Qd^;R1hn1Y~jN~tApaL8>##U zd65bo8)79CplWxr#z4!6HvLz&N7_5AN#x;kLG?zQ(#p|lj<8VUlKY=Aw!ATqeL-VG z42gA!^cMNPj>(`ZMEbCrnkg*QTsn*u(nQPWI9pA{MQ=IsPTzd7q5E#7+z>Ch=fx$~ z;J|?(5jTo5UWGvsJa(Sx0?S#56+8SD!I^tftyeh_{5_31l6&Hywtn`bbqYDqGZXI( zCG7hBgvksX2ak8+)hB4jnxlO@A32C_RM&g&qDSb~3kM&)@A_j1*oTO@nicGUyv+%^ z=vB)4(q!ykzT==Z)3*3{atJ5}2PV*?Uw+HhN&+RvKvZL3p9E?gHjv{6zM!A|z|UHK z-r6jeLxbGn0D@q5aBzlco|nG2tr}N@m;CJX(4#Cn&p&sLKwzLFx1A5izu?X_X4x8r@K*d~7>t1~ zDW1Mv5O&WOxbzFC`DQ6yNJ(^u9vJdj$fl2dq`!Yba_0^vQHXV)vqv1gssZYzBct!j zHr9>ydtM8wIs}HI4=E}qAkv|BPWzh3^_yLH(|kdb?x56^BlDC)diWyPd*|f!`^12_U>TD^^94OCN0lVv~Sgvs94ecpE^}VY$w`qr_>Ue zTfH~;C<3H<0dS5Rkf_f@1x$Gms}gK#&k()IC0zb^QbR!YLoll)c$Agfi6MKI0dP_L z=Uou&u~~^2onea2%XZ@>`0x^L8CK6=I{ge;|HXMj)-@o~h&O{CuuwBX8pVqjJ*o}5 z#8&oF_p=uSo~8vn?R0!AMWvcbZmsrj{ZswRt(aEdbi~;HeVqIe)-6*1L%5u$Gbs}| zjFh?KL&U(rC2izSGtwP5FnsR@6$-1toz?RvLD^k~h9NfZgzHE7m!!7s6(;)RKo2z} zB$Ci@h({l?arO+vF;s35h=|WpefaOtKVx>l399}EsX@Oe3>>4MPy%h&^3N_`UTAHJ zI$u(|TYC~E4)|JwkWW3F!Tib=NzjHs5ii2uj0^m|Qlh-2VnB#+X~RZ|`SA*}}&8j9IDv?F;(Y^1=Z0?wWz;ikB zewU>MAXDi~O7a~?jx1x=&8GcR-fTp>{2Q`7#BE#N6D@FCp`?ht-<1|y(NArxE_WIu zP+GuG=Qq>SHWtS2M>34xwEw^uvo4|9)4s|Ac=ud?nHQ>ax@LvBqusFcjH0}{T3ZPQ zLO1l<@B_d-(IS682}5KA&qT1+{3jxKolW+1zL4inqBS-D>BohA!K5++41tM@ z@xe<-qz27}LnV#5lk&iC40M||JRmZ*A##K3+!j93eouU8@q-`W0r%7N`V$cR&JV;iX(@cS{#*5Q>~4BEDA)EikLSP@>Oo&Bt1Z~&0d5)COI%3$cLB_M?dK# z{yv2OqW!al-#AEs&QFd;WL5zCcp)JmCKJEdNsJlL9K@MnPegK23?G|O%v`@N{rIRa zi^7a}WBCD77@VQ-z_v{ZdRsWYrYgC$<^gRQwMCi6);%R~uIi31OMS}=gUTE(GKmCI z$zM>mytL{uNN+a&S38^ez(UT=iSw=l2f+a4)DyCA1Cs_N-r?Q@$3KTYosY!;pzQ0k zzh1G|kWCJjc(oZVBji@kN%)UBw(s{KaYGy=i{g3{)Z+&H8t2`^IuLLKWT6lL<-C(! zSF9K4xd-|VO;4}$s?Z7J_dYqD#Mt)WCDnsR{Kpjq275uUq6`v0y*!PHyS(}Zmv)_{>Vose9-$h8P0|y;YG)Bo}$(3Z%+Gs0RBmFiW!^5tBmDK-g zfe5%B*27ib+7|A*Fx5e)2%kIxh7xWoc3pZcXS2zik!63lAG1;sC1ja>BqH7D zODdi5lKW$$AFvxgC-l-)!c+9@YMC7a`w?G(P#MeEQ5xID#<}W$3bSmJ`8V*x2^3qz zVe<^^_8GHqYGF$nIQm0Xq2kAgYtm#UC1A(=&85w;rmg#v906 zT;RyMgbMpYOmS&S9c38^40oUp?!}#_84`aEVw;T;r%gTZkWeU;;FwM@0y0adt{-OK z(vGnPSlR=Nv2OUN!2=xazlnHPM9EWxXg2EKf0kI{iQb#FoP>xCB<)QY>OAM$Dcdbm zU6dU|%Mo(~avBYSjRc13@|s>axhrPl@Sr81{RSZUdz4(=|82XEbV*JAX6Lfbgqgz584lYgi0 z2-E{0XCVON$wHfvaLs;=dqhQJ&6aLn$D#0i(FkAVrXG9LGm3pSTf&f~RQb6|1_;W> z?n-;&hrq*~L=(;u#jS`*Yvh@3hU-33y_Kv1nxqrsf>pHVF&|OKkoC)4DWK%I!yq?P z=vXo8*_1iEWo8xCa{HJ4tzxOmqS0&$q+>LroMKI*V-rxhOc%3Y!)Y|N6p4PLE>Yek>Y(^KRECg8<|%g*nQib_Yc#A5q8Io z6Ig&V>k|~>B6KE%h4reAo*DfOH)_01tE0nWOxX0*YTJgyw7moaI^7gW*WBAeiLbD?FV9GSB zPv3`SX*^GRBM;zledO`!EbdBO_J@fEy)B{-XUTVQv}Qf~PSDpK9+@I`7G7|>Dgbbu z_7sX9%spVo$%qwRwgzq7!_N;#Td08m5HV#?^dF-EV1o)Q=Oa+rs2xH#g;ykLbwtCh znUnA^dW!XjspJ;otq$yV@I^s9Up(5k7rqhQd@OLMyyxVLj_+$#Vc*}Usevp^I(^vH zmDgHc0VMme|K&X?9&lkN{yq_(If)O`oUPW8X}1R5pSVBpfJe0t{sPA(F#`eONTh_) zxeLqHMfJX#?P(@6w4CqRE@Eiza; z;^5)Kk=^5)KDvd9Q<`=sJU8rjjxPmtWMTmzcH={o$U)j=QBuHarp?=}c??!`3d=H$nrJMyr3L-& zA#m?t(NqLM?I3mGgWA_C+0}BWy3-Gj7bR+d+U?n*mN$%5P`ugrB{PeV>jDUn;eVc- zzeMB1mI4?fVJatrNyq|+zn=!AiN~<}eoM#4uSx^K?Iw>P2*r=k`$<3kT00BE_1c(02MRz4(Hq`L^M&xt!pV2 zn+#U3@j~PUR>xIy+P>51iPayk-mqIK_5rlQMSe5&tDkKJk_$i(X&;K(11YGpEc-K= zq4Ln%^j>Zi_+Ae9eYEq_<`D+ddb8_aY!N;)(&EHFAk@Ekg&41ABmOXfWTo)Z&KotA zh*jgDGFYQ^y=m)<_LCWB+v48DTJw*5dwMm_YP0*_{@HANValf?kV-Ic3xsC}#x2h8 z`q5}d8IRmqWk%gR)s~M}(Qas5+`np^jW^oEd-pzERRPMXj$kS17g?H#4^trtKtq;C?;c ztd|%|WP2w2Nzg@)^V}!Gv++QF2!@FP9~DFVISRW6S?eP{H;;8EH;{>X_}NGj^0cg@ z!2@A>-CTcoN02^r6@c~^QUa={0xwK0v4i-tQ9wQq^=q*-{;zJ{Qe%7Qd!&X2>rV@4 z&wznCz*63_vw4>ZF8~%QCM?=vfzW0r_4O^>UA@otm_!N%mH)!ERy&b!n3*E*@?9d^ zu}s^By@FAhG(%?xgJMuMzuJw2&@$-oK>n z=UF}rt%vuaP9fzIFCYN-1&b#r^Cl6RDFIWsEsM|ROf`E?O(cy{BPO2Ie~kT+^kI^i zp>Kbc@C?}3vy-$ZFVX#-cx)Xj&G^ibX{pWggtr(%^?HeQL@Z( zM-430g<{>vT*)jK4aY9(a{lSy{8vxLbP~n1MXwM527ne#SHCC^F_2@o`>c>>KCq9c(4c$VSyMl*y3Nq1s+!DF| z^?d9PipQN(mw^j~{wJ^VOXDCaL$UtwwTpyv8IAwGOg<|NSghkAR1GSNLZ1JwdGJYm zP}t<=5=sNNUEjc=g(y)1n5)ynX(_$1-uGuDR*6Y^Wgg(LT)Jp><5X|}bt z_qMa&QP?l_n+iVS>v%s2Li_;AIeC=Ca^v1jX4*gvB$?H?2%ndnqOaK5-J%7a} zIF{qYa&NfVY}(fmS0OmXA70{znljBOiv5Yod!vFU{D~*3B3Ka{P8?^ zfhlF6o7aNT$qi8(w<}OPw5fqA7HUje*r*Oa(YV%*l0|9FP9KW@U&{VSW{&b0?@y)M zs%4k1Ax;TGYuZ9l;vP5@?3oQsp3)rjBeBvQQ>^B;z5pc=(yHhHtq6|0m(h4envn_j787fizY@V`o(!SSyE7vlMT zbo=Z1c=atz*G!kwzGB;*uPL$Ei|EbZLh8o+1BUMOpnU(uX&OG1MV@|!&HOOeU#t^x zr9=w2ow!SsTuJWT7%Wmt14U_M*3XiWBWHxqCVZI0_g0`}*^&yEG9RK9fHK8e+S^m? zfCNn$JTswUVbiC#>|=wS{t>-MI1aYPLtzO5y|LJ9nm>L6*wpr_m!)A2Fb1RceX&*|5|MwrvOk4+!0p99B9AgP*9D{Yt|x=X}O% zgIG$MrTB=n-!q%ROT|SzH#A$Xm;|ym)0>1KR}Yl0hr-KO&qMrV+0Ej3d@?FcgZ+B3 ztEk16g#2)@x=(ko8k7^Tq$*5pfZHC@O@}`SmzT1(V@x&NkZNM2F#Q-Go7-uf_zKC( zB(lHZ=3@dHaCOf6C!6i8rDL%~XM@rVTJbZL09?ht@r^Z_6x}}atLjvH^4Vk#Ibf(^LiBJFqorm?A=lE zzFmwvp4bT@Nv2V>YQT92X;t9<2s|Ru5#w?wCvlhcHLcsq0TaFLKy(?nzezJ>CECqj zggrI~Hd4LudM(m{L@ezfnpELsRFVFw>fx;CqZtie`$BXRn#Ns%AdoE$-Pf~{9A8rV zf7FbgpKmVzmvn-z(g+&+-ID=v`;6=)itq8oM*+Uz**SMm_{%eP_c0{<%1JGiZS19o z@Gj7$Se~0lsu}w!%;L%~mIAO;AY-2i`9A*ZfFs=X!LTd6nWOZ7BZH2M{l2*I>Xu)0 z`<=;ObglnXcVk!T>e$H?El}ra0WmPZ$YAN0#$?|1v26^(quQre8;k20*dpd4N{i=b zuN=y}_ew9SlE~R{2+Rh^7%PA1H5X(p8%0TpJ=cqa$65XL)$#ign-y!qij3;2>j}I; ziO@O|aYfn&up5F`YtjGw68rD3{OSGNYmBnl?zdwY$=RFsegTZ=kkzRQ`r7ZjQP!H( zp4>)&zf<*N!tI00xzm-ME_a{_I!TbDCr;8E;kCH4LlL-tqLxDuBn-+xgPk37S&S2^ z2QZumkIimwz!c@!r0)j3*(jPIs*V!iLTRl0Cpt_UVNUgGZzdvs0(-yUghJfKr7;=h zD~y?OJ-bWJg;VdZ^r@vlDoeGV&8^--!t1AsIMZ5S440HCVr%uk- z2wV>!W1WCvFB~p$P$$_}|H5>uBeAe>`N1FI8AxM|pq%oNs;ED8x+tb44E) zTj{^fbh@eLi%5AqT?;d>Es5D*Fi{Bpk)q$^iF!!U`r2hHAO_?#!aYmf>G+jHsES4W zgpTKY59d?hsb~F0WE&dUp6lPt;Pm zcbTUqRryw^%{ViNW%Z(o8}dd00H(H-MmQmOiTq{}_rnwOr*Ybo7*}3W-qBT!#s0Ie z-s<1rvvJx_W;ViUD`04%1pra*Yw0BcGe)fDKUK8aF#BwBwMPU;9`!6E(~!043?SZx z13K%z@$$#2%2ovVlgFIPp7Q6(vO)ud)=*%ZSucL2Dh~K4B|%q4KnSpj#n@(0B})!9 z8p*hY@5)NDn^&Pmo;|!>erSYg`LkO?0FB@PLqRvc>4IsUM5O&>rRv|IBRxi(RX(gJ ztQ2;??L~&Mv;aVr5Q@(?y^DGo%pO^~zijld41aA0KKsy_6FeHIn?fNHP-z>$OoWer zjZ5hFQTy*-f7KENRiCE$ZOp4|+Wah|2=n@|W=o}bFM}Y@0e62+_|#fND5cwa3;P{^pEzlJbF1Yq^}>=wy8^^^$I2M_MH(4Dw{F6hm+vrWV5!q;oX z;tTNhz5`-V={ew|bD$?qcF^WPR{L(E%~XG8eJx(DoGzt2G{l8r!QPJ>kpHeOvCv#w zr=SSwMDaUX^*~v%6K%O~i)<^6`{go>a3IdfZ8hFmz&;Y@P%ZygShQZ2DSHd`m5AR= zx$wWU06;GYwXOf(%MFyj{8rPFXD};JCe85Bdp4$YJ2$TzZ7Gr#+SwCvBI1o$QP0(c zy`P51FEBV2HTisM3bHqpmECT@H!Y2-bv2*SoSPoO?wLe{M#zDTy@ujAZ!Izzky~3k zRA1RQIIoC*Mej1PH!sUgtkR0VCNMX(_!b65mo66iM*KQ7xT8t2eev$v#&YdUXKwGm z7okYAqYF&bveHeu6M5p9xheRCTiU8PFeb1_Rht0VVSbm%|1cOVobc8mvqcw!RjrMRM#~=7xibH&Fa5Imc|lZ{eC|R__)OrFg4@X_ ze+kk*_sDNG5^ELmHnZ7Ue?)#6!O)#Nv*Dl2mr#2)w{#i-;}0*_h4A%HidnmclH#;Q zmQbq+P4DS%3}PpPm7K_K3d2s#k~x+PlTul7+kIKol0@`YN1NG=+&PYTS->AdzPv!> zQvzT=)9se*Jr1Yq+C{wbK82gAX`NkbXFZ)4==j4t51{|-v!!$H8@WKA={d>CWRW+g z*`L>9rRucS`vbXu0rzA1#AQ(W?6)}1+oJSF=80Kf_2r~Qm-EJ6bbB3k`80rCv(0d` zvCf3;L2ovYG_TES%6vSuoKfIHC6w;V31!oqHM8-I8AFzcd^+_86!EcCOX|Ta9k1!s z_Vh(EGIIsI3fb&dF$9V8v(sTBC%!#<&KIGF;R+;MyC0~}$gC}}= zR`DbUVc&Bx`lYykFZ4{R{xRaUQkWCGCQlEc;!mf=+nOk$RUg*7 z;kP7CVLEc$CA7@6VFpsp3_t~m)W0aPxjsA3e5U%SfY{tp5BV5jH-5n?YX7*+U+Zs%LGR>U- z!x4Y_|4{gx?ZPJobISy991O znrmrC3otC;#4^&Rg_iK}XH(XX+eUHN0@Oe06hJk}F?`$)KmH^eWz@@N%wEc)%>?Ft z#9QAroDeyfztQ5Qe{m*#R#T%-h*&XvSEn@N$hYRTCMXS|EPwzF3IIysD2waj`vQD{ zv_#^Pgr?s~I*NE=acf@dWVRNWTr(GN0wrL)Z2=`Dr>}&ZDNX|+^Anl{Di%v1Id$_p zK5_H5`RDjJx`BW7hc85|> zHMMsWJ4KTMRHGu+vy*kBEMjz*^K8VtU=bXJYdhdZ-?jTXa$&n)C?QQIZ7ln$qbGlr zS*TYE+ppOrI@AoPP=VI-OXm}FzgXRL)OPvR$a_=SsC<3Jb+>5makX|U!}3lx4tX&L z^C<{9TggZNoeX!P1jX_K5HkEVnQ#s2&c#umzV6s2U-Q;({l+j^?hi7JnQ7&&*oOy9 z(|0asVTWUCiCnjcOnB2pN0DpuTglKq;&SFOQ3pUdye*eT<2()7WKbXp1qq9=bhMWlF-7BHT|i3TEIT77AcjD(v=I207wi-=vyiw5mxgPdTVUC z&h^FEUrXwWs9en2C{ywZp;nvS(Mb$8sBEh-*_d-OEm%~p1b2EpcwUdf<~zmJmaSTO zSX&&GGCEz-M^)G$fBvLC2q@wM$;n4jp+mt0MJFLuJ%c`tSp8$xuP|G81GEd2ci$|M z4XmH{5$j?rqDWoL4vs!}W&!?!rtj=6WKJcE>)?NVske(p;|#>vL|M_$as=mi-n-()a*OU3Okmk0wC<9y7t^D(er-&jEEak2!NnDiOQ99Wx8{S8}=Ng!e0tzj*#T)+%7;aM$ z&H}|o|J1p{IK0Q7JggAwipvHvko6>Epmh4RFRUr}$*2K4dz85o7|3#Bec9SQ4Y*;> zXWjT~f+d)dp_J`sV*!w>B%)#GI_;USp7?0810&3S=WntGZ)+tzhZ+!|=XlQ&@G@~3 z-dw@I1>9n1{+!x^Hz|xC+P#Ab`E@=vY?3%Bc!Po~e&&&)Qp85!I|U<-fCXy*wMa&t zgDk!l;gk;$taOCV$&60z+}_$ykz=Ea*)wJQ3-M|p*EK(cvtIre0Pta~(95J7zoxBN zS(yE^3?>88AL0Wfuou$BM{lR1hkrRibz=+I9ccwd`ZC*{NNqL)3pCcw^ygMmrG^Yp zn5f}Xf>%gncC=Yq96;rnfp4FQL#{!Y*->e82rHgY4Zwy{`JH}b9*qr^VA{%~Z}jtp z_t$PlS6}5{NtTqXHN?uI8ut8rOaD#F1C^ls73S=b_yI#iZDOGz3#^L@YheGd>L;<( z)U=iYj;`{>VDNzIxcjbTk-X3keXR8Xbc`A$o5# zKGSk-7YcoBYuAFFSCjGi;7b<;n-*`USs)IX z=0q6WZ=L!)PkYtZE-6)azhXV|+?IVGTOmMCHjhkBjfy@k1>?yFO3u!)@cl{fFAXnRYsWk)kpT?X{_$J=|?g@Q}+kFw|%n!;Zo}|HE@j=SFMvT8v`6Y zNO;tXN^036nOB2%=KzxB?n~NQ1K8IO*UE{;Xy;N^ZNI#P+hRZOaHATz9(=)w=QwV# z`z3+P>9b?l-@$@P3<;w@O1BdKh+H;jo#_%rr!ute{|YX4g5}n?O7Mq^01S5;+lABE+7`&_?mR_z7k|Ja#8h{!~j)| zbBX;*fsbUak_!kXU%HfJ2J+G7;inu#uRjMb|8a){=^))y236LDZ$$q3LRlat1D)%7K0!q5hT5V1j3qHc7MG9 z_)Q=yQ>rs>3%l=vu$#VVd$&IgO}Za#?aN!xY>-<3PhzS&q!N<=1Q7VJBfHjug^4|) z*fW^;%3}P7X#W3d;tUs3;`O&>;NKZBMR8au6>7?QriJ@gBaorz-+`pUWOP73DJL=M z(33uT6Gz@Sv40F6bN|H=lpcO z^AJl}&=TIjdevuDQ!w0K*6oZ2JBOhb31q!XDArFyKpz!I$p4|;c}@^bX{>AXdt7Bm zaLTk?c%h@%xq02reu~;t@$bv`b3i(P=g}~ywgSFpM;}b$zAD+=I!7`V~}ARB(Wx0C(EAq@?GuxOL9X+ffbkn3+Op0*80TqmpAq~EXmv%cq36celXmRz z%0(!oMp&2?`W)ALA&#|fu)MFp{V~~zIIixOxY^YtO5^FSox8v$#d0*{qk0Z)pNTt0QVZ^$`4vImEB>;Lo2!7K05TpY-sl#sWBz_W-aDIV`Ksabi zvpa#93Svo!70W*Ydh)Qzm{0?CU`y;T^ITg-J9nfWeZ-sbw)G@W?$Eomf%Bg2frfh5 zRm1{|E0+(4zXy){$}uC3%Y-mSA2-^I>Tw|gQx|7TDli_hB>``)Q^aZ`LJC2V3U$SABP}T)%}9g2pF9dT}aC~!rFFgkl1J$ z`^z{Arn3On-m%}r}TGF8KQe*OjSJ=T|caa_E;v89A{t@$yT^(G9=N9F?^kT*#s3qhJq!IH5|AhnqFd z0B&^gm3w;YbMNUKU>naBAO@fbz zqw=n!@--}o5;k6DvTW9pw)IJVz;X}ncbPVrmH>4x);8cx;q3UyiML1PWp%bxSiS|^ zC5!kc4qw%NSOGQ*Kcd#&$30=lDvs#*4W4q0u8E02U)7d=!W7+NouEyuF1dyH$D@G& zaFaxo9Ex|ZXA5y{eZT*i*dP~INSMAi@mvEX@q5i<&o&#sM}Df?Og8n8Ku4vOux=T% zeuw~z1hR}ZNwTn8KsQHKLwe2>p^K`YWUJEdVEl|mO21Bov!D0D$qPoOv=vJJ`)|%_ z>l%`eexY7t{BlVKP!`a^U@nM?#9OC*t76My_E_<16vCz1x_#82qj2PkWiMWgF8bM9 z(1t4VdHcJ;B~;Q%x01k_gQ0>u2*OjuEWNOGX#4}+N?Gb5;+NQMqp}Puqw2HnkYuKA zzKFWGHc&K>gwVgI1Sc9OT1s6fq=>$gZU!!xsilA$fF`kLdGoX*^t}ao@+^WBpk>`8 z4v_~gK|c2rCq#DZ+H)$3v~Hoi=)=1D==e3P zpKrRQ+>O^cyTuWJ%2}__0Z9SM_z9rptd*;-9uC1tDw4+A!=+K%8~M&+Zk#13hY$Y$ zo-8$*8dD5@}XDi19RjK6T^J~DIXbF5w&l?JLHMrf0 zLv0{7*G!==o|B%$V!a=EtVHdMwXLtmO~vl}P6;S(R2Q>*kTJK~!}gloxj)m|_LYK{ zl(f1cB=EON&wVFwK?MGn^nWuh@f95SHatPs(jcwSY#Dnl1@_gkOJ5=f`%s$ZHljRH0 z+c%lrb=Gi&N&1>^L_}#m>=U=(oT^vTA&3!xXNyqi$pdW1BDJ#^{h|2tZc{t^vag3& zAD7*8C`chNF|27itjBUo^CCDyEpJLX3&u+(L;YeeMwnXEoyN(ytoEabcl$lSgx~Ltatn}b$@j_yyMrBb03)shJE*$;Mw=;mZd&8e>IzE+4WIoH zCSZE7WthNUL$|Y#m!Hn?x7V1CK}V`KwW2D$-7&ODy5Cj;!_tTOOo1Mm%(RUt)#$@3 zhurA)t<7qik%%1Et+N1?R#hdBB#LdQ7{%-C zn$(`5e0eFh(#c*hvF>WT*07fk$N_631?W>kfjySN8^XC9diiOd#s?4tybICF;wBjp zIPzilX3{j%4u7blhq)tnaOBZ_`h_JqHXuI7SuIlNTgBk9{HIS&3|SEPfrvcE<@}E` zKk$y*nzsqZ{J{uWW9;#n=de&&h>m#A#q)#zRonr(?mDOYU&h&aQWD;?Z(22wY?t$U3qo`?{+amA$^TkxL+Ex2dh`q7iR&TPd0Ymwzo#b? zP$#t=elB5?k$#uE$K>C$YZbYUX_JgnXA`oF_Ifz4H7LEOW~{Gww&3s=wH4+j8*TU| zSX%LtJWqhr-xGNSe{;(16kxnak6RnZ{0qZ^kJI5X*It_YuynSpi(^-}Lolr{)#z_~ zw!(J-8%7Ybo^c3(mED`Xz8xecP35a6M8HarxRn%+NJBE;dw>>Y2T&;jzRd4FSDO3T zt*y+zXCtZQ0bP0yf6HRpD|WmzP;DR^-g^}{z~0x~z4j8m zucTe%k&S9Nt-?Jb^gYW1w6!Y3AUZ0Jcq;pJ)Exz%7k+mUOm6%ApjjSmflfKwBo6`B zhNb@$NHTJ>guaj9S{@DX)!6)b-Shav=DNKWy(V00k(D!v?PAR0f0vDNq*#mYmUp6> z76KxbFDw5U{{qx{BRj(>?|C`82ICKbfLxoldov-M?4Xl+3;I4GzLHyPOzYw7{WQST zPNYcx5onA%MAO9??41Po*1zW(Y%Zzn06-lUp{s<3!_9vv9HBjT02On0Hf$}NP;wF) zP<`2p3}A^~1YbvOh{ePMx$!JGUPX-tbBzp3mDZMY;}h;sQ->!p97GA)9a|tF(Gh{1$xk7 zUw?ELkT({Xw!KIr);kTRb1b|UL`r2_`a+&UFVCdJ)1T#fdh;71EQl9790Br0m_`$x z9|ZANuchFci8GNZ{XbP=+uXSJRe(;V5laQz$u18#?X*9}x7cIEbnr%<=1cX3EIu7$ zhHW6pe5M(&qEtsqRa>?)*{O;OJT+YUhG5{km|YI7I@JL_3Hwao9aXneiSA~a* z|Lp@c-oMNyeAEuUz{F?kuou3x#C*gU?lon!RC1s37gW^0Frc`lqQWH&(J4NoZg3m8 z;Lin#8Q+cFPD7MCzj}#|ws7b@?D9Q4dVjS4dpco=4yX5SSH=A@U@yqPdp@?g?qeia zH=Tt_9)G=6C2QIPsi-QipnK(mc0xXIN;j$WLf@n8eYvMk;*H-Q4tK%(3$CN}NGgO8n}fD~+>?<3UzvsrMf*J~%i;VKQHbF%TPalFi=#sgj)(P#SM^0Q=Tr>4kJVw8X3iWsP|e8tj}NjlMdWp z@2+M4HQu~3!=bZpjh;;DIDk&X}=c8~kn)FWWH z2KL1w^rA5&1@@^X%MjZ7;u(kH=YhH2pJPFQe=hn>tZd5RC5cfGYis8s9PKaxi*}-s6*W zRA^PwR=y^5Z){!(4D9-KC;0~;b*ploznFOaU`bJ_7U?qAi#mTo!&rIECRL$_y@yI27x2?W+zqDBD5~KCVYKFZLK+>ABC(Kj zeAll)KMgIlAG`r^rS{loBrGLtzhHY8$)<_S<(Dpkr(Ym@@vnQ&rS@FC*>2@XCH}M+an74WcRDcoQ+a3@A z9tYhl5$z7bMdTvD2r&jztBuo37?*k~wcU9GK2-)MTFS-lux-mIRYUuGUCI~V$?s#< z?1qAWb(?ZLm(N>%S%y10COdaq_Tm5c^%ooIxpR=`3e4C|@O5wY+eLik&XVi5oT7oe zmxH)Jd*5eo@!7t`x8!K=-+zJ-Sz)B_V$)s1pW~CDU$=q^&ABvf6S|?TOMB-RIm@CoFg>mjIQE)?+A1_3s6zmFU_oW&BqyMz1mY*IcP_2knjq5 zqw~JK(cVsmzc7*EvTT2rvpeqhg)W=%TOZ^>f`rD4|7Z5fq*2D^lpCttIg#ictgqZ$P@ru6P#f$x#KfnfTZj~LG6U_d-kE~`;kU_X)`H5so@?C zWmb!7x|xk@0L~0JFall*@ltyiL^)@3m4MqC7(7H0sH!WidId1#f#6R{Q&A!XzO1IAcIx;$k66dumt6lpUw@nL2MvqJ5^kbOVZ<^2jt5-njy|2@`07}0w z;M%I1$FCoLy`8xp8Tk)bFr;7aJeQ9KK6p=O$U0-&JYYy8woV*>b+FB?xLX`=pirYM z5K$BA(u)+jR{?O2r$c_Qvl?M{=Ar{yQ!UVsVn4k@0!b?_lA;dVz9uaQUgBH8Oz(Sb zrEs;&Ey>_ex8&!N{PmQjp+-Hlh|OA&wvDai#GpU=^-B70V0*LF=^bi+Nhe_o|azZ%~ZZ1$}LTmWt4aoB1 zPgccm$EwYU+jrdBaQFxQfn5gd(gM`Y*Ro1n&Zi?j=(>T3kmf94vdhf?AuS8>$Va#P zGL5F+VHpxdsCUa}+RqavXCobI-@B;WJbMphpK2%6t=XvKWWE|ruvREgM+|V=i6;;O zx$g=7^`$XWn0fu!gF=Xe9cMB8Z_SelD>&o&{1XFS`|nInK3BXlaeD*rc;R-#osyIS zWv&>~^TLIyBB6oDX+#>3<_0+2C4u2zK^wmHXXDD9_)kmLYJ!0SzM|%G9{pi)`X$uf zW}|%%#LgyK7m(4{V&?x_0KEDq56tk|0YNY~B(Sr|>WVz-pO3A##}$JCT}5P7DY+@W z#gJv>pA5>$|E3WO2tV7G^SuymB?tY`ooKcN3!vaQMnBNk-WATF{-$#}FyzgtJ8M^; zUK6KWSG)}6**+rZ&?o@PK3??uN{Q)#+bDP9i1W&j)oaU5d0bIWJ_9T5ac!qc?x66Q z$KUSZ`nYY94qfN_dpTFr8OW~A?}LD;Yty-BA)-be5Z3S#t2Io%q+cAbnGj1t$|qFR z9o?8B7OA^KjCYL=-!p}w(dkC^G6Nd%_I=1))PC0w5}ZZGJxfK)jP4Fwa@b-SYBw?% zdz9B-<`*B2dOn(N;mcTm%Do)rIvfXRNFX&1h`?>Rzuj~Wx)$p13nrDlS8-jwq@e@n zNIj_|8or==8~1h*Ih?w*8K7rYkGlwlTWAwLKc5}~dfz3y`kM&^Q|@C%1VAp_$wnw6zG~W4O+^ z>i?NY?oXf^Puc~+fDM$VgRNBpOZj{2cMP~gCqWAX4 z7>%$ux8@a&_B(pt``KSt;r+sR-$N;jdpY>|pyvPiN)9ohd*>mVST3wMo)){`B(&eX z1?zZJ-4u9NZ|~j1rdZYq4R$?swf}<6(#ex%7r{kh%U@kT)&kWuAszS%oJts=*OcL9 zaZwK<5DZw%1IFHXgFplP6JiL^dk8+SgM$D?8X+gE4172hXh!WeqIO>}$I9?Nry$*S zQ#f)RuH{P7RwA3v9f<-w>{PSzom;>(i&^l{E0(&Xp4A-*q-@{W1oE3K;1zb{&n28dSC2$N+6auXe0}e4b z)KLJ?5c*>@9K#I^)W;uU_Z`enquTUxr>mNq z1{0_puF-M7j${rs!dxxo3EelGodF1TvjV;Zpo;s{5f1pyCuRp=HDZ?s#IA4f?h|-p zGd|Mq^4hDa@Bh!c4ZE?O&x&XZ_ptZGYK4$9F4~{%R!}G1leCBx`dtNUS|K zL-7J5s4W@%mhXg1!}a4PD%!t&Qn%f_oquRajn3@C*)`o&K9o7V6DwzVMEhjVdDJ1fjhr#@=lp#@4EBqi=CCQ>73>R(>QKPNM&_Jpe5G`n4wegeC`FYEPJ{|vwS>$-`fuRSp3927qOv|NC3T3G-0 zA{K`|+tQy1yqE$ShWt8ny&5~)%ITb@^+x$w0)f&om;P8B)@}=Wzy59BwUfZ1vqw87 za2lB8J(&*l#(V}Id8SyQ0C(2amzkz3EqG&Ed0Jq1)$|&>4_|NIe=5|n=3?siFV0fI z{As5DLW^gs|B-b4C;Hd(SM-S~GQhzb>HgF2|2Usww0nL^;x@1eaB)=+Clj+$fF@H( z-fqP??~QMT$KI-#m;QC*&6vkp&8699G3)Bq0*kFZXINw=b9OVaed(3(3kS|IZ)CM? zJdnW&%t8MveBuK21uiYj)_a{Fnw0OErMzMN?d$QoPwkhOwcP&p+t>P)4tHlYw-pPN z^oJ=uc$Sl>pv@fZH~ZqxSvdhF@F1s=oZawpr^-#l{IIOGG=T%QXjtwPhIg-F@k@uIlr?J->Ia zpEUQ*=4g|XYn4Gez&aHr*;t$u3oODPmc2Ku)2Og|xjc%w;q!Zz+zY)*3{7V8bK4;& zYV82FZ+8?v)`J|G1w4I0fWdKg|2b#iaazCv;|?(W-q}$o&Y}Q5d@BRk^jL7#{kbCK zSgkyu;=DV+or2)AxCBgq-nj5=@n^`%T#V+xBGEkW4lCqrE)LMv#f;AvD__cQ@Eg3`~x| zW+h9mofSXCq5|M)9|ez(#X?-sxB%Go8};sJ?2abp(Y!lyi>k)|{M*Z$c{e1-K4ky` MPgg&ebxsLQ025IeI{*Lx literal 0 HcmV?d00001 diff --git a/launchpad_app/web/index.html b/launchpad_app/web/index.html new file mode 100644 index 00000000..bd3fab39 --- /dev/null +++ b/launchpad_app/web/index.html @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + Fluent UI + + + + + + + + + + + + + + \ No newline at end of file diff --git a/launchpad_app/web/manifest.json b/launchpad_app/web/manifest.json new file mode 100644 index 00000000..8c012917 --- /dev/null +++ b/launchpad_app/web/manifest.json @@ -0,0 +1,23 @@ +{ + "name": "example", + "short_name": "example", + "start_url": ".", + "display": "standalone", + "background_color": "#0175C2", + "theme_color": "#0175C2", + "description": "A new Flutter project.", + "orientation": "portrait-primary", + "prefer_related_applications": false, + "icons": [ + { + "src": "icons/Icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "icons/Icon-512.png", + "sizes": "512x512", + "type": "image/png" + } + ] +} diff --git a/launchpad_app/web/splash/img/dark-1x.png b/launchpad_app/web/splash/img/dark-1x.png new file mode 100644 index 0000000000000000000000000000000000000000..57a72fb40da58566a887f9985dae42e204d4828f GIT binary patch literal 4659 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 literal 0 HcmV?d00001 diff --git a/launchpad_app/web/splash/img/light-1x.png b/launchpad_app/web/splash/img/light-1x.png new file mode 100644 index 0000000000000000000000000000000000000000..57a72fb40da58566a887f9985dae42e204d4828f GIT binary patch literal 4659 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 literal 0 HcmV?d00001 diff --git a/launchpad_app/web/splash/style.css b/launchpad_app/web/splash/style.css new file mode 100644 index 00000000..d222ab6d --- /dev/null +++ b/launchpad_app/web/splash/style.css @@ -0,0 +1,43 @@ +body, html { + margin:0; + height:100%; + background: #42a5f5; + background-image: url("img/light-background.png"); + background-size: 100% 100%; +} + +.center { + margin: 0; + position: absolute; + top: 50%; + left: 50%; + -ms-transform: translate(-50%, -50%); + transform: translate(-50%, -50%); +} + +.contain { + display:block; + width:100%; height:100%; + object-fit: contain; +} + +.stretch { + display:block; + width:100%; height:100%; +} + +.cover { + display:block; + width:100%; height:100%; + object-fit: cover; +} + +@media (prefers-color-scheme: dark) { + body { + margin:0; + height:100%; + background: #42a5f5; + background-image: url("img/dark-background.png"); + background-size: 100% 100%; + } +} diff --git a/launchpad_app/windows/.gitignore b/launchpad_app/windows/.gitignore new file mode 100644 index 00000000..d492d0d9 --- /dev/null +++ b/launchpad_app/windows/.gitignore @@ -0,0 +1,17 @@ +flutter/ephemeral/ + +# Visual Studio user-specific files. +*.suo +*.user +*.userosscache +*.sln.docstates + +# Visual Studio build-related files. +x64/ +x86/ + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ diff --git a/launchpad_app/windows/CMakeLists.txt b/launchpad_app/windows/CMakeLists.txt new file mode 100644 index 00000000..1633297a --- /dev/null +++ b/launchpad_app/windows/CMakeLists.txt @@ -0,0 +1,95 @@ +cmake_minimum_required(VERSION 3.14) +project(example LANGUAGES CXX) + +set(BINARY_NAME "example") + +cmake_policy(SET CMP0063 NEW) + +set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") + +# Configure build options. +get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) +if(IS_MULTICONFIG) + set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" + CACHE STRING "" FORCE) +else() + if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + set(CMAKE_BUILD_TYPE "Debug" CACHE + STRING "Flutter build mode" FORCE) + set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS + "Debug" "Profile" "Release") + endif() +endif() + +set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") +set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") +set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") +set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") + +# Use Unicode for all projects. +add_definitions(-DUNICODE -D_UNICODE) + +# Compilation settings that should be applied to most targets. +function(APPLY_STANDARD_SETTINGS TARGET) + target_compile_features(${TARGET} PUBLIC cxx_std_17) + target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") + target_compile_options(${TARGET} PRIVATE /EHsc) + target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") + target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") +endfunction() + +set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") + +# Flutter library and tool build rules. +add_subdirectory(${FLUTTER_MANAGED_DIR}) + +# Application build +add_subdirectory("runner") + +# Generated plugin build rules, which manage building the plugins and adding +# them to the application. +include(flutter/generated_plugins.cmake) + + +# === Installation === +# Support files are copied into place next to the executable, so that it can +# run in place. This is done instead of making a separate bundle (as on Linux) +# so that building and running from within Visual Studio will work. +set(BUILD_BUNDLE_DIR "$") +# Make the "install" step default, as it's required to run. +set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) +if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) + set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) +endif() + +set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") +set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") + +install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + COMPONENT Runtime) + +install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) + +if(PLUGIN_BUNDLED_LIBRARIES) + install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" + DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" + COMPONENT Runtime) +endif() + +# Fully re-copy the assets directory on each build to avoid having stale files +# from a previous install. +set(FLUTTER_ASSET_DIR_NAME "flutter_assets") +install(CODE " + file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") + " COMPONENT Runtime) +install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" + DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) + +# Install the AOT library on non-Debug builds only. +install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" + CONFIGURATIONS Profile;Release + COMPONENT Runtime) diff --git a/launchpad_app/windows/flutter/CMakeLists.txt b/launchpad_app/windows/flutter/CMakeLists.txt new file mode 100644 index 00000000..b2e4bd8d --- /dev/null +++ b/launchpad_app/windows/flutter/CMakeLists.txt @@ -0,0 +1,103 @@ +cmake_minimum_required(VERSION 3.14) + +set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") + +# Configuration provided via flutter tool. +include(${EPHEMERAL_DIR}/generated_config.cmake) + +# TODO: Move the rest of this into files in ephemeral. See +# https://github.com/flutter/flutter/issues/57146. +set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") + +# === Flutter Library === +set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") + +# Published to parent scope for install step. +set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) +set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) +set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) +set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) + +list(APPEND FLUTTER_LIBRARY_HEADERS + "flutter_export.h" + "flutter_windows.h" + "flutter_messenger.h" + "flutter_plugin_registrar.h" + "flutter_texture_registrar.h" +) +list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") +add_library(flutter INTERFACE) +target_include_directories(flutter INTERFACE + "${EPHEMERAL_DIR}" +) +target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") +add_dependencies(flutter flutter_assemble) + +# === Wrapper === +list(APPEND CPP_WRAPPER_SOURCES_CORE + "core_implementations.cc" + "standard_codec.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_PLUGIN + "plugin_registrar.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") +list(APPEND CPP_WRAPPER_SOURCES_APP + "flutter_engine.cc" + "flutter_view_controller.cc" +) +list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") + +# Wrapper sources needed for a plugin. +add_library(flutter_wrapper_plugin STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} +) +apply_standard_settings(flutter_wrapper_plugin) +set_target_properties(flutter_wrapper_plugin PROPERTIES + POSITION_INDEPENDENT_CODE ON) +set_target_properties(flutter_wrapper_plugin PROPERTIES + CXX_VISIBILITY_PRESET hidden) +target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) +target_include_directories(flutter_wrapper_plugin PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_plugin flutter_assemble) + +# Wrapper sources needed for the runner. +add_library(flutter_wrapper_app STATIC + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_APP} +) +apply_standard_settings(flutter_wrapper_app) +target_link_libraries(flutter_wrapper_app PUBLIC flutter) +target_include_directories(flutter_wrapper_app PUBLIC + "${WRAPPER_ROOT}/include" +) +add_dependencies(flutter_wrapper_app flutter_assemble) + +# === Flutter tool backend === +# _phony_ is a non-existent file to force this command to run every time, +# since currently there's no way to get a full input/output list from the +# flutter tool. +set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") +set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) +add_custom_command( + OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} + ${PHONY_OUTPUT} + COMMAND ${CMAKE_COMMAND} -E env + ${FLUTTER_TOOL_ENVIRONMENT} + "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" + windows-x64 $ + VERBATIM +) +add_custom_target(flutter_assemble DEPENDS + "${FLUTTER_LIBRARY}" + ${FLUTTER_LIBRARY_HEADERS} + ${CPP_WRAPPER_SOURCES_CORE} + ${CPP_WRAPPER_SOURCES_PLUGIN} + ${CPP_WRAPPER_SOURCES_APP} +) diff --git a/launchpad_app/windows/flutter/generated_plugin_registrant.cc b/launchpad_app/windows/flutter/generated_plugin_registrant.cc new file mode 100644 index 00000000..1c0f14af --- /dev/null +++ b/launchpad_app/windows/flutter/generated_plugin_registrant.cc @@ -0,0 +1,29 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include + +void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterAcrylicPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterAcrylicPlugin")); + NetworkInfoPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("NetworkInfoPlusWindowsPlugin")); + ScreenRetrieverPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ScreenRetrieverPlugin")); + SystemThemePluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SystemThemePlugin")); + UrlLauncherWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("UrlLauncherWindows")); + WindowManagerPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowManagerPlugin")); +} diff --git a/launchpad_app/windows/flutter/generated_plugin_registrant.h b/launchpad_app/windows/flutter/generated_plugin_registrant.h new file mode 100644 index 00000000..dc139d85 --- /dev/null +++ b/launchpad_app/windows/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void RegisterPlugins(flutter::PluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/launchpad_app/windows/flutter/generated_plugins.cmake b/launchpad_app/windows/flutter/generated_plugins.cmake new file mode 100644 index 00000000..c96582c4 --- /dev/null +++ b/launchpad_app/windows/flutter/generated_plugins.cmake @@ -0,0 +1,29 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + flutter_acrylic + network_info_plus + screen_retriever + system_theme + url_launcher_windows + window_manager +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin) diff --git a/launchpad_app/windows/runner/CMakeLists.txt b/launchpad_app/windows/runner/CMakeLists.txt new file mode 100644 index 00000000..de2d8916 --- /dev/null +++ b/launchpad_app/windows/runner/CMakeLists.txt @@ -0,0 +1,17 @@ +cmake_minimum_required(VERSION 3.14) +project(runner LANGUAGES CXX) + +add_executable(${BINARY_NAME} WIN32 + "flutter_window.cpp" + "main.cpp" + "utils.cpp" + "win32_window.cpp" + "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" + "Runner.rc" + "runner.exe.manifest" +) +apply_standard_settings(${BINARY_NAME}) +target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") +target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) +target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") +add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/launchpad_app/windows/runner/Runner.rc b/launchpad_app/windows/runner/Runner.rc new file mode 100644 index 00000000..5fdea291 --- /dev/null +++ b/launchpad_app/windows/runner/Runner.rc @@ -0,0 +1,121 @@ +// Microsoft Visual C++ generated resource script. +// +#pragma code_page(65001) +#include "resource.h" + +#define APSTUDIO_READONLY_SYMBOLS +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 2 resource. +// +#include "winres.h" + +///////////////////////////////////////////////////////////////////////////// +#undef APSTUDIO_READONLY_SYMBOLS + +///////////////////////////////////////////////////////////////////////////// +// English (United States) resources + +#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) +LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US + +#ifdef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// TEXTINCLUDE +// + +1 TEXTINCLUDE +BEGIN + "resource.h\0" +END + +2 TEXTINCLUDE +BEGIN + "#include ""winres.h""\r\n" + "\0" +END + +3 TEXTINCLUDE +BEGIN + "\r\n" + "\0" +END + +#endif // APSTUDIO_INVOKED + + +///////////////////////////////////////////////////////////////////////////// +// +// Icon +// + +// Icon with lowest ID value placed first to ensure application icon +// remains consistent on all systems. +IDI_APP_ICON ICON "resources\\app_icon.ico" + + +///////////////////////////////////////////////////////////////////////////// +// +// Version +// + +#ifdef FLUTTER_BUILD_NUMBER +#define VERSION_AS_NUMBER FLUTTER_BUILD_NUMBER +#else +#define VERSION_AS_NUMBER 1,0,0 +#endif + +#ifdef FLUTTER_BUILD_NAME +#define VERSION_AS_STRING #FLUTTER_BUILD_NAME +#else +#define VERSION_AS_STRING "1.0.0" +#endif + +VS_VERSION_INFO VERSIONINFO + FILEVERSION VERSION_AS_NUMBER + PRODUCTVERSION VERSION_AS_NUMBER + FILEFLAGSMASK VS_FFI_FILEFLAGSMASK +#ifdef _DEBUG + FILEFLAGS VS_FF_DEBUG +#else + FILEFLAGS 0x0L +#endif + FILEOS VOS__WINDOWS32 + FILETYPE VFT_APP + FILESUBTYPE 0x0L +BEGIN + BLOCK "StringFileInfo" + BEGIN + BLOCK "040904e4" + BEGIN + VALUE "CompanyName", "com.example" "\0" + VALUE "FileDescription", "example" "\0" + VALUE "FileVersion", VERSION_AS_STRING "\0" + VALUE "InternalName", "example" "\0" + VALUE "LegalCopyright", "Copyright (C) 2022 com.example. All rights reserved." "\0" + VALUE "OriginalFilename", "example.exe" "\0" + VALUE "ProductName", "example" "\0" + VALUE "ProductVersion", VERSION_AS_STRING "\0" + END + END + BLOCK "VarFileInfo" + BEGIN + VALUE "Translation", 0x409, 1252 + END +END + +#endif // English (United States) resources +///////////////////////////////////////////////////////////////////////////// + + + +#ifndef APSTUDIO_INVOKED +///////////////////////////////////////////////////////////////////////////// +// +// Generated from the TEXTINCLUDE 3 resource. +// + + +///////////////////////////////////////////////////////////////////////////// +#endif // not APSTUDIO_INVOKED diff --git a/launchpad_app/windows/runner/flutter_window.cpp b/launchpad_app/windows/runner/flutter_window.cpp new file mode 100644 index 00000000..b43b9095 --- /dev/null +++ b/launchpad_app/windows/runner/flutter_window.cpp @@ -0,0 +1,61 @@ +#include "flutter_window.h" + +#include + +#include "flutter/generated_plugin_registrant.h" + +FlutterWindow::FlutterWindow(const flutter::DartProject& project) + : project_(project) {} + +FlutterWindow::~FlutterWindow() {} + +bool FlutterWindow::OnCreate() { + if (!Win32Window::OnCreate()) { + return false; + } + + RECT frame = GetClientArea(); + + // The size here must match the window dimensions to avoid unnecessary surface + // creation / destruction in the startup path. + flutter_controller_ = std::make_unique( + frame.right - frame.left, frame.bottom - frame.top, project_); + // Ensure that basic setup of the controller was successful. + if (!flutter_controller_->engine() || !flutter_controller_->view()) { + return false; + } + RegisterPlugins(flutter_controller_->engine()); + SetChildContent(flutter_controller_->view()->GetNativeWindow()); + return true; +} + +void FlutterWindow::OnDestroy() { + if (flutter_controller_) { + flutter_controller_ = nullptr; + } + + Win32Window::OnDestroy(); +} + +LRESULT +FlutterWindow::MessageHandler(HWND hwnd, UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + // Give Flutter, including plugins, an opportunity to handle window messages. + if (flutter_controller_) { + std::optional result = + flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, + lparam); + if (result) { + return *result; + } + } + + switch (message) { + case WM_FONTCHANGE: + flutter_controller_->engine()->ReloadSystemFonts(); + break; + } + + return Win32Window::MessageHandler(hwnd, message, wparam, lparam); +} diff --git a/launchpad_app/windows/runner/flutter_window.h b/launchpad_app/windows/runner/flutter_window.h new file mode 100644 index 00000000..6da0652f --- /dev/null +++ b/launchpad_app/windows/runner/flutter_window.h @@ -0,0 +1,33 @@ +#ifndef RUNNER_FLUTTER_WINDOW_H_ +#define RUNNER_FLUTTER_WINDOW_H_ + +#include +#include + +#include + +#include "win32_window.h" + +// A window that does nothing but host a Flutter view. +class FlutterWindow : public Win32Window { + public: + // Creates a new FlutterWindow hosting a Flutter view running |project|. + explicit FlutterWindow(const flutter::DartProject& project); + virtual ~FlutterWindow(); + + protected: + // Win32Window: + bool OnCreate() override; + void OnDestroy() override; + LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, + LPARAM const lparam) noexcept override; + + private: + // The project to run. + flutter::DartProject project_; + + // The Flutter instance hosted by this window. + std::unique_ptr flutter_controller_; +}; + +#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/launchpad_app/windows/runner/main.cpp b/launchpad_app/windows/runner/main.cpp new file mode 100644 index 00000000..bcb57b0e --- /dev/null +++ b/launchpad_app/windows/runner/main.cpp @@ -0,0 +1,43 @@ +#include +#include +#include + +#include "flutter_window.h" +#include "utils.h" + +int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, + _In_ wchar_t *command_line, _In_ int show_command) { + // Attach to console when present (e.g., 'flutter run') or create a + // new console when running with a debugger. + if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { + CreateAndAttachConsole(); + } + + // Initialize COM, so that it is available for use in the library and/or + // plugins. + ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); + + flutter::DartProject project(L"data"); + + std::vector command_line_arguments = + GetCommandLineArguments(); + + project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); + + FlutterWindow window(project); + Win32Window::Point origin(10, 10); + Win32Window::Size size(1280, 720); + if (!window.CreateAndShow(L"example", origin, size)) { + return EXIT_FAILURE; + } + window.SetQuitOnClose(true); + + ::MSG msg; + while (::GetMessage(&msg, nullptr, 0, 0)) { + ::TranslateMessage(&msg); + ::DispatchMessage(&msg); + } + + ::CoUninitialize(); + return EXIT_SUCCESS; +} diff --git a/launchpad_app/windows/runner/resource.h b/launchpad_app/windows/runner/resource.h new file mode 100644 index 00000000..66a65d1e --- /dev/null +++ b/launchpad_app/windows/runner/resource.h @@ -0,0 +1,16 @@ +//{{NO_DEPENDENCIES}} +// Microsoft Visual C++ generated include file. +// Used by Runner.rc +// +#define IDI_APP_ICON 101 + +// Next default values for new objects +// +#ifdef APSTUDIO_INVOKED +#ifndef APSTUDIO_READONLY_SYMBOLS +#define _APS_NEXT_RESOURCE_VALUE 102 +#define _APS_NEXT_COMMAND_VALUE 40001 +#define _APS_NEXT_CONTROL_VALUE 1001 +#define _APS_NEXT_SYMED_VALUE 101 +#endif +#endif diff --git a/launchpad_app/windows/runner/resources/app_icon.ico b/launchpad_app/windows/runner/resources/app_icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c04e20caf6370ebb9253ad831cc31de4a9c965f6 GIT binary patch literal 33772 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 literal 0 HcmV?d00001 diff --git a/launchpad_app/windows/runner/runner.exe.manifest b/launchpad_app/windows/runner/runner.exe.manifest new file mode 100644 index 00000000..c977c4a4 --- /dev/null +++ b/launchpad_app/windows/runner/runner.exe.manifest @@ -0,0 +1,20 @@ + + + + + PerMonitorV2 + + + + + + + + + + + + + + + diff --git a/launchpad_app/windows/runner/utils.cpp b/launchpad_app/windows/runner/utils.cpp new file mode 100644 index 00000000..d19bdbbc --- /dev/null +++ b/launchpad_app/windows/runner/utils.cpp @@ -0,0 +1,64 @@ +#include "utils.h" + +#include +#include +#include +#include + +#include + +void CreateAndAttachConsole() { + if (::AllocConsole()) { + FILE *unused; + if (freopen_s(&unused, "CONOUT$", "w", stdout)) { + _dup2(_fileno(stdout), 1); + } + if (freopen_s(&unused, "CONOUT$", "w", stderr)) { + _dup2(_fileno(stdout), 2); + } + std::ios::sync_with_stdio(); + FlutterDesktopResyncOutputStreams(); + } +} + +std::vector GetCommandLineArguments() { + // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. + int argc; + wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); + if (argv == nullptr) { + return std::vector(); + } + + std::vector command_line_arguments; + + // Skip the first argument as it's the binary name. + for (int i = 1; i < argc; i++) { + command_line_arguments.push_back(Utf8FromUtf16(argv[i])); + } + + ::LocalFree(argv); + + return command_line_arguments; +} + +std::string Utf8FromUtf16(const wchar_t* utf16_string) { + if (utf16_string == nullptr) { + return std::string(); + } + int target_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, nullptr, 0, nullptr, nullptr); + if (target_length == 0) { + return std::string(); + } + std::string utf8_string; + utf8_string.resize(target_length); + int converted_length = ::WideCharToMultiByte( + CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, + -1, utf8_string.data(), + target_length, nullptr, nullptr); + if (converted_length == 0) { + return std::string(); + } + return utf8_string; +} diff --git a/launchpad_app/windows/runner/utils.h b/launchpad_app/windows/runner/utils.h new file mode 100644 index 00000000..3879d547 --- /dev/null +++ b/launchpad_app/windows/runner/utils.h @@ -0,0 +1,19 @@ +#ifndef RUNNER_UTILS_H_ +#define RUNNER_UTILS_H_ + +#include +#include + +// Creates a console for the process, and redirects stdout and stderr to +// it for both the runner and the Flutter library. +void CreateAndAttachConsole(); + +// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string +// encoded in UTF-8. Returns an empty std::string on failure. +std::string Utf8FromUtf16(const wchar_t* utf16_string); + +// Gets the command line arguments passed in as a std::vector, +// encoded in UTF-8. Returns an empty std::vector on failure. +std::vector GetCommandLineArguments(); + +#endif // RUNNER_UTILS_H_ diff --git a/launchpad_app/windows/runner/win32_window.cpp b/launchpad_app/windows/runner/win32_window.cpp new file mode 100644 index 00000000..3273c2c0 --- /dev/null +++ b/launchpad_app/windows/runner/win32_window.cpp @@ -0,0 +1,245 @@ +#include "win32_window.h" + +#include + +#include "resource.h" + +namespace { + +constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; + +// The number of Win32Window objects that currently exist. +static int g_active_window_count = 0; + +using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); + +// Scale helper to convert logical scaler values to physical using passed in +// scale factor +int Scale(int source, double scale_factor) { + return static_cast(source * scale_factor); +} + +// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. +// This API is only needed for PerMonitor V1 awareness mode. +void EnableFullDpiSupportIfAvailable(HWND hwnd) { + HMODULE user32_module = LoadLibraryA("User32.dll"); + if (!user32_module) { + return; + } + auto enable_non_client_dpi_scaling = + reinterpret_cast( + GetProcAddress(user32_module, "EnableNonClientDpiScaling")); + if (enable_non_client_dpi_scaling != nullptr) { + enable_non_client_dpi_scaling(hwnd); + FreeLibrary(user32_module); + } +} + +} // namespace + +// Manages the Win32Window's window class registration. +class WindowClassRegistrar { + public: + ~WindowClassRegistrar() = default; + + // Returns the singleton registar instance. + static WindowClassRegistrar* GetInstance() { + if (!instance_) { + instance_ = new WindowClassRegistrar(); + } + return instance_; + } + + // Returns the name of the window class, registering the class if it hasn't + // previously been registered. + const wchar_t* GetWindowClass(); + + // Unregisters the window class. Should only be called if there are no + // instances of the window. + void UnregisterWindowClass(); + + private: + WindowClassRegistrar() = default; + + static WindowClassRegistrar* instance_; + + bool class_registered_ = false; +}; + +WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; + +const wchar_t* WindowClassRegistrar::GetWindowClass() { + if (!class_registered_) { + WNDCLASS window_class{}; + window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); + window_class.lpszClassName = kWindowClassName; + window_class.style = CS_HREDRAW | CS_VREDRAW; + window_class.cbClsExtra = 0; + window_class.cbWndExtra = 0; + window_class.hInstance = GetModuleHandle(nullptr); + window_class.hIcon = + LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); + window_class.hbrBackground = 0; + window_class.lpszMenuName = nullptr; + window_class.lpfnWndProc = Win32Window::WndProc; + RegisterClass(&window_class); + class_registered_ = true; + } + return kWindowClassName; +} + +void WindowClassRegistrar::UnregisterWindowClass() { + UnregisterClass(kWindowClassName, nullptr); + class_registered_ = false; +} + +Win32Window::Win32Window() { + ++g_active_window_count; +} + +Win32Window::~Win32Window() { + --g_active_window_count; + Destroy(); +} + +bool Win32Window::CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size) { + Destroy(); + + const wchar_t* window_class = + WindowClassRegistrar::GetInstance()->GetWindowClass(); + + const POINT target_point = {static_cast(origin.x), + static_cast(origin.y)}; + HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); + UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); + double scale_factor = dpi / 96.0; + + HWND window = CreateWindow( + window_class, title.c_str(), WS_OVERLAPPEDWINDOW, + Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), + Scale(size.width, scale_factor), Scale(size.height, scale_factor), + nullptr, nullptr, GetModuleHandle(nullptr), this); + + if (!window) { + return false; + } + + return OnCreate(); +} + +// static +LRESULT CALLBACK Win32Window::WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + if (message == WM_NCCREATE) { + auto window_struct = reinterpret_cast(lparam); + SetWindowLongPtr(window, GWLP_USERDATA, + reinterpret_cast(window_struct->lpCreateParams)); + + auto that = static_cast(window_struct->lpCreateParams); + EnableFullDpiSupportIfAvailable(window); + that->window_handle_ = window; + } else if (Win32Window* that = GetThisFromHandle(window)) { + return that->MessageHandler(window, message, wparam, lparam); + } + + return DefWindowProc(window, message, wparam, lparam); +} + +LRESULT +Win32Window::MessageHandler(HWND hwnd, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept { + switch (message) { + case WM_DESTROY: + window_handle_ = nullptr; + Destroy(); + if (quit_on_close_) { + PostQuitMessage(0); + } + return 0; + + case WM_DPICHANGED: { + auto newRectSize = reinterpret_cast(lparam); + LONG newWidth = newRectSize->right - newRectSize->left; + LONG newHeight = newRectSize->bottom - newRectSize->top; + + SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, + newHeight, SWP_NOZORDER | SWP_NOACTIVATE); + + return 0; + } + case WM_SIZE: { + RECT rect = GetClientArea(); + if (child_content_ != nullptr) { + // Size and position the child window. + MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, + rect.bottom - rect.top, TRUE); + } + return 0; + } + + case WM_ACTIVATE: + if (child_content_ != nullptr) { + SetFocus(child_content_); + } + return 0; + } + + return DefWindowProc(window_handle_, message, wparam, lparam); +} + +void Win32Window::Destroy() { + OnDestroy(); + + if (window_handle_) { + DestroyWindow(window_handle_); + window_handle_ = nullptr; + } + if (g_active_window_count == 0) { + WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); + } +} + +Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { + return reinterpret_cast( + GetWindowLongPtr(window, GWLP_USERDATA)); +} + +void Win32Window::SetChildContent(HWND content) { + child_content_ = content; + SetParent(content, window_handle_); + RECT frame = GetClientArea(); + + MoveWindow(content, frame.left, frame.top, frame.right - frame.left, + frame.bottom - frame.top, true); + + SetFocus(child_content_); +} + +RECT Win32Window::GetClientArea() { + RECT frame; + GetClientRect(window_handle_, &frame); + return frame; +} + +HWND Win32Window::GetHandle() { + return window_handle_; +} + +void Win32Window::SetQuitOnClose(bool quit_on_close) { + quit_on_close_ = quit_on_close; +} + +bool Win32Window::OnCreate() { + // No-op; provided for subclasses. + return true; +} + +void Win32Window::OnDestroy() { + // No-op; provided for subclasses. +} diff --git a/launchpad_app/windows/runner/win32_window.h b/launchpad_app/windows/runner/win32_window.h new file mode 100644 index 00000000..17ba4311 --- /dev/null +++ b/launchpad_app/windows/runner/win32_window.h @@ -0,0 +1,98 @@ +#ifndef RUNNER_WIN32_WINDOW_H_ +#define RUNNER_WIN32_WINDOW_H_ + +#include + +#include +#include +#include + +// A class abstraction for a high DPI-aware Win32 Window. Intended to be +// inherited from by classes that wish to specialize with custom +// rendering and input handling +class Win32Window { + public: + struct Point { + unsigned int x; + unsigned int y; + Point(unsigned int x, unsigned int y) : x(x), y(y) {} + }; + + struct Size { + unsigned int width; + unsigned int height; + Size(unsigned int width, unsigned int height) + : width(width), height(height) {} + }; + + Win32Window(); + virtual ~Win32Window(); + + // Creates and shows a win32 window with |title| and position and size using + // |origin| and |size|. New windows are created on the default monitor. Window + // sizes are specified to the OS in physical pixels, hence to ensure a + // consistent size to will treat the width height passed in to this function + // as logical pixels and scale to appropriate for the default monitor. Returns + // true if the window was created successfully. + bool CreateAndShow(const std::wstring& title, + const Point& origin, + const Size& size); + + // Release OS resources associated with window. + void Destroy(); + + // Inserts |content| into the window tree. + void SetChildContent(HWND content); + + // Returns the backing Window handle to enable clients to set icon and other + // window properties. Returns nullptr if the window has been destroyed. + HWND GetHandle(); + + // If true, closing this window will quit the application. + void SetQuitOnClose(bool quit_on_close); + + // Return a RECT representing the bounds of the current client area. + RECT GetClientArea(); + + protected: + // Processes and route salient window messages for mouse handling, + // size change and DPI. Delegates handling of these to member overloads that + // inheriting classes can handle. + virtual LRESULT MessageHandler(HWND window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Called when CreateAndShow is called, allowing subclass window-related + // setup. Subclasses should return false if setup fails. + virtual bool OnCreate(); + + // Called when Destroy is called. + virtual void OnDestroy(); + + private: + friend class WindowClassRegistrar; + + // OS callback called by message pump. Handles the WM_NCCREATE message which + // is passed when the non-client area is being created and enables automatic + // non-client DPI scaling so that the non-client area automatically + // responsponds to changes in DPI. All other messages are handled by + // MessageHandler. + static LRESULT CALLBACK WndProc(HWND const window, + UINT const message, + WPARAM const wparam, + LPARAM const lparam) noexcept; + + // Retrieves a class instance pointer for |window| + static Win32Window* GetThisFromHandle(HWND const window) noexcept; + + bool quit_on_close_ = false; + + // window handle for top level window. + HWND window_handle_ = nullptr; + + // window handle for hosted content. + HWND child_content_ = nullptr; +}; + +#endif // RUNNER_WIN32_WINDOW_H_ From c93878a677db2274af563527825d4b157535b836 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 16:29:30 -0500 Subject: [PATCH 142/227] Basic readme --- launchpad_app/README.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 launchpad_app/README.md diff --git a/launchpad_app/README.md b/launchpad_app/README.md new file mode 100644 index 00000000..e21ae20e --- /dev/null +++ b/launchpad_app/README.md @@ -0,0 +1,3 @@ +# Launchpad (Flutter) + +This is the codebase for the new Flutter version of Launchpad! From b419d99c2d33bb44b1d54d0be9ad9bf546f8e4f9 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 16:34:25 -0500 Subject: [PATCH 143/227] Create a Sponsors model based on fluent_ui's example --- launchpad_app/lib/models/sponsor.dart | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 launchpad_app/lib/models/sponsor.dart diff --git a/launchpad_app/lib/models/sponsor.dart b/launchpad_app/lib/models/sponsor.dart new file mode 100644 index 00000000..97adf37a --- /dev/null +++ b/launchpad_app/lib/models/sponsor.dart @@ -0,0 +1,49 @@ +/// Current sponsors of Launchpad +const sponsors = [ + Sponsor( + username: 'bmcclure', + imageUrl: 'https://avatars.githubusercontent.com/u/277977?v=4', + name: 'Sander in \'t Hout', + ) +]; + +class Sponsor { + final String? username; + final String name; + final String imageUrl; + + const Sponsor({ + required this.username, + required this.name, + required this.imageUrl, + }); + + Sponsor copyWith({ + String? username, + String? name, + String? imageUrl, + }) { + return Sponsor( + username: username ?? this.username, + name: name ?? this.name, + imageUrl: imageUrl ?? this.imageUrl, + ); + } + + @override + String toString() => + 'Sponsor(username: $username, name: $name, imageUrl: $imageUrl)'; + + @override + bool operator ==(Object other) { + if (identical(this, other)) return true; + + return other is Sponsor && + other.username == username && + other.name == name && + other.imageUrl == imageUrl; + } + + @override + int get hashCode => username.hashCode ^ name.hashCode ^ imageUrl.hashCode; +} From 34aac56c71c08de1b7b7f023a03a7f19ac2f686d Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 16:42:12 -0500 Subject: [PATCH 144/227] Initial widgets copied from fluent_ui example --- launchpad_app/lib/widgets/card_highlight.dart | 140 ++++++++ launchpad_app/lib/widgets/changelog.dart | 143 +++++++++ .../lib/widgets/deferred_widget.dart | 121 +++++++ .../lib/widgets/material_equivalents.dart | 302 ++++++++++++++++++ launchpad_app/lib/widgets/page.dart | 141 ++++++++ launchpad_app/lib/widgets/sponsor.dart | 117 +++++++ launchpad_app/test/widget_test.dart | 30 ++ 7 files changed, 994 insertions(+) create mode 100644 launchpad_app/lib/widgets/card_highlight.dart create mode 100644 launchpad_app/lib/widgets/changelog.dart create mode 100644 launchpad_app/lib/widgets/deferred_widget.dart create mode 100644 launchpad_app/lib/widgets/material_equivalents.dart create mode 100644 launchpad_app/lib/widgets/page.dart create mode 100644 launchpad_app/lib/widgets/sponsor.dart create mode 100644 launchpad_app/test/widget_test.dart diff --git a/launchpad_app/lib/widgets/card_highlight.dart b/launchpad_app/lib/widgets/card_highlight.dart new file mode 100644 index 00000000..94b5e51f --- /dev/null +++ b/launchpad_app/lib/widgets/card_highlight.dart @@ -0,0 +1,140 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_syntax_view/flutter_syntax_view.dart'; + +class CardHighlight extends StatefulWidget { + const CardHighlight({ + Key? key, + this.backgroundColor, + required this.child, + required this.codeSnippet, + }) : super(key: key); + + final Widget child; + final String codeSnippet; + + final Color? backgroundColor; + + @override + State createState() => _CardHighlightState(); +} + +class _CardHighlightState extends State + with AutomaticKeepAliveClientMixin { + bool isOpen = false; + bool isCopying = false; + + final GlobalKey expanderKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + super.build(context); + final theme = FluentTheme.of(context); + + return Column(children: [ + Card( + backgroundColor: widget.backgroundColor, + borderRadius: const BorderRadius.vertical(top: Radius.circular(4.0)), + child: SizedBox( + width: double.infinity, + child: Align( + alignment: AlignmentDirectional.topStart, + child: widget.child, + ), + ), + ), + Expander( + key: expanderKey, + headerShape: (open) => const RoundedRectangleBorder( + borderRadius: BorderRadius.zero, + ), + onStateChanged: (state) { + // this is done because [onStateChanges] is called while the [Expander] + // is updating. By using this, we schedule the rebuilt of this widget + // to the next frame + WidgetsBinding.instance.addPostFrameCallback((timeStamp) { + if (mounted) setState(() => isOpen = state); + }); + }, + trailing: isOpen + ? Container( + height: 31, + constraints: const BoxConstraints(minWidth: 75), + child: Button( + style: ButtonStyle( + backgroundColor: isCopying + ? ButtonState.all( + theme.accentColor.defaultBrushFor(theme.brightness), + ) + : null, + ), + child: isCopying + ? Icon( + FluentIcons.check_mark, + color: theme.resources.textOnAccentFillColorPrimary, + size: 18, + ) + : Row(children: const [ + Icon(FluentIcons.copy), + SizedBox(width: 6.0), + Text('Copy') + ]), + onPressed: () { + Clipboard.setData(ClipboardData(text: widget.codeSnippet)); + setState(() => isCopying = true); + Future.delayed(const Duration(milliseconds: 1500), () { + isCopying = false; + if (mounted) setState(() {}); + }); + }, + ), + ) + : null, + header: const Text('Source code'), + content: SyntaxView( + code: widget.codeSnippet, + syntaxTheme: theme.brightness.isDark + ? SyntaxTheme.vscodeDark() + : SyntaxTheme.vscodeLight(), + ), + ), + ]); + } + + @override + bool get wantKeepAlive => true; +} + +const fluentHighlightTheme = { + 'root': TextStyle( + backgroundColor: Color(0x00ffffff), + color: Color(0xffdddddd), + ), + 'keyword': TextStyle( + color: Color.fromARGB(255, 255, 255, 255), fontWeight: FontWeight.bold), + 'selector-tag': + TextStyle(color: Color(0xffffffff), fontWeight: FontWeight.bold), + 'literal': TextStyle(color: Color(0xffffffff), fontWeight: FontWeight.bold), + 'section': TextStyle(color: Color(0xffffffff), fontWeight: FontWeight.bold), + 'link': TextStyle(color: Color(0xffffffff)), + 'subst': TextStyle(color: Color(0xffdddddd)), + 'string': TextStyle(color: Color(0xffdd8888)), + 'title': TextStyle(color: Color(0xffdd8888), fontWeight: FontWeight.bold), + 'name': TextStyle(color: Color(0xffdd8888), fontWeight: FontWeight.bold), + 'type': TextStyle(color: Color(0xffdd8888), fontWeight: FontWeight.bold), + 'attribute': TextStyle(color: Color(0xffdd8888)), + 'symbol': TextStyle(color: Color(0xffdd8888)), + 'bullet': TextStyle(color: Color(0xffdd8888)), + 'built_in': TextStyle(color: Color(0xffdd8888)), + 'addition': TextStyle(color: Color(0xffdd8888)), + 'variable': TextStyle(color: Color(0xffdd8888)), + 'template-tag': TextStyle(color: Color(0xffdd8888)), + 'template-variable': TextStyle(color: Color(0xffdd8888)), + 'comment': TextStyle(color: Color(0xff777777)), + 'quote': TextStyle(color: Color(0xff777777)), + 'deletion': TextStyle(color: Color(0xff777777)), + 'meta': TextStyle(color: Color(0xff777777)), + 'doctag': TextStyle(fontWeight: FontWeight.bold), + 'strong': TextStyle(fontWeight: FontWeight.bold), + 'emphasis': TextStyle(fontStyle: FontStyle.italic), +}; diff --git a/launchpad_app/lib/widgets/changelog.dart b/launchpad_app/lib/widgets/changelog.dart new file mode 100644 index 00000000..d4c828b1 --- /dev/null +++ b/launchpad_app/lib/widgets/changelog.dart @@ -0,0 +1,143 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/material.dart' as m; +import 'package:flutter_markdown/flutter_markdown.dart' + deferred as flutter_markdown; +import 'package:http/http.dart' as http; +import 'package:intl/intl.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'deferred_widget.dart'; + +List? changelog; + +class Changelog extends StatefulWidget { + const Changelog({Key? key}) : super(key: key); + + @override + State createState() => _ChangelogState(); +} + +class _ChangelogState extends State { + @override + void initState() { + super.initState(); + fetchChangelog(); + } + + void fetchChangelog() async { + final response = await http.get( + Uri.parse( + 'https://raw.githubusercontent.com/VolantisDev/Launchpad/master/CHANGELOG.md', + ), + ); + + if (response.statusCode == 200) { + final _changelog = response.body.split('\n')..removeRange(0, 2); + setState(() => changelog = _changelog); + } else { + debugPrint(response.body); + } + } + + @override + Widget build(BuildContext context) { + final theme = FluentTheme.of(context); + return DeferredWidget( + flutter_markdown.loadLibrary, + () => ContentDialog( + style: const ContentDialogThemeData(padding: EdgeInsets.zero), + constraints: const BoxConstraints(maxWidth: 600), + content: () { + if (changelog == null) return const ProgressRing(); + return SingleChildScrollView( + child: flutter_markdown.Markdown( + shrinkWrap: true, + data: changelog!.map((line) { + if (line.startsWith('## [')) { + final version = line.split(']').first.replaceAll('## [', ''); + // if (line.split('-').length == 2) { + // print('GO- ${line.split('-')[0]} - ${line.split('-')[1]}'); + // } + String date = line + .split('-') + .last + .replaceAll('[', '') + .replaceAll(']', ''); + + if (!date.startsWith('##')) { + final splitDate = date.split('/'); + final dateTime = DateTime( + int.parse(splitDate[2]), + int.parse(splitDate[1]), + int.parse(splitDate[0]), + ); + final formatter = DateFormat.MMMMEEEEd(); + date = '${formatter.format(dateTime)}\n'; + } else { + date = ''; + } + return '## $version\n$date'; + } + return line; + }).join('\n'), + onTapLink: (text, href, title) { + launchUrl(Uri.parse(href!)); + }, + styleSheet: flutter_markdown.MarkdownStyleSheet.fromTheme( + m.Theme.of(context), + ).copyWith( + a: TextStyle( + color: theme.accentColor.resolveFromReverseBrightness( + theme.brightness, + level: 1, + ), + ), + ), + padding: const EdgeInsets.all(20.0), + ), + ); + }(), + ), + ); + } +} + +// + +// class CodeElementBuilder extends flutter_markdown.MarkdownElementBuilder { +// @override +// Widget? visitElementAfter(md.Element element, TextStyle? preferredStyle) { +// var language = 'dart'; + +// if (element.attributes['class'] != null) { +// String lg = element.attributes['class'] as String; +// language = lg.substring(9); +// } +// return SizedBox( +// width: +// MediaQueryData.fromWindow(WidgetsBinding.instance.window).size.width, +// child: HighlightView( +// // The original code to be highlighted +// element.textContent, + +// // Specify language +// // It is recommended to give it a value for performance +// language: language, + +// // Specify highlight theme +// // All available themes are listed in `themes` folder +// // theme: MediaQueryData.fromWindow(WidgetsBinding.instance!.window) +// // .platformBrightness == +// // Brightness.light +// // ? atomOneLightTheme +// // : atomOneDarkTheme, + +// // Specify padding +// padding: const EdgeInsets.all(8), + +// // Specify text style +// // textStyle: GoogleFonts.robotoMono(), +// ), +// ); +// } +// } diff --git a/launchpad_app/lib/widgets/deferred_widget.dart b/launchpad_app/lib/widgets/deferred_widget.dart new file mode 100644 index 00000000..b1b940ba --- /dev/null +++ b/launchpad_app/lib/widgets/deferred_widget.dart @@ -0,0 +1,121 @@ +// Copyright 2019 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. +// Copied from https://github.com/flutter/gallery/blob/d030f1e5316310c48fc725f619eb980a0597366d/lib/deferred_widget.dart + +import 'dart:async'; +import 'package:flutter/material.dart'; + +typedef LibraryLoader = Future Function(); +typedef DeferredWidgetBuilder = Widget Function(); + +/// Wraps the child inside a deferred module loader. +/// +/// The child is created and a single instance of the Widget is maintained in +/// state as long as closure to create widget stays the same. +/// +class DeferredWidget extends StatefulWidget { + DeferredWidget( + this.libraryLoader, + this.createWidget, { + super.key, + Widget? placeholder, + }) : placeholder = placeholder ?? Container(); + + final LibraryLoader libraryLoader; + final DeferredWidgetBuilder createWidget; + final Widget placeholder; + static final Map> _moduleLoaders = {}; + static final Set _loadedModules = {}; + + static Future preload(LibraryLoader loader) { + if (!_moduleLoaders.containsKey(loader)) { + _moduleLoaders[loader] = loader().then((dynamic _) { + _loadedModules.add(loader); + }); + } + return _moduleLoaders[loader]!; + } + + @override + State createState() => _DeferredWidgetState(); +} + +class _DeferredWidgetState extends State { + _DeferredWidgetState(); + + Widget? _loadedChild; + DeferredWidgetBuilder? _loadedCreator; + + @override + void initState() { + /// If module was already loaded immediately create widget instead of + /// waiting for future or zone turn. + if (DeferredWidget._loadedModules.contains(widget.libraryLoader)) { + _onLibraryLoaded(); + } else { + DeferredWidget.preload(widget.libraryLoader) + .then((dynamic _) => _onLibraryLoaded()); + } + super.initState(); + } + + void _onLibraryLoaded() { + setState(() { + _loadedCreator = widget.createWidget; + _loadedChild = _loadedCreator!(); + }); + } + + @override + Widget build(BuildContext context) { + /// If closure to create widget changed, create new instance, otherwise + /// treat as const Widget. + if (_loadedCreator != widget.createWidget && _loadedCreator != null) { + _loadedCreator = widget.createWidget; + _loadedChild = _loadedCreator!(); + } + return _loadedChild ?? widget.placeholder; + } +} + +/// Displays a progress indicator and text description explaining that +/// the widget is a deferred component and is currently being installed. +class DeferredLoadingPlaceholder extends StatelessWidget { + const DeferredLoadingPlaceholder({ + super.key, + this.name = 'This widget', + }); + + final String name; + + @override + Widget build(BuildContext context) { + return Center( + child: Container( + decoration: BoxDecoration( + color: Colors.grey[700], + border: Border.all( + width: 20, + color: Colors.grey[700]!, + ), + borderRadius: const BorderRadius.all(Radius.circular(10))), + width: 250, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('$name is installing.', + style: Theme.of(context).textTheme.headlineMedium), + Container(height: 10), + Text( + '$name is a deferred component which are downloaded and installed at runtime.', + style: Theme.of(context).textTheme.bodyLarge), + Container(height: 20), + const Center(child: CircularProgressIndicator()), + ], + ), + ), + ); + } +} diff --git a/launchpad_app/lib/widgets/material_equivalents.dart b/launchpad_app/lib/widgets/material_equivalents.dart new file mode 100644 index 00000000..897b80c9 --- /dev/null +++ b/launchpad_app/lib/widgets/material_equivalents.dart @@ -0,0 +1,302 @@ +import 'dart:math'; + +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/material.dart' as m; + +class MaterialEquivalents extends StatefulWidget { + const MaterialEquivalents({Key? key}) : super(key: key); + + @override + State createState() => _MaterialEquivalentsState(); +} + +class _MaterialEquivalentsState extends State { + bool comboboxChecked = true; + bool radioChecked = true; + bool switchChecked = true; + + final List comboboxItems = [ + 'Item 1', + 'Item 2', + ]; + String? comboboxItem; + String dropdownItem = 'Item 1'; + final popupKey = GlobalKey(); + + double sliderValue = Random().nextDouble() * 100; + + final fieldController = TextEditingController(); + DateTime time = DateTime.now(); + + @override + void dispose() { + fieldController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + List> children = [ + [ + const Text('Button'), + Button( + child: const Text('Content'), + onPressed: () {}, + ), + m.OutlinedButton( + child: const Text('Content'), + onPressed: () {}, + ), + ], + [ + const Text('TextButton'), + TextButton( + child: const Text('Content'), + onPressed: () {}, + ), + m.TextButton( + child: const Text('Content'), + onPressed: () {}, + ), + ], + [ + const Text('FilledButton'), + FilledButton( + child: const Text('Content'), + onPressed: () {}, + ), + m.ElevatedButton( + child: const Text('Content'), + onPressed: () {}, + ), + ], + [ + const Text('IconButton'), + IconButton( + icon: const Icon(FluentIcons.graph_symbol), + onPressed: () {}, + ), + m.IconButton( + icon: const Icon(FluentIcons.graph_symbol), + onPressed: () {}, + ), + ], + [ + const Text('Checkbox'), + Checkbox( + checked: comboboxChecked, + onChanged: (v) => + setState(() => comboboxChecked = v ?? comboboxChecked), + ), + m.Checkbox( + value: comboboxChecked, + onChanged: (v) => + setState(() => comboboxChecked = v ?? comboboxChecked), + ), + ], + [ + const Text('RadioButton'), + RadioButton( + checked: radioChecked, + onChanged: (v) => setState(() => radioChecked = v), + ), + m.Radio( + groupValue: true, + value: radioChecked, + onChanged: (v) => setState(() => radioChecked = !radioChecked), + ), + ], + [ + const Text('ToggleSwitch'), + ToggleSwitch( + checked: switchChecked, + onChanged: (v) => setState(() => switchChecked = v), + ), + m.Switch( + value: switchChecked, + onChanged: (v) => setState(() => switchChecked = v), + ), + ], + [ + const Text('Slider'), + Slider( + value: sliderValue, + max: 100, + onChanged: (v) => setState(() => sliderValue = v), + ), + m.Slider( + value: sliderValue, + max: 100, + onChanged: (v) => setState(() => sliderValue = v), + ), + ], + [ + const Text('ProgressRing'), + const RepaintBoundary(child: ProgressRing()), + const RepaintBoundary(child: m.CircularProgressIndicator()), + ], + [ + const Text('ProgressBar'), + const RepaintBoundary(child: ProgressBar()), + const RepaintBoundary(child: m.LinearProgressIndicator()), + ], + [ + const Text('ComboBox'), + ComboBox( + items: comboboxItems + .map((e) => ComboBoxItem(child: Text(e), value: e)) + .toList(), + value: comboboxItem, + onChanged: (value) => setState(() => comboboxItem = value), + ), + m.DropdownButton( + items: comboboxItems + .map((e) => m.DropdownMenuItem(child: Text(e), value: e)) + .toList(), + value: comboboxItem, + onChanged: (value) => setState(() => comboboxItem = value), + ), + ], + [ + const Text('DropDownButton'), + DropDownButton( + items: comboboxItems + .map( + (e) => MenuFlyoutItem( + text: Text(e), + onPressed: () => setState(() => dropdownItem = e), + ), + ) + .toList(), + title: Text(dropdownItem), + ), + m.PopupMenuButton( + key: popupKey, + itemBuilder: (context) { + return comboboxItems + .map( + (e) => m.PopupMenuItem( + child: Text(e), + value: e, + ), + ) + .toList(); + }, + onSelected: (e) => setState(() => dropdownItem = e), + initialValue: dropdownItem, + position: m.PopupMenuPosition.under, + child: m.TextButton( + child: Text(dropdownItem), + onPressed: () { + popupKey.currentState?.showButtonMenu(); + }, + ), + ), + ], + [ + const Text('TextBox'), + TextBox(controller: fieldController), + m.TextField(controller: fieldController), + ], + [ + const Text('TimePicker'), + TimePicker( + selected: time, + onChanged: (value) => setState(() => time), + ), + m.TextButton( + child: const Text('Show Picker'), + onPressed: () async { + final newTime = await m.showTimePicker( + context: context, + initialTime: m.TimeOfDay( + hour: time.hour, + minute: time.minute, + ), + ); + if (newTime != null) { + time = DateTime( + time.year, + time.month, + time.day, + newTime.hour, + newTime.minute, + time.second, + ); + } + }, + ), + ], + [ + const Text('DatePicker'), + DatePicker( + selected: time, + onChanged: (value) => setState(() => time), + ), + m.TextButton( + child: const Text('Show Picker'), + onPressed: () async { + final newTime = await m.showDatePicker( + context: context, + initialDate: time, + firstDate: DateTime(time.year - 100), + lastDate: DateTime(time.year + 100), + ); + if (newTime != null) { + setState(() => time = newTime); + } + }, + ), + ], + [ + const Text('ListTile'), + ListTile( + leading: const Icon(FluentIcons.graph_symbol), + title: const Text('Content'), + onPressed: () {}, + ), + m.ListTile( + leading: const Icon(FluentIcons.graph_symbol), + title: const Text('Content'), + onTap: () {}, + ), + ], + [ + const Text('Tooltip'), + const Tooltip( + message: 'A fluent-styled tooltip', + child: Text('Hover'), + ), + const m.Tooltip( + message: 'A material-styled tooltip', + child: Text('Hover'), + ), + ], + ]; + + Widget buildColumn(int index) { + return Column( + children: children + .map( + (children) => Container( + constraints: const BoxConstraints(minHeight: 50.0), + alignment: AlignmentDirectional.center, + child: children[index], + ), + ) + .toList(), + ); + } + + return m.Material( + type: m.MaterialType.transparency, + child: Row(children: [ + Expanded(child: buildColumn(0)), + const m.VerticalDivider(), + Expanded(child: buildColumn(1)), + const m.VerticalDivider(), + Expanded(child: buildColumn(2)), + ]), + ); + } +} diff --git a/launchpad_app/lib/widgets/page.dart b/launchpad_app/lib/widgets/page.dart new file mode 100644 index 00000000..3d235bd1 --- /dev/null +++ b/launchpad_app/lib/widgets/page.dart @@ -0,0 +1,141 @@ +import 'dart:async'; + +import 'package:example/widgets/deferred_widget.dart'; + +import 'package:fluent_ui/fluent_ui.dart'; + +mixin PageMixin { + Widget description({required Widget content}) { + return Builder(builder: (context) { + return Padding( + padding: const EdgeInsetsDirectional.only(bottom: 4.0), + child: DefaultTextStyle( + style: FluentTheme.of(context).typography.body!, + child: content, + ), + ); + }); + } + + Widget subtitle({required Widget content}) { + return Builder(builder: (context) { + return Padding( + padding: const EdgeInsetsDirectional.only(top: 14.0, bottom: 2.0), + child: DefaultTextStyle( + style: FluentTheme.of(context).typography.subtitle!, + child: content, + ), + ); + }); + } +} + +abstract class Page extends StatelessWidget { + Page({super.key}) { + _pageIndex++; + } + + final StreamController _controller = StreamController.broadcast(); + Stream get stateStream => _controller.stream; + + @override + Widget build(BuildContext context); + + void setState(VoidCallback func) { + func(); + _controller.add(null); + } + + Widget description({required Widget content}) { + return Builder(builder: (context) { + return Padding( + padding: const EdgeInsetsDirectional.only(bottom: 4.0), + child: DefaultTextStyle( + style: FluentTheme.of(context).typography.body!, + child: content, + ), + ); + }); + } + + Widget subtitle({required Widget content}) { + return Builder(builder: (context) { + return Padding( + padding: const EdgeInsetsDirectional.only(top: 14.0, bottom: 2.0), + child: DefaultTextStyle( + style: FluentTheme.of(context).typography.subtitle!, + child: content, + ), + ); + }); + } +} + +int _pageIndex = -1; + +abstract class ScrollablePage extends Page { + ScrollablePage({super.key}); + + final scrollController = ScrollController(); + Widget buildHeader(BuildContext context) => const SizedBox.shrink(); + + Widget buildBottomBar(BuildContext context) => const SizedBox.shrink(); + + List buildScrollable(BuildContext context); + + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + key: PageStorageKey(_pageIndex), + scrollController: scrollController, + header: buildHeader(context), + children: buildScrollable(context), + bottomBar: buildBottomBar(context), + ); + } +} + +class EmptyPage extends Page { + final Widget? child; + + EmptyPage({ + this.child, + super.key, + }); + + @override + Widget build(BuildContext context) { + return child ?? const SizedBox.shrink(); + } +} + +typedef DeferredPageBuilder = Page Function(); + +class DeferredPage extends Page { + final LibraryLoader libraryLoader; + final DeferredPageBuilder createPage; + + DeferredPage({ + super.key, + required this.libraryLoader, + required this.createPage, + }); + + @override + Widget build(BuildContext context) { + return DeferredWidget(libraryLoader, () => createPage().build(context)); + } +} + +extension PageExtension on List { + List transform(BuildContext context) { + return map((page) { + return StreamBuilder( + stream: page.stateStream, + builder: (context, _) { + return page.build(context); + }, + ); + }).toList(); + } +} diff --git a/launchpad_app/lib/widgets/sponsor.dart b/launchpad_app/lib/widgets/sponsor.dart new file mode 100644 index 00000000..cda23109 --- /dev/null +++ b/launchpad_app/lib/widgets/sponsor.dart @@ -0,0 +1,117 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:url_launcher/link.dart'; + +class SponsorDialog extends StatelessWidget { + const SponsorDialog({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + assert(debugCheckHasFluentTheme(context)); + return ContentDialog( + constraints: const BoxConstraints(maxWidth: 600), + title: Row( + children: [ + const Icon(FluentIcons.diamond_user, size: 24.0), + const SizedBox(width: 8.0), + const Expanded(child: Text('Benefits')), + SmallIconButton( + child: Tooltip( + message: FluentLocalizations.of(context).closeButtonLabel, + child: IconButton( + icon: const Icon(FluentIcons.chrome_close), + onPressed: Navigator.of(context).pop, + ), + ), + ), + ], + ), + content: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Expanded( + child: _Tier( + name: 'Royal Secretary', + price: r'US$6 per month', + benefits: [ + 'General support', + 'Priority on issues fix', + 'Sponsor role on Discord', + 'Be the first to know when a new update rolls out', + ], + ), + ), + SizedBox(width: 10.0), + Expanded( + child: _Tier( + name: 'Royal Executor', + price: r'US$15 per month', + benefits: [ + 'General support', + 'Priority on issues fix', + 'Sponsor role on Discord', + 'Showcasing in the "Sponsors" section', + 'Be the first to know when a new update rolls out', + 'Private channel on Discord with dedicated help', + ], + ), + ) + ], + ), + actions: [ + Link( + uri: Uri.parse('https://www.patreon.com/bdlukaa'), + builder: (context, open) => FilledButton( + child: const Text('Become a Sponsor'), + onPressed: open, + ), + ), + ], + ); + } +} + +class _Tier extends StatelessWidget { + const _Tier({ + Key? key, + required this.name, + required this.price, + required this.benefits, + }) : super(key: key); + + final String name; + final String price; + + final List benefits; + + @override + Widget build(BuildContext context) { + final theme = FluentTheme.of(context); + return Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + name, + style: theme.typography.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + Text(price, style: theme.typography.caption), + const SizedBox(height: 20.0), + ...benefits.map((benefit) { + return Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Padding( + padding: EdgeInsetsDirectional.only(end: 6.0, top: 9.0), + child: Icon(FluentIcons.circle_fill, size: 4.0), + ), + Expanded(child: Text(benefit)), + ], + ); + }), + ], + ); + } +} diff --git a/launchpad_app/test/widget_test.dart b/launchpad_app/test/widget_test.dart new file mode 100644 index 00000000..092d222f --- /dev/null +++ b/launchpad_app/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:example/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +} From d0a824ab1b3347aba68403802e648a412437179a Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 16:47:36 -0500 Subject: [PATCH 145/227] Copy theme file from fluent_ui --- launchpad_app/lib/theme.dart | 87 ++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 launchpad_app/lib/theme.dart diff --git a/launchpad_app/lib/theme.dart b/launchpad_app/lib/theme.dart new file mode 100644 index 00000000..781933e2 --- /dev/null +++ b/launchpad_app/lib/theme.dart @@ -0,0 +1,87 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter/foundation.dart'; +import 'package:system_theme/system_theme.dart'; +import 'package:flutter_acrylic/flutter_acrylic.dart'; + +enum NavigationIndicators { sticky, end } + +class AppTheme extends ChangeNotifier { + AccentColor _color = systemAccentColor; + AccentColor get color => _color; + set color(AccentColor color) { + _color = color; + notifyListeners(); + } + + ThemeMode _mode = ThemeMode.system; + ThemeMode get mode => _mode; + set mode(ThemeMode mode) { + _mode = mode; + notifyListeners(); + } + + PaneDisplayMode _displayMode = PaneDisplayMode.auto; + PaneDisplayMode get displayMode => _displayMode; + set displayMode(PaneDisplayMode displayMode) { + _displayMode = displayMode; + notifyListeners(); + } + + NavigationIndicators _indicator = NavigationIndicators.sticky; + NavigationIndicators get indicator => _indicator; + set indicator(NavigationIndicators indicator) { + _indicator = indicator; + notifyListeners(); + } + + WindowEffect _windowEffect = WindowEffect.disabled; + WindowEffect get windowEffect => _windowEffect; + set windowEffect(WindowEffect windowEffect) { + _windowEffect = windowEffect; + notifyListeners(); + } + + void setEffect(WindowEffect effect, BuildContext context) { + Window.setEffect( + effect: effect, + color: [ + WindowEffect.solid, + WindowEffect.acrylic, + ].contains(effect) + ? FluentTheme.of(context).micaBackgroundColor.withOpacity(0.05) + : Colors.transparent, + dark: FluentTheme.of(context).brightness.isDark, + ); + } + + TextDirection _textDirection = TextDirection.ltr; + TextDirection get textDirection => _textDirection; + set textDirection(TextDirection direction) { + _textDirection = direction; + notifyListeners(); + } + + Locale? _locale; + Locale? get locale => _locale; + set locale(Locale? locale) { + _locale = locale; + notifyListeners(); + } +} + +AccentColor get systemAccentColor { + if ((defaultTargetPlatform == TargetPlatform.windows || + defaultTargetPlatform == TargetPlatform.android) && + !kIsWeb) { + return AccentColor.swatch({ + 'darkest': SystemTheme.accentColor.darkest, + 'darker': SystemTheme.accentColor.darker, + 'dark': SystemTheme.accentColor.dark, + 'normal': SystemTheme.accentColor.accent, + 'light': SystemTheme.accentColor.light, + 'lighter': SystemTheme.accentColor.lighter, + 'lightest': SystemTheme.accentColor.lightest, + }); + } + return Colors.blue; +} From a5954fe32a3bf5823e5aecd1a4c1529141296d65 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 16:48:52 -0500 Subject: [PATCH 146/227] Commit entire rest of sample project from fluent_ui as a starting point --- launchpad_app/lib/main.dart | 581 ++++++++++++++++++ launchpad_app/lib/routes/forms.dart | 5 + launchpad_app/lib/routes/inputs.dart | 4 + launchpad_app/lib/routes/navigation.dart | 3 + launchpad_app/lib/routes/surfaces.dart | 9 + launchpad_app/lib/routes/theming.dart | 4 + .../lib/screens/forms/auto_suggest_box.dart | 391 ++++++++++++ launchpad_app/lib/screens/forms/combobox.dart | 413 +++++++++++++ .../lib/screens/forms/date_picker.dart | 70 +++ launchpad_app/lib/screens/forms/text_box.dart | 89 +++ .../lib/screens/forms/time_picker.dart | 99 +++ launchpad_app/lib/screens/home.dart | 255 ++++++++ launchpad_app/lib/screens/inputs/button.dart | 321 ++++++++++ .../lib/screens/inputs/checkbox.dart | 130 ++++ launchpad_app/lib/screens/inputs/slider.dart | 98 +++ .../lib/screens/inputs/toggle_switch.dart | 84 +++ .../screens/navigation/navigation_view.dart | 261 ++++++++ .../lib/screens/navigation/tab_view.dart | 164 +++++ .../lib/screens/navigation/tree_view.dart | 277 +++++++++ launchpad_app/lib/screens/settings.dart | 290 +++++++++ .../lib/screens/surface/acrylic.dart | 223 +++++++ .../lib/screens/surface/commandbars.dart | 299 +++++++++ .../lib/screens/surface/content_dialog.dart | 97 +++ .../lib/screens/surface/expander.dart | 196 ++++++ .../lib/screens/surface/flyouts.dart | 410 ++++++++++++ .../lib/screens/surface/info_bars.dart | 195 ++++++ .../screens/surface/progress_indicators.dart | 82 +++ launchpad_app/lib/screens/surface/tiles.dart | 306 +++++++++ .../lib/screens/surface/tooltip.dart | 71 +++ launchpad_app/lib/screens/theming/colors.dart | 227 +++++++ launchpad_app/lib/screens/theming/icons.dart | 153 +++++ .../lib/screens/theming/reveal_focus.dart | 95 +++ .../lib/screens/theming/typography.dart | 143 +++++ 33 files changed, 6045 insertions(+) create mode 100644 launchpad_app/lib/main.dart create mode 100644 launchpad_app/lib/routes/forms.dart create mode 100644 launchpad_app/lib/routes/inputs.dart create mode 100644 launchpad_app/lib/routes/navigation.dart create mode 100644 launchpad_app/lib/routes/surfaces.dart create mode 100644 launchpad_app/lib/routes/theming.dart create mode 100644 launchpad_app/lib/screens/forms/auto_suggest_box.dart create mode 100644 launchpad_app/lib/screens/forms/combobox.dart create mode 100644 launchpad_app/lib/screens/forms/date_picker.dart create mode 100644 launchpad_app/lib/screens/forms/text_box.dart create mode 100644 launchpad_app/lib/screens/forms/time_picker.dart create mode 100644 launchpad_app/lib/screens/home.dart create mode 100644 launchpad_app/lib/screens/inputs/button.dart create mode 100644 launchpad_app/lib/screens/inputs/checkbox.dart create mode 100644 launchpad_app/lib/screens/inputs/slider.dart create mode 100644 launchpad_app/lib/screens/inputs/toggle_switch.dart create mode 100644 launchpad_app/lib/screens/navigation/navigation_view.dart create mode 100644 launchpad_app/lib/screens/navigation/tab_view.dart create mode 100644 launchpad_app/lib/screens/navigation/tree_view.dart create mode 100644 launchpad_app/lib/screens/settings.dart create mode 100644 launchpad_app/lib/screens/surface/acrylic.dart create mode 100644 launchpad_app/lib/screens/surface/commandbars.dart create mode 100644 launchpad_app/lib/screens/surface/content_dialog.dart create mode 100644 launchpad_app/lib/screens/surface/expander.dart create mode 100644 launchpad_app/lib/screens/surface/flyouts.dart create mode 100644 launchpad_app/lib/screens/surface/info_bars.dart create mode 100644 launchpad_app/lib/screens/surface/progress_indicators.dart create mode 100644 launchpad_app/lib/screens/surface/tiles.dart create mode 100644 launchpad_app/lib/screens/surface/tooltip.dart create mode 100644 launchpad_app/lib/screens/theming/colors.dart create mode 100644 launchpad_app/lib/screens/theming/icons.dart create mode 100644 launchpad_app/lib/screens/theming/reveal_focus.dart create mode 100644 launchpad_app/lib/screens/theming/typography.dart diff --git a/launchpad_app/lib/main.dart b/launchpad_app/lib/main.dart new file mode 100644 index 00000000..a15ccf03 --- /dev/null +++ b/launchpad_app/lib/main.dart @@ -0,0 +1,581 @@ +import 'package:fluent_ui/fluent_ui.dart' hide Page; +import 'package:flutter/foundation.dart'; +import 'package:flutter_acrylic/flutter_acrylic.dart' as flutter_acrylic; +import 'package:provider/provider.dart'; +import 'package:system_theme/system_theme.dart'; +import 'package:url_launcher/link.dart'; +import 'package:url_strategy/url_strategy.dart'; +import 'package:window_manager/window_manager.dart'; + +import 'screens/home.dart'; +import 'screens/settings.dart'; + +import 'routes/forms.dart' deferred as forms; +import 'routes/inputs.dart' deferred as inputs; +import 'routes/navigation.dart' deferred as navigation; +import 'routes/surfaces.dart' deferred as surfaces; +import 'routes/theming.dart' deferred as theming; + +import 'theme.dart'; +import 'widgets/deferred_widget.dart'; + +const String appTitle = 'Fluent UI Showcase for Flutter'; + +/// Checks if the current environment is a desktop environment. +bool get isDesktop { + if (kIsWeb) return false; + return [ + TargetPlatform.windows, + TargetPlatform.linux, + TargetPlatform.macOS, + ].contains(defaultTargetPlatform); +} + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // if it's not on the web, windows or android, load the accent color + if (!kIsWeb && + [ + TargetPlatform.windows, + TargetPlatform.android, + ].contains(defaultTargetPlatform)) { + SystemTheme.accentColor.load(); + } + + setPathUrlStrategy(); + + if (isDesktop) { + await flutter_acrylic.Window.initialize(); + await WindowManager.instance.ensureInitialized(); + windowManager.waitUntilReadyToShow().then((_) async { + await windowManager.setTitleBarStyle( + TitleBarStyle.hidden, + windowButtonVisibility: false, + ); + await windowManager.setSize(const Size(755, 545)); + await windowManager.setMinimumSize(const Size(350, 600)); + await windowManager.center(); + await windowManager.show(); + await windowManager.setPreventClose(true); + await windowManager.setSkipTaskbar(false); + }); + } + + runApp(const MyApp()); + + DeferredWidget.preload(forms.loadLibrary); + DeferredWidget.preload(inputs.loadLibrary); + DeferredWidget.preload(navigation.loadLibrary); + DeferredWidget.preload(surfaces.loadLibrary); + DeferredWidget.preload(theming.loadLibrary); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return ChangeNotifierProvider( + create: (_) => AppTheme(), + builder: (context, _) { + final appTheme = context.watch(); + return FluentApp( + title: appTitle, + themeMode: appTheme.mode, + debugShowCheckedModeBanner: false, + color: appTheme.color, + darkTheme: ThemeData( + brightness: Brightness.dark, + accentColor: appTheme.color, + visualDensity: VisualDensity.standard, + focusTheme: FocusThemeData( + glowFactor: is10footScreen() ? 2.0 : 0.0, + ), + ), + theme: ThemeData( + accentColor: appTheme.color, + visualDensity: VisualDensity.standard, + focusTheme: FocusThemeData( + glowFactor: is10footScreen() ? 2.0 : 0.0, + ), + ), + locale: appTheme.locale, + builder: (context, child) { + return Directionality( + textDirection: appTheme.textDirection, + child: NavigationPaneTheme( + data: NavigationPaneThemeData( + backgroundColor: appTheme.windowEffect != + flutter_acrylic.WindowEffect.disabled + ? Colors.transparent + : null, + ), + child: child!, + ), + ); + }, + initialRoute: '/', + routes: {'/': (context) => const MyHomePage()}, + ); + }, + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({Key? key}) : super(key: key); + + @override + _MyHomePageState createState() => _MyHomePageState(); +} + +class _MyHomePageState extends State with WindowListener { + bool value = false; + + int index = 0; + + final viewKey = GlobalKey(); + + final searchKey = GlobalKey(); + final searchFocusNode = FocusNode(); + final searchController = TextEditingController(); + + final List originalItems = [ + PaneItem( + icon: const Icon(FluentIcons.home), + title: const Text('Home'), + body: const HomePage(), + ), + PaneItemHeader(header: const Text('Inputs')), + PaneItem( + icon: const Icon(FluentIcons.button_control), + title: const Text('Button'), + body: DeferredWidget( + inputs.loadLibrary, + () => inputs.ButtonPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.checkbox_composite), + title: const Text('Checkbox'), + body: DeferredWidget( + inputs.loadLibrary, + () => inputs.CheckBoxPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.slider), + title: const Text('Slider'), + body: DeferredWidget( + inputs.loadLibrary, + () => inputs.SliderPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.toggle_left), + title: const Text('ToggleSwitch'), + body: DeferredWidget( + inputs.loadLibrary, + () => inputs.ToggleSwitchPage(), + ), + ), + PaneItemHeader(header: const Text('Form')), + PaneItem( + icon: const Icon(FluentIcons.text_field), + title: const Text('TextBox'), + body: DeferredWidget( + forms.loadLibrary, + () => forms.TextBoxPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.page_list), + title: const Text('AutoSuggestBox'), + body: DeferredWidget( + forms.loadLibrary, + () => forms.AutoSuggestBoxPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.combobox), + title: const Text('ComboBox'), + body: DeferredWidget( + forms.loadLibrary, + () => forms.ComboBoxPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.time_picker), + title: const Text('TimePicker'), + body: DeferredWidget( + forms.loadLibrary, + () => forms.TimePickerPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.date_time), + title: const Text('DatePicker'), + body: DeferredWidget( + forms.loadLibrary, + () => forms.DatePickerPage(), + ), + ), + PaneItemHeader(header: const Text('Navigation')), + PaneItem( + icon: const Icon(FluentIcons.navigation_flipper), + title: const Text('NavigationView'), + body: DeferredWidget( + navigation.loadLibrary, + () => navigation.NavigationViewPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.table_header_row), + title: const Text('TabView'), + body: DeferredWidget( + navigation.loadLibrary, + () => navigation.TabViewPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.bulleted_tree_list), + title: const Text('TreeView'), + body: DeferredWidget( + navigation.loadLibrary, + () => navigation.TreeViewPage(), + ), + ), + PaneItemHeader(header: const Text('Surfaces')), + PaneItem( + icon: const Icon(FluentIcons.un_set_color), + title: const Text('Acrylic'), + body: DeferredWidget( + surfaces.loadLibrary, + () => surfaces.AcrylicPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.customize_toolbar), + title: const Text('CommandBar'), + body: DeferredWidget( + surfaces.loadLibrary, + () => surfaces.CommandBarsPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.comment_urgent), + title: const Text('ContentDialog'), + body: DeferredWidget( + surfaces.loadLibrary, + () => surfaces.ContentDialogPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.expand_all), + title: const Text('Expander'), + body: DeferredWidget( + surfaces.loadLibrary, + () => surfaces.ExpanderPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.info_solid), + title: const Text('InfoBar'), + body: DeferredWidget( + surfaces.loadLibrary, + () => surfaces.InfoBarsPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.progress_ring_dots), + title: const Text('Progress Indicators'), + body: DeferredWidget( + surfaces.loadLibrary, + () => surfaces.ProgressIndicatorsPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.tiles), + title: const Text('Tiles'), + body: DeferredWidget( + surfaces.loadLibrary, + () => surfaces.TilesPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.hint_text), + title: const Text('Tooltip'), + body: DeferredWidget( + surfaces.loadLibrary, + () => surfaces.TooltipPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.pop_expand), + title: const Text('Flyout'), + body: DeferredWidget( + surfaces.loadLibrary, + () => surfaces.FlyoutPage(), + ), + ), + PaneItemHeader(header: const Text('Theming')), + PaneItem( + icon: const Icon(FluentIcons.color_solid), + title: const Text('Colors'), + body: DeferredWidget( + theming.loadLibrary, + () => theming.ColorsPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.font_color_a), + title: const Text('Typography'), + body: DeferredWidget( + theming.loadLibrary, + () => theming.TypographyPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.icon_sets_flag), + title: const Text('Icons'), + body: DeferredWidget( + theming.loadLibrary, + () => theming.IconsPage(), + ), + ), + PaneItem( + icon: const Icon(FluentIcons.focus), + title: const Text('Reveal Focus'), + body: DeferredWidget( + theming.loadLibrary, + () => theming.RevealFocusPage(), + ), + ), + ]; + final List footerItems = [ + PaneItemSeparator(), + PaneItem( + icon: const Icon(FluentIcons.settings), + title: const Text('Settings'), + body: Settings(), + ), + _LinkPaneItemAction( + icon: const Icon(FluentIcons.open_source), + title: const Text('Source code'), + link: 'https://github.com/bdlukaa/fluent_ui', + body: const SizedBox.shrink(), + ), + // TODO: mobile widgets, Scrollbar, BottomNavigationBar, RatingBar + ]; + + @override + void initState() { + windowManager.addListener(this); + super.initState(); + } + + @override + void dispose() { + windowManager.removeListener(this); + searchController.dispose(); + searchFocusNode.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final appTheme = context.watch(); + final theme = FluentTheme.of(context); + return NavigationView( + key: viewKey, + appBar: NavigationAppBar( + automaticallyImplyLeading: false, + title: () { + if (kIsWeb) { + return const Align( + alignment: AlignmentDirectional.centerStart, + child: Text(appTitle), + ); + } + return const DragToMoveArea( + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Text(appTitle), + ), + ); + }(), + actions: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + Padding( + padding: const EdgeInsetsDirectional.only(end: 8.0), + child: ToggleSwitch( + content: const Text('Dark Mode'), + checked: FluentTheme.of(context).brightness.isDark, + onChanged: (v) { + if (v) { + appTheme.mode = ThemeMode.dark; + } else { + appTheme.mode = ThemeMode.light; + } + }, + ), + ), + if (!kIsWeb) const WindowButtons(), + ]), + ), + pane: NavigationPane( + selected: index, + onChanged: (i) { + setState(() => index = i); + }, + header: SizedBox( + height: kOneLineTileHeight, + child: ShaderMask( + shaderCallback: (rect) { + final color = appTheme.color.resolveFromReverseBrightness( + theme.brightness, + level: theme.brightness == Brightness.light ? 0 : 2, + ); + return LinearGradient( + colors: [ + color, + color, + ], + ).createShader(rect); + }, + child: const FlutterLogo( + style: FlutterLogoStyle.horizontal, + size: 80.0, + textColor: Colors.white, + duration: Duration.zero, + ), + ), + ), + displayMode: appTheme.displayMode, + indicator: () { + switch (appTheme.indicator) { + case NavigationIndicators.end: + return const EndNavigationIndicator(); + case NavigationIndicators.sticky: + default: + return const StickyNavigationIndicator(); + } + }(), + items: originalItems, + autoSuggestBox: AutoSuggestBox( + key: searchKey, + focusNode: searchFocusNode, + controller: searchController, + items: originalItems.whereType().map((item) { + assert(item.title is Text); + final text = (item.title as Text).data!; + + return AutoSuggestBoxItem( + label: text, + value: text, + onSelected: () async { + final itemIndex = NavigationPane( + items: originalItems, + ).effectiveIndexOf(item); + + setState(() => index = itemIndex); + await Future.delayed(const Duration(milliseconds: 17)); + searchController.clear(); + }, + ); + }).toList(), + placeholder: 'Search', + ), + autoSuggestBoxReplacement: const Icon(FluentIcons.search), + footerItems: footerItems, + ), + onOpenSearch: () { + searchFocusNode.requestFocus(); + }, + ); + } + + @override + void onWindowClose() async { + bool _isPreventClose = await windowManager.isPreventClose(); + if (_isPreventClose) { + showDialog( + context: context, + builder: (_) { + return ContentDialog( + title: const Text('Confirm close'), + content: const Text('Are you sure you want to close this window?'), + actions: [ + FilledButton( + child: const Text('Yes'), + onPressed: () { + Navigator.pop(context); + windowManager.destroy(); + }, + ), + Button( + child: const Text('No'), + onPressed: () { + Navigator.pop(context); + }, + ), + ], + ); + }, + ); + } + } +} + +class WindowButtons extends StatelessWidget { + const WindowButtons({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + final ThemeData theme = FluentTheme.of(context); + + return SizedBox( + width: 138, + height: 50, + child: WindowCaption( + brightness: theme.brightness, + backgroundColor: Colors.transparent, + ), + ); + } +} + +class _LinkPaneItemAction extends PaneItem { + _LinkPaneItemAction({ + required super.icon, + required this.link, + required super.body, + super.title, + }); + + final String link; + + @override + Widget build( + BuildContext context, + bool selected, + VoidCallback? onPressed, { + PaneDisplayMode? displayMode, + bool showTextOnTop = true, + bool? autofocus, + int? itemIndex, + }) { + return Link( + uri: Uri.parse(link), + builder: (context, followLink) => super.build( + context, + selected, + followLink, + displayMode: displayMode, + showTextOnTop: showTextOnTop, + itemIndex: itemIndex, + autofocus: autofocus, + ), + ); + } +} diff --git a/launchpad_app/lib/routes/forms.dart b/launchpad_app/lib/routes/forms.dart new file mode 100644 index 00000000..10dd8ecb --- /dev/null +++ b/launchpad_app/lib/routes/forms.dart @@ -0,0 +1,5 @@ +export '../screens/forms/auto_suggest_box.dart'; +export '../screens/forms/combobox.dart'; +export '../screens/forms/date_picker.dart'; +export '../screens/forms/text_box.dart'; +export '../screens/forms/time_picker.dart'; diff --git a/launchpad_app/lib/routes/inputs.dart b/launchpad_app/lib/routes/inputs.dart new file mode 100644 index 00000000..b46e7974 --- /dev/null +++ b/launchpad_app/lib/routes/inputs.dart @@ -0,0 +1,4 @@ +export '../screens/inputs/button.dart'; +export '../screens/inputs/checkbox.dart'; +export '../screens/inputs/slider.dart'; +export '../screens/inputs/toggle_switch.dart'; diff --git a/launchpad_app/lib/routes/navigation.dart b/launchpad_app/lib/routes/navigation.dart new file mode 100644 index 00000000..78cf5596 --- /dev/null +++ b/launchpad_app/lib/routes/navigation.dart @@ -0,0 +1,3 @@ +export '../screens/navigation/navigation_view.dart'; +export '../screens/navigation/tab_view.dart'; +export '../screens/navigation/tree_view.dart'; diff --git a/launchpad_app/lib/routes/surfaces.dart b/launchpad_app/lib/routes/surfaces.dart new file mode 100644 index 00000000..530d1cc2 --- /dev/null +++ b/launchpad_app/lib/routes/surfaces.dart @@ -0,0 +1,9 @@ +export '../screens/surface/acrylic.dart'; +export '../screens/surface/commandbars.dart'; +export '../screens/surface/content_dialog.dart'; +export '../screens/surface/expander.dart'; +export '../screens/surface/flyouts.dart'; +export '../screens/surface/info_bars.dart'; +export '../screens/surface/progress_indicators.dart'; +export '../screens/surface/tiles.dart'; +export '../screens/surface/tooltip.dart'; diff --git a/launchpad_app/lib/routes/theming.dart b/launchpad_app/lib/routes/theming.dart new file mode 100644 index 00000000..c9e002ef --- /dev/null +++ b/launchpad_app/lib/routes/theming.dart @@ -0,0 +1,4 @@ +export '../screens/theming/colors.dart'; +export '../screens/theming/icons.dart'; +export '../screens/theming/reveal_focus.dart'; +export '../screens/theming/typography.dart'; diff --git a/launchpad_app/lib/screens/forms/auto_suggest_box.dart b/launchpad_app/lib/screens/forms/auto_suggest_box.dart new file mode 100644 index 00000000..4cfe4c37 --- /dev/null +++ b/launchpad_app/lib/screens/forms/auto_suggest_box.dart @@ -0,0 +1,391 @@ +import 'package:example/widgets/card_highlight.dart'; +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +class AutoSuggestBoxPage extends StatefulWidget { + const AutoSuggestBoxPage({Key? key}) : super(key: key); + + @override + State createState() => _AutoSuggestBoxPageState(); +} + +class _AutoSuggestBoxPageState extends State + with PageMixin { + String? selectedCat; + Cat? selectedObjectCat; + bool enabled = true; + + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: PageHeader( + title: const Text('AutoSuggestBox'), + commandBar: ToggleSwitch( + content: const Text('Disabled'), + checked: !enabled, + onChanged: (v) => setState(() => enabled = !v), + ), + ), + children: [ + const Text( + 'A text control that makes suggestions to users as they type. The app ' + 'is notified when text has been changed by the user and is responsible ' + 'for providing relevant suggestions for this control to display.', + ), + subtitle(content: const Text('A basic AutoSuggestBox')), + CardHighlight( + child: Row(children: [ + SizedBox( + width: 350.0, + child: AutoSuggestBox( + enabled: enabled, + items: cats + .map>( + (cat) => AutoSuggestBoxItem( + value: cat, + label: cat, + onFocusChange: (focused) { + if (focused) debugPrint('Focused $cat'); + }, + ), + ) + .toList(), + onSelected: (item) { + setState(() => selectedCat = item.value); + }, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsetsDirectional.only(start: 8.0), + child: Text(selectedCat ?? ''), + ), + ), + ]), + codeSnippet: ''' +String? selectedCat; + +AutoSuggestBox( + items: cats.map((cat) { + return AutoSuggestBoxItem( + value: cat, + label: cat, + onFocusChange: (focused) { + if (focused) { + debugPrint('Focused \$cat'); + } + } + ); + }).toList(), + onSelected: (item) { + setState(() => selected = item); + }, +), + +const cats = [ + 'Abyssinian', + 'Aegean', + 'American Bobtail', + 'American Curl', + ... +];''', + ), + const Text( + 'The control can be used with a custom value class. With this feature,' + ' AutoSuggestBox can be used as a replacement of a ComboBox.', + ), + subtitle( + content: const Text('A AutoSuggestBox with a custom type "Cat"')), + CardHighlight( + child: Row(children: [ + SizedBox( + width: 350.0, + child: AutoSuggestBox( + enabled: enabled, + items: objectCats + .map>( + (cat) => AutoSuggestBoxItem( + value: cat, + label: cat.name, + onFocusChange: (focused) { + if (focused) { + debugPrint('Focused $cat'); + } + }, + ), + ) + .toList(), + onSelected: (item) { + setState(() => selectedObjectCat = item.value); + }, + ), + ), + Expanded( + child: Padding( + padding: const EdgeInsetsDirectional.only(start: 8.0), + child: Text(selectedObjectCat != null + ? 'Cat #${selectedObjectCat!.id} "${selectedObjectCat!.name}" ${selectedObjectCat!.hasTag ? '[🏷 TAGGED]' : "[❌ NON TAGGED]"}' + : ''), + ), + ), + ]), + codeSnippet: ''' +class Cat { + final int id; + final String name; + final bool hasTag; + + const Cat(this.id, this.name, this.hasTag); +} + +Cat? selectedObjectCat; + +AutoSuggestBox( + items: objectCats + .map>( + (cat) => AutoSuggestBoxItem( + value: cat, + label: cat.name, + onFocusChange: (focused) { + if (focused) { + debugPrint('Focused #\${cat.id} - \${cat.name}'); + } + }, + ), + ) + .toList(), + onSelected: (item) { + setState(() => selectedObjectCat = item.value); + }, +), + +const objectCats = [ + Cat(1, 'Abyssinian', true), + Cat(2, 'Aegean', true), + Cat(3, 'American Bobtail', false), + Cat(4, 'American Curl', true), + Cat(5, 'American Ringtail', false), + Cat(6, 'American Shorthair', true), + ... +];''', + ), + ], + ); + } +} + +const cats = [ + 'Abyssinian', + 'Aegean', + 'American Bobtail', + 'American Curl', + 'American Ringtail', + 'American Shorthair', + 'American Wirehair', + 'Aphrodite Giant', + 'Arabian Mau', + 'Asian cat', + 'Asian Semi-longhair', + 'Australian Mist', + 'Balinese', + 'Bambino', + 'Bengal', + 'Birman', + 'Bombay', + 'Brazilian Shorthair', + 'British Longhair', + 'British Shorthair', + 'Burmese', + 'Burmilla', + 'California Spangled', + 'Chantilly-Tiffany', + 'Chartreux', + 'Chausie', + 'Colorpoint Shorthair', + 'Cornish Rex', + 'Cymric', + 'Cyprus', + 'Devon Rex', + 'Donskoy', + 'Dragon Li', + 'Dwelf', + 'Egyptian Mau', + 'European Shorthair', + 'Exotic Shorthair', + 'Foldex', + 'German Rex', + 'Havana Brown', + 'Highlander', + 'Himalayan', + 'Japanese Bobtail', + 'Javanese', + 'Kanaani', + 'Khao Manee', + 'Kinkalow', + 'Korat', + 'Korean Bobtail', + 'Korn Ja', + 'Kurilian Bobtail', + 'Lambkin', + 'LaPerm', + 'Lykoi', + 'Maine Coon', + 'Manx', + 'Mekong Bobtail', + 'Minskin', + 'Napoleon', + 'Munchkin', + 'Nebelung', + 'Norwegian Forest Cat', + 'Ocicat', + 'Ojos Azules', + 'Oregon Rex', + 'Oriental Bicolor', + 'Oriental Longhair', + 'Oriental Shorthair', + 'Persian (modern)', + 'Persian (traditional)', + 'Peterbald', + 'Pixie-bob', + 'Ragamuffin', + 'Ragdoll', + 'Raas', + 'Russian Blue', + 'Russian White', + 'Sam Sawet', + 'Savannah', + 'Scottish Fold', + 'Selkirk Rex', + 'Serengeti', + 'Serrade Petit', + 'Siamese', + 'Siberian or´Siberian Forest Cat', + 'Singapura', + 'Snowshoe', + 'Sokoke', + 'Somali', + 'Sphynx', + 'Suphalak', + 'Thai', + 'Thai Lilac', + 'Tonkinese', + 'Toyger', + 'Turkish Angora', + 'Turkish Van', + 'Turkish Vankedisi', + 'Ukrainian Levkoy', + 'Wila Krungthep', + 'York Chocolate', +]; + +const objectCats = [ + Cat(1, 'Abyssinian', true), + Cat(2, 'Aegean', true), + Cat(3, 'American Bobtail', false), + Cat(4, 'American Curl', true), + Cat(5, 'American Ringtail', false), + Cat(6, 'American Shorthair', true), + Cat(7, 'American Wirehair', false), + Cat(8, 'Aphrodite Giant', true), + Cat(9, 'Arabian Mau', false), + Cat(10, 'Asian cat', true), + Cat(11, 'Asian Semi-longhair', false), + Cat(12, 'Australian Mist', false), + Cat(13, 'Balinese', false), + Cat(14, 'Bambino', false), + Cat(15, 'Bengal', true), + Cat(16, 'Birman', false), + Cat(17, 'Bombay', true), + Cat(18, 'Brazilian Shorthair', false), + Cat(19, 'British Longhair', true), + Cat(20, 'British Shorthair', false), + Cat(21, 'Burmese', true), + Cat(22, 'Burmilla', false), + Cat(23, 'California Spangled', false), + Cat(24, 'Chantilly-Tiffany', true), + Cat(25, 'Chartreux', true), + Cat(26, 'Chausie', false), + Cat(27, 'Colorpoint Shorthair', true), + Cat(28, 'Cornish Rex', false), + Cat(29, 'Cymric', false), + Cat(30, 'Cyprus', false), + Cat(31, 'Devon Rex', false), + Cat(32, 'Donskoy', false), + Cat(33, 'Dragon Li', false), + Cat(34, 'Dwelf', false), + Cat(35, 'Egyptian Mau', true), + Cat(36, 'European Shorthair', false), + Cat(37, 'Exotic Shorthair', false), + Cat(38, 'Foldex', false), + Cat(39, 'German Rex', true), + Cat(40, 'Havana Brown', true), + Cat(41, 'Highlander', true), + Cat(42, 'Himalayan', true), + Cat(43, 'Japanese Bobtail', true), + Cat(44, 'Javanese', true), + Cat(45, 'Kanaani', true), + Cat(46, 'Khao Manee', true), + Cat(47, 'Kinkalow', true), + Cat(48, 'Korat', false), + Cat(49, 'Korean Bobtail', true), + Cat(50, 'Korn Ja', false), + Cat(51, 'Kurilian Bobtail', true), + Cat(52, 'Lambkin', false), + Cat(53, 'LaPerm', true), + Cat(54, 'Lykoi', true), + Cat(55, 'Maine Coon', false), + Cat(56, 'Manx', true), + Cat(57, 'Mekong Bobtail', true), + Cat(58, 'Minskin', false), + Cat(59, 'Napoleon', true), + Cat(60, 'Munchkin', false), + Cat(61, 'Nebelung', false), + Cat(62, 'Norwegian Forest Cat', false), + Cat(63, 'Ocicat', true), + Cat(64, 'Ojos Azules', true), + Cat(65, 'Oregon Rex', false), + Cat(66, 'Oriental Bicolor', true), + Cat(67, 'Oriental Longhair', false), + Cat(68, 'Oriental Shorthair', true), + Cat(69, 'Persian (modern)', true), + Cat(70, 'Persian (traditional)', true), + Cat(71, 'Peterbald', false), + Cat(72, 'Pixie-bob', false), + Cat(73, 'Ragamuffin', true), + Cat(74, 'Ragdoll', true), + Cat(75, 'Raas', true), + Cat(76, 'Russian Blue', true), + Cat(77, 'Russian White', true), + Cat(78, 'Sam Sawet', false), + Cat(79, 'Savannah', false), + Cat(80, 'Scottish Fold', false), + Cat(81, 'Selkirk Rex', false), + Cat(82, 'Serengeti', false), + Cat(83, 'Serrade Petit', false), + Cat(84, 'Siamese', false), + Cat(85, 'Siberian or´Siberian Forest Cat', false), + Cat(86, 'Singapura', true), + Cat(87, 'Snowshoe', false), + Cat(88, 'Sokoke', false), + Cat(89, 'Somali', false), + Cat(90, 'Sphynx', true), + Cat(91, 'Suphalak', true), + Cat(92, 'Thai', false), + Cat(93, 'Thai Lilac', false), + Cat(94, 'Tonkinese', false), + Cat(95, 'Toyger', true), + Cat(96, 'Turkish Angora', true), + Cat(97, 'Turkish Van', false), + Cat(98, 'Turkish Vankedisi', false), + Cat(99, 'Ukrainian Levkoy', false), + Cat(100, 'Wila Krungthep', true), + Cat(101, 'York Chocolate', true), +]; + +class Cat { + final int id; + final String name; + final bool hasTag; + + const Cat(this.id, this.name, this.hasTag); +} diff --git a/launchpad_app/lib/screens/forms/combobox.dart b/launchpad_app/lib/screens/forms/combobox.dart new file mode 100644 index 00000000..d5585efb --- /dev/null +++ b/launchpad_app/lib/screens/forms/combobox.dart @@ -0,0 +1,413 @@ +import 'package:example/widgets/card_highlight.dart'; +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +import 'auto_suggest_box.dart'; + +class ComboBoxPage extends StatefulWidget { + const ComboBoxPage({Key? key}) : super(key: key); + + @override + State createState() => _ComboBoxPageState(); +} + +class _ComboBoxPageState extends State with PageMixin { + String? selectedColor = 'Green'; + String? selectedCat; + double fontSize = 20.0; + bool disabled = false; + final comboboxKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: PageHeader( + title: const Text('ComboBox'), + commandBar: ToggleSwitch( + checked: disabled, + onChanged: (v) { + setState(() => disabled = v); + }, + content: const Text('Disabled'), + ), + ), + children: [ + const Text( + 'Use a combo box (also known as a drop-down list) to present a list of ' + 'items that a user can select from. A combo box starts in a compact ' + 'state and expands to show a list of selectable items.\n\n' + 'When the combo box is closed, it either displays the current selection ' + 'or is empty if there is no selected item. When the user expands the ' + 'combo box, it displays the list of selectable items.\n\n' + 'Use a ComboBox when you need to conserve on-screen space and when ' + 'users select only one option at a time. A ComboBox shows only the ' + 'currently selected item.', + ), + subtitle( + content: const Text( + 'A ComboBox with items defined inline and its width set', + ), + ), + CardHighlight( + child: + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + ComboBox( + isExpanded: false, + popupColor: colors[selectedColor], + value: selectedColor, + items: colors.entries.map((e) { + return ComboBoxItem( + child: Text(e.key), + value: e.key, + ); + }).toList(), + onChanged: disabled + ? null + : (color) { + setState(() => selectedColor = color); + }, + ), + Container( + margin: const EdgeInsetsDirectional.only(top: 8.0), + height: 30, + width: 100, + color: colors[selectedColor], + ), + ]), + codeSnippet: '''// Green by default +Color selectedColor = 'Green'; + +ComboBox( + value: selectedColor, + items: colors.entries.map((e) { + return ComboBoxItem( + child: Text(e.key), + value: e.key, + ); + }).toList(), + onChanged: disabled ? null : (color) => setState(() => selectedColor = color), +),''', + ), + subtitle( + content: const Text('A ComboBox with a long list of items'), + ), + CardHighlight( + child: Wrap(spacing: 10.0, runSpacing: 10.0, children: [ + ComboBox( + isExpanded: false, + value: selectedCat, + items: cats.map>((e) { + return ComboBoxItem( + child: Text(e), + value: e, + ); + }).toList(), + onChanged: disabled + ? null + : (color) { + setState(() => selectedCat = color); + }, + placeholder: const Text('Select a cat breed'), + ), + Container( + margin: const EdgeInsetsDirectional.only(top: 8.0), + height: 30, + child: Text(selectedCat ?? ''), + ), + ]), + codeSnippet: '''List cats= [...]; + +ComboBox( + value: selectedCat, + items: cats.map>((e) { + return ComboBoxItem( + child: Text(e), + value: e, + ); + }).toList(), + onChanged: disabled + ? null + : (color) { + setState(() => selectedCat = color); + }, + placeholder: const Text('Select a cat breed'), +),''', + ), + subtitle(content: const Text('An editable ComboBox')), + description( + content: const Text( + 'By default, a combo box lets the user select from a pre-defined ' + 'list of options. However, there are cases where the list contains ' + 'only a subset of valid values, and the user should be able to enter ' + 'other values that aren\'t listed. To support this, you can make the' + ' combo box editable.', + ), + ), + CardHighlight( + child: + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + SizedBox( + width: 150, + child: EditableComboBox( + isExpanded: false, + value: fontSize.toInt(), + items: fontSizes.map>((fontSize) { + return ComboBoxItem( + child: Text('${fontSize.toInt()}'), + value: fontSize.toInt(), + ); + }).toList(), + onChanged: disabled + ? null + : (size) { + setState( + () => fontSize = (size ?? fontSize).toDouble()); + }, + placeholder: const Text('Font size'), + onFieldSubmitted: (text) { + try { + final newSize = int.parse(text); + + if (newSize < 8 || newSize > 100) { + throw UnsupportedError( + 'The font size must be a number between 8 and 100.', + ); + } + + setState(() => fontSize = newSize.toDouble()); + } catch (e) { + showDialog( + context: context, + builder: (context) { + return ContentDialog( + content: const Text( + 'The font size must be a number between 8 and 100.', + ), + actions: [ + FilledButton( + child: const Text('Close'), + onPressed: Navigator.of(context).pop, + ), + ], + ); + }, + ); + } + return '${fontSize.toInt()}'; + }, + ), + ), + Container( + margin: const EdgeInsetsDirectional.only(top: 8.0), + constraints: const BoxConstraints(minHeight: 50.0), + child: Text( + 'You can set the font size for this text', + style: TextStyle(fontSize: fontSize), + ), + ), + ]), + codeSnippet: '''static const fontSizes = [ + 8, + 9, + ..., +]; + +double fontSize = 20.0; + +EditableComboBox( + value: fontSize.toInt(), + items: cats.map>((e) { + return ComboBoxItem( + child: Text('\$e'), + value: e.toInt(), + ); + }).toList(), + onChanged: disabled + ? null + : (size) { + setState(() => fontSize = size?.toDouble() ?? fontSize); + }, + placeholder: const Text('Select a font size'), + onFieldSubmitted: (String text) { + // When the value in the text field is changed, this callback is called + // It's up to the developer to handle the text change + + try { + final newSize = int.parse(text); + + if (newSize < 8 || newSize > 100) { + throw UnsupportedError( + 'The font size must be a number between 8 and 100.', + ); + } + + setState(() => fontSize = newSize.toDouble()); + } catch (e) { + showDialog( + context: context, + builder: (context) { + return ContentDialog( + content: const Text( + 'The font size must be a number between 8 and 100.', + ), + actions: [ + FilledButton( + child: const Text('Close'), + onPressed: Navigator.of(context).pop, + ), + ], + ); + }, + ); + } + return fontSize.toInt().toString(); + }, +),''', + ), + subtitle(content: const Text('A ComboBox Form Field')), + CardHighlight( + child: + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Form( + autovalidateMode: AutovalidateMode.always, + child: ComboboxFormField( + popupColor: colors[selectedColor], + value: selectedColor, + items: colors.entries.map((e) { + return ComboBoxItem( + child: Text(e.key), + value: e.key, + ); + }).toList(), + onChanged: disabled + ? null + : (color) => setState(() => selectedColor = color), + validator: (text) { + if (text == null || text.isEmpty) { + return 'Please provide a value'; + } + + final acceptedValues = colors.keys.skip(4); + + if (!acceptedValues.contains(text)) { + return '$text is not a valid value today'; + } + + return null; + }, + ), + ), + Container( + margin: const EdgeInsetsDirectional.only(top: 8.0), + height: 30, + width: 100, + color: colors[selectedColor], + ), + ]), + codeSnippet: '''Map colors = { ... }; +Color selectedColor = 'Green'; + +Form( + autovalidateMode: AutovalidateMode.always, + child: ComboboxFormField( + value: selectedColor, + items: colors.entries.map((e) { + return ComboBoxItem( + child: Text(e.key), + value: e.key, + ); + }).toList(), + onChanged: disabled ? null : (color) => setState(() => selectedColor = color), + validator: (text) { + if (text == null || text.isEmpty) { + return 'Please provide a value'; + } + + final acceptedValues = colors.keys.skip(4); + + if (!acceptedValues.contains(text)) { + return '\$text is not a valid value today'; + } + + return null; + }, + ), +),''', + ), + subtitle(content: const Text('Open popup programatically')), + CardHighlight( + child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + ComboBox( + key: comboboxKey, + isExpanded: false, + popupColor: colors[selectedColor], + value: selectedColor, + items: colors.entries.map((e) { + return ComboBoxItem( + child: Text(e.key), + value: e.key, + ); + }).toList(), + onChanged: disabled + ? null + : (color) => setState(() => selectedColor = color), + ), + const SizedBox(width: 8.0), + Button( + child: const Text('Open popup'), + onPressed: + disabled ? null : () => comboboxKey.currentState?.openPopup(), + ), + ]), + codeSnippet: + '''// A GlobalKey is used to access the current +// state of the combo box. With it, it's possible to call .openPopup() and .closePopup() +// which will open and close the popup, respectively +// +// It is possible to use the key with ComboBox and EditableComboBox +final comboboxKey = GlobalKey(); + +ComboBox( + key: comboboxKey, + // define the other properties here + ... +), + +Button( + child: const Text('Open popup'), + onPressed: () => comboboxKey.currentState?.openPopup(), +),''', + ), + ], + ); + } + + Map colors = { + 'Blue': Colors.blue, + 'Green': Colors.green, + 'Red': Colors.red, + 'Yellow': Colors.yellow, + 'Grey': Colors.grey, + 'Magenta': Colors.magenta, + 'Orange': Colors.orange, + 'Purple': Colors.purple, + 'Teal': Colors.teal, + }; + + static const fontSizes = [ + 8, + 9, + 10, + 11, + 12, + 14, + 16, + 18, + 20, + 24, + 28, + 36, + 48, + 72, + ]; +} diff --git a/launchpad_app/lib/screens/forms/date_picker.dart b/launchpad_app/lib/screens/forms/date_picker.dart new file mode 100644 index 00000000..840cae3f --- /dev/null +++ b/launchpad_app/lib/screens/forms/date_picker.dart @@ -0,0 +1,70 @@ +import 'package:example/widgets/card_highlight.dart'; +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +class DatePickerPage extends StatefulWidget { + const DatePickerPage({Key? key}) : super(key: key); + + @override + State createState() => _DatePickerPageState(); +} + +class _DatePickerPageState extends State with PageMixin { + DateTime? simpleTime; + DateTime? hiddenTime; + + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: const PageHeader(title: Text('DatePicker')), + children: [ + const Text( + 'Use a DatePicker to let users set a date in your app, for example to ' + 'schedule an appointment. The DatePicker displays three controls for ' + 'month, date, and year. These controls are easy to use with touch or ' + 'mouse, and they can be styled and configured in several different ways.' + '\n\nThe entry point displays the chosen date, and when the user ' + 'selects the entry point, a picker surface expands vertically from the ' + 'middle for the user to make a selection. The date picker overlays ' + 'other UI; it doesn\'t push other UI out of the way.', + ), + subtitle(content: const Text('A simple DatePicker with a header')), + CardHighlight( + child: Align( + alignment: AlignmentDirectional.centerStart, + child: DatePicker( + header: 'Pick a date', + selected: simpleTime, + onChanged: (time) => setState(() => simpleTime = time), + ), + ), + codeSnippet: '''DateTime? selected; + +DatePicker( + header: 'Pick a date', + selected: selected, + onChanged: (time) => setState(() => selected = time), +),''', + ), + subtitle(content: const Text('A DatePicker with year hidden')), + CardHighlight( + child: Align( + alignment: AlignmentDirectional.centerStart, + child: DatePicker( + selected: hiddenTime, + onChanged: (v) => setState(() => hiddenTime = v), + showYear: false, + ), + ), + codeSnippet: '''DateTime? selected; + +DatePicker( + selected: selected, + onChanged: (time) => setState(() => selected = time), + showYear: false, +),''', + ), + ], + ); + } +} diff --git a/launchpad_app/lib/screens/forms/text_box.dart b/launchpad_app/lib/screens/forms/text_box.dart new file mode 100644 index 00000000..a4ee7946 --- /dev/null +++ b/launchpad_app/lib/screens/forms/text_box.dart @@ -0,0 +1,89 @@ +import 'package:example/widgets/card_highlight.dart'; +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +class TextBoxPage extends ScrollablePage { + TextBoxPage({super.key}); + + @override + Widget buildHeader(BuildContext context) { + return const PageHeader(title: Text('TextBox')); + } + + @override + List buildScrollable(BuildContext context) { + return [ + const Text( + 'The TextBox control lets a user type text into an app. It\'s typically ' + 'used to capture a single line of text, but can be configured to capture ' + 'multiple lines of text. The text displays on the screen in a simple, ' + 'uniform, plaintext format.\n\n' + 'TextBox has a number of features that can simplify text entry. It comes ' + 'with a familiar, built-in context menu with support for copying and ' + 'pasting text. The "clear all" button lets a user quickly delete all ' + 'text that has been entered. It also has spell checking capabilities ' + 'built in and enabled by default.', + ), + subtitle(content: const Text('A simple TextBox')), + CardHighlight( + child: Row(children: const [ + Expanded(child: TextBox()), + SizedBox(width: 10.0), + Expanded(child: TextBox(enabled: false, placeholder: 'Disabled')) + ]), + codeSnippet: '''TextBox()''', + ), + subtitle( + content: const Text('A TextBox with a header and placeholder text'), + ), + const CardHighlight( + child: TextBox( + header: 'Enter your name:', + placeholder: 'Name', + expands: false, + ), + codeSnippet: '''TextBox( + header: 'Enter your name:', + placeholder: 'Name', + expands: false, +),''', + ), + subtitle( + content: const Text('A read-only TextBox with various properties set'), + ), + const CardHighlight( + child: TextBox( + readOnly: true, + placeholder: 'I am super excited to be here!', + style: TextStyle( + fontFamily: 'Arial', + fontSize: 24.0, + letterSpacing: 8, + color: Color(0xFF5178BE), + fontStyle: FontStyle.italic, + ), + ), + codeSnippet: '''TextBox( + readOnly: true, + placeholder: 'I am super excited to be here', + style: TextStyle( + fontFamily: 'Arial, + fontSize: 24.0, + letterSpacing: 8, + color: Color(0xFF5178BE), + fontStyle: FontStyle.italic, + ), +),''', + ), + subtitle(content: const Text('A multi-line TextBox')), + const CardHighlight( + child: TextBox( + maxLines: null, + ), + codeSnippet: '''TextBox( + maxLines: null, +),''', + ), + ]; + } +} diff --git a/launchpad_app/lib/screens/forms/time_picker.dart b/launchpad_app/lib/screens/forms/time_picker.dart new file mode 100644 index 00000000..4e4857b5 --- /dev/null +++ b/launchpad_app/lib/screens/forms/time_picker.dart @@ -0,0 +1,99 @@ +import 'package:example/widgets/card_highlight.dart'; +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +class TimePickerPage extends StatefulWidget { + const TimePickerPage({Key? key}) : super(key: key); + + @override + State createState() => _TimePickerPageState(); +} + +class _TimePickerPageState extends State with PageMixin { + DateTime? simpleTime; + DateTime? arrivalTime; + DateTime? hhTime; + + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: const PageHeader(title: Text('TimePicker')), + children: [ + const Text( + 'Use a TimePicker to let users set a time in your app, for example to ' + 'set a reminder. The TimePicker displays three controls for hour, ' + 'minute, and AM/PM. These controls are easy to use with touch or mouse, ' + 'and they can be styled and configured in several different ways.\n\n' + 'The entry point displays the chosen time, and when the user selects ' + 'the entry point, a picker surface expands vertically from the middle ' + 'for the user to make a selection. The time picker overlays other UI; ' + 'it doesn\'t push other UI out of the way.', + ), + subtitle(content: const Text('A simple TimePicker')), + CardHighlight( + child: Align( + alignment: AlignmentDirectional.centerStart, + child: TimePicker( + selected: simpleTime, + onChanged: (time) => setState(() => simpleTime = time), + ), + ), + codeSnippet: '''DateTime? selected; + +TimePicker( + selected: selected, + onChanged: (time) => setState(() => selected = time), +),''', + ), + subtitle( + content: const Text( + 'A TimePicker with a header and minute increments specified', + ), + ), + CardHighlight( + child: Align( + alignment: AlignmentDirectional.centerStart, + child: TimePicker( + header: 'Arrival time', + selected: arrivalTime, + onChanged: (time) => setState(() => arrivalTime = time), + minuteIncrement: 15, + ), + ), + codeSnippet: '''DateTime? selected; + +TimePicker( + selected: selected, + onChanged: (time) => setState(() => selected = time), + header: 'Arrival time', + minuteIncrement: 15, +),''', + ), + subtitle( + content: const Text( + 'A TimePicker using a 24-hour clock', + ), + ), + CardHighlight( + child: Align( + alignment: AlignmentDirectional.centerStart, + child: TimePicker( + header: '24 hour clock', + selected: hhTime, + onChanged: (v) => setState(() => hhTime = v), + hourFormat: HourFormat.HH, + ), + ), + codeSnippet: '''DateTime? selected; + +TimePicker( + selected: selected, + onChanged: (time) => setState(() => selected = time), + header: '24 hour clock', + hourFormat: HourFormat.HH, +),''', + ), + ], + ); + } +} diff --git a/launchpad_app/lib/screens/home.dart b/launchpad_app/lib/screens/home.dart new file mode 100644 index 00000000..3cfd3fba --- /dev/null +++ b/launchpad_app/lib/screens/home.dart @@ -0,0 +1,255 @@ +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:url_launcher/link.dart'; + +import '../models/sponsor.dart'; +import '../widgets/changelog.dart'; +import '../widgets/material_equivalents.dart'; +import '../widgets/page.dart'; +import '../widgets/sponsor.dart'; + +class HomePage extends StatefulWidget { + const HomePage({Key? key}) : super(key: key); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State with PageMixin { + bool selected = true; + String? comboboxValue; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasFluentTheme(context)); + final theme = FluentTheme.of(context); + + return ScaffoldPage.scrollable( + header: PageHeader( + title: const Text('Fluent UI for Flutter Showcase App'), + commandBar: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ + Link( + uri: Uri.parse('https://github.com/bdlukaa/fluent_ui'), + builder: (context, open) => Tooltip( + message: 'Source code', + child: IconButton( + icon: const Icon(FluentIcons.open_source, size: 24.0), + onPressed: open, + ), + ), + ), + ]), + ), + children: [ + Card( + child: + Wrap(alignment: WrapAlignment.center, spacing: 10.0, children: [ + InfoLabel( + label: 'Inputs', + child: ToggleSwitch( + checked: selected, + onChanged: (v) => setState(() => selected = v), + ), + ), + SizedBox( + width: 100, + child: InfoLabel( + label: 'Forms', + child: ComboBox( + value: comboboxValue, + items: ['Item 1', 'Item 2'] + .map((e) => ComboBoxItem( + child: Text(e), + value: e, + )) + .toList(), + isExpanded: true, + onChanged: (v) => setState(() => comboboxValue = v), + ), + ), + ), + RepaintBoundary( + child: Padding( + padding: const EdgeInsetsDirectional.only(start: 4.0), + child: InfoLabel( + label: 'Progress', + child: const SizedBox( + height: 30, width: 30, child: ProgressRing()), + ), + ), + ), + InfoLabel( + label: 'Surfaces & Materials', + child: SizedBox( + height: 40, + width: 120, + child: Stack(children: [ + Container( + width: 120, + height: 50, + color: theme.accentColor.lightest, + ), + const Positioned.fill(child: Acrylic(luminosityAlpha: 0.5)), + ]), + ), + ), + InfoLabel( + label: 'Icons', + child: const Icon(FluentIcons.graph_symbol, size: 30.0), + ), + InfoLabel( + label: 'Colors', + child: SizedBox( + width: 40, + height: 30, + child: Wrap( + children: [ + ...Colors.accentColors, + Colors.successPrimaryColor, + Colors.warningPrimaryColor, + Colors.errorPrimaryColor, + Colors.grey, + ].map((color) { + return Container( + height: 10, + width: 10, + color: color, + ); + }).toList(), + ), + ), + ), + InfoLabel( + label: 'Typography', + child: ShaderMask( + shaderCallback: (rect) { + return LinearGradient( + colors: [ + Colors.white, + ...Colors.accentColors, + ], + ).createShader(rect); + }, + blendMode: BlendMode.srcATop, + child: const Text( + 'ABCDEFGH', + style: TextStyle(fontSize: 24, shadows: [ + Shadow(offset: Offset(1, 1)), + ]), + ), + ), + ), + ]), + ), + const SizedBox(height: 22.0), + IconButton( + onPressed: () { + showDialog( + context: context, + barrierDismissible: true, + builder: (context) => const Changelog(), + ); + }, + icon: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'What\'s new on 4.0.0', + style: theme.typography.body + ?.copyWith(fontWeight: FontWeight.bold), + ), + Text('June 21, 2022', style: theme.typography.caption), + Text( + 'A native look-and-feel out of the box', + style: theme.typography.bodyLarge, + ), + ], + ), + ), + const SizedBox(height: 22.0), + Row(children: [ + Text('SPONSORS', style: theme.typography.bodyStrong), + const SizedBox(width: 4.0), + const Icon(FluentIcons.heart_fill, size: 16.0), + ]), + const SizedBox(height: 4.0), + Wrap( + spacing: 10.0, + runSpacing: 10.0, + children: [ + ...sponsors.map((sponsor) { + return Link( + uri: Uri.parse('https://www.github.com/${sponsor.username}'), + builder: (context, open) { + return IconButton( + onPressed: open, + icon: SponsorButton( + imageUrl: sponsor.imageUrl, + username: sponsor.username ?? sponsor.name, + ), + ); + }, + ); + }), + IconButton( + onPressed: () { + showDialog( + context: context, + builder: (context) => const SponsorDialog(), + ); + }, + icon: Column(children: [ + SizedBox( + height: 60, + width: 60, + child: ShaderMask( + shaderCallback: (rect) { + return LinearGradient( + colors: [ + Colors.white.withOpacity(0.8), + ...Colors.accentColors, + ], + ).createShader(rect); + }, + blendMode: BlendMode.srcATop, + child: const Icon(FluentIcons.diamond_user, size: 60), + ), + ), + const Text('Become a Sponsor!'), + ]), + ), + ], + ), + subtitle(content: const Text('Equivalents with the material library')), + const MaterialEquivalents(), + ], + ); + } +} + +class SponsorButton extends StatelessWidget { + const SponsorButton({ + Key? key, + required this.imageUrl, + required this.username, + }) : super(key: key); + + final String imageUrl; + final String username; + + @override + Widget build(BuildContext context) { + return Column(children: [ + Container( + height: 60, + width: 60, + decoration: BoxDecoration( + image: DecorationImage( + image: NetworkImage(imageUrl), + ), + shape: BoxShape.circle, + ), + ), + Text(username), + ]); + } +} diff --git a/launchpad_app/lib/screens/inputs/button.dart b/launchpad_app/lib/screens/inputs/button.dart new file mode 100644 index 00000000..ee256852 --- /dev/null +++ b/launchpad_app/lib/screens/inputs/button.dart @@ -0,0 +1,321 @@ +// ignore_for_file: prefer_const_constructors + +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +import '../../widgets/card_highlight.dart'; + +class ButtonPage extends StatefulWidget { + const ButtonPage({Key? key}) : super(key: key); + + @override + State createState() => _ButtonPageState(); +} + +class _ButtonPageState extends State with PageMixin { + bool simpleDisabled = false; + bool filledDisabled = false; + bool iconDisabled = false; + bool toggleDisabled = false; + bool toggleState = false; + bool splitButtonDisabled = false; + bool radioButtonDisabled = false; + int radioButtonSelected = -1; + + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: const PageHeader(title: Text('Button')), + children: [ + const Text( + 'The Button control provides a Click event to respond to user input from a touch, mouse, keyboard, stylus, or other input device. You can put different kinds of content in a button, such as text or an image, or you can restyle a button to give it a new look.', + ), + subtitle(content: const Text('A simple button with text content')), + description( + content: const Text('A button that initiates an immediate action.'), + ), + CardHighlight( + child: Row(children: [ + Button( + child: const Text('Standard Button'), + onPressed: simpleDisabled ? null : () {}, + ), + const Spacer(), + ToggleSwitch( + checked: simpleDisabled, + onChanged: (v) { + setState(() { + simpleDisabled = v; + }); + }, + content: const Text('Disabled'), + ), + ]), + codeSnippet: '''Button( + child: const Text('Standard Button'), + onPressed: disabled ? null : () => debugPrint('pressed button'), +)''', + ), + subtitle(content: const Text('Accent Style applied to Button')), + CardHighlight( + child: Row(children: [ + FilledButton( + child: const Text('Filled Button'), + onPressed: filledDisabled ? null : () {}, + ), + const Spacer(), + ToggleSwitch( + checked: filledDisabled, + onChanged: (v) { + setState(() { + filledDisabled = v; + }); + }, + content: const Text('Disabled'), + ), + ]), + codeSnippet: '''FilledButton( + child: const Text('Filled Button'), + onPressed: disabled ? null : () => debugPrint('pressed button'), +)''', + ), + subtitle( + content: const Text('A Button with graphical content (IconButton)'), + ), + CardHighlight( + child: Row(children: [ + IconButton( + icon: const Icon(FluentIcons.graph_symbol, size: 24.0), + onPressed: iconDisabled ? null : () {}, + ), + const Spacer(), + ToggleSwitch( + checked: iconDisabled, + onChanged: (v) { + setState(() { + iconDisabled = v; + }); + }, + content: const Text('Disabled'), + ), + ]), + codeSnippet: '''IconButton( + icon: const Icon(FluentIcons.graph_symbol, size: 24.0), + onPressed: disabled ? null : () => debugPrint('pressed button'), +)''', + ), + subtitle( + content: const Text('A simple ToggleButton with text content')), + description( + content: const Text( + 'A ToggleButton looks like a Button, but works like a CheckBox. It ' + 'typically has two states, checked (on) or unchecked (off).', + ), + ), + CardHighlight( + child: Row(children: [ + ToggleButton( + child: const Text('Toggle Button'), + checked: toggleState, + onChanged: toggleDisabled + ? null + : (v) { + setState(() { + toggleState = v; + }); + }, + ), + const Spacer(), + ToggleSwitch( + checked: toggleDisabled, + onChanged: (v) { + setState(() { + toggleDisabled = v; + }); + }, + content: const Text('Disabled'), + ), + ]), + codeSnippet: '''bool checked = false; + +ToggleButton( + child: const Text('Toggle Button'), + checked: checked, + onPressed: disabled ? null : (v) => setState(() => checked = v), +)''', + ), + subtitle(content: const Text('DropDownButton')), + const Text( + 'A control that drops down a flyout of choices from which one can be chosen', + ), + CardHighlight( + child: Row(children: [ + DropDownButton( + title: Text('Email'), + items: [ + MenuFlyoutItem(text: const Text('Send'), onPressed: () {}), + MenuFlyoutItem(text: const Text('Reply'), onPressed: () {}), + MenuFlyoutItem(text: const Text('Reply all'), onPressed: () {}), + ], + ), + SizedBox(width: 10.0), + DropDownButton( + title: Icon(FluentIcons.edit_mail, size: 22.0), + items: [ + MenuFlyoutItem( + leading: Icon(FluentIcons.send), + text: const Text('Send'), + onPressed: () {}, + ), + MenuFlyoutItem( + leading: Icon(FluentIcons.reply), + text: const Text('Reply'), + onPressed: () {}, + ), + MenuFlyoutItem( + leading: Icon(FluentIcons.reply_all), + text: const Text('Reply all'), + onPressed: () {}, + ), + ], + ), + ]), + codeSnippet: '''DropDownButton( + title: Text('Email'), + items: [ + MenuFlyoutItem(text: const Text('Send'), onPressed: () {}), + MenuFlyoutItem(text: const Text('Reply'), onPressed: () {}), + MenuFlyoutItem(text: const Text('Reply all'), onPressed: () {}), + ], +)''', + ), + subtitle(content: const Text('SplitButton')), + description( + content: const Text( + 'Represents a button with two parts that can be invoked separately. ' + 'One part behaves like a standard button and the other part invokes ' + 'a flyout.', + ), + ), + CardHighlight( + child: Row(children: [ + SplitButtonBar( + buttons: [ + Button( + child: Container( + decoration: BoxDecoration( + color: splitButtonDisabled + ? FluentTheme.of(context).accentColor.darker + : FluentTheme.of(context).accentColor, + borderRadius: const BorderRadiusDirectional.horizontal( + start: Radius.circular(4.0), + ), + ), + height: 24, + width: 24, + ), + onPressed: splitButtonDisabled ? null : () {}, + ), + IconButton( + icon: const SizedBox( + // height: splitButtonHeight, + child: Icon(FluentIcons.chevron_down, size: 10.0), + ), + onPressed: splitButtonDisabled ? null : () {}, + ), + ], + ), + const Spacer(), + ToggleSwitch( + checked: splitButtonDisabled, + onChanged: (v) { + setState(() { + splitButtonDisabled = v; + }); + }, + content: const Text('Disabled'), + ), + ]), + codeSnippet: '''SplitButtonBar( + buttons: [ + Button( + child: Container( + height: 24.0, + width: 24.0, + color: Colors.green, + ), + onPressed: () {}, + ), + IconButton( + icon: const Icon(FluentIcons.chevron_down, size: 10.0), + onPressed: () {}, + ), + ], +)''', + ), + subtitle(content: const Text('RadioButton')), + description( + content: const Text( + 'Radio buttons, also called option buttons, let users select one option ' + 'from a collection of two or more mutually exclusive, but related, ' + 'options. Radio buttons are always used in groups, and each option is ' + 'represented by one radio button in the group.', + ), + ), + CardHighlight( + child: Row(children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate( + 3, + (index) { + return Padding( + padding: EdgeInsetsDirectional.only( + bottom: index == 2 ? 0.0 : 14.0), + child: RadioButton( + checked: radioButtonSelected == index, + onChanged: radioButtonDisabled + ? null + : (v) { + if (v) { + setState(() { + radioButtonSelected = index; + }); + } + }, + content: Text('RadioButton ${index + 1}'), + ), + ); + }, + ), + ), + const Spacer(), + ToggleSwitch( + checked: radioButtonDisabled, + onChanged: (v) { + setState(() { + radioButtonDisabled = v; + }); + }, + content: const Text('Disabled'), + ) + ]), + codeSnippet: '''int? selected; + +Column( + children: List.generate(3, (index) { + return RadioButton( + checked: selected == index, + onChanged: (checked) { + if (checked) { + setState(() => selected = index); + } + } + ); + }), +)''', + ), + ], + ); + } +} diff --git a/launchpad_app/lib/screens/inputs/checkbox.dart b/launchpad_app/lib/screens/inputs/checkbox.dart new file mode 100644 index 00000000..377e9167 --- /dev/null +++ b/launchpad_app/lib/screens/inputs/checkbox.dart @@ -0,0 +1,130 @@ +import 'package:example/widgets/card_highlight.dart'; +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +class CheckBoxPage extends StatefulWidget { + const CheckBoxPage({Key? key}) : super(key: key); + + @override + State createState() => _CheckBoxPageState(); +} + +class _CheckBoxPageState extends State with PageMixin { + bool firstChecked = false; + bool firstDisabled = false; + bool? secondChecked = false; + bool secondDisabled = false; + bool iconDisabled = false; + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: const PageHeader(title: Text('Checkbox')), + children: [ + const Text( + 'CheckBox controls let the user select a combination of binary options. In contrast, RadioButton controls allow the user to select from mutually exclusive options. The indeterminate state is used to indicate that an option is set for some, but not all, child options. Don\'t allow users to set an indeterminate state directly to indicate a third option.', + ), + subtitle(content: const Text('A 2-state Checkbox')), + CardHighlight( + child: Row(children: [ + Checkbox( + checked: firstChecked, + onChanged: firstDisabled + ? null + : (v) => setState(() => firstChecked = v!), + content: const Text('Two-state Checkbox'), + ), + const Spacer(), + ToggleSwitch( + checked: firstDisabled, + onChanged: (v) { + setState(() { + firstDisabled = v; + }); + }, + content: const Text('Disabled'), + ), + ]), + codeSnippet: '''bool checked = false; + +Checkbox( + checked: checked, + onPressed: disabled ? null : (v) => setState(() => checked = v), +)''', + ), + subtitle(content: const Text('A 3-state Checkbox')), + CardHighlight( + child: Row(children: [ + Checkbox( + checked: secondChecked, + // checked: null, + onChanged: secondDisabled + ? null + : (v) { + setState(() { + secondChecked = v == true + ? true + : v == false + ? null + : v == null + ? false + : true; + }); + }, + content: const Text('Three-state Checkbox'), + ), + const Spacer(), + ToggleSwitch( + checked: secondDisabled, + onChanged: (v) { + setState(() { + secondDisabled = v; + }); + }, + content: const Text('Disabled'), + ), + ]), + codeSnippet: '''bool checked = false; + +Checkbox( + checked: checked, + onPressed: disabled ? null : (v) { + setState(() { + // if v (the new value) is true, then true + // if v is false, then null (third state) + // if v is null (was third state before), then false + // otherwise (just to be safe), it's true + checked = (v == true + ? true + : v == false + ? null + : v == null + ? false + : true); + }); + }, +)''', + ), + subtitle( + content: const Text('Using a 3-state Checkbox (TreeView)'), + ), + Card( + child: TreeView( + items: [ + TreeViewItem( + content: const Text('Select all'), + children: treeViewItems, + ), + ], + selectionMode: TreeViewSelectionMode.multiple, + ), + ), + ], + ); + } + + final treeViewItems = [ + TreeViewItem(content: const Text('Option 1')), + TreeViewItem(content: const Text('Option 2')), + TreeViewItem(content: const Text('Option 3')), + ]; +} diff --git a/launchpad_app/lib/screens/inputs/slider.dart b/launchpad_app/lib/screens/inputs/slider.dart new file mode 100644 index 00000000..3a33beb6 --- /dev/null +++ b/launchpad_app/lib/screens/inputs/slider.dart @@ -0,0 +1,98 @@ +import 'package:example/widgets/card_highlight.dart'; +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +class SliderPage extends StatefulWidget { + const SliderPage({Key? key}) : super(key: key); + + @override + State createState() => _SliderPageState(); +} + +class _SliderPageState extends State with PageMixin { + bool disabled = false; + double firstValue = 23.0; + double verticalValue = 50.0; + + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: PageHeader( + title: const Text('Slider'), + commandBar: ToggleSwitch( + checked: disabled, + onChanged: (v) => setState(() => disabled = v), + content: const Text('Disabled'), + ), + ), + children: [ + const Text( + 'Use a Slider when you want your users to be able to set defined, ' + 'contiguous values (such as volume or brightness) or a range of discrete ' + 'values (such as screen resolution settings).\n\n' + 'A slider is a good choice when you know that users think of the value ' + 'as a relative quantity, not a numeric value. For example, users think ' + 'about setting their audio volume to low or medium—not about setting ' + 'the value to 2 or 5.', + ), + subtitle(content: const Text('A simple Slider')), + CardHighlight( + child: Row(children: [ + Slider( + label: '${firstValue.toInt()}', + value: firstValue, + onChanged: disabled + ? null + : (v) { + setState(() => firstValue = v); + }, + ), + const Spacer(), + Text('Output:\n${firstValue.toInt()}'), + ]), + codeSnippet: '''double value = 0; + +Slider( + label: '\${value.toInt()}', + value: value, + onChanged: disabled ? null : (v) => setState(() => value = v), +), +''', + ), + subtitle(content: const Text('A vertical slider')), + description( + content: const Text( + '''You can orient your slider horizontally or vertically. Use these guidelines to determine which layout to use. + + * Use a natural orientation. For example, if the slider represents a real-world value that is normally shown vertically (such as temperature), use a vertical orientation. + * If the control is used to seek within media, like in a video app, use a horizontal orientation. + * When using a slider in page that can be panned in one direction (horizontally or vertically), use a different orientation for the slider than the panning direction. Otherwise, users might swipe the slider and change its value accidentally when they try to pan the page. + * If you're still not sure which orientation to use, use the one that best fits your page layout.''', + ), + ), + CardHighlight( + child: Row(children: [ + Slider( + vertical: true, + label: '${verticalValue.toInt()}', + value: verticalValue, + onChanged: + disabled ? null : (v) => setState(() => verticalValue = v), + ), + const Spacer(), + Text('Output:\n${verticalValue.toInt()}'), + ]), + codeSnippet: '''double value = 0; + +Slider( + vertical: true, + label: '\${value.toInt()}', + value: value, + onChanged: disabled ? null : (v) => setState(() => value = v), +), +''', + ), + ], + ); + } +} diff --git a/launchpad_app/lib/screens/inputs/toggle_switch.dart b/launchpad_app/lib/screens/inputs/toggle_switch.dart new file mode 100644 index 00000000..4636ea3c --- /dev/null +++ b/launchpad_app/lib/screens/inputs/toggle_switch.dart @@ -0,0 +1,84 @@ +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:example/widgets/card_highlight.dart'; + +class ToggleSwitchPage extends StatefulWidget { + const ToggleSwitchPage({Key? key}) : super(key: key); + + @override + State createState() => _ToggleSwitchPageState(); +} + +class _ToggleSwitchPageState extends State with PageMixin { + bool disabled = false; + bool firstValue = false; + bool secondValue = true; + + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: PageHeader( + title: const Text('ToggleSwitch'), + commandBar: ToggleSwitch( + checked: disabled, + onChanged: (v) => setState(() => disabled = v), + content: const Text('Disabled'), + ), + ), + children: [ + const Text( + 'The toggle switch represents a physical switch that allows users to ' + 'turn things on or off, like a light switch. Use toggle switch controls ' + 'to present users with two mutually exclusive options (such as on/off), ' + 'where choosing an option provides immediate results.', + ), + subtitle(content: const Text('A simple ToggleSwitch')), + CardHighlight( + child: Align( + alignment: AlignmentDirectional.centerStart, + child: ToggleSwitch( + checked: firstValue, + onChanged: + disabled ? null : (v) => setState(() => firstValue = v), + content: Text(firstValue ? 'On' : 'Off'), + ), + ), + codeSnippet: '''bool checked = false; + +ToggleSwitch( + checked: checked, + onPressed: disabled ? null : (v) => setState(() => checked = v), +)''', + ), + subtitle( + content: const Text('A ToggleSwitch with custom header and content'), + ), + CardHighlight( + child: Row(children: [ + InfoLabel( + label: 'Header', + child: ToggleSwitch( + checked: secondValue, + onChanged: + disabled ? null : (v) => setState(() => secondValue = v), + content: Text(secondValue ? 'Working' : 'Do work'), + ), + ), + if (secondValue) + const Padding( + padding: EdgeInsets.symmetric(horizontal: 8.0), + child: ProgressRing(), + ) + ]), + codeSnippet: '''bool checked = false; + +ToggleSwitch( + checked: checked, + onPressed: disabled ? null : (v) => setState(() => checked = v), + content: Text(checked ? 'Working' : 'Do work'), +)''', + ), + ], + ); + } +} diff --git a/launchpad_app/lib/screens/navigation/navigation_view.dart b/launchpad_app/lib/screens/navigation/navigation_view.dart new file mode 100644 index 00000000..9eefb41e --- /dev/null +++ b/launchpad_app/lib/screens/navigation/navigation_view.dart @@ -0,0 +1,261 @@ +import 'package:example/widgets/card_highlight.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +import '../../widgets/page.dart'; + +class NavigationViewPage extends StatefulWidget { + const NavigationViewPage({Key? key}) : super(key: key); + + @override + State createState() => _NavigationViewPageState(); +} + +class _NavigationViewPageState extends State + with PageMixin { + static const double itemHeight = 300.0; + + int topIndex = 0; + + PaneDisplayMode displayMode = PaneDisplayMode.open; + String pageTransition = 'default'; + static const List pageTransitions = [ + 'default', + 'entrance', + 'drill in', + 'horizontal', + ]; + + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: const PageHeader(title: Text('NavigationView')), + children: [ + const Text( + 'The NavigationView control provides top-level navigation for your app. ' + 'It adapts to a variety of screen sizes and supports both top and left ' + 'navigation styles.', + ), + const SizedBox(height: 10.0), + ...buildDisplayMode( + PaneDisplayMode.top, + 'Top display mode', + 'The pane is positioned above the content.', + ), + ...buildDisplayMode( + PaneDisplayMode.open, + 'Open display mode', + 'The pane is expanded and positioned to the left of the content.', + ), + ...buildDisplayMode( + PaneDisplayMode.compact, + 'Compact display mode', + 'The pane shows only icons until opened and is positioned to the left ' + 'of the content. When opened, the pane overlays the content.', + ), + ...buildDisplayMode( + PaneDisplayMode.minimal, + 'Minimal display mode', + 'Only the menu button is shown until the pane is opened. When opened, ' + 'the pane overlays the left side of the content.', + ), + ], + ); + } + + List buildDisplayMode( + PaneDisplayMode displayMode, + String title, + String desc, + ) { + if (displayMode != this.displayMode) return []; + return [ + Wrap(runSpacing: 10.0, spacing: 10.0, children: [ + InfoLabel( + label: 'Display mode', + child: ComboBox( + value: displayMode, + items: ([...PaneDisplayMode.values]..remove(PaneDisplayMode.auto)) + .map((mode) { + return ComboBoxItem(child: Text(mode.name), value: mode); + }).toList(), + onChanged: (mode) => setState( + () => this.displayMode = mode ?? displayMode, + ), + ), + ), + InfoLabel( + label: 'Page Transition', + child: ComboBox( + items: pageTransitions + .map((e) => ComboBoxItem(child: Text(e), value: e)) + .toList(), + value: pageTransition, + onChanged: (transition) => setState( + () => pageTransition = transition ?? pageTransition, + ), + ), + ), + ]), + subtitle(content: Text(title)), + description(content: Text(desc)), + CardHighlight( + child: SizedBox( + height: itemHeight, + child: NavigationView( + appBar: const NavigationAppBar( + title: Text('NavigationView'), + ), + pane: NavigationPane( + selected: topIndex, + onChanged: (index) => setState(() => topIndex = index), + displayMode: displayMode, + items: [ + PaneItem( + icon: const Icon(FluentIcons.home), + title: const Text('Home'), + body: const _NavigationBodyItem(), + ), + PaneItem( + icon: const Icon(FluentIcons.issue_tracking), + title: const Text('Track orders'), + infoBadge: const InfoBadge(source: Text('8')), + body: const _NavigationBodyItem( + header: 'Badging', + content: Text( + 'Badging is a non-intrusive and intuitive way to display ' + 'notifications or bring focus to an area within an app - ' + 'whether that be for notifications, indicating new content, ' + 'or showing an alert. An InfoBadge is a small piece of UI ' + 'that can be added into an app and customized to display a ' + 'number, icon, or a simple dot.', + ), + ), + ), + PaneItemExpander( + icon: const Icon(FluentIcons.account_management), + title: const Text('Account'), + body: const _NavigationBodyItem( + header: 'PaneItemExpander', + content: Text( + 'Some apps may have a more complex hierarchical structure ' + 'that requires more than just a flat list of navigation ' + 'items. You may want to use top-level navigation items to ' + 'display categories of pages, with children items displaying ' + 'specific pages. It is also useful if you have hub-style ' + 'pages that only link to other pages. For these kinds of ' + 'cases, you should create a hierarchical NavigationView.', + ), + ), + items: [ + PaneItemHeader(header: const Text('Apps')), + PaneItem( + icon: const Icon(FluentIcons.mail), + title: const Text('Mail'), + body: const _NavigationBodyItem(), + ), + PaneItem( + icon: const Icon(FluentIcons.calendar), + title: const Text('Calendar'), + body: const _NavigationBodyItem(), + ), + ], + ), + ], + footerItems: [ + PaneItem( + icon: const Icon(FluentIcons.settings), + title: const Text('Settings'), + body: const _NavigationBodyItem(), + ), + ], + ), + transitionBuilder: pageTransition == 'default' + ? null + : (child, animation) { + switch (pageTransition) { + case 'entrance': + return EntrancePageTransition( + child: child, + animation: animation, + ); + case 'drill in': + return DrillInPageTransition( + child: child, + animation: animation, + ); + case 'horizontal': + return HorizontalSlidePageTransition( + child: child, + animation: animation, + ); + default: + throw UnsupportedError( + '$pageTransition is not a supported transition', + ); + } + }, + ), + ), + codeSnippet: '''NavigationView( + appBar: const NavigationAppBar( + title: Text('NavigationView'), + ), + pane: NavigationPane( + selected: topIndex, + onChanged: (index) => setState(() => topIndex = index), + displayMode: displayMode, + items: [ + PaneItem( + icon: const Icon(FluentIcons.home), + title: const Text('Home'), + body: BodyItem(), + ), + PaneItem( + icon: const Icon(FluentIcons.issue_tracking), + title: const Text('Track an order'), + infoBadge: const InfoBadge(source: Text('8')), + body: BodyItem(), + ), + PaneItemExpander( + icon: const Icon(FluentIcons.account_management), + title: const Text('Account'), + body: BodyItem(), + items: [ + PaneItem( + icon: const Icon(FluentIcons.mail), + title: const Text('Mail'), + body: BodyItem(), + ), + PaneItem( + icon: const Icon(FluentIcons.calendar), + title: const Text('Calendar'), + body: BodyItem(), + ), + ], + ), + ], + ), +)''', + ), + ]; + } +} + +class _NavigationBodyItem extends StatelessWidget { + const _NavigationBodyItem({ + Key? key, + this.header, + this.content, + }) : super(key: key); + + final String? header; + final Widget? content; + + @override + Widget build(BuildContext context) { + return ScaffoldPage.withPadding( + header: PageHeader(title: Text(header ?? 'This is a header text')), + content: content ?? const SizedBox.shrink(), + ); + } +} diff --git a/launchpad_app/lib/screens/navigation/tab_view.dart b/launchpad_app/lib/screens/navigation/tab_view.dart new file mode 100644 index 00000000..da8349ea --- /dev/null +++ b/launchpad_app/lib/screens/navigation/tab_view.dart @@ -0,0 +1,164 @@ +import 'dart:math'; + +import 'package:example/widgets/card_highlight.dart'; +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +class TabViewPage extends StatefulWidget { + const TabViewPage({Key? key}) : super(key: key); + + @override + State createState() => _TabViewPageState(); +} + +class _TabViewPageState extends State with PageMixin { + int currentIndex = 0; + List? tabs; + + TabWidthBehavior tabWidthBehavior = TabWidthBehavior.equal; + CloseButtonVisibilityMode closeButtonVisibilityMode = + CloseButtonVisibilityMode.always; + bool showScrollButtons = true; + bool wheelScroll = false; + + Tab generateTab(int index) { + late Tab tab; + tab = Tab( + text: Text('Document $index'), + semanticLabel: 'Document #$index', + icon: const FlutterLogo(), + body: Container( + color: + Colors.accentColors[Random().nextInt(Colors.accentColors.length)], + ), + onClosed: () { + setState(() { + tabs!.remove(tab); + + if (currentIndex > 0) currentIndex--; + }); + }, + ); + return tab; + } + + @override + Widget build(BuildContext context) { + tabs ??= List.generate(3, generateTab); + return ScaffoldPage.scrollable( + header: const PageHeader(title: Text('TabView')), + children: [ + const Text( + 'The TabView control is a way to display a set of tabs and their ' + 'respective content. TabViews are useful for displaying several pages ' + '(or documents) of content while giving a user the capability to ' + 'rearrange, open, or close new tabs.', + ), + subtitle( + content: const Text( + 'A TabView with support for adding, closing and rearraging tabs', + ), + ), + Card( + child: Wrap( + spacing: 10.0, + runSpacing: 10.0, + crossAxisAlignment: WrapCrossAlignment.end, + children: [ + SizedBox( + width: 150, + child: InfoLabel( + label: 'Tab width behavior', + child: ComboBox( + isExpanded: true, + value: tabWidthBehavior, + items: TabWidthBehavior.values.map((behavior) { + return ComboBoxItem( + child: Text(behavior.name), + value: behavior, + ); + }).toList(), + onChanged: (behavior) { + if (behavior != null) { + setState(() => tabWidthBehavior = behavior); + } + }, + ), + ), + ), + SizedBox( + width: 150, + child: InfoLabel( + label: 'Close button visbility', + child: ComboBox( + isExpanded: true, + value: closeButtonVisibilityMode, + items: CloseButtonVisibilityMode.values.map((mode) { + return ComboBoxItem( + child: Text(mode.name), + value: mode, + ); + }).toList(), + onChanged: (mode) { + if (mode != null) { + setState(() => closeButtonVisibilityMode = mode); + } + }, + ), + ), + ), + Checkbox( + checked: showScrollButtons, + onChanged: (v) => setState(() => showScrollButtons = v!), + content: const Text('Show scroll buttons'), + ), + Checkbox( + checked: wheelScroll, + onChanged: (v) => setState(() => wheelScroll = v!), + content: const Text('Wheel scroll'), + ), + ], + ), + ), + CardHighlight( + child: SizedBox( + height: 400, + child: TabView( + tabs: tabs!, + currentIndex: currentIndex, + onChanged: (index) => setState(() => currentIndex = index), + tabWidthBehavior: tabWidthBehavior, + closeButtonVisibility: closeButtonVisibilityMode, + showScrollButtons: showScrollButtons, + wheelScroll: wheelScroll, + onNewPressed: () { + setState(() { + final index = tabs!.length + 1; + final tab = generateTab(index); + tabs!.add(tab); + }); + }, + onReorder: (oldIndex, newIndex) { + setState(() { + if (oldIndex < newIndex) { + newIndex -= 1; + } + final item = tabs!.removeAt(oldIndex); + tabs!.insert(newIndex, item); + + if (currentIndex == newIndex) { + currentIndex = oldIndex; + } else if (currentIndex == oldIndex) { + currentIndex = newIndex; + } + }); + }, + ), + ), + // TODO: TabView snippets + codeSnippet: '''''', + ), + ], + ); + } +} diff --git a/launchpad_app/lib/screens/navigation/tree_view.dart b/launchpad_app/lib/screens/navigation/tree_view.dart new file mode 100644 index 00000000..7188a5dd --- /dev/null +++ b/launchpad_app/lib/screens/navigation/tree_view.dart @@ -0,0 +1,277 @@ +import 'package:example/widgets/card_highlight.dart'; +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +class TreeViewPage extends StatefulWidget { + const TreeViewPage({Key? key}) : super(key: key); + + @override + State createState() => _TreeViewPageState(); +} + +class _TreeViewPageState extends State with PageMixin { + final treeViewKey = GlobalKey(); + + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: const PageHeader(title: Text('TreeView')), + children: [ + const Text( + 'The tree view control enables a hierarchical list with expanding and ' + 'collapsing nodes that contain nested items. It can be used to ' + 'illustrate a folder structure or nested relationships in your UI.\n\n' + 'The tree view uses a combination of indentation and icons to represent ' + 'the nested relationship between parent nodes and child nodes. Collapsed ' + 'nodes use a chevron pointing to the right, and expanded nodes use a ' + 'chevron pointing down.', + ), + subtitle( + content: const Text('A TreeView with Multi-selection enabled'), + ), + CardHighlight( + child: TreeView( + key: treeViewKey, + selectionMode: TreeViewSelectionMode.multiple, + shrinkWrap: true, + items: items, + onItemInvoked: (item, reason) async => + debugPrint('onItemInvoked(reason=$reason): $item'), + onSelectionChanged: (selectedItems) async => debugPrint( + 'onSelectionChanged: ${selectedItems.map((i) => i.value)}'), + onSecondaryTap: (item, details) async { + debugPrint('onSecondaryTap $item at ${details.globalPosition}'); + }, + ), + codeSnippet: r'''final items = [ + TreeViewItem( + content: const Text('Personal Documents'), + value: 'personal_docs', + children: [ + TreeViewItem( + content: const Text('Home Remodel'), + value: 'home_remodel', + children: [ + TreeViewItem( + content: const Text('Contractor Contact Info'), + value: 'contr_cont_inf', + ), + TreeViewItem( + content: const Text('Paint Color Scheme'), + value: 'paint_color_scheme', + ), + TreeViewItem( + content: const Text('Flooring weedgrain type'), + value: 'flooring_weedgrain_type', + ), + TreeViewItem( + content: const Text('Kitchen cabinet style'), + value: 'kitch_cabinet_style', + ), + ], + ), + TreeViewItem( + content: const Text('Tax Documents'), + value: 'tax_docs', + children: [ + TreeViewItem(content: const Text('2017'), value: "tax_2017"), + TreeViewItem( + content: const Text('Middle Years'), + value: 'tax_middle_years', + children: [ + TreeViewItem(content: const Text('2018'), value: "tax_2018"), + TreeViewItem(content: const Text('2019'), value: "tax_2019"), + TreeViewItem(content: const Text('2020'), value: "tax_2020"), + ], + ), + TreeViewItem(content: const Text('2021'), value: "tax_2021"), + TreeViewItem(content: const Text('Current Year'), value: "tax_cur"), + ], + ), + ], + ), +]; + +TreeView( + selectionMode: TreeViewSelectionMode.multiple, + shrinkWrap: true, + items: items, + onItemInvoked: (item) async => debugPrint('onItemInvoked: \$item'), + onSelectionChanged: (selectedItems) async => debugPrint( + 'onSelectionChanged: \${selectedItems.map((i) => i.value)}'), + onSecondaryTap: (item, details) async { + debugPrint('onSecondaryTap $item at ${details.globalPosition}'); + }, +) +''', + ), + subtitle(content: const Text('A TreeView with lazy-loading items')), + CardHighlight( + child: Column( + children: [ + TreeView( + shrinkWrap: true, + items: lazyItems, + onItemInvoked: (item, reason) async => + debugPrint('onItemInvoked(reason=$reason): $item'), + onSelectionChanged: (selectedItems) async => debugPrint( + 'onSelectionChanged: ${selectedItems.map((i) => i.value)}'), + onSecondaryTap: (item, details) async { + debugPrint( + 'onSecondaryTap $item at ${details.globalPosition}'); + }, + ), + ], + ), + codeSnippet: r'''final lazyItems = [ + TreeViewItem( + content: const Text('Item with lazy loading'), + value: 'lazy_load', + // This means the item will be expandable, although there are no + // children yet. + lazy: true, + // Ensure the list is modifiable. + children: [], + onExpandToggle: (item, getsExpanded) async { + // If it's already populated, return. + if (item.children.isNotEmpty) return; + + // Do your fetching... + await Future.delayed(const Duration(seconds: 2)); + + // ...and add the fetched nodes. + item.children.addAll([ + TreeViewItem( + content: const Text('Lazy item 1'), + value: 'lazy_1', + ), + TreeViewItem( + content: const Text('Lazy item 2'), + value: 'lazy_2', + ), + TreeViewItem( + content: const Text('Lazy item 3'), + value: 'lazy_3', + ), + TreeViewItem( + content: const Text( + 'Lazy item 4 (this text should not overflow)', + overflow: TextOverflow.ellipsis, + ), + value: 'lazy_4', + ), + ]); + }, + ), +]; + +TreeView( + shrinkWrap: true, + items: lazyItems, + onItemInvoked: (item) async => debugPrint('onItemInvoked: $item'), + onSelectionChanged: (selectedItems) async => debugPrint( + 'onSelectionChanged: ${selectedItems.map((i) => i.value)}'), + onSecondaryTap: (item, details) async { + debugPrint('onSecondaryTap $item at ${details.globalPosition}'); + }, +) +''', + ), + ], + ); + } + + late final items = [ + TreeViewItem( + content: const Text('Personal Documents'), + value: 'personal_docs', + children: [ + TreeViewItem( + content: const Text('Home Remodel'), + value: 'home_remodel', + children: [ + TreeViewItem( + content: const Text('Contractor Contact Info'), + value: 'contr_cont_inf', + ), + TreeViewItem( + content: const Text('Paint Color Scheme'), + value: 'paint_color_scheme', + ), + TreeViewItem( + content: const Text('Flooring weedgrain type'), + value: 'flooring_weedgrain_type', + ), + TreeViewItem( + content: const Text('Kitchen cabinet style'), + value: 'kitch_cabinet_style', + ), + ], + ), + TreeViewItem( + content: const Text('Tax Documents'), + value: 'tax_docs', + children: [ + TreeViewItem(content: const Text('2017'), value: "tax_2017"), + TreeViewItem( + content: const Text('Middle Years'), + value: 'tax_middle_years', + children: [ + TreeViewItem(content: const Text('2018'), value: "tax_2018"), + TreeViewItem( + content: const Text('2019'), + value: "tax_2019", + selected: true), + TreeViewItem(content: const Text('2020'), value: "tax_2020"), + ], + ), + TreeViewItem(content: const Text('2021'), value: "tax_2021"), + TreeViewItem(content: const Text('Current Year'), value: "tax_cur"), + ], + ), + ], + ), + ]; + + late final lazyItems = [ + TreeViewItem( + content: const Text('Item with lazy loading'), + value: 'lazy_load', + // This means the item will be expandable, although there are no + // children yet. + lazy: true, + // Ensure the list is modifiable. + children: [], + onExpandToggle: (item, getsExpanded) async { + // If it's already populated, return. + if (item.children.isNotEmpty) return; + + // Do your fetching... + await Future.delayed(const Duration(seconds: 2)); + + // ...and add the fetched nodes. + item.children.addAll([ + TreeViewItem( + content: const Text('Lazy item 1'), + value: 'lazy_1', + ), + TreeViewItem( + content: const Text('Lazy item 2'), + value: 'lazy_2', + ), + TreeViewItem( + content: const Text('Lazy item 3'), + value: 'lazy_3', + ), + TreeViewItem( + content: const Text( + 'Lazy item 4 (this text should not overflow)', + overflow: TextOverflow.ellipsis, + ), + value: 'lazy_4', + ), + ]); + }, + ), + ]; +} diff --git a/launchpad_app/lib/screens/settings.dart b/launchpad_app/lib/screens/settings.dart new file mode 100644 index 00000000..e1a5fe86 --- /dev/null +++ b/launchpad_app/lib/screens/settings.dart @@ -0,0 +1,290 @@ +// ignore_for_file: constant_identifier_names + +import 'package:flutter/foundation.dart'; + +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:flutter_acrylic/flutter_acrylic.dart'; +import 'package:provider/provider.dart'; + +import '../theme.dart'; +import '../widgets/page.dart'; + +const List accentColorNames = [ + 'System', + 'Yellow', + 'Orange', + 'Red', + 'Magenta', + 'Purple', + 'Blue', + 'Teal', + 'Green', +]; + +bool get kIsWindowEffectsSupported { + return !kIsWeb && + [ + TargetPlatform.windows, + TargetPlatform.linux, + TargetPlatform.macOS, + ].contains(defaultTargetPlatform); +} + +const _LinuxWindowEffects = [ + WindowEffect.disabled, + WindowEffect.transparent, +]; + +const _WindowsWindowEffects = [ + WindowEffect.disabled, + WindowEffect.solid, + WindowEffect.transparent, + WindowEffect.aero, + WindowEffect.acrylic, + WindowEffect.mica, + WindowEffect.tabbed, +]; + +const _MacosWindowEffects = [ + WindowEffect.disabled, + WindowEffect.titlebar, + WindowEffect.selection, + WindowEffect.menu, + WindowEffect.popover, + WindowEffect.sidebar, + WindowEffect.headerView, + WindowEffect.sheet, + WindowEffect.windowBackground, + WindowEffect.hudWindow, + WindowEffect.fullScreenUI, + WindowEffect.toolTip, + WindowEffect.contentBackground, + WindowEffect.underWindowBackground, + WindowEffect.underPageBackground, +]; + +List get currentWindowEffects { + if (kIsWeb) return []; + + if (defaultTargetPlatform == TargetPlatform.windows) { + return _WindowsWindowEffects; + } else if (defaultTargetPlatform == TargetPlatform.linux) { + return _LinuxWindowEffects; + } else if (defaultTargetPlatform == TargetPlatform.macOS) { + return _MacosWindowEffects; + } + + return []; +} + +class Settings extends ScrollablePage { + Settings({super.key}); + + @override + Widget buildHeader(BuildContext context) { + return const PageHeader(title: Text('Settings')); + } + + @override + List buildScrollable(BuildContext context) { + assert(debugCheckHasMediaQuery(context)); + final appTheme = context.watch(); + const spacer = SizedBox(height: 10.0); + const biggerSpacer = SizedBox(height: 40.0); + + const supportedLocales = FluentLocalizations.supportedLocales; + final currentLocale = + appTheme.locale ?? Localizations.maybeLocaleOf(context); + + return [ + Text('Theme mode', style: FluentTheme.of(context).typography.subtitle), + spacer, + ...List.generate(ThemeMode.values.length, (index) { + final mode = ThemeMode.values[index]; + return Padding( + padding: const EdgeInsetsDirectional.only(bottom: 8.0), + child: RadioButton( + checked: appTheme.mode == mode, + onChanged: (value) { + if (value) { + appTheme.mode = mode; + + if (kIsWindowEffectsSupported) { + // some window effects require on [dark] to look good. + // appTheme.setEffect(WindowEffect.disabled, context); + appTheme.setEffect(appTheme.windowEffect, context); + } + } + }, + content: Text('$mode'.replaceAll('ThemeMode.', '')), + ), + ); + }), + biggerSpacer, + Text( + 'Navigation Pane Display Mode', + style: FluentTheme.of(context).typography.subtitle, + ), + spacer, + ...List.generate(PaneDisplayMode.values.length, (index) { + final mode = PaneDisplayMode.values[index]; + return Padding( + padding: const EdgeInsetsDirectional.only(bottom: 8.0), + child: RadioButton( + checked: appTheme.displayMode == mode, + onChanged: (value) { + if (value) appTheme.displayMode = mode; + }, + content: Text( + mode.toString().replaceAll('PaneDisplayMode.', ''), + ), + ), + ); + }), + biggerSpacer, + Text('Navigation Indicator', + style: FluentTheme.of(context).typography.subtitle), + spacer, + ...List.generate(NavigationIndicators.values.length, (index) { + final mode = NavigationIndicators.values[index]; + return Padding( + padding: const EdgeInsetsDirectional.only(bottom: 8.0), + child: RadioButton( + checked: appTheme.indicator == mode, + onChanged: (value) { + if (value) appTheme.indicator = mode; + }, + content: Text( + mode.toString().replaceAll('NavigationIndicators.', ''), + ), + ), + ); + }), + biggerSpacer, + Text('Accent Color', style: FluentTheme.of(context).typography.subtitle), + spacer, + Wrap(children: [ + Tooltip( + child: _buildColorBlock(appTheme, systemAccentColor), + message: accentColorNames[0], + ), + ...List.generate(Colors.accentColors.length, (index) { + final color = Colors.accentColors[index]; + return Tooltip( + message: accentColorNames[index + 1], + child: _buildColorBlock(appTheme, color), + ); + }), + ]), + if (kIsWindowEffectsSupported) ...[ + biggerSpacer, + Text( + 'Window Transparency (${defaultTargetPlatform.toString().replaceAll('TargetPlatform.', '')})', + style: FluentTheme.of(context).typography.subtitle, + ), + spacer, + ...List.generate(currentWindowEffects.length, (index) { + final mode = currentWindowEffects[index]; + return Padding( + padding: const EdgeInsetsDirectional.only(bottom: 8.0), + child: RadioButton( + checked: appTheme.windowEffect == mode, + onChanged: (value) { + if (value) { + appTheme.windowEffect = mode; + appTheme.setEffect(mode, context); + } + }, + content: Text( + mode.toString().replaceAll('WindowEffect.', ''), + ), + ), + ); + }), + ], + biggerSpacer, + Text('Text Direction', + style: FluentTheme.of(context).typography.subtitle), + spacer, + ...List.generate(TextDirection.values.length, (index) { + final direction = TextDirection.values[index]; + return Padding( + padding: const EdgeInsetsDirectional.only(bottom: 8.0), + child: RadioButton( + checked: appTheme.textDirection == direction, + onChanged: (value) { + if (value) { + appTheme.textDirection = direction; + } + }, + content: Text( + '$direction' + .replaceAll('TextDirection.', '') + .replaceAll('rtl', 'Right to left') + .replaceAll('ltr', 'Left to right'), + ), + ), + ); + }).reversed, + Text('Locale', style: FluentTheme.of(context).typography.subtitle), + spacer, + Wrap( + spacing: 15.0, + runSpacing: 10.0, + children: List.generate( + supportedLocales.length, + (index) { + final locale = supportedLocales[index]; + + return Padding( + padding: const EdgeInsetsDirectional.only(bottom: 8.0), + child: RadioButton( + checked: currentLocale == locale, + onChanged: (value) { + if (value) { + appTheme.locale = locale; + } + }, + content: Text('$locale'), + ), + ); + }, + ), + ), + ]; + } + + Widget _buildColorBlock(AppTheme appTheme, AccentColor color) { + return Padding( + padding: const EdgeInsets.all(2.0), + child: Button( + onPressed: () { + appTheme.color = color; + }, + style: ButtonStyle( + padding: ButtonState.all(EdgeInsets.zero), + backgroundColor: ButtonState.resolveWith((states) { + if (states.isPressing) { + return color.light; + } else if (states.isHovering) { + return color.lighter; + } + return color; + }), + ), + child: Container( + height: 40, + width: 40, + alignment: AlignmentDirectional.center, + child: appTheme.color == color + ? Icon( + FluentIcons.check_mark, + color: color.basedOnLuminance(), + size: 22.0, + ) + : null, + ), + ), + ); + } +} diff --git a/launchpad_app/lib/screens/surface/acrylic.dart b/launchpad_app/lib/screens/surface/acrylic.dart new file mode 100644 index 00000000..77128771 --- /dev/null +++ b/launchpad_app/lib/screens/surface/acrylic.dart @@ -0,0 +1,223 @@ +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +import '../settings.dart'; + +const questionMark = Padding( + padding: EdgeInsetsDirectional.only(start: 4.0), + child: Icon(FluentIcons.status_circle_question_mark, size: 14.0), +); + +InlineSpan _buildLabel(String label, String description) { + return TextSpan( + text: label, + children: [ + WidgetSpan( + child: Tooltip( + useMousePosition: false, + message: description, + child: questionMark, + ), + ), + ], + ); +} + +class AcrylicPage extends StatefulWidget { + const AcrylicPage({Key? key}) : super(key: key); + + @override + State createState() => _AcrylicPageState(); +} + +class _AcrylicPageState extends State with PageMixin { + double tintOpacity = 0.8; + double luminosityOpacity = 0.8; + double blurAmout = 30; + double elevation = 0; + Color? color; + + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: const PageHeader(title: Text('Acrylic')), + children: [ + const Text( + 'A translucent material recommended for panel backgrounds. Acrylic is a ' + 'type of Brush that creates a translucent texture. You can apply acrylic ' + 'to app surfaces to add depth and help establish a visual hierarchy.', + ), + subtitle(content: const Text('Default background acrylic brush.')), + Card( + child: SizedBox( + height: 300, + width: 500, + child: Stack(children: const [ + _AcrylicChildren(), + Positioned.fill( + child: Padding( + padding: EdgeInsets.all(12.0), + child: Acrylic(), + ), + ), + ]), + ), + ), + subtitle(content: const Text('Custom acrylic brush.')), + Card( + child: SizedBox( + height: 300, + width: 500, + child: Row(children: [ + Expanded( + child: Stack(children: [ + const _AcrylicChildren(), + Positioned.fill( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Acrylic( + tintAlpha: tintOpacity, + luminosityAlpha: luminosityOpacity, + blurAmount: blurAmout, + elevation: elevation, + tint: color, + ), + ), + ), + ]), + ), + Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + InfoLabel.rich( + label: _buildLabel( + 'Tint color', + 'the color/tint overlay layer.', + ), + child: ComboBox( + placeholder: const Text('Tint color '), + onChanged: (c) => setState(() => color = c), + value: color, + items: [ + ComboBoxItem( + child: Row(children: [ + buildColorBox(Colors.white), + const SizedBox(width: 10.0), + const Text('White'), + ]), + value: Colors.white, + ), + ComboBoxItem( + child: Row(children: [ + buildColorBox(const Color(0xE4000000)), + const SizedBox(width: 10.0), + const Text('Black'), + ]), + value: const Color(0xE4000000), + ), + ...List.generate(Colors.accentColors.length, (index) { + final color = Colors.accentColors[index]; + return ComboBoxItem( + child: Row(children: [ + buildColorBox(color), + const SizedBox(width: 10.0), + Text(accentColorNames[index + 1]), + ]), + value: color, + ); + }), + ], + ), + ), + InfoLabel.rich( + label: _buildLabel( + 'Tint opacity', + 'the opacity of the tint layer.', + ), + child: Slider( + value: tintOpacity, + min: 0.0, + max: 1.0, + onChanged: (v) => setState(() => tintOpacity = v), + ), + ), + InfoLabel.rich( + label: _buildLabel( + 'Tint luminosity opacity', + 'controls the amount of saturation that is allowed through ' + 'the acrylic surface from the background.', + ), + child: Slider( + value: luminosityOpacity, + min: 0.0, + max: 1.0, + onChanged: (v) => setState(() => luminosityOpacity = v), + ), + ), + InfoLabel( + label: 'Blur amount', + child: Slider( + value: blurAmout, + min: 0.0, + max: 100, + onChanged: (v) => setState(() => blurAmout = v), + ), + ), + InfoLabel( + label: 'Elevation', + child: Slider( + value: elevation, + min: 0, + max: 20, + onChanged: (v) => setState(() => elevation = v), + ), + ), + ]), + ]), + ), + ), + ], + ); + } + + Widget buildColorBox(Color color) { + const double boxSize = 16.0; + return Container( + height: boxSize, + width: boxSize, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(4.0), + ), + ); + } +} + +class _AcrylicChildren extends StatelessWidget { + const _AcrylicChildren({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return Stack(children: [ + Container( + height: 200, + width: 100, + color: Colors.blue.lightest, + ), + Align( + alignment: AlignmentDirectional.center, + child: Container( + height: 152, + width: 152, + color: Colors.magenta, + ), + ), + Align( + alignment: AlignmentDirectional.bottomEnd, + child: Container( + height: 100, + width: 80, + color: Colors.yellow, + ), + ), + ]); + } +} diff --git a/launchpad_app/lib/screens/surface/commandbars.dart b/launchpad_app/lib/screens/surface/commandbars.dart new file mode 100644 index 00000000..d6bc2490 --- /dev/null +++ b/launchpad_app/lib/screens/surface/commandbars.dart @@ -0,0 +1,299 @@ +import 'package:fluent_ui/fluent_ui.dart'; + +import '../../widgets/page.dart'; + +class CommandBarsPage extends StatefulWidget { + const CommandBarsPage({Key? key}) : super(key: key); + + @override + State createState() => _CommandBarsPageState(); +} + +class _CommandBarsPageState extends State with PageMixin { + final simpleCommandBarItems = [ + CommandBarBuilderItem( + builder: (context, mode, w) => Tooltip( + message: "Create something new!", + child: w, + ), + wrappedItem: CommandBarButton( + icon: const Icon(FluentIcons.add), + label: const Text('New'), + onPressed: () {}, + ), + ), + CommandBarBuilderItem( + builder: (context, mode, w) => Tooltip( + message: "Delete what is currently selected!", + child: w, + ), + wrappedItem: CommandBarButton( + icon: const Icon(FluentIcons.delete), + label: const Text('Delete'), + onPressed: () {}, + ), + ), + CommandBarButton( + icon: const Icon(FluentIcons.archive), + label: const Text('Archive'), + onPressed: () {}, + ), + CommandBarButton( + icon: const Icon(FluentIcons.move), + label: const Text('Move'), + onPressed: () {}, + ), + const CommandBarButton( + icon: Icon(FluentIcons.cancel), + label: Text('Disabled'), + onPressed: null, + ), + ]; + + final moreCommandBarItems = [ + CommandBarButton( + icon: const Icon(FluentIcons.reply), + label: const Text('Reply'), + onPressed: () {}, + ), + CommandBarButton( + icon: const Icon(FluentIcons.reply_all), + label: const Text('Reply All'), + onPressed: () {}, + ), + CommandBarButton( + icon: const Icon(FluentIcons.forward), + label: const Text('Forward'), + onPressed: () {}, + ), + CommandBarButton( + icon: const Icon(FluentIcons.search), + label: const Text('Search'), + onPressed: () {}, + ), + CommandBarButton( + icon: const Icon(FluentIcons.pin), + label: const Text('Pin'), + onPressed: () {}, + ), + CommandBarButton( + icon: const Icon(FluentIcons.unpin), + label: const Text('Unpin'), + onPressed: () {}, + ), + ]; + + final evenMoreCommandBarItems = [ + CommandBarButton( + icon: const Icon(FluentIcons.accept), + label: const Text('Accept'), + onPressed: () {}, + ), + CommandBarButton( + icon: const Icon(FluentIcons.calculator_multiply), + label: const Text('Reject'), + onPressed: () {}, + ), + CommandBarButton( + icon: const Icon(FluentIcons.share), + label: const Text('Share'), + onPressed: () {}, + ), + CommandBarButton( + icon: const Icon(FluentIcons.add_favorite), + label: const Text('Add Favorite'), + onPressed: () {}, + ), + CommandBarButton( + icon: const Icon(FluentIcons.back), + label: const Text('Backward'), + onPressed: () {}, + ), + CommandBarButton( + icon: const Icon(FluentIcons.forward), + label: const Text('Forward'), + onPressed: () {}, + ), + ]; + + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: PageHeader( + title: const Text('CommandBar'), + commandBar: CommandBar( + mainAxisAlignment: MainAxisAlignment.end, + primaryItems: [ + ...simpleCommandBarItems, + ], + ), + ), + children: [ + const Text( + 'Command bars provide users with easy access to your app\'s most ' + 'common tasks. Command bars can provide access to app-level or ' + 'page-specific commands and can be used with any navigation pattern.', + ), + subtitle(content: const Text('Simple command bar (no wrapping)')), + CommandBar( + overflowBehavior: CommandBarOverflowBehavior.noWrap, + primaryItems: [ + ...simpleCommandBarItems, + ], + ), + subtitle( + content: const Text( + 'Command bar with many items (wrapping, auto-compact < 600px)', + ), + ), + CommandBar( + overflowBehavior: CommandBarOverflowBehavior.wrap, + compactBreakpointWidth: 600, + primaryItems: [ + ...simpleCommandBarItems, + const CommandBarSeparator(), + ...moreCommandBarItems, + ], + ), + subtitle( + content: const Text( + 'Carded compact command bar with many items (clipped)', + ), + ), + ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 230), + child: CommandBarCard( + child: CommandBar( + overflowBehavior: CommandBarOverflowBehavior.clip, + isCompact: true, + primaryItems: [ + ...simpleCommandBarItems, + const CommandBarSeparator(), + ...moreCommandBarItems, + ], + ), + ), + ), + subtitle( + content: const Text( + 'Carded compact command bar with many items (dynamic overflow)', + ), + ), + CommandBarCard( + child: CommandBar( + overflowBehavior: CommandBarOverflowBehavior.dynamicOverflow, + primaryItems: [ + ...simpleCommandBarItems, + const CommandBarSeparator(), + ...moreCommandBarItems, + const CommandBarSeparator(), + ...evenMoreCommandBarItems, + ], + ), + ), + subtitle( + content: const Text( + 'End-aligned command bar with many items (dynamic overflow, auto-compact < 900px)', + ), + ), + CommandBar( + mainAxisAlignment: MainAxisAlignment.end, + overflowBehavior: CommandBarOverflowBehavior.dynamicOverflow, + compactBreakpointWidth: 900, + primaryItems: [ + ...simpleCommandBarItems, + const CommandBarSeparator(), + ...moreCommandBarItems, + const CommandBarSeparator(), + ...evenMoreCommandBarItems, + ], + ), + subtitle( + content: const Text( + 'End-aligned command bar with permanent secondary items (dynamic overflow)', + ), + ), + CommandBar( + mainAxisAlignment: MainAxisAlignment.end, + overflowBehavior: CommandBarOverflowBehavior.dynamicOverflow, + primaryItems: [ + ...simpleCommandBarItems, + const CommandBarSeparator(), + ...moreCommandBarItems, + ], + secondaryItems: evenMoreCommandBarItems, + ), + subtitle( + content: const Text( + 'Command bar with secondary items (wrapping)', + ), + ), + CommandBar( + overflowBehavior: CommandBarOverflowBehavior.wrap, + primaryItems: simpleCommandBarItems, + secondaryItems: moreCommandBarItems, + ), + subtitle( + content: const Text( + 'Carded complex command bar with many items (horizontal scrolling)', + ), + ), + CommandBarCard( + child: Row(children: [ + Expanded( + child: CommandBar( + overflowBehavior: CommandBarOverflowBehavior.scrolling, + primaryItems: [ + ...simpleCommandBarItems, + const CommandBarSeparator(), + ...moreCommandBarItems, + ], + ), + ), + // End-aligned button(s) + CommandBar( + overflowBehavior: CommandBarOverflowBehavior.noWrap, + primaryItems: [ + CommandBarButton( + icon: const Icon(FluentIcons.refresh), + onPressed: () {}, + ), + ], + ), + ]), + ), + subtitle( + content: const Text( + 'Carded complex command bar with many items (dynamic overflow)', + ), + ), + CommandBarCard( + child: Row(children: [ + Expanded( + child: CommandBar( + overflowBehavior: CommandBarOverflowBehavior.dynamicOverflow, + overflowItemAlignment: MainAxisAlignment.end, + primaryItems: [ + ...simpleCommandBarItems, + const CommandBarSeparator(), + ...moreCommandBarItems, + ], + secondaryItems: evenMoreCommandBarItems, + ), + ), + // End-aligned button(s) + CommandBar( + overflowBehavior: CommandBarOverflowBehavior.noWrap, + primaryItems: [ + CommandBarButton( + icon: const Icon(FluentIcons.refresh), + onPressed: () {}, + ), + ], + ), + ]), + ), + ], + ); + } +} diff --git a/launchpad_app/lib/screens/surface/content_dialog.dart b/launchpad_app/lib/screens/surface/content_dialog.dart new file mode 100644 index 00000000..bf92c6fc --- /dev/null +++ b/launchpad_app/lib/screens/surface/content_dialog.dart @@ -0,0 +1,97 @@ +import 'package:example/widgets/card_highlight.dart'; +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +class ContentDialogPage extends StatefulWidget { + const ContentDialogPage({Key? key}) : super(key: key); + + @override + State createState() => _ContentDialogPageState(); +} + +class _ContentDialogPageState extends State with PageMixin { + String? result = ''; + + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: const PageHeader(title: Text('ContentDialog')), + children: [ + const Text( + 'Dialog controls are modal UI overlays that provide contextual app ' + 'information. They block interactions with the app window until being ' + 'explicitly dismissed. They often request some kind of action from the ' + 'user.', + ), + subtitle(content: const Text('A basic content dialog with content')), + CardHighlight( + child: Row(children: [ + Button( + child: const Text('Show dialog'), + onPressed: () => showContentDialog(context), + ), + const SizedBox(width: 10.0), + Text(result ?? ''), + const Spacer(), + ]), + codeSnippet: '''Button( + child: const Text('Show dialog'), + onPressed: () => showContentDialog(context), +), + +void showContentDialog(BuildContext context) async { + final result = await showDialog( + context: context, + builder: (context) => ContentDialog( + title: const Text('Delete file permanently?'), + content: const Text( + 'If you delete this file, you won't be able to recover it. Do you want to delete it?', + ), + actions: [ + Button( + child: const Text('Delete'), + onPressed: () { + Navigator.pop(context, 'User deleted file'); + // Delete file here + }, + ), + FilledButton( + child: const Text('Cancel'), + onPressed: () => Navigator.pop(context, 'User canceled dialog'), + ), + ], + ), + ); + setState(() {}); +}''', + ), + ], + ); + } + + void showContentDialog(BuildContext context) async { + result = await showDialog( + context: context, + builder: (context) => ContentDialog( + title: const Text('Delete file permanently?'), + content: const Text( + 'If you delete this file, you won\'t be able to recover it. Do you want to delete it?', + ), + actions: [ + Button( + child: const Text('Delete'), + onPressed: () { + Navigator.pop(context, 'User deleted file'); + // Delete file here + }, + ), + FilledButton( + child: const Text('Cancel'), + onPressed: () => Navigator.pop(context, 'User canceled dialog'), + ), + ], + ), + ); + setState(() {}); + } +} diff --git a/launchpad_app/lib/screens/surface/expander.dart b/launchpad_app/lib/screens/surface/expander.dart new file mode 100644 index 00000000..9534a305 --- /dev/null +++ b/launchpad_app/lib/screens/surface/expander.dart @@ -0,0 +1,196 @@ +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +import '../../widgets/card_highlight.dart'; + +class ExpanderPage extends StatefulWidget { + const ExpanderPage({Key? key}) : super(key: key); + + @override + State createState() => _ExpanderPageState(); +} + +class _ExpanderPageState extends State with PageMixin { + final expanderKey = GlobalKey(); + + bool crostOpen = false; + List crosts = [ + 'Classic', + 'Whole wheat', + 'Gluten free', + ]; + String crost = 'Whole wheat'; + List sizes = [ + 'Regular', + 'Thin', + 'Pan', + 'Stuffed', + ]; + String size = 'Pan'; + bool checked = false; + + @override + Widget build(BuildContext context) { + final open = expanderKey.currentState?.isExpanded ?? false; + return ScaffoldPage.scrollable( + header: const PageHeader(title: Text('Expander')), + children: [ + description( + content: const Text( + 'The Expander control lets you show or hide less important content ' + 'that\'s related to a piece of primary content that\'s always visible. ' + 'Items contained in the Header are always visible. The user can expand ' + 'and collapse the Content area, where secondary content is displayed, ' + 'by interacting with the header. When the content area is expanded, it ' + 'pushes other UI elements out of the way; it does not overlay other UI. ' + 'The Expander can expand upwards or downwards.\n\n' + 'Both the Header and Content areas can contain any content, from simple ' + 'text to complex UI layouts. For example, you can use the control to ' + 'show additional options for an item.\n\n' + 'Use an Expander when some primary content should always be visible, ' + 'but related secondary content may be hidden until needed. This UI is ' + 'commonly used when display space is limited and when information or ' + 'options can be grouped together. Hiding the secondary content until ' + 'it\'s needed can also help to focus the user on the most important ' + 'parts of your app.', + ), + ), + subtitle(content: const Text('Simple expander')), + description( + content: const Text( + 'In this example, the trailing vanishes when the expander is open.', + ), + ), + CardHighlight( + child: Expander( + header: const Text('Choose your crost'), + onStateChanged: (open) => setState(() => crostOpen = open), + trailing: crostOpen + ? null + : Text( + '$crost, $size', + style: FluentTheme.of(context).typography.caption, + ), + content: + Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: crosts + .map( + (e) => Padding( + padding: const EdgeInsetsDirectional.only(bottom: 8.0), + child: RadioButton( + checked: crost == e, + onChanged: (selected) { + if (selected) setState(() => crost = e); + }, + content: Text(e), + ), + ), + ) + .toList(), + ), + const SizedBox(width: 12.0), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: sizes + .map((e) => Padding( + padding: + const EdgeInsetsDirectional.only(bottom: 8.0), + child: RadioButton( + checked: size == e, + onChanged: (selected) { + if (selected) setState(() => size = e); + }, + content: Text(e), + ), + )) + .toList(), + ), + ]), + ), + codeSnippet: '''Expander( + leading: RadioButton( + checked: checked, + onChanged: (v) => setState(() => checked = v), + ), + header: Text('This text is in header'), + content: Text('This text is in content'), +)''', + ), + subtitle(content: const Text('Scrollable content')), + const CardHighlight( + child: Expander( + header: Text('Open to see the scrollable text'), + content: SizedBox( + height: 300, + child: SingleChildScrollView( + child: SelectableText( + '''Lorem ipsum dolor sit amet, consectetur adipiscing elit. Duis porta lectus lacus, ut viverra ex aliquet at. Sed ac tempus magna. Ut velit diam, condimentum ac bibendum sit amet, aliquam at quam. Mauris bibendum, elit ut mollis molestie, neque risus lacinia libero, id fringilla lacus odio a nisl. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Aliquam viverra tincidunt diam, id porta justo iaculis ac. Aenean ornare bibendum rutrum. Aenean dignissim egestas augue id elementum. Suspendisse dapibus, felis nec varius porta, purus turpis sodales est, sit amet consectetur velit turpis in orci. Curabitur sed tortor purus. Donec ut ligula tortor. Quisque ac nulla dui. Praesent sed diam id dui pharetra facilisis. Maecenas lacinia augue eu metus luctus, vitae efficitur ex accumsan. Sed viverra tellus quis ex tempus, sit amet aliquam mauris hendrerit. Proin tempus nisl mauris, eget ultricies ligula aliquet id. + +Fusce molestie quis augue vel eleifend. Praesent ligula velit, porta id diam sed, malesuada molestie odio. Proin egestas nisl vel leo accumsan, vel ullamcorper ipsum dapibus. Curabitur libero augue, porttitor dictum mauris ut, dignissim blandit lacus. Suspendisse lacinia augue elit, sit amet auctor eros pretium sit amet. Proin ullamcorper augue nulla, sit amet rhoncus nisl gravida ac. Aenean auctor ligula in nibh fermentum fermentum. Aliquam erat volutpat. Sed molestie vulputate diam, id rhoncus augue mattis vitae. Ut tempus tempus dui, in imperdiet elit tincidunt id. Integer congue urna eu nisl bibendum accumsan. Aliquam commodo tempor turpis sit amet suscipit. + +Donec sit amet semper sem. Pellentesque commodo mi in est sagittis ultricies in ut elit. Donec vulputate commodo vestibulum. Pellentesque pulvinar tortor vel suscipit hendrerit. Vestibulum interdum, est et aliquam dapibus, tellus elit pharetra nisl, in volutpat sapien ipsum in velit. Donec gravida erat tellus, et molestie diam interdum sed. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Duis dolor nisl, viverra sed pellentesque molestie, tempor ut enim. Duis suscipit massa sed dolor suscipit mollis. In lobortis efficitur egestas. Integer blandit, dolor eu tristique mollis, lorem urna convallis arcu, non iaculis elit purus vel velit. Nam eget aliquet quam, vitae luctus urna. Nunc vehicula sagittis risus, vitae pretium lacus ornare semper. In ornare massa vitae odio consequat, eu lacinia mi imperdiet. Vivamus at augue diam. Fusce eu purus magna. + +Fusce tempor, dolor in porttitor porttitor, turpis leo ullamcorper urna, vitae ultrices lorem augue eget nulla. Nulla sodales venenatis tellus quis feugiat. Phasellus sit amet condimentum nulla. Quisque felis lorem, tempus quis odio id, tincidunt volutpat ante. Fusce ultrices dui vel lorem tincidunt, in pellentesque ligula luctus. Morbi luctus est vitae eros blandit dictum. Quisque convallis diam sed arcu volutpat, eget placerat turpis cursus. Aliquam dapibus finibus luctus. Praesent vestibulum viverra risus, nec sollicitudin mi mattis eu. Nulla vestibulum, nibh eget sagittis placerat, elit eros egestas libero, eu luctus justo ante eget tellus. Etiam quis lacus gravida, consequat diam in, laoreet sapien. + +Fusce nunc neque, imperdiet id justo non, porttitor finibus massa. Ut quis risus quis tellus ultricies accumsan et et lorem. Nam pulvinar luctus velit, ut vehicula neque sagittis nec. Integer commodo, metus auctor rutrum finibus, tellus justo feugiat leo, sit amet tempus est justo eu augue. Cras eget nibh ac enim bibendum lobortis. Sed ultricies nunc elit, imperdiet consectetur velit scelerisque eu. Aliquam suscipit libero vel nibh porttitor, vel sodales nisi viverra. Duis vitae rutrum metus, vitae accumsan massa. Sed congue, est interdum commodo facilisis, leo libero blandit tellus, a dapibus tortor odio eget ex. Nunc aliquet nulla vel augue pulvinar, vel luctus risus sagittis. Sed non sodales urna. Phasellus quis sapien placerat, ultricies risus ut, hendrerit mi. Donec pretium ligula non arcu posuere porttitor. Pellentesque eleifend mollis ex non eleifend. Nam sed elit mollis mauris laoreet aliquam eget vel elit.''', + ), + ), + ), + ), + codeSnippet: '''Expander( + header: Text('Open to see the scrollable text'), + content: SizedBox( + height: 300, + child: SingleChildScrollView( + child: Text('A LONG TEXT HERE'), + ), + ), +)''', + ), + subtitle(content: const Text('Expander opened programatically')), + CardHighlight( + child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Expanded( + child: Expander( + key: expanderKey, + header: const Text('This text is in header'), + content: const Text('This text is in content'), + onStateChanged: (open) => setState(() {}), + ), + ), + const SizedBox(width: 20), + ToggleSwitch( + checked: open, + onChanged: (v) { + setState(() { + expanderKey.currentState?.isExpanded = v; + }); + }, + content: Text(open ? 'Close' : 'Open'), + ), + ]), + codeSnippet: '''final expanderKey = GlobalKey(); + +Expander( + key: expanderKey, + header: Text('This text is in header'), + content: Text('This text is in content'), + onStateChanged: (open) { + print('state changed to open=$open'); + }, +) + +/// Toggles the current expander state +/// +/// if it's open, now it's closed, and vice versa +void toggle() { + final open = expanderKey.currentState?.open ?? false; + + expanderKey.currentState?.open = !open; +}'''), + ], + ); + } +} diff --git a/launchpad_app/lib/screens/surface/flyouts.dart b/launchpad_app/lib/screens/surface/flyouts.dart new file mode 100644 index 00000000..28d61f76 --- /dev/null +++ b/launchpad_app/lib/screens/surface/flyouts.dart @@ -0,0 +1,410 @@ +import 'package:example/widgets/card_highlight.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +class FlyoutPage extends StatefulWidget { + const FlyoutPage({Key? key}) : super(key: key); + + @override + State createState() => _FlyoutShowcaseState(); +} + +class _FlyoutShowcaseState extends State { + Typography get typography => FluentTheme.of(context).typography; + + FlyoutController buttonController = FlyoutController(); + FlyoutController flyoutController = FlyoutController(); + + @override + void dispose() { + buttonController.dispose(); + flyoutController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: const PageHeader(title: Text('Flyouts')), + children: [ + const Text( + 'A Flyout displays lightweight UI that is either information, or requires user interaction. Unlike a dialog, a Flyout can be light dismissed by clicking or tapping off of it. Use it to collect input from the user, show more details about an item, or ask the user to confirm an action.', + ), + Text('A button with a Flyout', style: typography.subtitle), + const SizedBox(height: 10.0), + CardHighlight( + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Flyout( + controller: buttonController, + content: (context) { + return FlyoutContent( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'All items will be removed. Do you want to continue?', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12.0), + Button( + child: const Text('Yes, empty my cart'), + onPressed: buttonController.close, + ), + ], + ), + ); + }, + child: Button( + child: const Text('Empty cart'), + onPressed: buttonController.open, + ), + ), + ), + codeSnippet: + '''// define the controller. It'll be responsible to open/close the flyout programatically +FlyoutController buttonController = FlyoutController(); + +Flyout( + controller: buttonController, + // [content] is the content of the flyout popup, opened when the user presses + // the button + content: (context) { + return FlyoutContent( + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'All items will be removed. Do you want to continue?', + style: TextStyle(fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12.0), + Button( + child: const Text('Yes, empty my cart'), + onPressed: buttonController.close, + ), + ], + ), + ); + }, + child: Button( + child: const Text('Empty cart'), + onPressed: buttonController.open, + ) +)''', + ), + const SizedBox(height: 20.0), + DefaultTextStyle( + style: const TextStyle(color: Colors.white), + child: Wrap(spacing: 12.0, runSpacing: 12.0, children: [ + _flyoutOnHover(), + _flyoutOnPress(), + _flyoutOnLongPress(), + _flyoutWithController(), + ]), + ), + DefaultTextStyle( + style: const TextStyle(color: Colors.white), + child: Wrap(spacing: 12.0, runSpacing: 12.0, children: [ + _flyoutAtStart(), + _flyoutAtCenter(), + _flyoutAtEnd(), + _flyoutAtCustomPosition(), + ]), + ), + const PageHeader(title: Text('Menu Flyouts'), padding: 0.0), + const Text( + 'A MenuFlyout displays lightweight UI that is light dismissed by clicking or tapping off of it. Use it to let the user choose from a contextual list of simple commands or options.', + ), + DefaultTextStyle( + style: const TextStyle(color: Colors.white), + child: Wrap(spacing: 12.0, runSpacing: 12.0, children: [ + _menuFlyout(), + _menuFlyoutWithSubItem(), + ]), + ), + ], + ); + } + + Widget _flyoutOnHover() { + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Flyout on hover', style: typography.subtitle), + Padding( + padding: const EdgeInsetsDirectional.only(top: 10.0), + child: Flyout( + openMode: FlyoutOpenMode.hover, + content: (context) { + return const FlyoutContent( + child: Text('This is a flyout shown on hover'), + ); + }, + child: Container( + color: Colors.orange, + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: const Text('Hover to show flyout'), + ), + ), + ), + ]); + } + + Widget _flyoutOnPress() { + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Flyout on click/press', style: typography.subtitle), + Padding( + padding: const EdgeInsetsDirectional.only(top: 10.0), + child: Flyout( + openMode: FlyoutOpenMode.press, + content: (context) { + return const FlyoutContent( + child: Text('This is a flyout shown on press'), + ); + }, + child: Container( + color: Colors.red, + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: const Text('Click/press to show flyout'), + ), + ), + ), + ]); + } + + Widget _flyoutOnLongPress() { + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Flyout on long click/press', style: typography.subtitle), + Padding( + padding: const EdgeInsetsDirectional.only(top: 10.0), + child: Flyout( + openMode: FlyoutOpenMode.longPress, + content: (context) { + return const FlyoutContent( + child: Text('This is a flyout shown on long press'), + ); + }, + child: Container( + color: Colors.magenta, + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: const Text('Long press/click to show flyout'), + ), + ), + ), + ]); + } + + Widget _flyoutWithController() { + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Flyout with controller', style: typography.subtitle), + Padding( + padding: const EdgeInsetsDirectional.only(top: 10.0), + child: Flyout( + controller: flyoutController, + content: (context) { + return const FlyoutContent( + child: Text('This is a flyout shown with the controller'), + ); + }, + child: GestureDetector( + onTap: flyoutController.open, + child: Container( + color: Colors.purple, + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: const Text('Click/press to show flyout'), + ), + ), + ), + ), + ]); + } + + Widget _flyoutAtStart() { + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Flyout displayed at start', style: typography.subtitle), + Padding( + padding: const EdgeInsetsDirectional.only(top: 10.0), + child: Flyout( + openMode: FlyoutOpenMode.press, + placement: FlyoutPlacement.start, + content: (context) { + return const FlyoutContent( + child: Text('This is a flyout shown at the start of the child'), + ); + }, + child: Container( + color: Colors.blue, + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: const Text('Press to show a flyout at start'), + ), + ), + ), + ]); + } + + Widget _flyoutAtCenter() { + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Flyout displayed at center', style: typography.subtitle), + Padding( + padding: const EdgeInsetsDirectional.only(top: 10.0), + child: Flyout( + openMode: FlyoutOpenMode.press, + placement: FlyoutPlacement.center, + content: (context) { + return const FlyoutContent( + child: Text('This is a flyout shown at the center of the child'), + ); + }, + child: Container( + color: Colors.teal, + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: const Text('Press to show a flyout at center'), + ), + ), + ), + ]); + } + + Widget _flyoutAtEnd() { + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Flyout displayed at end', style: typography.subtitle), + Padding( + padding: const EdgeInsetsDirectional.only(top: 10.0), + child: Flyout( + openMode: FlyoutOpenMode.press, + placement: FlyoutPlacement.end, + content: (context) { + return const FlyoutContent( + child: Text('This is a flyout shown at the end of the child'), + ); + }, + child: Container( + color: Colors.green, + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: const Text('Press to show a flyout at end'), + ), + ), + ), + ]); + } + + Widget _flyoutAtCustomPosition() { + return Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Text('Flyout displayed at custom position', style: typography.subtitle), + Padding( + padding: const EdgeInsetsDirectional.only(top: 10.0), + child: Flyout( + openMode: FlyoutOpenMode.press, + placement: FlyoutPlacement.full, + content: (context) { + return const Align( + alignment: AlignmentDirectional.topEnd, + child: FlyoutContent( + child: Text( + 'This is a flyout shown at a custom position on the window', + ), + ), + ); + }, + child: Container( + color: Colors.yellow, + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: const Text( + 'Press to show a flyout at custom position', + style: TextStyle(color: Colors.black), + ), + ), + ), + ), + ]); + } + + Widget _menuFlyout() { + return Flyout( + content: (context) { + return MenuFlyout( + items: [ + MenuFlyoutItem( + leading: const Icon(FluentIcons.share), + text: const Text('Share'), + onPressed: () {}, + ), + MenuFlyoutItem( + leading: const Icon(FluentIcons.copy), + text: const Text('Copy'), + onPressed: () {}, + ), + MenuFlyoutItem( + leading: const Icon(FluentIcons.delete), + text: const Text('Delete'), + onPressed: () {}, + ), + const MenuFlyoutSeparator(), + MenuFlyoutItem( + text: const Text('Rename'), + onPressed: () {}, + ), + MenuFlyoutItem( + text: const Text('Select'), + onPressed: () {}, + ), + ], + ); + }, + openMode: FlyoutOpenMode.press, + child: Container( + color: Colors.orange, + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: const Text('Click to show flyout'), + ), + ); + } + + Widget _menuFlyoutWithSubItem() { + return Flyout( + placement: FlyoutPlacement.end, + content: (context) { + return MenuFlyout( + items: [ + MenuFlyoutSubItem( + text: const Text('New'), + items: [ + MenuFlyoutItem( + text: const Text('Plain Text Document'), + onPressed: () {}, + ), + MenuFlyoutItem( + text: const Text('Rich Text Document'), + onPressed: () {}, + ), + MenuFlyoutItem( + text: const Text('Other formats...'), + onPressed: () {}, + ), + ], + ), + MenuFlyoutItem( + text: const Text('Open'), + onPressed: () {}, + ), + MenuFlyoutItem( + text: const Text('Save'), + onPressed: () {}, + ), + const MenuFlyoutSeparator(), + MenuFlyoutItem( + text: const Text('Exit'), + onPressed: () {}, + ), + ], + ); + }, + openMode: FlyoutOpenMode.press, + child: Container( + color: Colors.orange, + padding: const EdgeInsets.symmetric(horizontal: 10.0), + child: const Text('Click to show flyout with subitem'), + ), + ); + } +} diff --git a/launchpad_app/lib/screens/surface/info_bars.dart b/launchpad_app/lib/screens/surface/info_bars.dart new file mode 100644 index 00000000..39e37922 --- /dev/null +++ b/launchpad_app/lib/screens/surface/info_bars.dart @@ -0,0 +1,195 @@ +import 'package:example/widgets/card_highlight.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +import '../../widgets/page.dart'; + +class InfoBarsPage extends StatefulWidget { + const InfoBarsPage({Key? key}) : super(key: key); + + @override + State createState() => _InfoBarsPageState(); +} + +class _InfoBarsPageState extends State with PageMixin { + // First info bar + bool _firstOpen = true; + InfoBarSeverity severity = InfoBarSeverity.info; + + // Second info bar + bool _secondOpen = true; + bool _isLong = false; + bool _hasActionButton = true; + bool _isIconVisible = true; + + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: const PageHeader(title: Text('InfoBar')), + children: [ + const Text( + 'Use an InfoBar control when a user should be informed of, acknowledge,' + ' or take action on a changed application state. By default the notification' + ' will remain in the content area until closed by the user but will ' + 'not necessarily break user flow.', + ), + subtitle( + content: const Text( + 'A closable InfoBar with options to change its severity', + ), + ), + CardHighlight( + backgroundColor: FluentTheme.of(context).micaBackgroundColor, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_firstOpen) + InfoBar( + title: const Text('Title'), + content: const Text( + 'Essential app message for your users to be informed of, ' + 'acknowledge, or take action on.', + ), + severity: severity, + isLong: true, + onClose: () => setState(() => _firstOpen = false), + ), + const SizedBox(height: 10.0), + const Divider(), + const SizedBox(height: 10.0), + Checkbox( + checked: _firstOpen, + onChanged: (v) => setState(() => _firstOpen = v!), + content: const Text('Is open'), + ), + Container( + margin: const EdgeInsetsDirectional.only(top: 10.0), + width: 150.0, + child: ComboBox( + isExpanded: true, + items: InfoBarSeverity.values + .map( + (severity) => ComboBoxItem( + child: Text(severity.name), + value: severity, + ), + ) + .toList(), + value: severity, + onChanged: (v) => setState(() => severity = v ?? severity), + popupColor: () { + switch (severity) { + case InfoBarSeverity.info: + break; + case InfoBarSeverity.warning: + return Colors.warningPrimaryColor; + case InfoBarSeverity.error: + return Colors.errorPrimaryColor; + case InfoBarSeverity.success: + return Colors.successPrimaryColor; + } + }(), + ), + ), + ], + ), + ), + codeSnippet: '''InfoBar( + title: const Text('Title'), + content: const Text( + 'Essential app message for your users to be informed of, ' + 'acknowledge, or take action on.', + ), + severity: $severity, + isLong: true, +)''', + ), + subtitle( + content: const Text( + 'A closable InfoBar with a long and short message and action button', + ), + ), + CardHighlight( + backgroundColor: FluentTheme.of(context).micaBackgroundColor, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (_secondOpen) + InfoBar( + title: const Text('Title'), + content: Text( + _isLong + ? 'Essential app message for your users to be informed ' + 'of, acknowledge, or take action on. Lorem Ipsum is ' + 'simply dummy text of the printing and typesetting ' + 'industry. Lorem Ipsum has been the industry\'s ' + 'standard dummy text ever since the 1500s, when an ' + 'unknown printer took a galley of type and scrambled ' + 'it to make a type specimen book.' + : 'A short essential message', + ), + severity: severity, + isLong: _isLong, + onClose: () => setState(() => _secondOpen = false), + action: _hasActionButton + ? Button( + child: const Text('Action'), + onPressed: () {}, + ) + : null, + isIconVisible: _isIconVisible, + ), + const SizedBox(height: 10.0), + const Divider(), + const SizedBox(height: 10.0), + Checkbox( + checked: _secondOpen, + onChanged: (v) => setState(() => _secondOpen = v!), + content: const Text('Is open'), + ), + Checkbox( + checked: _isLong, + onChanged: (v) => setState(() => _isLong = v!), + content: const Text('Is long'), + ), + Checkbox( + checked: _hasActionButton, + onChanged: (v) => setState(() => _hasActionButton = v!), + content: const Text('Has action button'), + ), + Checkbox( + checked: _isIconVisible, + onChanged: (v) => setState(() => _isIconVisible = v!), + content: const Text('Is icon visible'), + ), + ], + ), + ), + codeSnippet: '''InfoBar( + title: const Text('Title'), + content: Text( + ${_isLong ? '"Essential app message for your users to be informed ' + 'of, acknowledge, or take action on. Lorem Ipsum is ' + 'simply dummy text of the printing and typesetting ' + 'industry. Lorem Ipsum has been the industry\'s ' + 'standard dummy text ever since the 1500s, when an ' + 'unknown printer took a galley of type and scrambled ' + 'it to make a type specimen book."' : '"A short essential message"'} + ), + severity: $severity, + isLong: true, + ${_hasActionButton ? '''action: Button( + child: const Text('Action'), + onPressed: () {}, + )''' : null} +)''', + ), + ], + ); + } +} diff --git a/launchpad_app/lib/screens/surface/progress_indicators.dart b/launchpad_app/lib/screens/surface/progress_indicators.dart new file mode 100644 index 00000000..d5adea83 --- /dev/null +++ b/launchpad_app/lib/screens/surface/progress_indicators.dart @@ -0,0 +1,82 @@ +import 'dart:math'; + +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +import '../../widgets/card_highlight.dart'; + +class ProgressIndicatorsPage extends StatefulWidget { + const ProgressIndicatorsPage({Key? key}) : super(key: key); + + @override + State createState() => _ProgressIndicatorsPageState(); +} + +class _ProgressIndicatorsPageState extends State + with PageMixin { + double determinateValue = Random().nextDouble() * 100; + @override + Widget build(BuildContext context) { + return ScaffoldPage.scrollable( + header: const PageHeader(title: Text('Progress controls')), + children: [ + description( + content: const Text( + 'A progress control provides feedback to the user that a long-running ' + 'operation is underway. It can mean that the user cannot interact with ' + 'the app when the progress indicator is visible, and can also indicate ' + 'how long the wait time might be, depending on the indicator used.', + ), + ), + subtitle(content: const Text('Indeterminate Progress Indicators')), + description( + content: const Text( + 'The indeterminate state shows that an operation is underway and its ' + 'completion time is unknown.', + ), + ), + CardHighlight( + child: RepaintBoundary( + child: Row(children: const [ + ProgressBar(), + SizedBox(width: 20.0), + ProgressRing(), + ]), + ), + codeSnippet: '''// indeterminate progress bar +ProgressBar(), + +// indeterminate progress ring +ProgressRing(),''', + ), + subtitle(content: const Text('Determinate Progress Indicators')), + description( + content: const Text( + 'The determinate state shows the percentage completed of a task. ' + 'This should be used during an operation whose duration is known, but ' + 'its progress should not block the user\'s interaction with the app.', + ), + ), + CardHighlight( + child: Row(children: [ + ProgressBar(value: determinateValue), + const SizedBox(width: 20.0), + ProgressRing(value: determinateValue), + const Spacer(), + InfoLabel( + label: 'Progress: ${determinateValue.toInt()}', + child: Slider( + value: determinateValue, + onChanged: (v) => setState(() => determinateValue = v), + ), + ), + ]), + codeSnippet: '''// determinate progress bar +ProgressBar(value: ${determinateValue.toInt()}), + +// determinate progress ring +ProgressRing(value: ${determinateValue.toInt()}),'''), + ], + ); + } +} diff --git a/launchpad_app/lib/screens/surface/tiles.dart b/launchpad_app/lib/screens/surface/tiles.dart new file mode 100644 index 00000000..2b577ec0 --- /dev/null +++ b/launchpad_app/lib/screens/surface/tiles.dart @@ -0,0 +1,306 @@ +import 'package:example/widgets/card_highlight.dart'; +import 'package:fluent_ui/fluent_ui.dart'; +import 'package:example/widgets/page.dart'; + +class TilesPage extends StatefulWidget { + const TilesPage({Key? key}) : super(key: key); + + @override + State createState() => _TilesPageState(); +} + +class _TilesPageState extends State with PageMixin { + final shuffledIcons = FluentIcons.allIcons.values.toList()..shuffle(); + + // first + final firstController = ScrollController(); + String firstSelected = ''; + + // second + final secondController = ScrollController(); + List selected = []; + + // third + String thirdSelected = ''; + final thirdController = ScrollController(); + + @override + Widget build(BuildContext context) { + final theme = FluentTheme.of(context); + return ScaffoldPage.scrollable( + header: const PageHeader(title: Text('Tiles')), + children: [ + description( + content: const Text( + 'A fluent-styled list tile. Usually used inside a ListView', + ), + ), + subtitle(content: const Text('Basic ListView with selectable tiles')), + CardHighlight( + child: Container( + height: 400, + width: 350, + decoration: BoxDecoration( + border: Border.all( + color: theme.resources.surfaceStrokeColorDefault, + ), + ), + child: ListView.builder( + controller: firstController, + shrinkWrap: true, + itemCount: contacts.length, + itemBuilder: (context, index) { + final contact = contacts[index]; + return ListTile.selectable( + title: Text(contact), + selected: firstSelected == contact, + onSelectionChange: (v) { + setState(() => firstSelected = contact); + }, + ); + }, + ), + ), + codeSnippet: '''String selectedContact = ''; + +const contacts = ['Kendall', 'Collins', ...]; + +ListView.builder( + itemCount: contacts.length, + itemBuilder: (context, index) { + final contact = contacts[index]; + return ListTile.selectable( + title: Text(contact), + selected: selectedContact == contact, + onSelectionChange: (v) => setState(() => selectedContact = contact), + ); + } +),''', + ), + subtitle( + content: const Text('ListViewItems with many properties applied'), + ), + CardHighlight( + child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Container( + height: 400, + width: 350, + decoration: BoxDecoration( + border: Border.all( + color: theme.resources.surfaceStrokeColorDefault, + ), + ), + child: ListView.builder( + controller: secondController, + shrinkWrap: true, + itemCount: contacts.length, + itemBuilder: (context, index) { + final contact = contacts[index]; + return ListTile.selectable( + leading: const CircleAvatar(radius: 15.0), + title: Text(contact), + subtitle: const Text('With a custom subtitle'), + trailing: Icon(shuffledIcons[index]), + selectionMode: ListTileSelectionMode.multiple, + selected: selected.contains(contact), + onSelectionChange: (selected) { + setState(() { + if (selected) { + this.selected.add(contact); + } else { + this.selected.remove(contact); + } + }); + }, + ); + }, + ), + ), + ]), + codeSnippet: '''List selectedContacts = []; + +const contacts = ['Kendall', 'Collins', ...]; + +ListView.builder( + itemCount: contacts.length, + itemBuilder: (context, index) { + final contact = contacts[index]; + return ListTile.selectable( + title: Text(contact), + selected: selectedContacts.contains(contact), + selectionMode: ListTileSelectionMode.multiple, + onSelectionChange: (selected) { + setState(() { + if (selected) { + selectedContacts.add(contact); + } else { + selectedContacts.remove(contact); + } + }); + }, + ); + } +),''', + ), + subtitle( + content: const Text('ListViewItems with images'), + ), + CardHighlight( + child: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Container( + height: 400, + width: 550, + decoration: BoxDecoration( + border: Border.all( + color: theme.resources.surfaceStrokeColorDefault, + ), + ), + child: ListView.builder( + controller: thirdController, + shrinkWrap: true, + itemCount: contacts.length, + itemBuilder: (context, index) { + final contact = contacts[index]; + return ListTile.selectable( + leading: SizedBox( + height: 100, + child: AspectRatio( + aspectRatio: 16 / 9, + child: ColoredBox( + color: Colors.accentColors[index ~/ 20], + child: const Placeholder(), + ), + ), + ), + title: Text(contact), + subtitle: const Text('With a custom subtitle'), + selectionMode: ListTileSelectionMode.single, + selected: thirdSelected == contact, + onSelectionChange: (selected) { + setState(() { + if (selected) { + thirdSelected = contact; + } + }); + }, + ); + }, + ), + ), + ]), + codeSnippet: '''String selectedContact = ''; + +const contacts = ['Kendall', 'Collins', ...]; + +ListView.builder( + itemCount: contacts.length, + itemBuilder: (context, index) { + final contact = contacts[index]; + return ListTile.selectable( + leading: SizedBox( + height: 100, + child: AspectRatio( + aspectRatio: 16 / 9, + child: ColoredBox( + color: Colors.accentColors[index ~/ 20], + child: const Placeholder(), + ), + ), + ), + title: Text(contact), + subtitle: const Text('With a custom subtitle'), + selectionMode: ListTileSelectionMode.single, + selected: selectedContact == contact, + onSelectionChange: (v) => setState(() => selectedContact = contact), + ); + } +),''', + ), + ], + ); + } +} + +const String _contactsList = '''Kendall +Collins +Adatum Corporation +Henry +Ross +Adventure Works Cycles +Vance +DeLeon +Alpine Ski House +Victoria +Burke +Bellows College +Amber +Rodriguez +Best For You Organics Company +Amari +Rivera +Contoso, Ltd. +Jessie +Irwin +Contoso Pharmaceuticals +Quinn +Campbell +Contoso Suites +Olivia +Wilson +Consolidated Messenger +Ana +Bowman +Fabrikam, Inc. +Shawn +Hughes +Fabrikam Residences +Oscar +Ward +First Up Consultants +Madison +Butler +Fourth Coffee +Graham +Barnes +Graphic Design Institute +Anthony +Ivanov +Humongous Insurance +Michael +Peltier +Lamna Healthcare Company +Morgan +Connors +Liberty's Delightful Sinful Bakery & Cafe +Andre +Lawson +Lucerne Publishing +Preston +Morales +Margie's Travel +Briana +Hernandez +Nod Publishers +Nicole +Wagner +Northwind Traders +Mario +Rogers +Proseware, Inc. +Eugenia +Lopez +Relecloud +Nathan +Rigby +School of Fine Art +Ellis +Turner +Southridge Video +Miguel +Reyes +Tailspin Toys +Hayden +Cook +Tailwind Traders'''; + +late List contacts = _contactsList.split('\n'); diff --git a/launchpad_app/lib/screens/surface/tooltip.dart b/launchpad_app/lib/screens/surface/tooltip.dart new file mode 100644 index 00000000..674e2052 --- /dev/null +++ b/launchpad_app/lib/screens/surface/tooltip.dart @@ -0,0 +1,71 @@ +import 'package:example/widgets/card_highlight.dart'; +import 'package:example/widgets/page.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +class TooltipPage extends ScrollablePage { + TooltipPage({super.key}); + + @override + Widget buildHeader(BuildContext context) { + return const PageHeader(title: Text('Tooltip')); + } + + @override + List buildScrollable(BuildContext context) { + return [ + const Text( + 'A ToolTip shows more information about a UI element. You might show information about what the element does, or what the user should do. The ToolTip is shown when a user hovers over or presses and holds the UI element.', + ), + subtitle(content: const Text('Button with a simple tooltip')), + CardHighlight( + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Tooltip( + message: 'Simple ToolTip', + child: Button( + child: const Text('Button with a simple tooltip'), + onPressed: () {}, + ), + ), + ), + codeSnippet: '''Tooltip( + message: 'Simple ToolTip', + child: Button( + child: const Text('Button with a simple tooltip'), + onPressed: () {}, + ), +),''', + ), + subtitle( + content: const Text( + 'Button with an horizontal tooltip at the left without mouse position', + ), + ), + CardHighlight( + child: Align( + alignment: AlignmentDirectional.centerStart, + child: Tooltip( + message: 'Horizontal ToolTip', + displayHorizontally: true, + useMousePosition: false, + style: const TooltipThemeData(preferBelow: true), + child: IconButton( + icon: const Icon(FluentIcons.graph_symbol, size: 24.0), + onPressed: () {}, + ), + ), + ), + codeSnippet: '''Tooltip( + message: 'Horizontal ToolTip', + displayHorizontally: true, + useMousePosition: false, + style: const TooltipThemeData(preferBelow: true), + child: IconButton( + icon: const Icon(FluentIcons.graph_symbol, size: 24.0), + onPressed: () {}, + ), +),''', + ), + ]; + } +} diff --git a/launchpad_app/lib/screens/theming/colors.dart b/launchpad_app/lib/screens/theming/colors.dart new file mode 100644 index 00000000..da4e6d5b --- /dev/null +++ b/launchpad_app/lib/screens/theming/colors.dart @@ -0,0 +1,227 @@ +import 'package:clipboard/clipboard.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +import '../../widgets/page.dart'; +import 'icons.dart'; + +const _primaryNames = [ + 'Yellow', + 'Orange', + 'Red', + 'Magenta', + 'Purple', + 'Blue', + 'Teal', + 'Green', +]; + +class ColorsPage extends ScrollablePage { + ColorsPage({super.key}); + + @override + Widget buildHeader(BuildContext context) { + return const PageHeader(title: Text('Colors Showcase')); + } + + @override + Widget buildBottomBar(BuildContext context) { + return const SizedBox( + width: double.infinity, + child: InfoBar( + title: Text('Tip:'), + content: + Text('You can click on any color to copy it to the clipboard!'), + ), + ); + } + + @override + List buildScrollable(BuildContext context) { + const Divider divider = Divider( + style: DividerThemeData( + verticalMargin: EdgeInsets.all(10), + horizontalMargin: EdgeInsets.all(10), + ), + ); + return [ + const SizedBox(height: 14.0), + InfoLabel( + label: 'Primary Colors', + child: Wrap( + spacing: 10, + runSpacing: 10, + children: List.generate(Colors.accentColors.length, (index) { + final name = _primaryNames[index]; + final color = Colors.accentColors[index]; + return ColorBlock( + name: name, + color: color, + clipboard: 'Colors.${name.toLowerCase()}', + ); + }), + ), + ), + divider, + InfoLabel( + label: 'Info Colors', + child: Wrap( + spacing: 10, + runSpacing: 10, + children: [ + const ColorBlock( + name: 'Warning 1', + color: Colors.warningPrimaryColor, + clipboard: 'Colors.warningPrimaryColor', + ), + ColorBlock( + name: 'Warning 2', + color: Colors.warningSecondaryColor, + clipboard: 'Colors.warningSecondaryColor', + ), + const ColorBlock( + name: 'Error 1', + color: Colors.errorPrimaryColor, + clipboard: 'Colors.errorPrimaryColor', + ), + ColorBlock( + name: 'Error 2', + color: Colors.errorSecondaryColor, + clipboard: 'Colors.errorSecondaryColor', + ), + const ColorBlock( + name: 'Success 1', + color: Colors.successPrimaryColor, + clipboard: 'Colors.successPrimaryColor', + ), + ColorBlock( + name: 'Success 2', + color: Colors.successSecondaryColor, + clipboard: 'Colors.successSecondaryColor', + ), + ], + ), + ), + divider, + InfoLabel( + label: 'All Shades', + child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Row(children: const [ + ColorBlock( + name: 'Black', + color: Colors.black, + clipboard: 'Colors.black', + ), + ColorBlock( + name: 'White', + color: Colors.white, + clipboard: 'Colors.white', + ), + ]), + const SizedBox(height: 10), + Wrap( + children: List.generate(22, (index) { + final factor = (index + 1) * 10; + return ColorBlock( + name: 'Grey#$factor', + color: Colors.grey[factor], + clipboard: 'Colors.grey[$factor]', + ); + }), + ), + const SizedBox(height: 10), + Wrap( + children: accent, + runSpacing: 10, + spacing: 10, + ), + ]), + ), + ]; + } + + List get accent { + List children = []; + for (var i = 0; i < Colors.accentColors.length; i++) { + final accent = Colors.accentColors[i]; + final name = _primaryNames[i]; + children.add( + Column( + // mainAxisSize: MainAxisSize.min, + children: List.generate(accent.swatch.length, (index) { + final variant = accent.swatch.keys.toList()[index]; + final color = accent.swatch[variant]!; + return ColorBlock( + name: name, + color: color, + variant: variant, + clipboard: 'Colors.${name.toLowerCase()}.$variant', + ); + }), + ), + ); + } + return children; + } +} + +class ColorBlock extends StatelessWidget { + const ColorBlock({ + Key? key, + required this.name, + required this.color, + this.variant, + required this.clipboard, + }) : super(key: key); + + final String name; + final Color color; + final String? variant; + final String clipboard; + + @override + Widget build(BuildContext context) { + final textColor = color.basedOnLuminance(); + return Tooltip( + message: '\n$clipboard\n(tap to copy to clipboard)\n', + child: HoverButton( + onPressed: () async { + await FlutterClipboard.copy(clipboard); + showCopiedSnackbar(context, clipboard); + }, + cursor: SystemMouseCursors.copy, + builder: (context, states) { + return FocusBorder( + focused: states.isFocused, + useStackApproach: true, + renderOutside: false, + child: Container( + height: 85, + width: 85, + padding: const EdgeInsets.all(6.0), + color: color, + child: Column( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + name, + style: TextStyle( + color: textColor, + fontWeight: FontWeight.w600, + letterSpacing: 0.5, + ), + ), + const Spacer(), + if (variant != null) + Text( + variant!, + style: TextStyle(color: textColor), + ), + ], + ), + ), + ); + }, + ), + ); + } +} diff --git a/launchpad_app/lib/screens/theming/icons.dart b/launchpad_app/lib/screens/theming/icons.dart new file mode 100644 index 00000000..03c9ad9a --- /dev/null +++ b/launchpad_app/lib/screens/theming/icons.dart @@ -0,0 +1,153 @@ +import 'package:clipboard/clipboard.dart'; +import 'package:fluent_ui/fluent_ui.dart'; + +void showCopiedSnackbar(BuildContext context, String copiedText) { + showSnackbar( + context, + Snackbar( + content: RichText( + text: TextSpan( + text: 'Copied ', + style: const TextStyle(color: Colors.white), + children: [ + TextSpan( + text: copiedText, + style: TextStyle( + color: Colors.blue.resolveFromReverseBrightness( + FluentTheme.of(context).brightness, + ), + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + extended: true, + ), + ); +} + +class IconsPage extends StatefulWidget { + const IconsPage({Key? key}) : super(key: key); + + @override + _IconsPageState createState() => _IconsPageState(); +} + +class _IconsPageState extends State { + String filterText = ''; + + @override + Widget build(BuildContext context) { + assert(debugCheckHasFluentTheme(context)); + + final entries = FluentIcons.allIcons.entries.where( + (icon) => + filterText.isEmpty || + // Remove '_' + icon.key + .replaceAll('_', '') + // toLowerCase + .toLowerCase() + .contains(filterText + .toLowerCase() + // Remove spaces + .replaceAll(' ', '')), + ); + + return ScaffoldPage( + header: PageHeader( + title: const Text('Fluent Icons Gallery showcase'), + commandBar: SizedBox( + width: 240.0, + child: Tooltip( + message: 'Filter by name', + child: TextBox( + suffix: const Icon(FluentIcons.search), + placeholder: 'Type to filter icons by name (e.g "logo")', + onChanged: (value) => setState(() { + filterText = value; + }), + ), + ), + ), + ), + bottomBar: const SizedBox( + width: double.infinity, + child: InfoBar( + title: Text('Tip:'), + content: Text( + 'You can click on any icon to copy its name to the clipboard!', + ), + ), + ), + content: GridView.builder( + padding: EdgeInsetsDirectional.only( + start: PageHeader.horizontalPadding(context), + end: PageHeader.horizontalPadding(context), + ), + gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( + maxCrossAxisExtent: 150, + mainAxisSpacing: 10.0, + crossAxisSpacing: 10.0, + ), + itemCount: entries.length, + itemBuilder: (context, index) { + final e = entries.elementAt(index); + return HoverButton( + onPressed: () async { + final copyText = 'FluentIcons.${e.key}'; + await FlutterClipboard.copy(copyText); + showCopiedSnackbar(context, copyText); + }, + cursor: SystemMouseCursors.copy, + builder: (context, states) { + return FocusBorder( + focused: states.isFocused, + renderOutside: false, + child: Tooltip( + useMousePosition: false, + message: + '\nFluentIcons.${e.key}\n(tap to copy to clipboard)\n', + child: RepaintBoundary( + child: AnimatedContainer( + duration: FluentTheme.of(context).fasterAnimationDuration, + decoration: BoxDecoration( + color: ButtonThemeData.uncheckedInputColor( + FluentTheme.of(context), + states, + transparentWhenNone: true, + ), + borderRadius: BorderRadius.circular(20.0), + ), + padding: const EdgeInsets.all(6.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(e.value, size: 40), + Padding( + padding: const EdgeInsetsDirectional.only(top: 8.0), + child: Text( + snakeCasetoSentenceCase(e.key), + textAlign: TextAlign.center, + overflow: TextOverflow.fade, + ), + ), + ], + ), + ), + ), + ), + ); + }, + ); + }, + ), + ); + } + + static String snakeCasetoSentenceCase(String original) { + return '${original[0].toUpperCase()}${original.substring(1)}' + .replaceAll(RegExp(r'(_|-)+'), ' '); + } +} diff --git a/launchpad_app/lib/screens/theming/reveal_focus.dart b/launchpad_app/lib/screens/theming/reveal_focus.dart new file mode 100644 index 00000000..cb98cd0e --- /dev/null +++ b/launchpad_app/lib/screens/theming/reveal_focus.dart @@ -0,0 +1,95 @@ +import 'dart:math'; + +import 'package:example/widgets/card_highlight.dart'; +import 'package:fluent_ui/fluent_ui.dart' hide Page; + +import '../../widgets/page.dart'; + +class RevealFocusPage extends Page { + final FocusNode focus = FocusNode(); + + RevealFocusPage({super.key}); + + @override + Widget build(BuildContext context) { + final theme = FluentTheme.of(context); + return ScaffoldPage.withPadding( + header: PageHeader( + title: const Text('Reveal Focus'), + commandBar: Button( + child: const Text('Focus'), + onPressed: () => focus.requestFocus(), + ), + ), + content: Column( + children: [ + description( + content: const Text( + 'Reveal Focus is a lighting effect for 10-foot experiences, such ' + 'as Xbox One and television screens. It animates the border of ' + 'focusable elements, such as buttons, when the user moves gamepad ' + 'or keyboard focus to them. It\'s turned off by default, but ' + 'it\'s simple to enable.', + ), + ), + subtitle(content: const Text('Enabling reveal focus')), + CardHighlight( + codeSnippet: '''FocusTheme( + data: FocusThemeData( + borderRadius: BorderRaidus.zero, + glowFactor: 4.0, + ), + child: ..., +)''', + child: Center( + child: FocusTheme( + data: FocusThemeData( + borderRadius: BorderRadius.zero, + // glowColor: theme.accentColor.withOpacity(0.2), + glowFactor: 4.0, + primaryBorder: BorderSide( + width: 2.0, + color: theme.inactiveColor, + ), + ), + child: Wrap( + runSpacing: 10.0, + spacing: 10.0, + alignment: WrapAlignment.center, + children: [ + buildCard(focus), + buildCard(), + buildCard(), + buildCard(), + ], + ), + ), + ), + ), + ], + ), + ); + } + + Widget buildCard([FocusNode? node]) { + final color = + Colors.accentColors[Random().nextInt(Colors.accentColors.length - 1)]; + return HoverButton( + focusNode: node, + onPressed: () {}, + builder: (context, states) { + return FocusBorder( + focused: states.isFocused, + useStackApproach: false, + child: Card( + backgroundColor: color, + child: const SizedBox( + width: 50.0, + height: 50.0, + ), + ), + ); + }, + ); + } +} diff --git a/launchpad_app/lib/screens/theming/typography.dart b/launchpad_app/lib/screens/theming/typography.dart new file mode 100644 index 00000000..0298c4da --- /dev/null +++ b/launchpad_app/lib/screens/theming/typography.dart @@ -0,0 +1,143 @@ +import 'package:fluent_ui/fluent_ui.dart'; + +import '../settings.dart'; + +class TypographyPage extends StatefulWidget { + const TypographyPage({Key? key}) : super(key: key); + + @override + _TypographyPageState createState() => _TypographyPageState(); +} + +class _TypographyPageState extends State { + Color? color; + double scale = 1.0; + + Widget buildColorBox(Color color) { + const double boxSize = 25.0; + return Container( + height: boxSize, + width: boxSize, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(4.0), + ), + ); + } + + @override + Widget build(BuildContext context) { + assert(debugCheckHasFluentTheme(context)); + Typography typography = FluentTheme.of(context).typography; + color ??= typography.display!.color; + typography = typography.apply(displayColor: color!); + const Widget spacer = SizedBox(height: 4.0); + return ScaffoldPage.withPadding( + header: PageHeader( + title: const Text('Typography showcase'), + commandBar: SizedBox( + width: 180.0, + child: Tooltip( + message: 'Pick a text color', + child: ComboBox( + placeholder: const Text('Text Color'), + onChanged: (c) => setState(() => color = c), + value: color, + items: [ + ComboBoxItem( + child: Row(children: [ + buildColorBox(Colors.white), + const SizedBox(width: 10.0), + const Text('White'), + ]), + value: Colors.white, + ), + ComboBoxItem( + child: Row(children: [ + buildColorBox(const Color(0xE4000000)), + const SizedBox(width: 10.0), + const Text('Black'), + ]), + value: const Color(0xE4000000), + ), + ...List.generate(Colors.accentColors.length, (index) { + final color = Colors.accentColors[index]; + return ComboBoxItem( + child: Row(children: [ + buildColorBox(color), + const SizedBox(width: 10.0), + Text(accentColorNames[index + 1]), + ]), + value: color, + ); + }), + ], + ), + ), + ), + ), + content: Row(crossAxisAlignment: CrossAxisAlignment.start, children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Divider( + style: DividerThemeData(horizontalMargin: EdgeInsets.zero), + ), + const SizedBox(height: 4.0), + const Text( + 'The Windows type ramp establishes crucial relationships ' + 'between the type styles on a page, helping users read content ' + 'easily.', + ), + Expanded( + child: ListView(children: [ + Text('Display', + style: typography.display?.apply(fontSizeFactor: scale)), + spacer, + Text('Title Large', + style: + typography.titleLarge?.apply(fontSizeFactor: scale)), + spacer, + Text('Title', + style: typography.title?.apply(fontSizeFactor: scale)), + spacer, + Text('Subtitle', + style: typography.subtitle?.apply(fontSizeFactor: scale)), + spacer, + Text('Body Large', + style: + typography.bodyLarge?.apply(fontSizeFactor: scale)), + spacer, + Text('Body Strong', + style: + typography.bodyStrong?.apply(fontSizeFactor: scale)), + spacer, + Text('Body', + style: typography.body?.apply(fontSizeFactor: scale)), + spacer, + Text('Caption', + style: typography.caption?.apply(fontSizeFactor: scale)), + spacer, + ]), + ), + ], + ), + ), + Semantics( + label: 'Scale', + child: Slider( + vertical: true, + value: scale, + onChanged: (v) => setState(() => scale = v), + label: scale.toStringAsFixed(2), + max: 2, + min: 0.5, + // style: SliderThemeData(useThumbBall: false), + ), + ), + ]), + ); + } +} From d97c2b972a9e5f3d971f6373a25732f486fd1e56 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sat, 17 Dec 2022 20:14:57 -0500 Subject: [PATCH 147/227] Update the menu, logo, and clean up the initial dashboard a bit --- .../assets/graphics/light/logo_square.svg | 1 + .../assets/graphics/light/logo_wide.svg | 196 +++++++++++++ launchpad_app/assets/graphics/logo_square.svg | 1 + launchpad_app/assets/graphics/logo_wide.svg | 206 ++++++++++++++ launchpad_app/lib/main.dart | 269 ++++-------------- launchpad_app/lib/screens/home.dart | 113 +------- launchpad_app/pubspec.lock | 21 ++ launchpad_app/pubspec.yaml | 3 + 8 files changed, 491 insertions(+), 319 deletions(-) create mode 100644 launchpad_app/assets/graphics/light/logo_square.svg create mode 100644 launchpad_app/assets/graphics/light/logo_wide.svg create mode 100644 launchpad_app/assets/graphics/logo_square.svg create mode 100644 launchpad_app/assets/graphics/logo_wide.svg diff --git a/launchpad_app/assets/graphics/light/logo_square.svg b/launchpad_app/assets/graphics/light/logo_square.svg new file mode 100644 index 00000000..7a77eb9c --- /dev/null +++ b/launchpad_app/assets/graphics/light/logo_square.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launchpad_app/assets/graphics/light/logo_wide.svg b/launchpad_app/assets/graphics/light/logo_wide.svg new file mode 100644 index 00000000..0f8d4a55 --- /dev/null +++ b/launchpad_app/assets/graphics/light/logo_wide.svg @@ -0,0 +1,196 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launchpad_app/assets/graphics/logo_square.svg b/launchpad_app/assets/graphics/logo_square.svg new file mode 100644 index 00000000..34697f48 --- /dev/null +++ b/launchpad_app/assets/graphics/logo_square.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/launchpad_app/assets/graphics/logo_wide.svg b/launchpad_app/assets/graphics/logo_wide.svg new file mode 100644 index 00000000..df103d41 --- /dev/null +++ b/launchpad_app/assets/graphics/logo_wide.svg @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/launchpad_app/lib/main.dart b/launchpad_app/lib/main.dart index a15ccf03..b32cd964 100644 --- a/launchpad_app/lib/main.dart +++ b/launchpad_app/lib/main.dart @@ -1,6 +1,7 @@ import 'package:fluent_ui/fluent_ui.dart' hide Page; import 'package:flutter/foundation.dart'; import 'package:flutter_acrylic/flutter_acrylic.dart' as flutter_acrylic; +import 'package:flutter_svg/flutter_svg.dart'; import 'package:provider/provider.dart'; import 'package:system_theme/system_theme.dart'; import 'package:url_launcher/link.dart'; @@ -10,16 +11,12 @@ import 'package:window_manager/window_manager.dart'; import 'screens/home.dart'; import 'screens/settings.dart'; -import 'routes/forms.dart' deferred as forms; import 'routes/inputs.dart' deferred as inputs; -import 'routes/navigation.dart' deferred as navigation; -import 'routes/surfaces.dart' deferred as surfaces; -import 'routes/theming.dart' deferred as theming; import 'theme.dart'; import 'widgets/deferred_widget.dart'; -const String appTitle = 'Fluent UI Showcase for Flutter'; +const String appTitle = 'Launchpad - Game Launching Multitool'; /// Checks if the current environment is a desktop environment. bool get isDesktop { @@ -53,8 +50,8 @@ void main() async { TitleBarStyle.hidden, windowButtonVisibility: false, ); - await windowManager.setSize(const Size(755, 545)); - await windowManager.setMinimumSize(const Size(350, 600)); + await windowManager.setSize(const Size(900, 600)); + await windowManager.setMinimumSize(const Size(400, 400)); await windowManager.center(); await windowManager.show(); await windowManager.setPreventClose(true); @@ -64,11 +61,7 @@ void main() async { runApp(const MyApp()); - DeferredWidget.preload(forms.loadLibrary); DeferredWidget.preload(inputs.loadLibrary); - DeferredWidget.preload(navigation.loadLibrary); - DeferredWidget.preload(surfaces.loadLibrary); - DeferredWidget.preload(theming.loadLibrary); } class MyApp extends StatelessWidget { @@ -147,209 +140,76 @@ class _MyHomePageState extends State with WindowListener { title: const Text('Home'), body: const HomePage(), ), - PaneItemHeader(header: const Text('Inputs')), - PaneItem( - icon: const Icon(FluentIcons.button_control), - title: const Text('Button'), - body: DeferredWidget( - inputs.loadLibrary, - () => inputs.ButtonPage(), - ), - ), - PaneItem( - icon: const Icon(FluentIcons.checkbox_composite), - title: const Text('Checkbox'), - body: DeferredWidget( - inputs.loadLibrary, - () => inputs.CheckBoxPage(), - ), - ), - PaneItem( - icon: const Icon(FluentIcons.slider), - title: const Text('Slider'), - body: DeferredWidget( - inputs.loadLibrary, - () => inputs.SliderPage(), - ), - ), - PaneItem( - icon: const Icon(FluentIcons.toggle_left), - title: const Text('ToggleSwitch'), - body: DeferredWidget( - inputs.loadLibrary, - () => inputs.ToggleSwitchPage(), - ), - ), - PaneItemHeader(header: const Text('Form')), - PaneItem( - icon: const Icon(FluentIcons.text_field), - title: const Text('TextBox'), - body: DeferredWidget( - forms.loadLibrary, - () => forms.TextBoxPage(), - ), - ), - PaneItem( - icon: const Icon(FluentIcons.page_list), - title: const Text('AutoSuggestBox'), - body: DeferredWidget( - forms.loadLibrary, - () => forms.AutoSuggestBoxPage(), - ), - ), - PaneItem( - icon: const Icon(FluentIcons.combobox), - title: const Text('ComboBox'), - body: DeferredWidget( - forms.loadLibrary, - () => forms.ComboBoxPage(), - ), - ), - PaneItem( - icon: const Icon(FluentIcons.time_picker), - title: const Text('TimePicker'), - body: DeferredWidget( - forms.loadLibrary, - () => forms.TimePickerPage(), - ), - ), - PaneItem( - icon: const Icon(FluentIcons.date_time), - title: const Text('DatePicker'), - body: DeferredWidget( - forms.loadLibrary, - () => forms.DatePickerPage(), - ), - ), - PaneItemHeader(header: const Text('Navigation')), + PaneItemHeader(header: const Icon(FluentIcons.game)), + // TODO Add links to the most recent 5 games edited, launched, or built PaneItem( - icon: const Icon(FluentIcons.navigation_flipper), - title: const Text('NavigationView'), - body: DeferredWidget( - navigation.loadLibrary, - () => navigation.NavigationViewPage(), - ), - ), - PaneItem( - icon: const Icon(FluentIcons.table_header_row), - title: const Text('TabView'), - body: DeferredWidget( - navigation.loadLibrary, - () => navigation.TabViewPage(), - ), - ), - PaneItem( - icon: const Icon(FluentIcons.bulleted_tree_list), - title: const Text('TreeView'), - body: DeferredWidget( - navigation.loadLibrary, - () => navigation.TreeViewPage(), - ), - ), - PaneItemHeader(header: const Text('Surfaces')), - PaneItem( - icon: const Icon(FluentIcons.un_set_color), - title: const Text('Acrylic'), - body: DeferredWidget( - surfaces.loadLibrary, - () => surfaces.AcrylicPage(), - ), + icon: const Icon(FluentIcons.game), + title: const Text('All Games'), + body: const Text('To be replaced with a page widget.'), ), PaneItem( - icon: const Icon(FluentIcons.customize_toolbar), - title: const Text('CommandBar'), - body: DeferredWidget( - surfaces.loadLibrary, - () => surfaces.CommandBarsPage(), - ), + icon: const Icon(FluentIcons.add), + title: const Text('Add Game'), + body: const Text('To be replaced with a page widget.'), ), PaneItem( - icon: const Icon(FluentIcons.comment_urgent), - title: const Text('ContentDialog'), - body: DeferredWidget( - surfaces.loadLibrary, - () => surfaces.ContentDialogPage(), - ), - ), - PaneItem( - icon: const Icon(FluentIcons.expand_all), - title: const Text('Expander'), - body: DeferredWidget( - surfaces.loadLibrary, - () => surfaces.ExpanderPage(), - ), + icon: const Icon(FluentIcons.search), + title: const Text('Find Installed Games'), + body: const Text('To be replaced with a page widget.'), ), + PaneItemHeader(header: const Icon(FluentIcons.library)), + // TODO Add links to all enabled and installed game platforms here PaneItem( - icon: const Icon(FluentIcons.info_solid), - title: const Text('InfoBar'), - body: DeferredWidget( - surfaces.loadLibrary, - () => surfaces.InfoBarsPage(), - ), + icon: const Icon(FluentIcons.library), + title: const Text('All Platforms'), + body: const Text('Show manage list for platform entities.'), ), PaneItem( - icon: const Icon(FluentIcons.progress_ring_dots), - title: const Text('Progress Indicators'), - body: DeferredWidget( - surfaces.loadLibrary, - () => surfaces.ProgressIndicatorsPage(), - ), + icon: const Icon(FluentIcons.download), + title: const Text('Find More Platforms'), + body: const Text('Show manage list for platform entities.'), ), + PaneItemHeader(header: const Icon(FluentIcons.archive)), PaneItem( - icon: const Icon(FluentIcons.tiles), - title: const Text('Tiles'), - body: DeferredWidget( - surfaces.loadLibrary, - () => surfaces.TilesPage(), - ), + icon: const Icon(FluentIcons.archive), + title: const Text('Backups'), + body: const Text('.'), ), + PaneItemHeader(header: const Icon(FluentIcons.cloud)), + // TODO Add links to all enabled web services here PaneItem( - icon: const Icon(FluentIcons.hint_text), - title: const Text('Tooltip'), - body: DeferredWidget( - surfaces.loadLibrary, - () => surfaces.TooltipPage(), - ), + icon: const Icon(FluentIcons.cloud), + title: const Text('All Services'), + body: const Text('Show manage list for platform entities.'), ), PaneItem( - icon: const Icon(FluentIcons.pop_expand), - title: const Text('Flyout'), - body: DeferredWidget( - surfaces.loadLibrary, - () => surfaces.FlyoutPage(), - ), + icon: const Icon(FluentIcons.accounts), + title: const Text('Add Service'), + body: const Text('.'), ), - PaneItemHeader(header: const Text('Theming')), PaneItem( - icon: const Icon(FluentIcons.color_solid), - title: const Text('Colors'), - body: DeferredWidget( - theming.loadLibrary, - () => theming.ColorsPage(), - ), + icon: const Icon(FluentIcons.download), + title: const Text('Find More Services'), + body: const Text('.'), ), + PaneItemHeader(header: const Icon(FluentIcons.puzzle)), PaneItem( - icon: const Icon(FluentIcons.font_color_a), - title: const Text('Typography'), - body: DeferredWidget( - theming.loadLibrary, - () => theming.TypographyPage(), - ), + icon: const Icon(FluentIcons.puzzle), + title: const Text('Modules'), + body: const Text('.'), ), PaneItem( - icon: const Icon(FluentIcons.icon_sets_flag), - title: const Text('Icons'), - body: DeferredWidget( - theming.loadLibrary, - () => theming.IconsPage(), - ), + icon: const Icon(FluentIcons.download), + title: const Text('Extend Launchpad'), + body: const Text('.'), ), + PaneItemHeader(header: const Text('Inputs')), PaneItem( - icon: const Icon(FluentIcons.focus), - title: const Text('Reveal Focus'), + icon: const Icon(FluentIcons.button_control), + title: const Text('Button'), body: DeferredWidget( - theming.loadLibrary, - () => theming.RevealFocusPage(), + inputs.loadLibrary, + () => inputs.ButtonPage(), ), ), ]; @@ -363,10 +223,9 @@ class _MyHomePageState extends State with WindowListener { _LinkPaneItemAction( icon: const Icon(FluentIcons.open_source), title: const Text('Source code'), - link: 'https://github.com/bdlukaa/fluent_ui', + link: 'https://github.com/VolantisDev/Launchpad/tree/flutter', body: const SizedBox.shrink(), ), - // TODO: mobile widgets, Scrollbar, BottomNavigationBar, RatingBar ]; @override @@ -429,26 +288,12 @@ class _MyHomePageState extends State with WindowListener { setState(() => index = i); }, header: SizedBox( - height: kOneLineTileHeight, - child: ShaderMask( - shaderCallback: (rect) { - final color = appTheme.color.resolveFromReverseBrightness( - theme.brightness, - level: theme.brightness == Brightness.light ? 0 : 2, - ); - return LinearGradient( - colors: [ - color, - color, - ], - ).createShader(rect); - }, - child: const FlutterLogo( - style: FlutterLogoStyle.horizontal, - size: 80.0, - textColor: Colors.white, - duration: Duration.zero, - ), + height: kOneLineTileHeight + 5, + child: SvgPicture.asset( + FluentTheme.of(context).brightness.isDark + ? 'assets/graphics/logo_wide.svg' + : 'assets/graphics/light/logo_wide.svg', + semanticsLabel: 'Launchpad', ), ), displayMode: appTheme.displayMode, diff --git a/launchpad_app/lib/screens/home.dart b/launchpad_app/lib/screens/home.dart index 3cfd3fba..2349fbd3 100644 --- a/launchpad_app/lib/screens/home.dart +++ b/launchpad_app/lib/screens/home.dart @@ -25,10 +25,11 @@ class _HomePageState extends State with PageMixin { return ScaffoldPage.scrollable( header: PageHeader( - title: const Text('Fluent UI for Flutter Showcase App'), + title: const Text('Your Game Launching Multitool'), commandBar: Row(mainAxisAlignment: MainAxisAlignment.end, children: [ Link( - uri: Uri.parse('https://github.com/bdlukaa/fluent_ui'), + uri: Uri.parse( + 'https://github.com/VolantisDev/Launchpad/tree/flutter'), builder: (context, open) => Tooltip( message: 'Source code', child: IconButton( @@ -40,106 +41,6 @@ class _HomePageState extends State with PageMixin { ]), ), children: [ - Card( - child: - Wrap(alignment: WrapAlignment.center, spacing: 10.0, children: [ - InfoLabel( - label: 'Inputs', - child: ToggleSwitch( - checked: selected, - onChanged: (v) => setState(() => selected = v), - ), - ), - SizedBox( - width: 100, - child: InfoLabel( - label: 'Forms', - child: ComboBox( - value: comboboxValue, - items: ['Item 1', 'Item 2'] - .map((e) => ComboBoxItem( - child: Text(e), - value: e, - )) - .toList(), - isExpanded: true, - onChanged: (v) => setState(() => comboboxValue = v), - ), - ), - ), - RepaintBoundary( - child: Padding( - padding: const EdgeInsetsDirectional.only(start: 4.0), - child: InfoLabel( - label: 'Progress', - child: const SizedBox( - height: 30, width: 30, child: ProgressRing()), - ), - ), - ), - InfoLabel( - label: 'Surfaces & Materials', - child: SizedBox( - height: 40, - width: 120, - child: Stack(children: [ - Container( - width: 120, - height: 50, - color: theme.accentColor.lightest, - ), - const Positioned.fill(child: Acrylic(luminosityAlpha: 0.5)), - ]), - ), - ), - InfoLabel( - label: 'Icons', - child: const Icon(FluentIcons.graph_symbol, size: 30.0), - ), - InfoLabel( - label: 'Colors', - child: SizedBox( - width: 40, - height: 30, - child: Wrap( - children: [ - ...Colors.accentColors, - Colors.successPrimaryColor, - Colors.warningPrimaryColor, - Colors.errorPrimaryColor, - Colors.grey, - ].map((color) { - return Container( - height: 10, - width: 10, - color: color, - ); - }).toList(), - ), - ), - ), - InfoLabel( - label: 'Typography', - child: ShaderMask( - shaderCallback: (rect) { - return LinearGradient( - colors: [ - Colors.white, - ...Colors.accentColors, - ], - ).createShader(rect); - }, - blendMode: BlendMode.srcATop, - child: const Text( - 'ABCDEFGH', - style: TextStyle(fontSize: 24, shadows: [ - Shadow(offset: Offset(1, 1)), - ]), - ), - ), - ), - ]), - ), const SizedBox(height: 22.0), IconButton( onPressed: () { @@ -153,13 +54,13 @@ class _HomePageState extends State with PageMixin { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - 'What\'s new on 4.0.0', + 'What\'s new in Launchpad 10', style: theme.typography.body ?.copyWith(fontWeight: FontWeight.bold), ), - Text('June 21, 2022', style: theme.typography.caption), + Text('Dec 17, 2022', style: theme.typography.caption), Text( - 'A native look-and-feel out of the box', + 'A brand new desktop app!', style: theme.typography.bodyLarge, ), ], @@ -219,8 +120,6 @@ class _HomePageState extends State with PageMixin { ), ], ), - subtitle(content: const Text('Equivalents with the material library')), - const MaterialEquivalents(), ], ); } diff --git a/launchpad_app/pubspec.lock b/launchpad_app/pubspec.lock index c1e8799f..5981d017 100644 --- a/launchpad_app/pubspec.lock +++ b/launchpad_app/pubspec.lock @@ -242,6 +242,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.16" + flutter_svg: + dependency: "direct main" + description: + name: flutter_svg + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" flutter_syntax_view: dependency: "direct main" description: @@ -415,6 +422,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.2" + path_drawing: + dependency: transitive + description: + name: path_drawing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" + path_parsing: + dependency: transitive + description: + name: path_parsing + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" path_provider: dependency: "direct main" description: diff --git a/launchpad_app/pubspec.yaml b/launchpad_app/pubspec.yaml index ea3a7731..a984f591 100644 --- a/launchpad_app/pubspec.yaml +++ b/launchpad_app/pubspec.yaml @@ -36,6 +36,7 @@ dependencies: characters: ^1.2.1 network_info_plus: ^3.0.1 flutter_native_splash: ^2.2.16 + flutter_svg: ^1.1.6 dev_dependencies: flutter_test: @@ -44,6 +45,8 @@ dev_dependencies: flutter: uses-material-design: true + assets: + - assets/graphics/logo_wide.svg flutter_native_splash: color: "#42a5f5" From b2ba85d0a1e34eba10663a14a01a40917a9951e7 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sun, 18 Dec 2022 19:54:13 -0500 Subject: [PATCH 148/227] Update launch and tasks --- .vscode/launch.json | 46 +++++++-------- .vscode/tasks.json | 141 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 149 insertions(+), 38 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index ff2049ef..944da2da 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,29 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "name": "Launchpad (app)", + "cwd": "launchpad_app", + "program": "lib/main.dart", + "request": "launch", + "type": "dart" + }, + { + "name": "Launchpad (app profile mode)", + "cwd": "launchpad_app", + "program": "lib/main.dart", + "request": "launch", + "type": "dart", + "flutterMode": "profile" + }, + { + "name": "Launchpad (app release mode)", + "cwd": "launchpad_app", + "program": "lib/main.dart", + "request": "launch", + "type": "dart", + "flutterMode": "release" + }, { "name": "Launchpad (Debug)", "type": "autohotkey", @@ -31,29 +54,6 @@ "console": "internalConsole", "stopAtEntry": false, "internalConsoleOptions": "openOnSessionStart" - }, - { - "name": "Launchpad (app)", - "cwd": "launchpad_app", - "program": "lib/main.dart", - "request": "launch", - "type": "dart" - }, - { - "name": "Launchpad (app profile mode)", - "cwd": "launchpad_app", - "program": "lib/main.dart", - "request": "launch", - "type": "dart", - "flutterMode": "profile" - }, - { - "name": "Launchpad (app release mode)", - "cwd": "launchpad_app", - "program": "lib/main.dart", - "request": "launch", - "type": "dart", - "flutterMode": "release" } ] } diff --git a/.vscode/tasks.json b/.vscode/tasks.json index fb00b155..0b848d80 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,28 +2,125 @@ "version": "2.0.0", "tasks": [ { - "label": "update-includes", - "command": "Vendor/AutoHotKey/AutoHotkey64.exe", - "type": "process", + "label": "flutter: flutter pub run build_runner watch", + "type": "flutter", + "command": "flutter", "args": [ - "Scripts/UpdateIncludes.ahk" + "pub", + "run", + "build_runner", + "watch", + "--delete-conflicting-outputs" + ], + "presentation": { + "reveal": "silent", + "panel": "dedicated", + "close": true + }, + "problemMatcher": [ + "$dart-build_runner" ], + "group": { + "kind": "build", + "isDefault": true + }, "options": { - "cwd": "${workspaceFolder}" + "cwd": "${workspaceFolder}/launchpad_app" }, - "problemMatcher": [] + "isBackground": true, + "detail": "", + "runOptions": { + "runOn": "folderOpen" + } }, { - "label": "build-launchpad", - "command": "Vendor/AutoHotKey/AutoHotkey64.exe", + "label": "flutter: flutter pub run build_runner build", + "type": "flutter", + "command": "flutter", + "args": [ + "pub", + "run", + "build_runner", + "build", + "--delete-conflicting-outputs" + ], + "presentation": { + "reveal": "always", + "panel": "shared", + "showReuseMessage": true + }, + "problemMatcher": [ + "$dart-build_runner" + ], + "options": { + "cwd": "${workspaceFolder}/launchpad_app" + }, + "group": "none", + "detail": "" + }, + { + "label": "flutter pub get", + "type": "flutter", + "command": "flutter", + "args": [ + "pub", + "get" + ], + "presentation": { + "echo": true, + "reveal": "silent", + "focus": false, + "panel": "shared", + "showReuseMessage": true, + "clear": false + }, + "options": { + "cwd": "${workspaceFolder}/launchpad_app" + }, + "group": "none", + "detail": "" + }, + { + "label": "flutter-launcher-icons", + "type": "flutter", + "command": "flutter", + "args": [ + "pub", + "run", + "flutter_launcher_icons" + ], + "group": "build", + "options": { + "cwd": "${workspaceFolder}/launchpad_app" + } + }, + { + "label": "flutter-native-splash", + "type": "flutter", + "command": "flutter", + "args": [ + "pub", + "run", + "flutter_native_splash:create" + ], + "group": "build", + "options": { + "cwd": "${workspaceFolder}/launchpad_app" + } + }, + { + "label": "build-overlay", + "command": "msbuild", "type": "process", "args": [ - "Scripts/Build.ahk" + "Launchpad.sln", + "-property:Configuration=Release" ], "options": { "cwd": "${workspaceFolder}" }, - "problemMatcher": [] + "group": "build", + "problemMatcher": "$tsc" }, { "label": "build-overlay-debug", @@ -36,20 +133,34 @@ "options": { "cwd": "${workspaceFolder}" }, + "group": "none", "problemMatcher": "$tsc" }, { - "label": "build-overlay", - "command": "msbuild", + "label": "update-includes (AHK)", + "command": "Vendor/AutoHotKey/AutoHotkey64.exe", "type": "process", "args": [ - "Launchpad.sln", - "-property:Configuration=Release" + "Scripts/UpdateIncludes.ahk" ], "options": { "cwd": "${workspaceFolder}" }, - "problemMatcher": "$tsc" + "group": "build", + "problemMatcher": [] + }, + { + "label": "build-launchpad (AHK)", + "command": "Vendor/AutoHotKey/AutoHotkey64.exe", + "type": "process", + "args": [ + "Scripts/Build.ahk" + ], + "options": { + "cwd": "${workspaceFolder}" + }, + "group": "build", + "problemMatcher": [] } ] } From 3f752b7ed4a603f5902a4567c9d0624d10e19c55 Mon Sep 17 00:00:00 2001 From: Ben McClure Date: Sun, 18 Dec 2022 19:56:08 -0500 Subject: [PATCH 149/227] Random app setup stuff --- launchpad_app/analysis_options.yaml | 6 +- launchpad_app/assets/graphics/icon.png | Bin 0 -> 8038 bytes launchpad_app/assets/graphics/icon.svg | 58 + .../assets/graphics/light/splash.png | Bin 0 -> 18220 bytes .../assets/graphics/light/splash.svg | 196 ++ launchpad_app/assets/graphics/splash.png | Bin 0 -> 17833 bytes launchpad_app/assets/graphics/splash.svg | 196 ++ launchpad_app/build.yaml | 5 + launchpad_app/lib/gen/assets.gen.dart | 97 + launchpad_app/lib/main.dart | 20 +- .../lib/screens/forms/auto_suggest_box.dart | 4 +- launchpad_app/lib/screens/forms/combobox.dart | 4 +- .../lib/screens/forms/date_picker.dart | 4 +- launchpad_app/lib/screens/forms/text_box.dart | 4 +- .../lib/screens/forms/time_picker.dart | 4 +- launchpad_app/lib/screens/inputs/button.dart | 2 +- .../lib/screens/inputs/checkbox.dart | 4 +- launchpad_app/lib/screens/inputs/slider.dart | 4 +- .../lib/screens/inputs/toggle_switch.dart | 4 +- .../screens/navigation/navigation_view.dart | 2 +- .../lib/screens/navigation/tab_view.dart | 4 +- .../lib/screens/navigation/tree_view.dart | 4 +- .../lib/screens/surface/acrylic.dart | 2 +- .../lib/screens/surface/content_dialog.dart | 4 +- .../lib/screens/surface/expander.dart | 2 +- .../lib/screens/surface/flyouts.dart | 2 +- .../lib/screens/surface/info_bars.dart | 2 +- .../screens/surface/progress_indicators.dart | 2 +- launchpad_app/lib/screens/surface/tiles.dart | 4 +- .../lib/screens/surface/tooltip.dart | 4 +- .../lib/screens/theming/reveal_focus.dart | 2 +- launchpad_app/lib/widgets/page.dart | 2 +- launchpad_app/pubspec.lock | 1573 +++++++++++++++-- launchpad_app/pubspec.yaml | 149 +- launchpad_app/test/widget_test.dart | 2 +- 35 files changed, 2099 insertions(+), 273 deletions(-) create mode 100644 launchpad_app/assets/graphics/icon.png create mode 100644 launchpad_app/assets/graphics/icon.svg create mode 100644 launchpad_app/assets/graphics/light/splash.png create mode 100644 launchpad_app/assets/graphics/light/splash.svg create mode 100644 launchpad_app/assets/graphics/splash.png create mode 100644 launchpad_app/assets/graphics/splash.svg create mode 100644 launchpad_app/build.yaml create mode 100644 launchpad_app/lib/gen/assets.gen.dart diff --git a/launchpad_app/analysis_options.yaml b/launchpad_app/analysis_options.yaml index a3be6b82..e07e9e94 100644 --- a/launchpad_app/analysis_options.yaml +++ b/launchpad_app/analysis_options.yaml @@ -1 +1,5 @@ -include: package:flutter_lints/flutter.yaml \ No newline at end of file +include: package:flutter_lints/flutter.yaml + +analyzer: + errors: + invalid_annotation_target: ignore diff --git a/launchpad_app/assets/graphics/icon.png b/launchpad_app/assets/graphics/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..99f674ffbbcd2bdb15e9072acb369020d5e79903 GIT binary patch literal 8038 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 V5SrD~HyDi^P*>K0*DKmY{12#u_z?gA literal 0 HcmV?d00001 diff --git a/launchpad_app/assets/graphics/icon.svg b/launchpad_app/assets/graphics/icon.svg new file mode 100644 index 00000000..76d36c8a --- /dev/null +++ b/launchpad_app/assets/graphics/icon.svg @@ -0,0 +1,58 @@ + + + + + + + + + + + + + diff --git a/launchpad_app/assets/graphics/light/splash.png b/launchpad_app/assets/graphics/light/splash.png new file mode 100644 index 0000000000000000000000000000000000000000..ac413759c11d6df6db8b7d466af4e7a3a880a07e GIT binary patch literal 18220 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