HaikuDepot: Multiple improvements for user-ratings

* Display of the user-ratings listing improved
* When a user-rating is created / edited, the pkg is updated
* Creation date of the user-rating is unpacked shown
* Ability to create a user-rating with a comment, but no numerical rating
* Stars display show grey if no numerical rating present
* Improvements to error reporting when problem arise
* Parsing of the 'revision' field of the version working
* Removed debug logging for the text engine
* Other minor tweaks

Change-Id: I99f881ab1426641ef4177eec2d3bcacc7cb74e95
This commit is contained in:
Andrew Lindesay
2018-06-09 17:07:03 +00:00
committed by waddlesplash
parent 466255e324
commit a9edb9bffa
24 changed files with 913 additions and 384 deletions
Binary file not shown.
Binary file not shown.
+9 -18
View File
@@ -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"
+24 -2
View File
@@ -2,7 +2,8 @@
* Copyright 2018, Andrew Lindesay <[email protected]>.
* 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)
#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
+52 -8
View File
@@ -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", &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());
}
}
+13 -5
View File
@@ -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;
}
+7 -2
View File
@@ -1,6 +1,6 @@
/*
* Copyright 2013-2014, Stephan Aßmus <[email protected]>.
* Copyright 2016-2017, Andrew Lindesay <[email protected]>.
* Copyright 2016-2018, Andrew Lindesay <[email protected]>.
* All rights reserved. Distributed under the terms of the MIT License.
*/
#ifndef PACKAGE_INFO_H
@@ -12,6 +12,7 @@
#include <Referenceable.h>
#include <package/PackageInfo.h>
#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;
};
@@ -1,5 +1,5 @@
/*
* Copyright 2017, Andrew Lindesay <[email protected]>.
* Copyright 2017-2018, Andrew Lindesay <[email protected]>.
* 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());
+115
View File
@@ -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)
{
+11 -1
View File
@@ -7,7 +7,9 @@
#define SERVER_HELPER_H
#include <HttpHeaders.h>
#include <Message.h>
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
+209 -64
View File
@@ -10,11 +10,13 @@
#include <AppFileInfo.h>
#include <Application.h>
#include <AutoDeleter.h>
#include <Autolock.h>
#include <File.h>
#include <HttpHeaders.h>
#include <HttpRequest.h>
#include <Json.h>
#include <JsonTextWriter.h>
#include <Message.h>
#include <Roster.h>
#include <Url.h>
@@ -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);
}
@@ -8,6 +8,7 @@
#include <Application.h>
#include <JsonWriter.h>
#include <String.h>
#include <package/PackageVersion.h>
@@ -21,6 +22,18 @@ using BPackageKit::BPackageVersion;
typedef List<BString, false> 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;
@@ -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
@@ -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;
+12
View File
@@ -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;
@@ -17,6 +17,7 @@
#include <SpaceLayoutItem.h>
#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
+92 -11
View File
@@ -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<BLocker> 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<BLocker> 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<BLocker> 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"),
+4
View File
@@ -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;
+60 -106
View File
@@ -1,5 +1,6 @@
/*
* Copyright 2013-2014, Stephan Aßmus <[email protected]>.
* Copyright 2018, Andrew Lindesay <[email protected]>.
* All rights reserved. Distributed under the terms of the MIT License.
*/
@@ -15,6 +16,7 @@
#include <CardLayout.h>
#include <Catalog.h>
#include <ColumnListView.h>
#include <DateFormat.h>
#include <Font.h>
#include <GridView.h>
#include <LayoutBuilder.h>
@@ -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<BLayoutBuilder::Group<void*> > 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<BLayoutBuilder::Group<
BLayoutBuilder::Group<void *> > > 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();
}
}
+224 -131
View File
@@ -1,6 +1,6 @@
/*
* Copyright 2014, Stephan Aßmus <superstippi@gmx.de>.
* Copyright 2016, Andrew Lindesay <apl@lindesay.co.nz>.
* Copyright 2016-2018, Andrew Lindesay <apl@lindesay.co.nz>.
* All rights reserved. Distributed under the terms of the MIT License.
*/
@@ -21,8 +21,10 @@
#include <ScrollView.h>
#include <StringView.h>
#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);
}
@@ -1,5 +1,6 @@
/*
* Copyright 2014, Stephan Aßmus <superstippi@gmx.de>.
* Copyright 2018, Andrew Lindesay <apl@lindesay.co.nz>.
* 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;
+3 -2
View File
@@ -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)
+31 -18
View File
@@ -1,5 +1,6 @@
/*
* Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
* Copyright 2018, Andrew Lindesay <apl@lindesay.co.nz>.
* All rights reserved. Distributed under the terms of the MIT License.
*/
@@ -10,12 +11,15 @@
#include <LayoutUtils.h>
#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);
}
}
+6 -1
View File
@@ -1,5 +1,6 @@
/*
* Copyright 2013-2014, Stephan Aßmus <superstippi@gmx.de>.
* Copyright 2018, Andrew Lindesay <apl@lindesay.co.nz>.
* 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;
};