diff --git a/data/artwork/icons/HaikuDepot_StarBlue b/data/artwork/icons/HaikuDepot_StarBlue new file mode 100644 index 0000000000..bcb5e41bcf Binary files /dev/null and b/data/artwork/icons/HaikuDepot_StarBlue differ diff --git a/data/artwork/icons/HaikuDepot_StarGray b/data/artwork/icons/HaikuDepot_StarGray new file mode 100644 index 0000000000..8ee6cb57cf Binary files /dev/null and b/data/artwork/icons/HaikuDepot_StarGray differ diff --git a/src/apps/haikudepot/HaikuDepot.rdef b/src/apps/haikudepot/HaikuDepot.rdef index e69fcb0688..35897fec87 100644 --- a/src/apps/haikudepot/HaikuDepot.rdef +++ b/src/apps/haikudepot/HaikuDepot.rdef @@ -65,32 +65,23 @@ resource vector_icon { $"010C00" }; -resource(501, "star") #'VICN' array { - $"6E636966010300AAFF010A0AB43438BC6638BEF3B4C9C18338C9B238C311C165" - $"C59EC928BEF3C457B84FC928BAD5C165010A00010000" +resource(510, "starblue") #'VICN' array { + $"6E636966010300AAFF010A0A40223A38213835432C5A4049545A4B435F384638" + $"010A00010000" }; -resource(502, "thumbs up") #'VICN' array { - $"6E63696601020016020000003DC000BD80000000004B30004960000056FF4A01" - $"060DF6FFFF0358404C4E4C524CC3AF4C3E5046503A50364EBC65C5F5344C3041" - $"3046303F383C343C3C3C3F3A3F3A3F383C323CBB103C303E2C3C2C402C403040" - $"2E40344638C16DBB23483C50404C405440010A00010000" +resource(520, "stargray") #'VICN' array { + $"6E6369660105AB010A0A40223A38213835432C5A4049545A4B435F384638010A" + $"00010000" }; -resource(503, "thumbs down") #'VICN' array { - $"6E63696601020016020000003DC000BD80000000004B30004960000056FF4A01" - $"060DF6FFFF035440344A344E34C217343A30423036303232BACDB98A30342C3F" - $"2C3A2C413444304438443B463B463B48384E38C46F38503A5438543C543C503C" - $"523C4C4248BFD5C45C44444C4048405040010A00010000" -}; - -resource(504, "installed") #'VICN' array { +resource(530, "installed") #'VICN' array { $"6E636966030369D90504011704011F0202043E24C4AC24B93B24243E24B93B24" $"C4AC3E58B93B58C4AC58583E58C4AC58B93B0A06333A3C42493051363E4F2D41" $"010A0002000100" }; -resource(505, "arrow left") #'VICN' array { +resource(540, "arrow left") #'VICN' array { $"6E6369660304006603005900020006020000003C6000C000000000004C000048" $"A0000080FF80FF00B300010A0722353622362C482C483E363E3648030A000100" $"1240A32D00000000000040A32444CEA044D04B01178322040A0101001240A32D" @@ -98,7 +89,7 @@ resource(505, "arrow left") #'VICN' array { $"000040A32442FA1242FD72" }; -resource(506, "arrow right") #'VICN' array { +resource(550, "arrow right") #'VICN' array { $"6E6369660304006603005900020006020000003C6000C000000000004C000048" $"A0000080FF80FF00B300010A0748353448343E223E222C342C3422030A000100" $"1240A32D00000000000040A32444D19644D04B01178322040A0101001240A32D" diff --git a/src/apps/haikudepot/HaikuDepotConstants.h b/src/apps/haikudepot/HaikuDepotConstants.h index 7ca466b908..a3346ef56d 100644 --- a/src/apps/haikudepot/HaikuDepotConstants.h +++ b/src/apps/haikudepot/HaikuDepotConstants.h @@ -2,7 +2,8 @@ * Copyright 2018, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ - +#ifndef HAIKU_DEPOT_CONSTANTS_H +#define HAIKU_DEPOT_CONSTANTS_H enum { MSG_MAIN_WINDOW_CLOSED = 'mwcl', @@ -12,11 +13,32 @@ enum { MSG_ADD_VISIBLE_PACKAGES = 'avpk', MSG_UPDATE_SELECTED_PACKAGE = 'uspk', MSG_CLIENT_TOO_OLD = 'oldc', + MSG_NETWORK_TRANSPORT_ERROR = 'nett', + MSG_SERVER_ERROR = 'svre', + MSG_SERVER_DATA_CHANGED = 'svdc', + MSG_DID_ADD_USER_RATING = 'adur', + MSG_DID_UPDATE_USER_RATING = 'upur' }; +#define RATING_MISSING -1.0f +#define RATING_MIN 0.0f + + #define HD_ERROR_BASE (B_ERRORS_END + 1) #define HD_NETWORK_INACCESSIBLE (HD_ERROR_BASE + 1) #define HD_CLIENT_TOO_OLD (HD_ERROR_BASE + 2) #define HD_ERR_NOT_MODIFIED (HD_ERROR_BASE + 3) -#define HD_ERR_NO_DATA (HD_ERROR_BASE + 4) \ No newline at end of file +#define HD_ERR_NO_DATA (HD_ERROR_BASE + 4) + + +// These constants reference resources in 'HaikuDepot.ref' +enum { + RSRC_STAR_BLUE = 510, + RSRC_STAR_GREY = 520, + RSRC_INSTALLED = 530, + RSRC_ARROW_LEFT = 540, + RSRC_ARROW_RIGHT = 550, +}; + +#endif // HAIKU_DEPOT_CONSTANTS_H \ No newline at end of file diff --git a/src/apps/haikudepot/model/Model.cpp b/src/apps/haikudepot/model/Model.cpp index eb8b4ecb5a..d5803c07cc 100644 --- a/src/apps/haikudepot/model/Model.cpp +++ b/src/apps/haikudepot/model/Model.cpp @@ -651,6 +651,11 @@ Model::SetShowDevelopPackages(bool show) // #pragma mark - information retrieval +/*! Initially only superficial data is loaded from the server into the data + model of the packages. When the package is viewed, additional data needs + to be populated including ratings. This method takes care of that. +*/ + void Model::PopulatePackage(const PackageInfoRef& package, uint32 flags) { @@ -697,7 +702,7 @@ Model::PopulatePackage(const PackageInfoRef& package, uint32 flags) BAutolock locker(&fLock); package->ClearUserRatings(); - int index = 0; + int32 index = 0; while (true) { BString name; name << index++; @@ -706,11 +711,19 @@ Model::PopulatePackage(const PackageInfoRef& package, uint32 flags) if (items.FindMessage(name, &item) != B_OK) break; + BString code; + if (item.FindString("code", &code) != B_OK) { + printf("corrupt user rating at index %" B_PRIi32 "\n", + index); + continue; + } + BString user; BMessage userInfo; if (item.FindMessage("user", &userInfo) != B_OK || userInfo.FindString("nickname", &user) != B_OK) { - // Ignore, we need the user name + printf("ignored user rating [%s] without a user " + "nickname\n", code.String()); continue; } @@ -723,7 +736,8 @@ Model::PopulatePackage(const PackageInfoRef& package, uint32 flags) if (item.FindDouble("rating", &rating) != B_OK) rating = -1; if (comment.Length() == 0 && rating == -1) { - // No useful information given. + printf("rating [%s] has no comment or rating so will be" + "ignored\n", code.String()); continue; } @@ -731,11 +745,13 @@ Model::PopulatePackage(const PackageInfoRef& package, uint32 flags) BString major = "?"; BString minor = "?"; BString micro = ""; + double revision = -1; BMessage version; if (item.FindMessage("pkgVersion", &version) == B_OK) { version.FindString("major", &major); version.FindString("minor", &minor); version.FindString("micro", µ); + version.FindDouble("revision", &revision); } BString versionString = major; versionString << "."; @@ -744,15 +760,43 @@ Model::PopulatePackage(const PackageInfoRef& package, uint32 flags) versionString << "."; versionString << micro; } + if (revision > 0) { + versionString << "-"; + versionString << (int) revision; + } + + BDateTime createTimestamp; + double createTimestampMillisF; + if (item.FindDouble("createTimestamp", + &createTimestampMillisF) == B_OK) { + double createTimestampSecsF = + createTimestampMillisF / 1000.0; + time_t createTimestampSecs = + (time_t) createTimestampSecsF; + createTimestamp.SetTime_t(createTimestampSecs); + } + // Add the rating to the PackageInfo - package->AddUserRating( - UserRating(UserInfo(user), rating, - comment, languageCode, versionString, 0, 0) - ); + UserRating userRating = UserRating(UserInfo(user), rating, + comment, languageCode, versionString, 0, 0, + createTimestamp); + package->AddUserRating(userRating); + + if (Logger::IsDebugEnabled()) { + printf("rating [%s] retrieved from server\n", + code.String()); + } + } + + if (Logger::IsDebugEnabled()) { + printf("did retrieve %" B_PRIi32 " user ratings for [%s]\n", + index - 1, packageName.String()); } } else { _MaybeLogJsonRpcError(info, "retrieve user ratings"); } + } else { + printf("unable to retrieve user ratings\n"); } } @@ -808,7 +852,7 @@ Model::_PopulatePackageChangelog(const PackageInfoRef& package) } } else { fprintf(stdout, "unable to obtain the changelog for the package" - "[%s]\n", packageName.String()); + " [%s]\n", packageName.String()); } } diff --git a/src/apps/haikudepot/model/PackageInfo.cpp b/src/apps/haikudepot/model/PackageInfo.cpp index c8d18ccb28..15e803885b 100644 --- a/src/apps/haikudepot/model/PackageInfo.cpp +++ b/src/apps/haikudepot/model/PackageInfo.cpp @@ -86,14 +86,16 @@ UserRating::UserRating() fLanguage(), fPackageVersion(), fUpVotes(0), - fDownVotes(0) + fDownVotes(0), + fCreateTimestamp() { } UserRating::UserRating(const UserInfo& userInfo, float rating, const BString& comment, const BString& language, - const BString& packageVersion, int32 upVotes, int32 downVotes) + const BString& packageVersion, int32 upVotes, int32 downVotes, + const BDateTime& createTimestamp) : fUserInfo(userInfo), fRating(rating), @@ -101,8 +103,10 @@ UserRating::UserRating(const UserInfo& userInfo, float rating, fLanguage(language), fPackageVersion(packageVersion), fUpVotes(upVotes), - fDownVotes(downVotes) + fDownVotes(downVotes), + fCreateTimestamp() { + fCreateTimestamp.SetTime_t(createTimestamp.Time_t()); } @@ -114,8 +118,10 @@ UserRating::UserRating(const UserRating& other) fLanguage(other.fLanguage), fPackageVersion(other.fPackageVersion), fUpVotes(other.fUpVotes), - fDownVotes(other.fDownVotes) + fDownVotes(other.fDownVotes), + fCreateTimestamp() { + fCreateTimestamp.SetTime_t(other.CreateTimestamp().Time_t()); } @@ -129,6 +135,7 @@ UserRating::operator=(const UserRating& other) fPackageVersion = other.fPackageVersion; fUpVotes = other.fUpVotes; fDownVotes = other.fDownVotes; + fCreateTimestamp.SetTime_t(other.fCreateTimestamp.Time_t()); return *this; } @@ -142,7 +149,8 @@ UserRating::operator==(const UserRating& other) const && fLanguage == other.fLanguage && fPackageVersion == other.fPackageVersion && fUpVotes == other.fUpVotes - && fDownVotes == other.fDownVotes; + && fDownVotes == other.fDownVotes + && fCreateTimestamp == other.fCreateTimestamp; } diff --git a/src/apps/haikudepot/model/PackageInfo.h b/src/apps/haikudepot/model/PackageInfo.h index 0cf570c5f4..1d83dc823d 100644 --- a/src/apps/haikudepot/model/PackageInfo.h +++ b/src/apps/haikudepot/model/PackageInfo.h @@ -1,6 +1,6 @@ /* * Copyright 2013-2014, Stephan Aßmus . - * Copyright 2016-2017, Andrew Lindesay . + * Copyright 2016-2018, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ #ifndef PACKAGE_INFO_H @@ -12,6 +12,7 @@ #include #include +#include "DateTime.h" #include "List.h" #include "PackageInfoListener.h" #include "SharedBitmap.h" @@ -51,7 +52,8 @@ public: const BString& comment, const BString& language, const BString& packageVersion, - int32 upVotes, int32 downVotes); + int32 upVotes, int32 downVotes, + const BDateTime& createTimestamp); UserRating(const UserRating& other); UserRating& operator=(const UserRating& other); @@ -73,6 +75,8 @@ public: { return fUpVotes; } int32 DownVotes() const { return fDownVotes; } + const BDateTime& CreateTimestamp() const + { return fCreateTimestamp; } private: UserInfo fUserInfo; float fRating; @@ -81,6 +85,7 @@ private: BString fPackageVersion; int32 fUpVotes; int32 fDownVotes; + BDateTime fCreateTimestamp; }; diff --git a/src/apps/haikudepot/server/PkgDataUpdateProcess.cpp b/src/apps/haikudepot/server/PkgDataUpdateProcess.cpp index be4b1ec8e9..45361c7dc1 100644 --- a/src/apps/haikudepot/server/PkgDataUpdateProcess.cpp +++ b/src/apps/haikudepot/server/PkgDataUpdateProcess.cpp @@ -1,5 +1,5 @@ /* - * Copyright 2017, Andrew Lindesay . + * Copyright 2017-2018, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ @@ -22,6 +22,7 @@ #include "DumpExportPkgJsonListener.h" #include "DumpExportPkgScreenshot.h" #include "DumpExportPkgVersion.h" +#include "HaikuDepotConstants.h" /*! This package listener (not at the JSON level) is feeding in the @@ -145,11 +146,13 @@ PackageFillingPkgListener::ConsumePackage(const PackageInfoRef& package, } } - if (!pkg->DerivedRatingIsNull()) { - RatingSummary summary; + RatingSummary summary; + summary.averageRating = RATING_MISSING; + + if (!pkg->DerivedRatingIsNull()) summary.averageRating = pkg->DerivedRating(); - package->SetRatingSummary(summary); - } + + package->SetRatingSummary(summary); if (!pkg->ProminenceOrderingIsNull()) package->SetProminence(pkg->ProminenceOrdering()); diff --git a/src/apps/haikudepot/server/ServerHelper.cpp b/src/apps/haikudepot/server/ServerHelper.cpp index d9033292b7..69e00a6102 100644 --- a/src/apps/haikudepot/server/ServerHelper.cpp +++ b/src/apps/haikudepot/server/ServerHelper.cpp @@ -16,6 +16,7 @@ #include "HaikuDepotConstants.h" #include "ServerSettings.h" +#include "WebAppInterface.h" #undef B_TRANSLATION_CONTEXT @@ -24,6 +25,120 @@ #define KEY_HEADER_MINIMUM_VERSION "X-Desktop-Application-Minimum-Version" +/*! This method will cause an alert to be shown to the user regarding a + JSON-RPC error that has been sent from the application server. It will + send a message to the application looper which will then relay the message + to the looper and then onto the user to see. +*/ + +void +ServerHelper::NotifyServerJsonRpcError(BMessage& error) +{ + BMessage message(MSG_SERVER_ERROR); + message.AddMessage("error", &error); + be_app->PostMessage(&message); +} + + +void +ServerHelper::AlertServerJsonRpcError(BMessage* message) +{ + BMessage error; + int32 errorCode = 0; + + if (message->FindMessage("error", &error) == B_OK) + errorCode = WebAppInterface::ErrorCodeFromResponse(error); + + BString alertText; + + switch (errorCode) { + case ERROR_CODE_VALIDATION: + // TODO; expand on that message. + alertText = B_TRANSLATE("A validation error has occurred"); + break; + case ERROR_CODE_OBJECTNOTFOUND: + alertText = B_TRANSLATE("A requested object or an object involved" + " in the request was not found on the server."); + break; + case ERROR_CODE_CAPTCHABADRESPONSE: + alertText = B_TRANSLATE("The response to the captcha was incorrect."); + break; + case ERROR_CODE_AUTHORIZATIONFAILURE: + case ERROR_CODE_AUTHORIZATIONRULECONFLICT: + alertText = B_TRANSLATE("Authorization or security issue"); + break; + default: + alertText.SetToFormat( + B_TRANSLATE("An unexpected error has been sent from the" + " HaikuDepot server [%" B_PRIi32 "]"), errorCode); + break; + } + + BAlert* alert = new BAlert( + B_TRANSLATE("Server Error"), + alertText, + B_TRANSLATE("OK")); + + alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); + alert->Go(); +} + + +void +ServerHelper::NotifyTransportError(status_t error) +{ + switch (error) { + case HD_CLIENT_TOO_OLD: + // this is handled earlier on because it requires some + // information from the HTTP request to create a sensible + // error message. + break; + + default: + { + BMessage message(MSG_NETWORK_TRANSPORT_ERROR); + message.AddInt64("errno", (int64) error); + be_app->PostMessage(&message); + break; + } + } +} + + +void +ServerHelper::AlertTransportError(BMessage* message) +{ + status_t errno = B_OK; + int64 errnoInt64; + message->FindInt64("errno", &errnoInt64); + errno = (status_t) errnoInt64; + + BString errorDescription("?"); + BString alertText; + + switch (errno) { + case HD_NETWORK_INACCESSIBLE: + errorDescription = B_TRANSLATE("Network Error"); + break; + default: + errorDescription.SetTo(strerror(errno)); + break; + } + + alertText.SetToFormat(B_TRANSLATE("A network transport error has arisen" + " communicating with the HaikuDepot server system: %s"), + errorDescription.String()); + + BAlert* alert = new BAlert( + B_TRANSLATE("Network Transport Error"), + alertText, + B_TRANSLATE("OK")); + + alert->SetFlags(alert->Flags() | B_CLOSE_ON_ESCAPE); + alert->Go(); +} + + void ServerHelper::NotifyClientTooOld(const BHttpHeaders& responseHeaders) { diff --git a/src/apps/haikudepot/server/ServerHelper.h b/src/apps/haikudepot/server/ServerHelper.h index b526cf8845..01ad48b217 100644 --- a/src/apps/haikudepot/server/ServerHelper.h +++ b/src/apps/haikudepot/server/ServerHelper.h @@ -7,7 +7,9 @@ #define SERVER_HELPER_H #include -#include + + +class BMessage; class ServerHelper { @@ -19,6 +21,14 @@ public: const BHttpHeaders& responseHeaders ); static void AlertClientTooOld(BMessage* message); + + static void NotifyTransportError(status_t error); + static void AlertTransportError(BMessage* message); + + static void NotifyServerJsonRpcError( + BMessage& error); + static void AlertServerJsonRpcError( + BMessage* message); }; #endif // SERVER_HELPER_H diff --git a/src/apps/haikudepot/server/WebAppInterface.cpp b/src/apps/haikudepot/server/WebAppInterface.cpp index 3d77e79517..e0b5956828 100644 --- a/src/apps/haikudepot/server/WebAppInterface.cpp +++ b/src/apps/haikudepot/server/WebAppInterface.cpp @@ -10,11 +10,13 @@ #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -363,56 +365,138 @@ WebAppInterface::RetrieveUserRating(const BString& packageName, const BString &repositoryCode, const BString& username, BMessage& message) { - BString jsonString = JsonBuilder() - .AddValue("jsonrpc", "2.0") - .AddValue("id", ++fRequestIndex) - .AddValue("method", "getUserRatingByUserAndPkgVersion") - .AddArray("params") - .AddObject() - .AddValue("userNickname", username) - .AddValue("pkgName", packageName) - .AddValue("pkgVersionArchitectureCode", architecture) - .AddValue("pkgVersionMajor", version.Major(), true) - .AddValue("pkgVersionMinor", version.Minor(), true) - .AddValue("pkgVersionMicro", version.Micro(), true) - .AddValue("pkgVersionPreRelease", version.PreRelease(), true) - .AddValue("pkgVersionRevision", (int)version.Revision()) - .AddValue("repositoryCode", repositoryCode) - .EndObject() - .EndArray() - .End(); + // BHttpRequest later takes ownership of this. + BMallocIO* requestEnvelopeData = new BMallocIO(); + BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); - return _SendJsonRequest("userrating", jsonString, 0, message); + requestEnvelopeWriter.WriteObjectStart(); + _WriteStandardJsonRpcEnvelopeValues(requestEnvelopeWriter, + "getUserRatingByUserAndPkgVersion"); + requestEnvelopeWriter.WriteObjectName("params"); + requestEnvelopeWriter.WriteArrayStart(); + + requestEnvelopeWriter.WriteObjectStart(); + + requestEnvelopeWriter.WriteObjectName("userNickname"); + requestEnvelopeWriter.WriteString(username.String()); + requestEnvelopeWriter.WriteObjectName("pkgName"); + requestEnvelopeWriter.WriteString(packageName.String()); + requestEnvelopeWriter.WriteObjectName("pkgVersionArchitectureCode"); + requestEnvelopeWriter.WriteString(architecture.String()); + requestEnvelopeWriter.WriteObjectName("repositoryCode"); + requestEnvelopeWriter.WriteString(repositoryCode.String()); + + if (version.Major().Length() > 0) { + requestEnvelopeWriter.WriteObjectName("pkgVersionMajor"); + requestEnvelopeWriter.WriteString(version.Major().String()); + } + + if (version.Minor().Length() > 0) { + requestEnvelopeWriter.WriteObjectName("pkgVersionMinor"); + requestEnvelopeWriter.WriteString(version.Minor().String()); + } + + if (version.Micro().Length() > 0) { + requestEnvelopeWriter.WriteObjectName("pkgVersionMicro"); + requestEnvelopeWriter.WriteString(version.Micro().String()); + } + + if (version.PreRelease().Length() > 0) { + requestEnvelopeWriter.WriteObjectName("pkgVersionPreRelease"); + requestEnvelopeWriter.WriteString(version.PreRelease().String()); + } + + if (version.Revision() != 0) { + requestEnvelopeWriter.WriteObjectName("pkgVersionRevision"); + requestEnvelopeWriter.WriteInteger(version.Revision()); + } + + requestEnvelopeWriter.WriteObjectEnd(); + requestEnvelopeWriter.WriteArrayEnd(); + requestEnvelopeWriter.WriteObjectEnd(); + + return _SendJsonRequest("userrating", requestEnvelopeData, + requestEnvelopeData->Position(), NEEDS_AUTHORIZATION, message); } status_t WebAppInterface::CreateUserRating(const BString& packageName, + const BPackageVersion& version, const BString& architecture, const BString& repositoryCode, const BString& languageCode, const BString& comment, const BString& stability, int rating, BMessage& message) { - BString jsonString = JsonBuilder() - .AddValue("jsonrpc", "2.0") - .AddValue("id", ++fRequestIndex) - .AddValue("method", "createUserRating") - .AddArray("params") - .AddObject() - .AddValue("pkgName", packageName) - .AddValue("pkgVersionArchitectureCode", architecture) - .AddValue("pkgVersionType", "LATEST") - .AddValue("userNickname", fUsername) - .AddValue("rating", rating) - .AddValue("userRatingStabilityCode", stability, true) - .AddValue("comment", comment) - .AddValue("repositoryCode", repositoryCode) - .AddValue("naturalLanguageCode", languageCode) - .EndObject() - .EndArray() - .End(); + // BHttpRequest later takes ownership of this. + BMallocIO* requestEnvelopeData = new BMallocIO(); + BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); - return _SendJsonRequest("userrating", jsonString, NEEDS_AUTHORIZATION, - message); + requestEnvelopeWriter.WriteObjectStart(); + _WriteStandardJsonRpcEnvelopeValues(requestEnvelopeWriter, + "createUserRating"); + requestEnvelopeWriter.WriteObjectName("params"); + requestEnvelopeWriter.WriteArrayStart(); + + requestEnvelopeWriter.WriteObjectStart(); + requestEnvelopeWriter.WriteObjectName("pkgName"); + requestEnvelopeWriter.WriteString(packageName.String()); + requestEnvelopeWriter.WriteObjectName("pkgVersionArchitectureCode"); + requestEnvelopeWriter.WriteString(architecture.String()); + requestEnvelopeWriter.WriteObjectName("repositoryCode"); + requestEnvelopeWriter.WriteString(repositoryCode.String()); + requestEnvelopeWriter.WriteObjectName("naturalLanguageCode"); + requestEnvelopeWriter.WriteString(languageCode.String()); + requestEnvelopeWriter.WriteObjectName("pkgVersionType"); + requestEnvelopeWriter.WriteString("SPECIFIC"); + requestEnvelopeWriter.WriteObjectName("userNickname"); + requestEnvelopeWriter.WriteString(fUsername.String()); + + if (!version.Major().IsEmpty()) { + requestEnvelopeWriter.WriteObjectName("pkgVersionMajor"); + requestEnvelopeWriter.WriteString(version.Major()); + } + + if (!version.Minor().IsEmpty()) { + requestEnvelopeWriter.WriteObjectName("pkgVersionMinor"); + requestEnvelopeWriter.WriteString(version.Minor()); + } + + if (!version.Micro().IsEmpty()) { + requestEnvelopeWriter.WriteObjectName("pkgVersionMicro"); + requestEnvelopeWriter.WriteString(version.Micro()); + } + + if (!version.PreRelease().IsEmpty()) { + requestEnvelopeWriter.WriteObjectName("pkgVersionPreRelease"); + requestEnvelopeWriter.WriteString(version.PreRelease()); + } + + if (version.Revision() != 0) { + requestEnvelopeWriter.WriteObjectName("pkgVersionRevision"); + requestEnvelopeWriter.WriteInteger(version.Revision()); + } + + if (rating > 0.0f) { + requestEnvelopeWriter.WriteObjectName("rating"); + requestEnvelopeWriter.WriteInteger(rating); + } + + if (stability.Length() > 0) { + requestEnvelopeWriter.WriteObjectName("userRatingStabilityCode"); + requestEnvelopeWriter.WriteString(stability); + } + + if (comment.Length() > 0) { + requestEnvelopeWriter.WriteObjectName("comment"); + requestEnvelopeWriter.WriteString(comment.String()); + } + + requestEnvelopeWriter.WriteObjectEnd(); + requestEnvelopeWriter.WriteArrayEnd(); + requestEnvelopeWriter.WriteObjectEnd(); + + return _SendJsonRequest("userrating", requestEnvelopeData, + requestEnvelopeData->Position(), NEEDS_AUTHORIZATION, message); } @@ -421,31 +505,56 @@ WebAppInterface::UpdateUserRating(const BString& ratingID, const BString& languageCode, const BString& comment, const BString& stability, int rating, bool active, BMessage& message) { - BString jsonString = JsonBuilder() - .AddValue("jsonrpc", "2.0") - .AddValue("id", ++fRequestIndex) - .AddValue("method", "updateUserRating") - .AddArray("params") - .AddObject() - .AddValue("code", ratingID) - .AddValue("rating", rating) - .AddValue("userRatingStabilityCode", stability, true) - .AddValue("comment", comment) - .AddValue("naturalLanguageCode", languageCode) - .AddValue("active", active) - .AddArray("filter") - .AddItem("ACTIVE") - .AddItem("NATURALLANGUAGE") - .AddItem("USERRATINGSTABILITY") - .AddItem("COMMENT") - .AddItem("RATING") - .EndArray() - .EndObject() - .EndArray() - .End(); + // BHttpRequest later takes ownership of this. + BMallocIO* requestEnvelopeData = new BMallocIO(); + BJsonTextWriter requestEnvelopeWriter(requestEnvelopeData); - return _SendJsonRequest("userrating", jsonString, NEEDS_AUTHORIZATION, - message); + requestEnvelopeWriter.WriteObjectStart(); + _WriteStandardJsonRpcEnvelopeValues(requestEnvelopeWriter, + "updateUserRating"); + + requestEnvelopeWriter.WriteObjectName("params"); + requestEnvelopeWriter.WriteArrayStart(); + + requestEnvelopeWriter.WriteObjectStart(); + + requestEnvelopeWriter.WriteObjectName("code"); + requestEnvelopeWriter.WriteString(ratingID.String()); + requestEnvelopeWriter.WriteObjectName("naturalLanguageCode"); + requestEnvelopeWriter.WriteString(languageCode.String()); + requestEnvelopeWriter.WriteObjectName("active"); + requestEnvelopeWriter.WriteBoolean(active); + + requestEnvelopeWriter.WriteObjectName("filter"); + requestEnvelopeWriter.WriteArrayStart(); + requestEnvelopeWriter.WriteString("ACTIVE"); + requestEnvelopeWriter.WriteString("NATURALLANGUAGE"); + requestEnvelopeWriter.WriteString("USERRATINGSTABILITY"); + requestEnvelopeWriter.WriteString("COMMENT"); + requestEnvelopeWriter.WriteString("RATING"); + requestEnvelopeWriter.WriteArrayEnd(); + + if (rating >= 0) { + requestEnvelopeWriter.WriteObjectName("rating"); + requestEnvelopeWriter.WriteInteger(rating); + } + + if (stability.Length() > 0) { + requestEnvelopeWriter.WriteObjectName("userRatingStabilityCode"); + requestEnvelopeWriter.WriteString(stability); + } + + if (comment.Length() > 0) { + requestEnvelopeWriter.WriteObjectName("comment"); + requestEnvelopeWriter.WriteString(comment); + } + + requestEnvelopeWriter.WriteObjectEnd(); + requestEnvelopeWriter.WriteArrayEnd(); + requestEnvelopeWriter.WriteObjectEnd(); + + return _SendJsonRequest("userrating", requestEnvelopeData, + requestEnvelopeData->Position(), NEEDS_AUTHORIZATION, message); } @@ -555,9 +664,45 @@ WebAppInterface::AuthenticateUser(const BString& nickName, } +/*! JSON-RPC invocations return a response. The response may be either + a result or it may be an error depending on the response structure. + If it is an error then there may be additional detail that is the + error code and message. This method will extract the error code + from the response. This method will return 0 if the payload does + not look like an error. +*/ + +int32 +WebAppInterface::ErrorCodeFromResponse(BMessage& response) +{ + BMessage error; + double code; + + if (response.FindMessage("error", &error) == B_OK + && error.FindDouble("code", &code) == B_OK) { + return (int32) code; + } + + return 0; +} + + // #pragma mark - private +void +WebAppInterface::_WriteStandardJsonRpcEnvelopeValues(BJsonWriter& writer, + const char* methodName) +{ + writer.WriteObjectName("jsonrpc"); + writer.WriteString("2.0"); + writer.WriteObjectName("id"); + writer.WriteInteger(++fRequestIndex); + writer.WriteObjectName("method"); + writer.WriteString(methodName); +} + + status_t WebAppInterface::_SendJsonRequest(const char* domain, BDataIO* requestData, size_t requestDataSize, uint32 flags, BMessage& reply) const @@ -658,10 +803,10 @@ WebAppInterface::_SendJsonRequest(const char* domain, BString jsonString, if (Logger::IsTraceEnabled()) printf("_SendJsonRequest(%s)\n", jsonString.String()); + // gets 'adopted' by the subsequent http request. BMemoryIO* data = new BMemoryIO( jsonString.String(), jsonString.Length() - 1); return _SendJsonRequest(domain, data, jsonString.Length() - 1, flags, reply); } - diff --git a/src/apps/haikudepot/server/WebAppInterface.h b/src/apps/haikudepot/server/WebAppInterface.h index 09690a78db..b6ce6f7219 100644 --- a/src/apps/haikudepot/server/WebAppInterface.h +++ b/src/apps/haikudepot/server/WebAppInterface.h @@ -8,6 +8,7 @@ #include +#include #include #include @@ -21,6 +22,18 @@ using BPackageKit::BPackageVersion; typedef List StringList; +/*! These are error codes that are sent back to the client from the server */ + +#define ERROR_CODE_NONE 0 +#define ERROR_CODE_VALIDATION -32800 +#define ERROR_CODE_OBJECTNOTFOUND -32801 +#define ERROR_CODE_CAPTCHABADRESPONSE -32802 +#define ERROR_CODE_AUTHORIZATIONFAILURE -32803 +#define ERROR_CODE_BADPKGICON -32804 +#define ERROR_CODE_LIMITEXCEEDED -32805 +#define ERROR_CODE_AUTHORIZATIONRULECONFLICT -32806 + + class WebAppInterface { public: WebAppInterface(); @@ -57,6 +70,7 @@ public: status_t CreateUserRating( const BString& packageName, + const BPackageVersion& version, const BString& architecture, const BString& repositoryCode, const BString& languageCode, @@ -92,7 +106,12 @@ public: const BString& passwordClear, BMessage& message); + static int32 ErrorCodeFromResponse(BMessage& response); + private: + void _WriteStandardJsonRpcEnvelopeValues( + BJsonWriter& writer, + const char* methodName); status_t _SendJsonRequest(const char* domain, BString jsonString, uint32 flags, BMessage& reply) const; diff --git a/src/apps/haikudepot/textview/TextDocumentLayout.cpp b/src/apps/haikudepot/textview/TextDocumentLayout.cpp index 8afa1142a2..6d00d78f46 100644 --- a/src/apps/haikudepot/textview/TextDocumentLayout.cpp +++ b/src/apps/haikudepot/textview/TextDocumentLayout.cpp @@ -25,9 +25,9 @@ public: virtual void TextChanged(const TextChangedEvent& event) { - printf("TextChanged(%" B_PRIi32 ", %" B_PRIi32 ")\n", - event.FirstChangedParagraph(), - event.ChangedParagraphCount()); +// printf("TextChanged(%" B_PRIi32 ", %" B_PRIi32 ")\n", +// event.FirstChangedParagraph(), +// event.ChangedParagraphCount()); // TODO: The event does not contain useful data. Make the event // system work so only the affected paragraphs are updated. // I think we need "first affected", "last affected" (both relative diff --git a/src/apps/haikudepot/textview/TextEditor.cpp b/src/apps/haikudepot/textview/TextEditor.cpp index bfd46ba850..eaf6b688d5 100644 --- a/src/apps/haikudepot/textview/TextEditor.cpp +++ b/src/apps/haikudepot/textview/TextEditor.cpp @@ -257,8 +257,6 @@ TextEditor::Insert(int32 offset, const BString& string) if (ret == B_OK) { _SetCaretOffset(offset + string.CountChars(), true, false, true); - - fDocument->PrintToStream(); } return ret; @@ -275,8 +273,6 @@ TextEditor::Remove(int32 offset, int32 length) if (ret == B_OK) { _SetCaretOffset(offset, true, false, true); - - fDocument->PrintToStream(); } return ret; @@ -293,8 +289,6 @@ TextEditor::Replace(int32 offset, int32 length, const BString& string) if (ret == B_OK) { _SetCaretOffset(offset + string.CountChars(), true, false, true); - - fDocument->PrintToStream(); } return ret; diff --git a/src/apps/haikudepot/ui/App.cpp b/src/apps/haikudepot/ui/App.cpp index af3543d509..a28aafaf47 100644 --- a/src/apps/haikudepot/ui/App.cpp +++ b/src/apps/haikudepot/ui/App.cpp @@ -108,6 +108,18 @@ App::MessageReceived(BMessage* message) ServerHelper::AlertClientTooOld(message); break; + case MSG_NETWORK_TRANSPORT_ERROR: + ServerHelper::AlertTransportError(message); + break; + + case MSG_SERVER_ERROR: + ServerHelper::AlertServerJsonRpcError(message); + break; + + case MSG_SERVER_DATA_CHANGED: + fMainWindow->PostMessage(message); + break; + default: BApplication::MessageReceived(message); break; diff --git a/src/apps/haikudepot/ui/FeaturedPackagesView.cpp b/src/apps/haikudepot/ui/FeaturedPackagesView.cpp index fd6cc9d234..d1ca2e0ee4 100644 --- a/src/apps/haikudepot/ui/FeaturedPackagesView.cpp +++ b/src/apps/haikudepot/ui/FeaturedPackagesView.cpp @@ -17,6 +17,7 @@ #include #include "BitmapView.h" +#include "HaikuDepotConstants.h" #include "MainWindow.h" #include "MarkupTextView.h" #include "MessagePackageListener.h" @@ -30,7 +31,8 @@ static const rgb_color kLightBlack = (rgb_color){ 60, 60, 60, 255 }; -static BitmapRef sInstalledIcon(new(std::nothrow) SharedBitmap(504), true); +static BitmapRef sInstalledIcon(new(std::nothrow) + SharedBitmap(RSRC_INSTALLED), true); // #pragma mark - PackageView diff --git a/src/apps/haikudepot/ui/MainWindow.cpp b/src/apps/haikudepot/ui/MainWindow.cpp index 0db941aa7d..d15b7f7d66 100644 --- a/src/apps/haikudepot/ui/MainWindow.cpp +++ b/src/apps/haikudepot/ui/MainWindow.cpp @@ -390,6 +390,26 @@ MainWindow::MessageReceived(BMessage* message) _AdoptModel(); break; + // this may be triggered by, for example, a user rating being added + // or having been altered. + case MSG_SERVER_DATA_CHANGED: + { + BString name; + if (message->FindString("name", &name) == B_OK) { + BAutolock locker(fModel.Lock()); + if (fPackageInfoView->Package()->Name() == name) { + _PopulatePackageAsync(true); + } else { + if (Logger::IsDebugEnabled()) { + printf("pkg [%s] is updated on the server, but is " + "not selected so will not be updated.\n", + name.String()); + } + } + } + break; + } + case MSG_PACKAGE_SELECTED: { BString name; @@ -624,8 +644,8 @@ MainWindow::StoreSettings(BMessage& settings) const void MainWindow::PackageChanged(const PackageInfoEvent& event) { - uint32 whatchedChanges = PKG_CHANGED_STATE | PKG_CHANGED_PROMINENCE; - if ((event.Changes() & whatchedChanges) != 0) { + uint32 watchedChanges = PKG_CHANGED_STATE | PKG_CHANGED_PROMINENCE; + if ((event.Changes() & watchedChanges) != 0) { PackageInfoRef ref(event.Package()); BMessage message(MSG_PACKAGE_CHANGED); message.AddPointer("package", ref.Get()); @@ -850,12 +870,7 @@ MainWindow::_AdoptPackage(const PackageInfoRef& package) fPackageListView->SelectPackage(package); } - // Trigger asynchronous package population from the web-app - { - AutoLocker lock(&fPackageToPopulateLock); - fPackageToPopulate = package; - } - release_sem_etc(fPackageToPopulateSem, 1, 0); + _PopulatePackageAsync(false); } @@ -1255,6 +1270,34 @@ MainWindow::_PackageActionWorker(void* arg) } +/*! This method will cause the package to have its data refreshed from + the server application. The refresh happens in the background; this method + is asynchronous. +*/ + +void +MainWindow::_PopulatePackageAsync(bool forcePopulate) +{ + // Trigger asynchronous package population from the web-app + { + AutoLocker lock(&fPackageToPopulateLock); + fPackageToPopulate = fPackageInfoView->Package(); + fForcePopulatePackage = forcePopulate; + } + release_sem_etc(fPackageToPopulateSem, 1, 0); + + if (Logger::IsDebugEnabled()) { + printf("pkg [%s] will be updated from the server.\n", + fPackageToPopulate->Name().String()); + } +} + + +/*! This method will run in the background. The thread will block until there + is a package to be updated. When the thread unblocks, it will update the + package with information from the server. +*/ + status_t MainWindow::_PopulatePackageWorker(void* arg) { @@ -1262,15 +1305,27 @@ MainWindow::_PopulatePackageWorker(void* arg) while (acquire_sem(window->fPackageToPopulateSem) == B_OK) { PackageInfoRef package; + bool force; { AutoLocker lock(&window->fPackageToPopulateLock); package = window->fPackageToPopulate; + force = window->fForcePopulatePackage; } if (package.Get() != NULL) { - window->fModel.PopulatePackage(package, - Model::POPULATE_USER_RATINGS | Model::POPULATE_SCREEN_SHOTS - | Model::POPULATE_CHANGELOG); + uint32 populateFlags = Model::POPULATE_USER_RATINGS + | Model::POPULATE_SCREEN_SHOTS + | Model::POPULATE_CHANGELOG; + + if (force) + populateFlags |= Model::POPULATE_FORCE; + + window->fModel.PopulatePackage(package, populateFlags); + + if (Logger::IsDebugEnabled()) { + printf("populating package [%s]\n", + package->Name().String()); + } } } @@ -1435,9 +1490,35 @@ MainWindow::_UpdateAvailableRepositories() } +bool +MainWindow::_SelectedPackageHasWebAppRepositoryCode() +{ + const PackageInfoRef& package = fPackageInfoView->Package(); + const DepotInfo* depot = fModel.DepotForName(package->DepotName()); + + BString repositoryCode; + + if (depot != NULL) + repositoryCode = depot->WebAppRepositoryCode(); + + return !repositoryCode.IsEmpty(); +} + + void MainWindow::_RatePackage() { + if (!_SelectedPackageHasWebAppRepositoryCode()) { + BAlert* alert = new(std::nothrow) BAlert( + B_TRANSLATE("rating_not_possible"), + B_TRANSLATE("Because there is no representation for this package " + "on the HaikuDepot server system, it is not possible to create " + "a new rating or edit an existing rating."), + B_TRANSLATE("OK")); + alert->Go(); + return; + } + if (fModel.Username().IsEmpty()) { BAlert* alert = new(std::nothrow) BAlert( B_TRANSLATE("Not logged in"), diff --git a/src/apps/haikudepot/ui/MainWindow.h b/src/apps/haikudepot/ui/MainWindow.h index 64b580f32d..8b89cd4776 100644 --- a/src/apps/haikudepot/ui/MainWindow.h +++ b/src/apps/haikudepot/ui/MainWindow.h @@ -57,6 +57,8 @@ private: virtual Model* GetModel(); private: + bool _SelectedPackageHasWebAppRepositoryCode(); + void _BuildMenu(BMenuBar* menuBar); void _BuildUserMenu(BMenuBar* menuBar); @@ -73,6 +75,7 @@ private: void _RefreshRepositories(bool force); void _RefreshPackageList(bool force); + void _PopulatePackageAsync(bool forcePopulate); void _StartRefreshWorker(bool force = false); static status_t _RefreshModelThreadWorker(void* arg); static status_t _PackageActionWorker(void* arg); @@ -127,6 +130,7 @@ private: thread_id fPopulatePackageWorker; PackageInfoRef fPackageToPopulate; + bool fForcePopulatePackage; BLocker fPackageToPopulateLock; sem_id fPackageToPopulateSem; diff --git a/src/apps/haikudepot/ui/PackageInfoView.cpp b/src/apps/haikudepot/ui/PackageInfoView.cpp index 4b4e86af8e..59c54020ab 100644 --- a/src/apps/haikudepot/ui/PackageInfoView.cpp +++ b/src/apps/haikudepot/ui/PackageInfoView.cpp @@ -1,5 +1,6 @@ /* * Copyright 2013-2014, Stephan Aßmus . + * Copyright 2018, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ @@ -15,6 +16,7 @@ #include #include #include +#include #include #include #include @@ -724,7 +726,6 @@ public: fWebsiteLinkView->SetViewUIColor(ViewUIColor(), kContentTint); BLayoutBuilder::Group<>(this, B_HORIZONTAL, 0.0f) -// .Add(BSpaceLayoutItem::CreateHorizontalStrut(32.0f)) .AddGroup(leftGroup, 1.0f) .Add(fScreenshotView) .AddGroup(B_HORIZONTAL) @@ -867,98 +868,71 @@ private: class RatingItemView : public BGroupView { public: - RatingItemView(const UserRating& rating, const BitmapRef& voteUpIcon, - const BitmapRef& voteDownIcon) + RatingItemView(const UserRating& rating) : BGroupView(B_HORIZONTAL, 0.0f) { SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint); - fAvatarView = new BitmapView("avatar view"); - if (rating.User().Avatar().Get() != NULL) { - fAvatarView->SetBitmap(rating.User().Avatar(), - SharedBitmap::SIZE_16); + BLayoutBuilder::Group > verticalGroup = + BLayoutBuilder::Group<>(this) + .AddGroup(B_VERTICAL, 0.0f); + + { + BStringView* userNicknameView = new BStringView("user-nickname", + rating.User().NickName()); + userNicknameView->SetFont(be_bold_font); + verticalGroup.Add(userNicknameView); } - fAvatarView->SetExplicitMinSize(BSize(16.0f, 16.0f)); - fNameView = new BStringView("user name", rating.User().NickName()); + BLayoutBuilder::Group > > ratingGroup = + verticalGroup.AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING); - BFont nameFont(be_bold_font); - nameFont.SetSize(std::max(9.0f, floorf(nameFont.Size() * 0.9f))); - fNameView->SetFont(&nameFont); - fNameView->SetExplicitMaxSize( - BSize(nameFont.StringWidth("xxxxxxxxxxxxxxxxxxxxxx"), B_SIZE_UNSET)); + if (rating.Rating() >= 0) { + RatingView* ratingView = new RatingView("package rating view"); + ratingView->SetRating(rating.Rating()); + ratingGroup.Add(ratingView); + } - fRatingView = new RatingView("package rating view"); - fRatingView->SetRating(rating.Rating()); + { + BDateFormat dateFormat; + BString createTimestampPresentation; - BString ratingLabel; - if (rating.Rating() >= 0.0f) - ratingLabel.SetToFormat("%.1f", rating.Rating()); - fRatingLabelView = new BStringView("rating label", ratingLabel); + dateFormat.Format(createTimestampPresentation, + rating.CreateTimestamp().Date(), B_MEDIUM_DATE_FORMAT); - BString versionLabel(B_TRANSLATE("for %Version%")); - versionLabel.ReplaceAll("%Version%", rating.PackageVersion()); - fPackageVersionView = new BStringView("package version", - versionLabel); - BFont versionFont(be_plain_font); - versionFont.SetSize(std::max(9.0f, floorf(versionFont.Size() * 0.85f))); - fPackageVersionView->SetFont(&versionFont); + BString ratingContextDescription( + B_TRANSLATE("%hd.timestamp% (version %hd.version%)")); + ratingContextDescription.ReplaceAll("%hd.timestamp%", + createTimestampPresentation); + ratingContextDescription.ReplaceAll("%hd.version%", + rating.PackageVersion()); - // TODO: User rating IDs to identify which rating to vote up or down -// BMessage* voteUpMessage = new BMessage(MSG_VOTE_UP); -// voteUpMessage->AddInt32("rating id", -1); -// BMessage* voteDownMessage = new BMessage(MSG_VOTE_DOWN); -// voteDownMessage->AddInt32("rating id", -1); -// -// fVoteUpIconView = new BitmapButton("vote up icon", voteUpMessage); -// fUpVoteCountView = new BStringView("up vote count", ""); -// fVoteDownIconView = new BitmapButton("vote down icon", voteDownMessage); -// fDownVoteCountView = new BStringView("up vote count", ""); -// -// fVoteUpIconView->SetBitmap(voteUpIcon, SharedBitmap::SIZE_16); -// fVoteDownIconView->SetBitmap(voteDownIcon, SharedBitmap::SIZE_16); -// -// fUpVoteCountView->SetFont(&versionFont); -// fUpVoteCountView->SetHighColor(kLightBlack); -// fDownVoteCountView->SetFont(&versionFont); -// fDownVoteCountView->SetHighColor(kLightBlack); -// -// BString voteCountLabel; -// voteCountLabel.SetToFormat("%" B_PRId32, rating.UpVotes()); -// fUpVoteCountView->SetText(voteCountLabel); -// voteCountLabel.SetToFormat("%" B_PRId32, rating.DownVotes()); -// fDownVoteCountView->SetText(voteCountLabel); + BStringView* ratingContextView = new BStringView("rating-context", + ratingContextDescription); + BFont versionFont(be_plain_font); + ratingContextView->SetFont(&versionFont); + ratingGroup.Add(ratingContextView); + } - fTextView = new TextView("rating text"); - ParagraphStyle paragraphStyle(fTextView->ParagraphStyle()); - paragraphStyle.SetJustify(true); - fTextView->SetParagraphStyle(paragraphStyle); + ratingGroup.AddGlue(); + ratingGroup.End(); - fTextView->SetText(rating.Comment()); + if (rating.Comment() > 0) { + TextView* textView = new TextView("rating-text"); + ParagraphStyle paragraphStyle(textView->ParagraphStyle()); + paragraphStyle.SetJustify(true); + textView->SetParagraphStyle(paragraphStyle); + textView->SetText(rating.Comment()); + verticalGroup.AddStrut(8.0f); + verticalGroup.Add(textView); + verticalGroup.AddStrut(8.0f); + } - BLayoutBuilder::Group<>(this) - .Add(fAvatarView, 0.2f) - .AddGroup(B_VERTICAL, 0.0f) - .AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING) - .Add(fNameView) - .Add(fRatingView) - .Add(fRatingLabelView) - .AddGlue(0.1f) - .Add(fPackageVersionView) - .AddGlue(5.0f) -// .AddGroup(B_HORIZONTAL, 0.0f, 0.0f) -// .Add(fVoteUpIconView) -// .Add(fUpVoteCountView) -// .AddStrut(B_USE_HALF_ITEM_SPACING) -// .Add(fVoteDownIconView) -// .Add(fDownVoteCountView) -// .End() - .End() - .Add(fTextView) + verticalGroup .End() - .SetInsets(B_USE_DEFAULT_SPACING) - ; + .SetInsets(B_USE_DEFAULT_SPACING); SetFlags(Flags() | B_WILL_DRAW); } @@ -976,19 +950,6 @@ public: StrokeLine(Bounds().LeftBottom(), Bounds().RightBottom()); } -private: - BitmapView* fAvatarView; - BStringView* fNameView; - RatingView* fRatingView; - BStringView* fRatingLabelView; - BStringView* fPackageVersionView; - -// BitmapView* fVoteUpIconView; -// BStringView* fUpVoteCountView; -// BitmapView* fVoteDownIconView; -// BStringView* fDownVoteCountView; - - TextView* fTextView; }; @@ -1062,9 +1023,7 @@ class UserRatingsView : public BGroupView { public: UserRatingsView() : - BGroupView("package ratings view", B_HORIZONTAL), - fThumbsUpIcon(BitmapRef(new SharedBitmap(502), true)), - fThumbsDownIcon(BitmapRef(new SharedBitmap(503), true)) + BGroupView("package ratings view", B_HORIZONTAL) { SetViewUIColor(B_PANEL_BACKGROUND_COLOR, kContentTint); @@ -1077,6 +1036,8 @@ public: BScrollView* scrollView = new RatingsScrollView( "ratings scroll view", ratingsContainerView); + scrollView->SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, + B_SIZE_UNLIMITED)); BLayoutBuilder::Group<>(this) .AddGroup(B_VERTICAL) @@ -1084,6 +1045,7 @@ public: .AddGlue() .SetInsets(0.0f, B_USE_DEFAULT_SPACING, 0.0f, 0.0f) .End() + .AddStrut(64.0) .Add(scrollView, 1.0f) .SetInsets(B_USE_DEFAULT_SPACING, -1.0f, -1.0f, -1.0f) ; @@ -1122,15 +1084,10 @@ public: // TODO: Sort by age or usefullness rating for (int i = count - 1; i >= 0; i--) { const UserRating& rating = userRatings.ItemAtFast(i); - // Prevent ratings from showing that have a comment which - // is in another language - if (!rating.Comment().IsEmpty() - && fPreferredLanguages.CountItems() > 0 - && !fPreferredLanguages.Contains(rating.Language())) { - continue; - } - RatingItemView* view = new RatingItemView(rating, fThumbsUpIcon, - fThumbsDownIcon); + // was previously filtering comments just for the current + // user's language, but as there are not so many comments at + // the moment, just show all of them for now. + RatingItemView* view = new RatingItemView(rating); fRatingContainerLayout->AddView(0, view); } } @@ -1181,8 +1138,6 @@ private: private: BGroupLayout* fRatingContainerLayout; RatingSummaryView* fRatingSummaryView; - BitmapRef fThumbsUpIcon; - BitmapRef fThumbsDownIcon; StringList fPreferredLanguages; }; @@ -1516,5 +1471,4 @@ PackageInfoView::Clear() fPackageListener->SetPackage(PackageInfoRef(NULL)); fPackage.Unset(); -} - +} \ No newline at end of file diff --git a/src/apps/haikudepot/ui/RatePackageWindow.cpp b/src/apps/haikudepot/ui/RatePackageWindow.cpp index 4906ac62ed..585eaa3e91 100644 --- a/src/apps/haikudepot/ui/RatePackageWindow.cpp +++ b/src/apps/haikudepot/ui/RatePackageWindow.cpp @@ -1,6 +1,6 @@ /* * Copyright 2014, Stephan Aßmus . - * Copyright 2016, Andrew Lindesay . + * Copyright 2016-2018, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ @@ -21,8 +21,10 @@ #include #include +#include "HaikuDepotConstants.h" #include "MarkupParser.h" #include "RatingView.h" +#include "ServerHelper.h" #include "TextDocumentView.h" #include "WebAppInterface.h" @@ -32,11 +34,12 @@ enum { - MSG_SEND = 'send', - MSG_PACKAGE_RATED = 'rpkg', - MSG_STABILITY_SELECTED = 'stbl', - MSG_LANGUAGE_SELECTED = 'lngs', - MSG_RATING_ACTIVE_CHANGED = 'rtac' + MSG_SEND = 'send', + MSG_PACKAGE_RATED = 'rpkg', + MSG_STABILITY_SELECTED = 'stbl', + MSG_LANGUAGE_SELECTED = 'lngs', + MSG_RATING_ACTIVE_CHANGED = 'rtac', + MSG_RATING_DETERMINATE_CHANGED = 'rdch' }; //! Layouts the scrollbar so it looks nice with no border and the document @@ -98,7 +101,8 @@ public: SetRatingView() : RatingView("rate package view"), - fPermanentRating(0.0f) + fPermanentRating(0.0f), + fRatingDeterminate(true) { SetExplicitMaxSize(BSize(B_SIZE_UNLIMITED, B_SIZE_UNSET)); SetRating(fPermanentRating); @@ -134,6 +138,24 @@ public: SetRating(rating); } +/*! By setting this to false, this indicates that there is no rating for the + set; ie NULL. The indeterminate rating is indicated by a pale grey + colored star. +*/ + + void SetRatingDeterminate(bool value) { + fRatingDeterminate = value; + Invalidate(); + } + +protected: + virtual const BBitmap* StarBitmap() + { + if (fRatingDeterminate) + return fStarBlueBitmap.Bitmap(SharedBitmap::SIZE_16); + return fStarGrayBitmap.Bitmap(SharedBitmap::SIZE_16); + } + private: float _RatingForMousePos(BPoint where) { @@ -141,6 +163,7 @@ private: } float fPermanentRating; + bool fRatingDeterminate; }; @@ -189,6 +212,10 @@ RatePackageWindow::RatePackageWindow(BWindow* parent, BRect frame, B_TRANSLATE("Your rating:")); fSetRatingView = new SetRatingView(); + fSetRatingView->SetRatingDeterminate(false); + fRatingDeterminateCheckBox = new BCheckBox("has rating", NULL, + new BMessage(MSG_RATING_DETERMINATE_CHANGED)); + fRatingDeterminateCheckBox->SetValue(B_CONTROL_OFF); fTextView = new TextDocumentView(); ScrollView* textScrollView = new ScrollView( @@ -258,7 +285,10 @@ RatePackageWindow::RatePackageWindow(BWindow* parent, BRect frame, BLayoutBuilder::Group<>(this, B_VERTICAL) .AddGrid() .Add(ratingLabel, 0, 0) - .Add(fSetRatingView, 1, 0) + .AddGroup(B_HORIZONTAL, B_USE_DEFAULT_SPACING, 1, 0) + .Add(fRatingDeterminateCheckBox) + .Add(fSetRatingView) + .End() .AddMenuField(fStabilityField, 0, 1) .AddMenuField(fCommentLanguageField, 0, 2) .End() @@ -284,12 +314,31 @@ RatePackageWindow::~RatePackageWindow() } +void +RatePackageWindow::DispatchMessage(BMessage* message, BHandler *handler) +{ + if (message->what == B_KEY_DOWN) { + int8 key; + // if the user presses escape, close the window. + if ((message->FindInt8("byte", &key) == B_OK) + && key == B_ESCAPE) { + Quit(); + return; + } + } + + BWindow::DispatchMessage(message, handler); +} + + void RatePackageWindow::MessageReceived(BMessage* message) { switch (message->what) { case MSG_PACKAGE_RATED: message->FindFloat("rating", &fRating); + fSetRatingView->SetRatingDeterminate(true); + fRatingDeterminateCheckBox->SetValue(B_CONTROL_ON); break; case MSG_STABILITY_SELECTED: @@ -300,6 +349,11 @@ RatePackageWindow::MessageReceived(BMessage* message) message->FindString("code", &fCommentLanguage); break; + case MSG_RATING_DETERMINATE_CHANGED: + fSetRatingView->SetRatingDeterminate( + fRatingDeterminateCheckBox->Value() == B_CONTROL_ON); + break; + case MSG_RATING_ACTIVE_CHANGED: { int32 value; @@ -308,6 +362,32 @@ RatePackageWindow::MessageReceived(BMessage* message) break; } + case MSG_DID_ADD_USER_RATING: + { + BAlert* alert = new(std::nothrow) BAlert( + B_TRANSLATE("user_rating"), + B_TRANSLATE("Your rating was uploaded successfully. " + "You can update or remove it at any time by visiting the " + "server application on the web."), + B_TRANSLATE("Close"), NULL, NULL, + B_WIDTH_AS_USUAL, B_WARNING_ALERT); + alert->Go(); + _RefreshPackageData(); + break; + } + + case MSG_DID_UPDATE_USER_RATING: + { + BAlert* alert = new(std::nothrow) BAlert( + B_TRANSLATE("user_rating"), + B_TRANSLATE("Your rating was updated."), + B_TRANSLATE("Close"), NULL, NULL, + B_WIDTH_AS_USUAL, B_WARNING_ALERT); + alert->Go(); + _RefreshPackageData(); + break; + } + case MSG_SEND: _SendRating(); break; @@ -318,6 +398,20 @@ RatePackageWindow::MessageReceived(BMessage* message) } } +/*! Refresh the data shown about the current page. This may be useful, for + example when somebody adds a rating and that changes the rating of the + package or they add a rating and want to see that immediately. The logic + should round-trip to the server so that actual data is shown. +*/ + +void +RatePackageWindow::_RefreshPackageData() +{ + BMessage message(MSG_SERVER_DATA_CHANGED); + message.AddString("name", fPackage->Name()); + be_app->PostMessage(&message); +} + void RatePackageWindow::SetPackage(const PackageInfoRef& package) @@ -360,8 +454,6 @@ RatePackageWindow::_SetWorkerThread(thread_id thread) bool enabled = thread < 0; -// fTextEditor->SetEnabled(enabled); -// fSetRatingView->SetEnabled(enabled); fStabilityField->SetEnabled(enabled); fCommentLanguageField->SetEnabled(enabled); fSendButton->SetEnabled(enabled); @@ -386,6 +478,68 @@ RatePackageWindow::_QueryRatingThreadEntry(void* data) } +/*! A server request has been made to the server and the server has responded + with some data. The data is known not to be an error and now the data can + be extracted into the user interface elements. +*/ + +void +RatePackageWindow::_RelayServerDataToUI(BMessage& response) +{ + if (Lock()) { + response.FindString("code", &fRatingID); + response.FindBool("active", &fRatingActive); + BString comment; + if (response.FindString("comment", &comment) == B_OK) { + MarkupParser parser; + fRatingText = parser.CreateDocumentFromMarkup(comment); + fTextView->SetTextDocument(fRatingText); + } + if (response.FindString("userRatingStabilityCode", + &fStability) == B_OK) { + int32 index = 0; + for (int32 i = fStabilityCodes.CountItems() - 1; i >= 0; i--) { + const StabilityRating& stability + = fStabilityCodes.ItemAtFast(i); + if (stability.Name() == fStability) { + index = i; + break; + } + } + BMenuItem* item = fStabilityField->Menu()->ItemAt(index); + if (item != NULL) + item->SetMarked(true); + } + if (response.FindString("naturalLanguageCode", + &fCommentLanguage) == B_OK) { + BMenuItem* item = fCommentLanguageField->Menu()->ItemAt( + fModel.SupportedLanguages().IndexOf(fCommentLanguage)); + if (item != NULL) + item->SetMarked(true); + } + double rating; + if (response.FindDouble("rating", &rating) == B_OK) { + fRating = (float)rating; + fSetRatingView->SetPermanentRating(fRating); + fSetRatingView->SetRatingDeterminate(true); + fRatingDeterminateCheckBox->SetValue(B_CONTROL_ON); + } else { + fSetRatingView->SetRatingDeterminate(false); + fRatingDeterminateCheckBox->SetValue(B_CONTROL_OFF); + } + + fRatingActiveCheckBox->SetValue(fRatingActive); + fRatingActiveCheckBox->Show(); + + fSendButton->SetLabel(B_TRANSLATE("Update")); + + Unlock(); + } else { + fprintf(stderr, "unable to acquire lock to update the ui\n"); + } +} + + void RatePackageWindow::_QueryRatingThread() { @@ -416,67 +570,50 @@ RatePackageWindow::_QueryRatingThread() if (depot != NULL) repositoryCode = depot->WebAppRepositoryCode(); - if (repositoryCode.Length() == 0) { + if (repositoryCode.IsEmpty()) { printf("unable to obtain the repository code for depot; %s\n", package->DepotName().String()); + BMessenger(this).SendMessage(B_QUIT_REQUESTED); } else { status_t status = interface.RetrieveUserRating( package->Name(), package->Version(), package->Architecture(), repositoryCode, username, info); - // info.PrintToStream(); - - BMessage result; - if (status == B_OK && info.FindMessage("result", &result) == B_OK - && Lock()) { - - result.FindString("code", &fRatingID); - result.FindBool("active", &fRatingActive); - BString comment; - if (result.FindString("comment", &comment) == B_OK) { - MarkupParser parser; - fRatingText = parser.CreateDocumentFromMarkup(comment); - fTextView->SetTextDocument(fRatingText); - } - if (result.FindString("userRatingStabilityCode", - &fStability) == B_OK) { - int32 index = 0; - for (int32 i = fStabilityCodes.CountItems() - 1; i >= 0; i--) { - const StabilityRating& stability - = fStabilityCodes.ItemAtFast(i); - if (stability.Name() == fStability) { - index = i; - break; + if (status == B_OK) { + // could be an error or could be a valid response envelope + // containing data. + switch (interface.ErrorCodeFromResponse(info)) { + case ERROR_CODE_NONE: + { + //info.PrintToStream(); + BMessage result; + if (info.FindMessage("result", &result) == B_OK) { + _RelayServerDataToUI(result); + } else { + fprintf(stderr, "bad response envelope missing 'result'" + "entry\n"); + ServerHelper::NotifyTransportError(B_BAD_VALUE); + BMessenger(this).SendMessage(B_QUIT_REQUESTED); } + break; } - BMenuItem* item = fStabilityField->Menu()->ItemAt(index); - if (item != NULL) - item->SetMarked(true); + case ERROR_CODE_OBJECTNOTFOUND: + // an expected response + fprintf(stderr, "there was no previous rating for this" + " user on this version of this package so a new rating" + " will be added.\n"); + break; + default: + ServerHelper::NotifyServerJsonRpcError(info); + BMessenger(this).SendMessage(B_QUIT_REQUESTED); + break; } - if (result.FindString("naturalLanguageCode", - &fCommentLanguage) == B_OK) { - BMenuItem* item = fCommentLanguageField->Menu()->ItemAt( - fModel.SupportedLanguages().IndexOf(fCommentLanguage)); - if (item != NULL) - item->SetMarked(true); - } - double rating; - if (result.FindDouble("rating", &rating) == B_OK) { - fRating = (float)rating; - fSetRatingView->SetPermanentRating(fRating); - } - - fRatingActiveCheckBox->SetValue(fRatingActive); - fRatingActiveCheckBox->Show(); - - fSendButton->SetLabel(B_TRANSLATE("Update")); - - Unlock(); } else { - fprintf(stderr, "rating query: Failed response: %s\n", + fprintf(stderr, "an error has arisen communicating with the" + " server to obtain data for an existing rating [%s]\n", strerror(status)); - if (!info.IsEmpty()) - info.PrintToStream(); + ServerHelper::NotifyTransportError(status); + BMessenger(this).SendMessage(B_QUIT_REQUESTED); } } @@ -501,6 +638,7 @@ RatePackageWindow::_SendRatingThread() return; } + BMessenger messenger = BMessenger(this); BString package = fPackage->Name(); BString architecture = fPackage->Architecture(); BString repositoryCode; @@ -533,86 +671,41 @@ RatePackageWindow::_SendRatingThread() status_t status; BMessage info; if (ratingID.Length() > 0) { + printf("will update the existing user rating [%s]\n", + ratingID.String()); status = interface.UpdateUserRating(ratingID, - languageCode, comment, stability, rating, active, info); + languageCode, comment, stability, rating, active, info); } else { - status = interface.CreateUserRating(package, architecture, - repositoryCode, languageCode, comment, stability, rating, info); + printf("will create a new user rating for pkg [%s]\n", + package.String()); + status = interface.CreateUserRating(package, fPackage->Version(), + architecture, repositoryCode, languageCode, comment, stability, + rating, info); } - BString error = B_TRANSLATE( - "There was a puzzling response from the web service."); - - BMessage result; if (status == B_OK) { - if (info.FindMessage("result", &result) == B_OK) { - error = ""; - } else if (info.FindMessage("error", &result) == B_OK) { - result.PrintToStream(); - BString message; - if (result.FindString("message", &message) == B_OK) { - if (message == "objectnotfound") { - error = B_TRANSLATE("The package was not found by the " - "web service. This probably means that it comes " - "from a depot which is not tracked there. Rating " - "such packages is unfortunately not supported."); - } else { - error << B_TRANSLATE(" It responded with: "); - error << message; - } + // could be an error or could be a valid response envelope + // containing data. + switch (interface.ErrorCodeFromResponse(info)) { + case ERROR_CODE_NONE: + { + if (ratingID.Length() > 0) + messenger.SendMessage(MSG_DID_UPDATE_USER_RATING); + else + messenger.SendMessage(MSG_DID_ADD_USER_RATING); + break; } + default: + ServerHelper::NotifyServerJsonRpcError(info); + break; } } else { - error = B_TRANSLATE( - "It was not possible to contact the web service."); + fprintf(stderr, "an error has arisen communicating with the" + " server to obtain data for an existing rating [%s]\n", + strerror(status)); + ServerHelper::NotifyTransportError(status); } - if (!error.IsEmpty()) { - BString failedTitle; - if (ratingID.Length() > 0) - failedTitle = B_TRANSLATE("Failed to update rating"); - else - failedTitle = B_TRANSLATE("Failed to rate package"); - - BAlert* alert = new(std::nothrow) BAlert( - failedTitle, - error, - B_TRANSLATE("Close"), NULL, NULL, - B_WIDTH_AS_USUAL, B_WARNING_ALERT); - - if (alert != NULL) - alert->Go(); - - fprintf(stderr, - B_TRANSLATE("Failed to create or update rating: %s\n"), - error.String()); - if (!info.IsEmpty()) - info.PrintToStream(); - - _SetWorkerThread(-1); - } else { - _SetWorkerThread(-1); - - fModel.PopulatePackage(fPackage, - Model::POPULATE_FORCE | Model::POPULATE_USER_RATINGS); - - BMessenger(this).SendMessage(B_QUIT_REQUESTED); - - BString message; - if (ratingID.Length() > 0) { - message = B_TRANSLATE("Your rating was updated successfully."); - } else { - message = B_TRANSLATE("Your rating was uploaded successfully. " - "You can update or remove it at any time by rating the " - "package again."); - } - - BAlert* alert = new(std::nothrow) BAlert( - B_TRANSLATE("Success"), - message, - B_TRANSLATE("Close")); - - if (alert != NULL) - alert->Go(); - } + messenger.SendMessage(B_QUIT_REQUESTED); + _SetWorkerThread(-1); } diff --git a/src/apps/haikudepot/ui/RatePackageWindow.h b/src/apps/haikudepot/ui/RatePackageWindow.h index 1835df97a4..b5c6889f79 100644 --- a/src/apps/haikudepot/ui/RatePackageWindow.h +++ b/src/apps/haikudepot/ui/RatePackageWindow.h @@ -1,5 +1,6 @@ /* * Copyright 2014, Stephan Aßmus . + * Copyright 2018, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ #ifndef RATE_PACKAGE_WINDOW_H @@ -26,11 +27,15 @@ public: Model& model); virtual ~RatePackageWindow(); + virtual void DispatchMessage(BMessage* message, + BHandler *handler); virtual void MessageReceived(BMessage* message); void SetPackage(const PackageInfoRef& package); private: + void _RelayServerDataToUI(BMessage& result); + void _SendRating(); void _SetWorkerThread(thread_id thread); @@ -41,6 +46,8 @@ private: static int32 _SendRatingThreadEntry(void* data); void _SendRatingThread(); + void _RefreshPackageData(); + private: Model& fModel; TextDocumentRef fRatingText; @@ -54,6 +61,7 @@ private: PackageInfoRef fPackage; SetRatingView* fSetRatingView; + BCheckBox* fRatingDeterminateCheckBox; BMenuField* fStabilityField; BMenuField* fCommentLanguageField; TextDocumentView* fTextView; diff --git a/src/apps/haikudepot/ui/ScreenshotWindow.cpp b/src/apps/haikudepot/ui/ScreenshotWindow.cpp index 48a341e485..3ef69a0f84 100644 --- a/src/apps/haikudepot/ui/ScreenshotWindow.cpp +++ b/src/apps/haikudepot/ui/ScreenshotWindow.cpp @@ -17,6 +17,7 @@ #include "BarberPole.h" #include "BitmapView.h" +#include "HaikuDepotConstants.h" #include "WebAppInterface.h" @@ -29,9 +30,9 @@ static const rgb_color kBackgroundColor = { 51, 102, 152, 255 }; // transparent regions static BitmapRef sNextButtonIcon( - new(std::nothrow) SharedBitmap(505), true); + new(std::nothrow) SharedBitmap(RSRC_ARROW_LEFT), true); static BitmapRef sPreviousButtonIcon( - new(std::nothrow) SharedBitmap(506), true); + new(std::nothrow) SharedBitmap(RSRC_ARROW_RIGHT), true); ScreenshotWindow::ScreenshotWindow(BWindow* parent, BRect frame) diff --git a/src/apps/haikudepot/ui_generic/RatingView.cpp b/src/apps/haikudepot/ui_generic/RatingView.cpp index bd733103c0..ca0c001b23 100644 --- a/src/apps/haikudepot/ui_generic/RatingView.cpp +++ b/src/apps/haikudepot/ui_generic/RatingView.cpp @@ -1,5 +1,6 @@ /* * Copyright 2013-2014, Stephan Aßmus . + * Copyright 2018, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ @@ -10,12 +11,15 @@ #include +#include "HaikuDepotConstants.h" + RatingView::RatingView(const char* name) : BView(name, B_WILL_DRAW), - fStarBitmap(501), - fRating(-1.0f) + fStarBlueBitmap(RSRC_STAR_BLUE), + fStarGrayBitmap(RSRC_STAR_GREY), + fRating(RATING_MISSING) { SetViewUIColor(B_PANEL_BACKGROUND_COLOR); SetLowUIColor(ViewUIColor()); @@ -33,16 +37,26 @@ RatingView::AttachedToWindow() AdoptParentColors(); } +/*! This method will return a star image that can be used repeatedly in the + user interface in order to signify the rating given by a user. It could + be grey if no rating is assigned. +*/ + +const BBitmap* +RatingView::StarBitmap() +{ + if (fRating < RATING_MIN) + return fStarGrayBitmap.Bitmap(SharedBitmap::SIZE_16); + return fStarBlueBitmap.Bitmap(SharedBitmap::SIZE_16); +} + void RatingView::Draw(BRect updateRect) { FillRect(updateRect, B_SOLID_LOW); + const BBitmap* star = StarBitmap(); - if (fRating < 0.0f) - return; - - const BBitmap* star = fStarBitmap.Bitmap(SharedBitmap::SIZE_16); if (star == NULL) { fprintf(stderr, "No star icon found in application resources.\n"); return; @@ -56,21 +70,20 @@ RatingView::Draw(BRect updateRect) x += 16 + 2; } - if (fRating >= 5.0f) - return; + if (fRating >= RATING_MIN && fRating < 5.0f) { + SetDrawingMode(B_OP_OVER); - SetDrawingMode(B_OP_OVER); + BRect rect(Bounds()); + rect.right = x - 2; + rect.left = ceilf(rect.left + (fRating / 5.0f) * rect.Width()); - BRect rect(Bounds()); - rect.right = x - 2; - rect.left = ceilf(rect.left + (fRating / 5.0f) * rect.Width()); + rgb_color color = LowColor(); + color.alpha = 190; + SetHighColor(color); - rgb_color color = LowColor(); - color.alpha = 190; - SetHighColor(color); - - SetDrawingMode(B_OP_ALPHA); - FillRect(rect, B_SOLID_HIGH); + SetDrawingMode(B_OP_ALPHA); + FillRect(rect, B_SOLID_HIGH); + } } diff --git a/src/apps/haikudepot/ui_generic/RatingView.h b/src/apps/haikudepot/ui_generic/RatingView.h index 3f439ead1e..05abe99cda 100644 --- a/src/apps/haikudepot/ui_generic/RatingView.h +++ b/src/apps/haikudepot/ui_generic/RatingView.h @@ -1,5 +1,6 @@ /* * Copyright 2013-2014, Stephan Aßmus . + * Copyright 2018, Andrew Lindesay . * All rights reserved. Distributed under the terms of the MIT License. */ #ifndef RATING_VIEW_H @@ -26,8 +27,12 @@ public: void SetRating(float rating); float Rating() const; +protected: + virtual const BBitmap* StarBitmap(); + SharedBitmap fStarBlueBitmap; + SharedBitmap fStarGrayBitmap; + private: - SharedBitmap fStarBitmap; float fRating; };