Primfeed integration V1

baseline Primfeed authentication/authorisation - working
chat interception login authentication - working
Primfeed Integration - WiP Authorisation connect/disconnect - working
Primfeed post photo support - working
Clean up ready for release
master
Beq 2025-03-08 17:17:21 +00:00
parent fb7592f1ca
commit c085682b54
28 changed files with 2784 additions and 10 deletions

View File

@ -96,6 +96,7 @@ set(viewer_SOURCE_FILES
dialogstack.cpp
exoflickr.cpp
exoflickrauth.cpp
fsprimfeedauth.cpp
exogroupmutelist.cpp
floatermedialists.cpp
fsareasearch.cpp
@ -164,6 +165,7 @@ set(viewer_SOURCE_FILES
fspose.cpp
fsposeranimator.cpp
fsposingmotion.cpp
fsprimfeedauth.cpp
fsradar.cpp
fsradarentry.cpp
fsradarlistctrl.cpp
@ -182,6 +184,8 @@ set(viewer_SOURCE_FILES
lfsimfeaturehandler.cpp
llflickrconnect.cpp
llfloaterflickr.cpp
fsprimfeedconnect.cpp
fsfloaterprimfeed.cpp
llpanelopenregionsettings.cpp
# <FS:Ansariel> [Legacy Bake]
llagentwearablesfetch.cpp
@ -909,6 +913,7 @@ set(viewer_HEADER_FILES
dialogstack.h
exoflickr.h
exoflickrauth.h
fsprimfeedauth.h
exogroupmutelist.h
floatermedialists.h
fsareasearch.h
@ -979,6 +984,7 @@ set(viewer_HEADER_FILES
fspose.h
fsposeranimator.h
fsposingmotion.h
fsprimfeedauth.h
fsradar.h
fsradarentry.h
fsradarlistctrl.h
@ -998,6 +1004,8 @@ set(viewer_HEADER_FILES
lfsimfeaturehandler.h
llflickrconnect.h
llfloaterflickr.h
fsprimfeedconnect.h
fsfloaterprimfeed.h
# <FS:Ansariel> [Legacy Bake]
llagentwearablesfetch.h
vjlocalmesh.h

View File

@ -254,6 +254,16 @@
is_running_function="Floater.IsOpen"
is_running_parameters="flickr"
/>
<command name="primfeed"
available_in_toybox="true"
icon="Command_Primfeed_Icon"
label_ref="Command_Primfeed_Label"
tooltip_ref="Command_Primfeed_Tooltip"
execute_function="Floater.Toggle"
execute_parameters="primfeed"
is_running_function="Floater.IsOpen"
is_running_parameters="primfeed"
/>
<command name="speak"
available_in_toybox="true"
icon="Command_Speak_Icon"

View File

@ -153,6 +153,17 @@
<key>Value</key>
<string>http://phoenixviewer.com/app/fsdata/grids.xml</string>
</map>
<key>FSPrimfeedViewerApiKey</key>
<map>
<key>Comment</key>
<string>Viewer key for API login.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>String</string>
<key>Value</key>
<string>xAcXYt8SBius3Lor4wHle8L96PDHYlAZuWYXIYQUdW4b09mjhQUAwiqmWp5UNYXLpq5GSUtuKHuDYLwaueACPkew93l6MRY8jfBKSH09kv0zyGglpky07X7X7Sp4Rzin</string>
</map>
<key>FSGridBuilderURL</key>
<map>
<key>Comment</key>
@ -18542,6 +18553,7 @@ Change of this parameter will affect the layout of buttons in notification toast
<string>world_map</string>
<string>preferences</string>
<string>flickr</string>
<string>primfeed</string>
</array>
<key>Backup</key>
<integer>0</integer>
@ -24729,6 +24741,39 @@ Change of this parameter will affect the layout of buttons in notification toast
<key>Value</key>
<integer>4</integer>
</map>
<key>FSLastSnapshotToPrimfeedHeight</key>
<map>
<key>Comment</key>
<string>The height of the last Primfeed snapshot, in px</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>S32</string>
<key>Value</key>
<integer>768</integer>
</map>
<key>FSLastSnapshotToPrimfeedWidth</key>
<map>
<key>Comment</key>
<string>The width of the last Primfeed snapshot, in px</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>S32</string>
<key>Value</key>
<integer>1024</integer>
</map>
<key>FSLastSnapshotToPrimfeedResolution</key>
<map>
<key>Comment</key>
<string>At what resolution should snapshots be posted on Primfeed. 0=Current Window, 1=320x240, 2=640x480, 3=800x600, 4=1024x768, 5=1280x1024, 6=1600x1200, 7=Custom</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>S32</string>
<key>Value</key>
<integer>4</integer>
</map>
<key>FSLastSnapshotToTwitterHeight</key>
<map>
<key>Comment</key>

View File

@ -1358,6 +1358,94 @@
<key>Value</key>
<integer>0</integer>
</map>
<key>FSPrimfeedOAuthToken</key>
<map>
<key>Comment</key>
<string>contains the secure authentication toke to post to your primfeed account (do not share)</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>String</string>
<key>Value</key>
<string></string>
</map>
<key>FSPrimfeedProfileLink</key>
<map>
<key>Comment</key>
<string>The profile page for the account associated with the currently linked Primfeed account</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>String</string>
<key>Value</key>
<string></string>
</map>
<key>FSPrimfeedPlan</key>
<map>
<key>Comment</key>
<string>The plan type associated with the currently linked Primfeed account</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>String</string>
<key>Value</key>
<string></string>
</map>
<key>FSPrimfeedUsername</key>
<map>
<key>Comment</key>
<string>The username associated with the currently linked Primfeed account</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>String</string>
<key>Value</key>
<string></string>
</map>
<key>FSPrimfeedCommercialContent</key>
<map>
<key>Comment</key>
<string>Does this post contain commercial content</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>Boolean</string>
<key>Value</key>
<integer>0</integer>
</map>
<key>FSPrimfeedAddToPublicGallery</key>
<map>
<key>Comment</key>
<string>Should this post go to the public gallery?</string>
<key>Persist</key>
<integer>0</integer>
<key>Type</key>
<string>Boolean</string>
<key>Value</key>
<integer>0</integer>
</map>
<key>FSPrimfeedAddToPublicGallery</key>
<map>
<key>Comment</key>
<string>Should this post go to the public gallery?</string>
<key>Persist</key>
<integer>0</integer>
<key>Type</key>
<string>Boolean</string>
<key>Value</key>
<integer>0</integer>
</map>
<key>FSPrimfeedOpenURLOnPost</key>
<map>
<key>Comment</key>
<string>if true open the URL in a browser when the post completes</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>Boolean</string>
<key>Value</key>
<integer>0</integer>
</map>
<key>FSProtectedFolders</key>
<map>
<key>Comment</key>
@ -1369,5 +1457,16 @@
<key>Value</key>
<array/>
</map>
<key>FSPrimfeedPhotoRating</key>
<map>
<key>Comment</key>
<string>Content rating to be shared with Primfeed.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>Integer</string>
<key>Value</key>
<integer>1</integer>
</map>
</map>
</llsd>

View File

@ -0,0 +1,910 @@
/**
* @file fsfloaterprimfeed.cpp
* @brief Implementation of primfeed floater
* @author beq@firestorm
*
* $LicenseInfo:firstyear=2025&license=fsviewerlgpl$
* Phoenix Firestorm Viewer Source Code
* Copyright (C) 2025, Beq Janus
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* The Phoenix Firestorm Project, Inc., 1831 Oakwood Drive, Fairmont, Minnesota 56031-3225 USA
* http://www.firestormviewer.org
* $/LicenseInfo$
*/
#include "llviewerprecompiledheaders.h"
#include "fsfloaterprimfeed.h"
#include "fsprimfeedconnect.h"
#include "llagent.h"
#include "llagentui.h"
#include "llcheckboxctrl.h"
#include "llcombobox.h"
#include "llfloaterreg.h"
#include "lliconctrl.h"
#include "llimagefiltersmanager.h"
#include "llresmgr.h" // LLLocale
#include "llsdserialize.h"
#include "llloadingindicator.h"
#include "llslurl.h"
#include "lltrans.h"
#include "llfloatersnapshot.h"
#include "llsnapshotlivepreview.h"
#include "llfloaterbigpreview.h"
#include "llviewerregion.h"
#include "llviewercontrol.h"
#include "llviewermedia.h"
#include "lltabcontainer.h"
#include "llviewerparcelmgr.h"
#include "llviewerregion.h"
#include <boost/regex.hpp>
#include "llspinctrl.h"
#include "llviewernetwork.h"
#include "llnotificationsutil.h"
#include "fsprimfeedauth.h"
#include "llviewernetwork.h"
static LLPanelInjector<FSPrimfeedPhotoPanel> t_panel_photo("fsprimfeedphotopanel");
static LLPanelInjector<FSPrimfeedAccountPanel> t_panel_account("fsprimfeedaccountpanel");
///////////////////////////
//FSPrimfeedPhotoPanel/////
///////////////////////////
FSPrimfeedPhotoPanel::FSPrimfeedPhotoPanel() :
mResolutionComboBox(nullptr),
mRefreshBtn(nullptr),
mBtnPreview(nullptr),
mWorkingLabel(nullptr),
mThumbnailPlaceholder(nullptr),
mDescriptionTextBox(nullptr),
mLocationCheckbox(nullptr),
mRatingComboBox(nullptr),
mBigPreviewFloater(nullptr),
mPostButton(nullptr)
{
mCommitCallbackRegistrar.add("SocialSharing.SendPhoto", [this](LLUICtrl* ctrl, const LLSD& data) { onSend(); });
mCommitCallbackRegistrar.add("SocialSharing.RefreshPhoto", [this](LLUICtrl* ctrl, const LLSD& data) { onClickNewSnapshot(); });
mCommitCallbackRegistrar.add("SocialSharing.BigPreview", [this](LLUICtrl* ctrl, const LLSD& data) { onClickBigPreview(); });
mCommitCallbackRegistrar.add("Primfeed.Info", [this](LLUICtrl* ctrl, const LLSD& param) {
const std::string url = param.asString();
LL_DEBUGS("primfeed") << "Info button clicked, opening " << url << LL_ENDL;
LLWeb::loadURLExternal(url);
});
}
FSPrimfeedPhotoPanel::~FSPrimfeedPhotoPanel()
{
if(mPreviewHandle.get())
{
mPreviewHandle.get()->die();
}
FSPrimfeedAuth::sPrimfeedAuthPump->stopListening("FSPrimfeedAccountPanel");
gSavedSettings.setS32("FSLastSnapshotToPrimfeedResolution", getChild<LLComboBox>("resolution_combobox")->getCurrentIndex());
gSavedSettings.setS32("FSLastSnapshotToPrimfeedWidth", getChild<LLSpinCtrl>("custom_snapshot_width")->getValue().asInteger());
gSavedSettings.setS32("FSLastSnapshotToPrimfeedHeight", getChild<LLSpinCtrl>("custom_snapshot_height")->getValue().asInteger());
}
bool FSPrimfeedPhotoPanel::postBuild()
{
setVisibleCallback([this](LLUICtrl * unused, bool visible) {
onVisibilityChange(visible);
});
mResolutionComboBox = getChild<LLUICtrl>("resolution_combobox");
mResolutionComboBox->setCommitCallback([this](LLUICtrl *, const LLSD&) { updateResolution(true); });
mFilterComboBox = getChild<LLUICtrl>("filters_combobox");
mFilterComboBox->setCommitCallback([this](LLUICtrl *, const LLSD&) { updateResolution(true); });
mRefreshBtn = getChild<LLUICtrl>("new_snapshot_btn");
mBtnPreview = getChild<LLButton>("big_preview_btn");
mWorkingLabel = getChild<LLUICtrl>("working_lbl");
mThumbnailPlaceholder = getChild<LLUICtrl>("thumbnail_placeholder");
mDescriptionTextBox = getChild<LLUICtrl>("photo_description");
mLocationCheckbox = getChild<LLUICtrl>("add_location_cb");
mCommercialCheckbox = getChild<LLUICtrl>("primfeed_commercial_content");
mPublicGalleryCheckbox = getChild<LLUICtrl>("primfeed_add_to_public_gallery");
mRatingComboBox = getChild<LLUICtrl>("rating_combobox");
mPostButton = getChild<LLUICtrl>("post_photo_btn");
mCancelButton = getChild<LLUICtrl>("cancel_photo_btn");
mBigPreviewFloater = dynamic_cast<LLFloaterBigPreview*>(LLFloaterReg::getInstance("big_preview"));
// Update custom resolution controls with lambdas
getChild<LLSpinCtrl>("custom_snapshot_width")->setCommitCallback([this](LLUICtrl *, const LLSD&) { updateResolution(true); });
getChild<LLSpinCtrl>("custom_snapshot_height")->setCommitCallback([this](LLUICtrl *, const LLSD&) { updateResolution(true); });
getChild<LLCheckBoxCtrl>("keep_aspect_ratio")->setCommitCallback([this](LLUICtrl *, const LLSD&) { updateResolution(true); });
getChild<LLComboBox>("resolution_combobox")->setCurrentByIndex(gSavedSettings.getS32("FSLastSnapshotToPrimfeedResolution"));
getChild<LLSpinCtrl>("custom_snapshot_width")->setValue(gSavedSettings.getS32("FSLastSnapshotToPrimfeedWidth"));
getChild<LLSpinCtrl>("custom_snapshot_height")->setValue(gSavedSettings.getS32("FSLastSnapshotToPrimfeedHeight"));
// Update filter list
std::vector<std::string> filter_list = LLImageFiltersManager::getInstance()->getFiltersList();
LLComboBox* filterbox = static_cast<LLComboBox *>(mFilterComboBox);
for (U32 i = 0; i < filter_list.size(); i++)
{
filterbox->add(filter_list[i]);
}
return LLPanel::postBuild();
}
//static
void FSFloaterPrimfeed::update()
{
if (LLFloaterReg::instanceVisible("primfeed"))
{
LLFloaterSnapshotBase::ImplBase::updatePreviewList( true, true );
}
}
// virtual
S32 FSPrimfeedPhotoPanel::notify(const LLSD& info)
{
if (info.has("snapshot-updating"))
{
// Disable the Post button and whatever else while the snapshot is not updated
// updateControls();
return 1;
}
if (info.has("snapshot-updated"))
{
// Enable the send/post/save buttons.
updateControls();
// The refresh button is initially hidden. We show it after the first update,
// i.e. after snapshot is taken
LLUICtrl * refresh_button = getRefreshBtn();
if (!refresh_button->getVisible())
{
refresh_button->setVisible(true);
}
return 1;
}
return 0;
}
void FSPrimfeedPhotoPanel::draw()
{
LLSnapshotLivePreview * previewp = static_cast<LLSnapshotLivePreview *>(mPreviewHandle.get());
// Enable interaction only if no transaction with the service is on-going (prevent duplicated posts)
auto can_post = !(FSPrimfeedConnect::instance().isTransactionOngoing()) && FSPrimfeedAuth::isAuthorized();
mCancelButton->setEnabled(can_post);
mDescriptionTextBox->setEnabled(can_post);
mRatingComboBox->setEnabled(can_post);
mResolutionComboBox->setEnabled(can_post);
mFilterComboBox->setEnabled(can_post);
mRefreshBtn->setEnabled(can_post);
mBtnPreview->setEnabled(can_post);
mLocationCheckbox->setEnabled(can_post);
// Reassign the preview floater if we have the focus and the preview exists
if (hasFocus() && isPreviewVisible())
{
attachPreview();
}
// Toggle the button state as appropriate
bool preview_active = (isPreviewVisible() && mBigPreviewFloater->isFloaterOwner(getParentByType<LLFloater>()));
mBtnPreview->setToggleState(preview_active);
// Display the preview if one is available
if (previewp && previewp->getThumbnailImage())
{
const LLRect& thumbnail_rect = mThumbnailPlaceholder->getRect();
const S32 thumbnail_w = previewp->getThumbnailWidth();
const S32 thumbnail_h = previewp->getThumbnailHeight();
// calc preview offset within the preview rect
const S32 local_offset_x = (thumbnail_rect.getWidth() - thumbnail_w) / 2 ;
const S32 local_offset_y = (thumbnail_rect.getHeight() - thumbnail_h) / 2 ;
S32 offset_x = thumbnail_rect.mLeft + local_offset_x;
S32 offset_y = thumbnail_rect.mBottom + local_offset_y;
gGL.matrixMode(LLRender::MM_MODELVIEW);
// Apply floater transparency to the texture unless the floater is focused.
F32 alpha = getTransparencyType() == TT_ACTIVE ? 1.0f : getCurrentTransparency();
LLColor4 color = LLColor4::white;
gl_draw_scaled_image(offset_x, offset_y,
thumbnail_w, thumbnail_h,
previewp->getThumbnailImage(), color % alpha);
}
// Update the visibility of the working (computing preview) label
mWorkingLabel->setVisible(!(previewp && previewp->getSnapshotUpToDate()));
// Enable Post if we have a preview to send and no on going connection being processed
mPostButton->setEnabled(can_post && (previewp && previewp->getSnapshotUpToDate()));
// Draw the rest of the panel on top of it
LLPanel::draw();
}
LLSnapshotLivePreview* FSPrimfeedPhotoPanel::getPreviewView()
{
LLSnapshotLivePreview* previewp = (LLSnapshotLivePreview*)mPreviewHandle.get();
return previewp;
}
void FSPrimfeedPhotoPanel::onVisibilityChange(bool visible)
{
if (visible)
{
if (mPreviewHandle.get())
{
LLSnapshotLivePreview* preview = getPreviewView();
if(preview)
{
LL_DEBUGS() << "opened, updating snapshot" << LL_ENDL;
preview->updateSnapshot(true);
}
}
else
{
LLRect full_screen_rect = getRootView()->getRect();
LLSnapshotLivePreview::Params p;
p.rect(full_screen_rect);
LLSnapshotLivePreview* previewp = new LLSnapshotLivePreview(p);
mPreviewHandle = previewp->getHandle();
previewp->setContainer(this);
previewp->setSnapshotType(LLSnapshotModel::SNAPSHOT_WEB);
previewp->setSnapshotFormat(LLSnapshotModel::SNAPSHOT_FORMAT_PNG);
previewp->setThumbnailSubsampled(true); // We want the preview to reflect the *saved* image
previewp->setAllowRenderUI(false); // We do not want the rendered UI in our snapshots
previewp->setAllowFullScreenPreview(false); // No full screen preview in SL Share mode
previewp->setThumbnailPlaceholderRect(mThumbnailPlaceholder->getRect());
updateControls();
}
}
}
void FSPrimfeedPhotoPanel::onClickNewSnapshot()
{
LLSnapshotLivePreview* previewp = getPreviewView();
if (previewp)
{
previewp->updateSnapshot(true);
}
}
void FSPrimfeedPhotoPanel::onClickBigPreview()
{
// Toggle the preview
if (isPreviewVisible())
{
LLFloaterReg::hideInstance("big_preview");
}
else
{
attachPreview();
LLFloaterReg::showInstance("big_preview");
}
}
bool FSPrimfeedPhotoPanel::isPreviewVisible()
{
return (mBigPreviewFloater && mBigPreviewFloater->getVisible());
}
void FSPrimfeedPhotoPanel::attachPreview()
{
if (mBigPreviewFloater)
{
LLSnapshotLivePreview* previewp = getPreviewView();
mBigPreviewFloater->setPreview(previewp);
mBigPreviewFloater->setFloaterOwner(getParentByType<LLFloater>());
}
}
void FSPrimfeedPhotoPanel::onSend()
{
sendPhoto();
}
bool FSPrimfeedPhotoPanel::onPrimfeedConnectStateChange(const LLSD& data)
{
if (FSPrimfeedAuth::isAuthorized())
{
sendPhoto();
}
return false;
}
void FSPrimfeedPhotoPanel::sendPhoto()
{
static const std::array<std::string,4> RATING_NAMES = {
"general", // 1
"moderate", // 2
"adult", // 3
"adult_plus" // 4
};
auto ratingToString = [&](int rating) -> std::string {
// clamp into [1,4]
int idx = llclamp(rating, 1, 4) - 1;
return RATING_NAMES[idx];
};
// Get the description (primfeed has no title/tags etc at this point)
std::string description = mDescriptionTextBox->getValue().asString();
// Get the content rating
int content_rating = mRatingComboBox->getValue().asInteger();
bool post_to_public_gallery = mPublicGalleryCheckbox->getValue().asBoolean();
bool commercial_content = mCommercialCheckbox->getValue().asBoolean();
// Get the image
LLSnapshotLivePreview* previewp = getPreviewView();
FSPrimfeedConnect::instance().setConnectionState(FSPrimfeedConnect::PRIMFEED_POSTING);
LLSD params;
params["rating"] = ratingToString(content_rating);
params["content"] = description;
params["is_commercial"] = commercial_content;
params["post_to_public_gallery"] = post_to_public_gallery;
// Add the location if required
bool add_location = mLocationCheckbox->getValue().asBoolean();
if (add_location)
{
// Get the SLURL for the location
LLSLURL slurl;
LLAgentUI::buildSLURL(slurl);
std::string slurl_string = slurl.getSLURLString();
params["location"] = slurl_string;
}
FSPrimfeedConnect::instance().uploadPhoto(params, previewp->getFormattedImage().get(),
[this](bool success, const std::string& url)
{
if (success)
{
FSPrimfeedConnect::instance().setConnectionState(FSPrimfeedConnect::PRIMFEED_POSTED);
static LLCachedControl<bool> open_url_on_post(gSavedPerAccountSettings, "FSPrimfeedOpenURLOnPost", true);
if (open_url_on_post)
{
LLWeb::loadURLExternal(url);
}
LLSD args;
args["PF_POSTURL"] = url;
LLNotificationsUtil::add("FSPrimfeedUploadComplete", args);
}
else
{
mWorkingLabel->setValue("Error posting to Primfeed");
mPostButton->setEnabled(true);
}
}
);
updateControls();
}
void FSPrimfeedPhotoPanel::clearAndClose()
{
mDescriptionTextBox->setValue("");
LLFloater* floater = getParentByType<LLFloater>();
if (floater)
{
floater->closeFloater();
if (mBigPreviewFloater)
{
mBigPreviewFloater->closeOnFloaterOwnerClosing(floater);
}
}
}
void FSPrimfeedPhotoPanel::updateControls()
{
LLSnapshotLivePreview* previewp = getPreviewView();
bool got_snap = previewp && previewp->getSnapshotUpToDate();
updateResolution(false);
}
void FSPrimfeedPhotoPanel::updateResolution(bool do_update)
{
LLComboBox* combobox = static_cast<LLComboBox *>(mResolutionComboBox);
LLComboBox* filterbox = static_cast<LLComboBox *>(mFilterComboBox);
std::string sdstring = combobox->getSelectedValue();
LLSD sdres;
std::stringstream sstream(sdstring);
LLSDSerialize::fromNotation(sdres, sstream, sdstring.size());
S32 width = sdres[0];
S32 height = sdres[1];
// Note : index 0 of the filter drop down is assumed to be "No filter" in whichever locale
std::string filter_name = (filterbox->getCurrentIndex() ? filterbox->getSimple() : "");
LLSnapshotLivePreview * previewp = static_cast<LLSnapshotLivePreview *>(mPreviewHandle.get());
if (previewp && combobox->getCurrentIndex() >= 0)
{
checkAspectRatio(width);
S32 original_width = 0 , original_height = 0 ;
previewp->getSize(original_width, original_height) ;
if (width == 0 || height == 0)
{
// take resolution from current window size
LL_DEBUGS() << "Setting preview res from window: " << gViewerWindow->getWindowWidthRaw() << "x" << gViewerWindow->getWindowHeightRaw() << LL_ENDL;
previewp->setSize(gViewerWindow->getWindowWidthRaw(), gViewerWindow->getWindowHeightRaw());
}
else if (width == -1 || height == -1)
{
// take resolution from custom size
LLSpinCtrl* width_spinner = getChild<LLSpinCtrl>("custom_snapshot_width");
LLSpinCtrl* height_spinner = getChild<LLSpinCtrl>("custom_snapshot_height");
S32 custom_width = width_spinner->getValue().asInteger();
S32 custom_height = height_spinner->getValue().asInteger();
if (checkImageSize(previewp, custom_width, custom_height, true, previewp->getMaxImageSize()))
{
width_spinner->set((F32)custom_width);
height_spinner->set((F32)custom_height);
}
LL_DEBUGS() << "Setting preview res from custom: " << custom_width << "x" << custom_height << LL_ENDL;
previewp->setSize(custom_width, custom_height);
}
else
{
// use the resolution from the selected pre-canned drop-down choice
LL_DEBUGS() << "Setting preview res selected from combo: " << width << "x" << height << LL_ENDL;
previewp->setSize(width, height);
}
previewp->getSize(width, height);
if ((original_width != width) || (original_height != height))
{
previewp->setSize(width, height);
if (do_update)
{
previewp->updateSnapshot(true, true);
updateControls();
}
}
// Get the old filter, compare to the current one "filter_name" and set if changed
std::string original_filter = previewp->getFilter();
if (original_filter != filter_name)
{
previewp->setFilter(filter_name);
if (do_update)
{
previewp->updateSnapshot(false, true);
updateControls();
}
}
}
bool custom_resolution = static_cast<LLComboBox *>(mResolutionComboBox)->getSelectedValue().asString() == "[i-1,i-1]";
getChild<LLSpinCtrl>("custom_snapshot_width")->setEnabled(custom_resolution);
getChild<LLSpinCtrl>("custom_snapshot_height")->setEnabled(custom_resolution);
getChild<LLCheckBoxCtrl>("keep_aspect_ratio")->setEnabled(custom_resolution);
}
void FSPrimfeedPhotoPanel::checkAspectRatio(S32 index)
{
LLSnapshotLivePreview *previewp = getPreviewView() ;
bool keep_aspect = false;
if (0 == index) // current window size
{
keep_aspect = true;
}
else if (-1 == index)
{
keep_aspect = getChild<LLCheckBoxCtrl>("keep_aspect_ratio")->get();
}
else // predefined resolution
{
keep_aspect = false;
}
if (previewp)
{
previewp->mKeepAspectRatio = keep_aspect;
}
}
LLUICtrl* FSPrimfeedPhotoPanel::getRefreshBtn()
{
return mRefreshBtn;
}
void FSPrimfeedPhotoPanel::onOpen(const LLSD& key)
{
if (!FSPrimfeedAuth::isAuthorized())
{
// Reauthorise if necessary.
FSPrimfeedAuth::initiateAuthRequest();
}
}
void FSPrimfeedPhotoPanel::uploadCallback(bool success, const LLSD& response)
{
LLSD args;
if(success && response["stat"].asString() == "ok")
{
FSPrimfeedConnect::instance().setConnectionState(FSPrimfeedConnect::PRIMFEED_POSTED);
args["PF_POSTURL"] = response["postUrl"];
LLNotificationsUtil::add("FSPrimfeedUploadComplete", args);
}
else
{
FSPrimfeedConnect::instance().setConnectionState(FSPrimfeedConnect::PRIMFEED_POST_FAILED);
}
}
void FSPrimfeedPhotoPanel::primfeedAuthResponse(bool success, const LLSD& response)
{
if(!success)
{
if(response.has("status") && response["status"].asString() == "reset")
{
LL_INFOS("Primfeed") << "Primfeed authorization has been reset." << LL_ENDL;
}
else
{
// Complain about failed auth here.
LL_WARNS("Primfeed") << "Primfeed authentication failed." << LL_ENDL;
}
}
onPrimfeedConnectStateChange(response);
}
bool FSPrimfeedPhotoPanel::checkImageSize(LLSnapshotLivePreview* previewp, S32& width, S32& height, bool isWidthChanged, S32 max_value)
{
S32 w = width ;
S32 h = height ;
if(previewp && previewp->mKeepAspectRatio)
{
if(gViewerWindow->getWindowWidthRaw() < 1 || gViewerWindow->getWindowHeightRaw() < 1)
{
return false;
}
//aspect ratio of the current window
F32 aspect_ratio = (F32)gViewerWindow->getWindowWidthRaw() / gViewerWindow->getWindowHeightRaw() ;
//change another value proportionally
if(isWidthChanged)
{
height = ll_round(width / aspect_ratio) ;
}
else
{
width = ll_round(height * aspect_ratio) ;
}
//bound w/h by the max_value
if(width > max_value || height > max_value)
{
if(width > height)
{
width = max_value ;
height = (S32)(width / aspect_ratio) ;
}
else
{
height = max_value ;
width = (S32)(height * aspect_ratio) ;
}
}
}
return (w != width || h != height) ;
}
///////////////////////////
//FSPrimfeedAccountPanel///
///////////////////////////
FSPrimfeedAccountPanel::FSPrimfeedAccountPanel() :
mAccountConnectedAsLabel(nullptr),
mAccountNameLink(nullptr),
mAccountPlan(nullptr),
mPanelButtons(nullptr),
mConnectButton(nullptr),
mDisconnectButton(nullptr)
{
mCommitCallbackRegistrar.add("SocialSharing.Connect", [this](LLUICtrl* ctrl, const LLSD& data) { onConnect(); });
mCommitCallbackRegistrar.add("SocialSharing.Disconnect", [this](LLUICtrl* ctrl, const LLSD& data) { onDisconnect(); });
FSPrimfeedAuth::sPrimfeedAuthPump->listen("FSPrimfeedAccountPanel",
[this](const LLSD& data) -> bool
{
bool success = data["success"].asBoolean();
this->primfeedAuthResponse(success, data);
return true;
}
);
setVisibleCallback([this](LLUICtrl *unused, bool visible) {
onVisibilityChange(visible);
});
}
bool FSPrimfeedAccountPanel::postBuild()
{
mAccountConnectedAsLabel= getChild<LLTextBox>("connected_as_label");
mAccountNameLink = getChild<LLTextBox>("primfeed_account_name");
mAccountPlan = getChild<LLTextBox>("primfeed_account_plan");
mPanelButtons = getChild<LLUICtrl>("panel_buttons");
mConnectButton = getChild<LLUICtrl>("connect_btn");
mDisconnectButton = getChild<LLUICtrl>("disconnect_btn");
LLSD dummy;
onPrimfeedConnectStateChange(dummy);
return LLPanel::postBuild();
}
void FSPrimfeedAccountPanel::draw()
{
FSPrimfeedConnect::EConnectionState connection_state = FSPrimfeedConnect::instance().getConnectionState();
//Disable the 'disconnect' button and the 'use another account' button when disconnecting in progress
bool disconnecting = connection_state == FSPrimfeedConnect::PRIMFEED_DISCONNECTING;
mDisconnectButton->setEnabled(!disconnecting);
//Disable the 'connect' button when a connection is in progress
bool connecting = ( connection_state == FSPrimfeedConnect::PRIMFEED_CONNECTING ||
connection_state == FSPrimfeedConnect::PRIMFEED_CONNECTED );
mConnectButton->setEnabled(!connecting);
LLPanel::draw();
}
void FSPrimfeedAccountPanel::primfeedAuthResponse(bool success, const LLSD& response)
{
if(!success)
{
LL_WARNS("Primfeed") << "Primfeed authentication failed." << LL_ENDL;
LLWeb::loadURLExternal("https://www.primfeed.com/login");
}
onPrimfeedConnectStateChange(response);
}
void FSPrimfeedAccountPanel::onVisibilityChange(bool visible)
{
if(visible)
{
//Connected
if(FSPrimfeedAuth::isAuthorized())
{
showConnectedLayout();
}
else
{
showDisconnectedLayout();
}
}
}
bool FSPrimfeedAccountPanel::onPrimfeedConnectStateChange(const LLSD& data)
{
if (FSPrimfeedAuth::isAuthorized())
{
showConnectedLayout();
}
else
{
showDisconnectedLayout();
}
onPrimfeedConnectInfoChange();
return false;
}
bool FSPrimfeedAccountPanel::onPrimfeedConnectInfoChange()
{
std::string clickable_name{""};
static LLCachedControl<std::string> primfeed_username(gSavedPerAccountSettings, "FSPrimfeedUsername");
static LLCachedControl<std::string> primfeed_profile_link(gSavedPerAccountSettings, "FSPrimfeedProfileLink");
static LLCachedControl<std::string> primfeed_plan(gSavedPerAccountSettings, "FSPrimfeedPlan");
//Strings of format [http://www.somewebsite.com Click Me] become clickable text
if (!primfeed_username().empty())
{
clickable_name = std::string("[") + std::string(primfeed_profile_link) + " " + std::string(primfeed_username) + "]";
}
mAccountNameLink->setText(clickable_name);
mAccountPlan->setText(primfeed_plan());
return false;
}
void FSPrimfeedAccountPanel::showConnectButton()
{
if(!mConnectButton->getVisible())
{
mConnectButton->setVisible(true);
mDisconnectButton->setVisible(false);
}
}
void FSPrimfeedAccountPanel::hideConnectButton()
{
if(mConnectButton->getVisible())
{
mConnectButton->setVisible(false);
mDisconnectButton->setVisible(true);
}
}
void FSPrimfeedAccountPanel::showDisconnectedLayout()
{
mAccountConnectedAsLabel->setText(getString("primfeed_disconnected"));
mAccountNameLink->setText(std::string(""));
mAccountPlan->setText(getString("primfeed_plan_unknown"));
showConnectButton();
}
void FSPrimfeedAccountPanel::showConnectedLayout()
{
mAccountConnectedAsLabel->setText(getString("primfeed_connected"));
hideConnectButton();
}
void FSPrimfeedAccountPanel::onConnect()
{
FSPrimfeedAuth::initiateAuthRequest();
}
void FSPrimfeedAccountPanel::onDisconnect()
{
FSPrimfeedAuth::resetAuthStatus();
}
////////////////////////
//FSFloaterPrimfeed/////
////////////////////////
FSFloaterPrimfeed::FSFloaterPrimfeed(const LLSD& key) : LLFloater(key),
mPrimfeedPhotoPanel(nullptr),
mStatusErrorText(nullptr),
mStatusLoadingText(nullptr),
mStatusLoadingIndicator(nullptr)
{
mCommitCallbackRegistrar.add("SocialSharing.Cancel", [this](LLUICtrl* ctrl, const LLSD& data) { onCancel(); });
}
void FSFloaterPrimfeed::onClose(bool app_quitting)
{
LLFloaterBigPreview* big_preview_floater = dynamic_cast<LLFloaterBigPreview*>(LLFloaterReg::getInstance("big_preview"));
if (big_preview_floater)
{
big_preview_floater->closeOnFloaterOwnerClosing(this);
}
LLFloater::onClose(app_quitting);
}
void FSFloaterPrimfeed::onCancel()
{
LLFloaterBigPreview* big_preview_floater = dynamic_cast<LLFloaterBigPreview*>(LLFloaterReg::getInstance("big_preview"));
if (big_preview_floater)
{
big_preview_floater->closeOnFloaterOwnerClosing(this);
}
closeFloater();
}
bool FSFloaterPrimfeed::postBuild()
{
// Keep tab of the Photo Panel
mPrimfeedPhotoPanel = static_cast<FSPrimfeedPhotoPanel*>(getChild<LLUICtrl>("panel_primfeed_photo"));
mPrimfeedAccountPanel = static_cast<FSPrimfeedAccountPanel*>(getChild<LLUICtrl>("panel_primfeed_account"));
// Connection status widgets
mStatusErrorText = getChild<LLTextBox>("connection_error_text");
mStatusLoadingText = getChild<LLTextBox>("connection_loading_text");
mStatusLoadingIndicator = getChild<LLUICtrl>("connection_loading_indicator");
return LLFloater::postBuild();
}
void FSFloaterPrimfeed::showPhotoPanel()
{
LLTabContainer* parent = dynamic_cast<LLTabContainer*>(mPrimfeedPhotoPanel->getParent());
if (!parent)
{
LL_WARNS() << "Cannot find panel container" << LL_ENDL;
return;
}
parent->selectTabPanel(mPrimfeedPhotoPanel);
}
void FSFloaterPrimfeed::draw()
{
if (mStatusErrorText && mStatusLoadingText && mStatusLoadingIndicator)
{
mStatusErrorText->setVisible(false);
mStatusLoadingText->setVisible(false);
mStatusLoadingIndicator->setVisible(false);
FSPrimfeedConnect::EConnectionState connection_state = FSPrimfeedConnect::instance().getConnectionState();
std::string status_text;
if (FSPrimfeedAuth::isAuthorized())
{
switch (connection_state)
{
case FSPrimfeedConnect::PRIMFEED_POSTING:
{
// Posting indicator
mStatusLoadingText->setVisible(true);
status_text = LLTrans::getString("SocialPrimfeedPosting");
mStatusLoadingText->setValue(status_text);
mStatusLoadingIndicator->setVisible(true);
break;
}
case FSPrimfeedConnect::PRIMFEED_POST_FAILED:
{
// Error posting to the service
mStatusErrorText->setVisible(true);
status_text = LLTrans::getString("SocialPrimfeedErrorPosting");
mStatusErrorText->setValue(status_text);
break;
}
}
}
else if (FSPrimfeedAuth::isPendingAuth())
{
// Show the status text when authorisation is pending
mStatusLoadingText->setVisible(true);
status_text = LLTrans::getString("SocialPrimfeedConnecting");
mStatusLoadingText->setValue(status_text);
}
else
{
// Show the status text when not authorised
mStatusErrorText->setVisible(true);
status_text = LLTrans::getString("SocialPrimfeedNotAuthorized");
mStatusErrorText->setValue(status_text);
}
}
LLFloater::draw();
}
void FSFloaterPrimfeed::onOpen(const LLSD& key)
{
mPrimfeedPhotoPanel->onOpen(key);
}
LLSnapshotLivePreview* FSFloaterPrimfeed::getPreviewView()
{
if(mPrimfeedPhotoPanel)
{
return mPrimfeedPhotoPanel->getPreviewView();
}
return nullptr;
}

View File

@ -0,0 +1,155 @@
/**
* @file fsfloaterprimfeed.cpp
* @brief Declaration of primfeed floater
* @author beq@firestorm
*
* $LicenseInfo:firstyear=2025&license=fsviewerlgpl$
* Phoenix Firestorm Viewer Source Code
* Copyright (C) 2025, Beq Janus
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* The Phoenix Firestorm Project, Inc., 1831 Oakwood Drive, Fairmont, Minnesota 56031-3225 USA
* http://www.firestormviewer.org
* $/LicenseInfo$
*/
#ifndef FS_FLOATERPRIMFEED_H
#define FS_FLOATERPRIMFEED_H
#include "llfloater.h"
#include "lltextbox.h"
#include "llviewertexture.h"
class LLIconCtrl;
class LLCheckBoxCtrl;
class LLSnapshotLivePreview;
class LLFloaterBigPreview;
/*
* (TODO) Beq: Refactor this with Flickr
* Primfeed floater is copied heavily from the LLFlaoterFlickr class and deliberately implemetns much of the underlying plumbinng into the connector class.
* Once this is bedded in and any initial issues are addressed, it would be sensible to refactor both the flickr and primfeed classes to share a common base.
* In particular a ref counted test for the livepreview would eliminate the need for the static update method in the app mainloop.
*/
class FSPrimfeedPhotoPanel : public LLPanel
{
public:
FSPrimfeedPhotoPanel();
~FSPrimfeedPhotoPanel();
bool postBuild();
S32 notify(const LLSD& info);
void draw();
LLSnapshotLivePreview* getPreviewView();
void onVisibilityChange(bool new_visibility);
void onClickNewSnapshot();
void onClickBigPreview();
void onSend();
bool onPrimfeedConnectStateChange(const LLSD& data);
void sendPhoto();
void clearAndClose();
void updateControls();
void updateResolution(bool do_update);
void checkAspectRatio(S32 index);
LLUICtrl* getRefreshBtn();
/*virtual*/ void onOpen(const LLSD& key);
void primfeedAuthResponse(bool success, const LLSD& response);
void uploadCallback(bool success, const LLSD& response);
private:
bool isPreviewVisible();
void attachPreview();
bool checkImageSize(LLSnapshotLivePreview* previewp, S32& width, S32& height, bool isWidthChanged, S32 max_value);
LLHandle<LLView> mPreviewHandle;
LLUICtrl * mResolutionComboBox;
LLUICtrl * mFilterComboBox;
LLUICtrl * mRefreshBtn;
LLUICtrl * mWorkingLabel;
LLUICtrl * mThumbnailPlaceholder;
LLUICtrl * mDescriptionTextBox;
LLUICtrl * mLocationCheckbox;
LLUICtrl * mCommercialCheckbox;
LLUICtrl * mPublicGalleryCheckbox;
LLUICtrl * mRatingComboBox;
LLUICtrl * mPostButton;
LLUICtrl * mCancelButton;
LLButton * mBtnPreview;
LLFloaterBigPreview * mBigPreviewFloater;
};
class FSPrimfeedAccountPanel : public LLPanel
{
public:
FSPrimfeedAccountPanel();
bool postBuild();
void draw();
private:
void onVisibilityChange(bool new_visibility);
void primfeedAuthResponse(bool success, const LLSD& response);
bool onPrimfeedConnectStateChange(const LLSD& data);
bool onPrimfeedConnectInfoChange();
void onConnect();
void onUseAnotherAccount();
void onDisconnect();
void showConnectButton();
void hideConnectButton();
void showDisconnectedLayout();
void showConnectedLayout();
LLTextBox * mAccountConnectedAsLabel;
LLTextBox * mAccountNameLink;
LLTextBox * mAccountPlan;
LLUICtrl * mPanelButtons;
LLUICtrl * mConnectButton;
LLUICtrl * mDisconnectButton;
};
class FSFloaterPrimfeed : public LLFloater
{
public:
FSFloaterPrimfeed(const LLSD& key);
static void update();
bool postBuild();
void draw();
void onClose(bool app_quitting);
void onCancel();
void showPhotoPanel();
void onOpen(const LLSD& key);
LLSnapshotLivePreview* getPreviewView();
private:
FSPrimfeedPhotoPanel* mPrimfeedPhotoPanel;
FSPrimfeedAccountPanel* mPrimfeedAccountPanel;
LLTextBox* mStatusErrorText;
LLTextBox* mStatusLoadingText;
LLUICtrl* mStatusLoadingIndicator;
};
#endif // LL_FSFLOATERPRIMFEED_H

View File

@ -0,0 +1,468 @@
/**
* @file fsprimfeedauth.cpp
* @file fsprimfeedauth.h
* @brief Primfeed Authorisation workflow class
* @author beq@firestorm
* $LicenseInfo:firstyear=2025&license=fsviewerlgpl$
* Phoenix Firestorm Viewer Source Code
* Copyright (C) 2025, Beq Janus
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* The Phoenix Firestorm Project, Inc., 1831 Oakwood Drive, Fairmont, Minnesota 56031-3225 USA
* http://www.firestormviewer.org
* $/LicenseInfo$
*
/*
* Handles Primfeed authentication and authorisation through a multi-factor OAuth flow.
*
* This module integrates with Primfeeds Third Party Viewers API.
* The authentication flow is as follows:
* 1. Initiate a login request:
* POST https://api.primfeed.com/pf/viewer/create-login-request
* Headers:
* pf-viewer-api-key: <viewer_api_key>
* pf-user-uuid: <avatar_uuid>
* Response:
* { "requestId": "<64-char string>" }
*
* 2. Redirect the user to:
* https://www.primfeed.com/oauth/viewer?r=<requestId>&v=<viewer_api_key>
*
* 3. The user is shown an approval screen. When they click Authorize,
* an in-world message is sent:
* #PRIMFEED_OAUTH: <oauth_token>
* We intercept this code through an onChat handle then call onOauthTokenReceived().
*
* 4. Validate the login request:
* POST https://api.primfeed.com/pf/viewer/validate-request
* Headers:
* Authorization: Bearer <oauth_token>
* pf-viewer-api-key: <viewer_api_key>
* pf-viewer-request-id: <requestId>
* Response: HTTP 204
*
* 5. Optionally, check user status:
* GET https://api.primfeed.com/pf/viewer/user
* Headers:
* Authorization: Bearer <oauth_token>
* pf-viewer-api-key: <viewer_api_key>
* Response: { "plan": "free" } (or "pro")
*/
#include "llviewerprecompiledheaders.h"
#include "fsprimfeedauth.h"
#include "fsprimfeedconnect.h"
#include "llimview.h"
#include "llnotificationsutil.h"
#include "llfloaterimnearbychathandler.h"
#include "llnotificationmanager.h"
#include "llagent.h"
#include "llevents.h"
#include "fscorehttputil.h"
#include "llwindow.h"
#include "llviewerwindow.h"
#include "lluri.h"
#include "llsdjson.h"
#include <string_view>
using Callback = FSPrimfeedAuth::authorized_callback_t;
// private instance variable
std::shared_ptr<FSPrimfeedAuth> FSPrimfeedAuth::sPrimfeedAuth;
std::unique_ptr<LLEventPump> FSPrimfeedAuth::sPrimfeedAuthPump = std::make_unique<LLEventStream>("PrimfeedAuthResponse");
// Helper callback that unpacks HTTP POST response data.
void FSPrimfeedAuthResponse(LLSD const &aData, Callback callback)
{
LLSD header = aData[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS][LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_HEADERS];
LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(
aData[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]);
const LLSD::Binary &rawData = aData[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_RAW].asBinary();
std::string result;
result.assign(rawData.begin(), rawData.end());
// Assume JSON response.
LLSD resultLLSD;
if(!result.empty())
{
resultLLSD = LlsdFromJson(boost::json::parse(result));
}
callback((status.getType() == HTTP_OK ||
status.getType() == HTTP_NO_CONTENT), resultLLSD);
}
void FSPrimfeedAuth::initiateAuthRequest()
{
// This function is called to initiate the authentication request.
// It should be called when the user clicks the "Authenticate" button.
// Also triggered on opening the floater.
// The actual implementation is in the create() method.
if (!isAuthorized())
{
if (sPrimfeedAuth)
{
LLNotificationsUtil::add("PrimfeedAuthorisationAlreadyInProgress");
return;
}
// If no token stored, begin the login request; otherwise check user status.
sPrimfeedAuth = FSPrimfeedAuth::create(
[](bool success, const LLSD &response)
{
LLSD event_data = response;
event_data["success"] = success;
sPrimfeedAuthPump->post(event_data);
// Now that auth is complete, clear the static pointer.
sPrimfeedAuth.reset();
}
);
}
else
{
LLNotificationsUtil::add("PrimfeedAlreadyAuthorized");
}
}
void FSPrimfeedAuth::resetAuthStatus()
{
sPrimfeedAuth.reset();
gSavedPerAccountSettings.setString("FSPrimfeedOAuthToken", "");
gSavedPerAccountSettings.setString("FSPrimfeedProfileLink", "");
gSavedPerAccountSettings.setString("FSPrimfeedPlan", "");
gSavedPerAccountSettings.setString("FSPrimfeedUsername", "");
LLSD event_data;
event_data["status"] = "reset";
event_data["success"] = "false";
sPrimfeedAuthPump->post(event_data);
}
FSPrimfeedAuth::FSPrimfeedAuth(authorized_callback_t callback)
: mCallback(callback), mAuthenticating(false)
{
mInstantMessageConnection = LLIMModel::instance().addNewMsgCallback(
[this](const LLSD &message) {
LL_DEBUGS("FSPrimfeedAuth") << "Received chat message: " << message["message"].asString() << LL_ENDL;
this->onChatMessage(message);
});
mChatMessageConnection = LLNotificationsUI::LLNotificationManager::instance().getChatHandler()->addNewChatCallback(
[this](const LLSD &message) {
LL_DEBUGS("FSPrimfeedAuth") << "Received instant message: " << message["message"].asString() << LL_ENDL;
this->onChatMessage(message);
});
}
FSPrimfeedAuth::~FSPrimfeedAuth()
{
if (mChatMessageConnection.connected())
{
try
{
mChatMessageConnection.disconnect();
}
catch (const std::exception& e)
{
LL_WARNS("FSPrimfeedAuth") << "Exception during chat connection disconnect: " << e.what() << LL_ENDL;
}
catch (...)
{
LL_WARNS("FSPrimfeedAuth") << "Unknown exception during chat connection disconnect." << LL_ENDL;
}
}
if (mInstantMessageConnection.connected())
{
try
{
mInstantMessageConnection.disconnect();
}
catch (const std::exception& e)
{
LL_WARNS("FSPrimfeedAuth") << "Exception during instant message disconnect: " << e.what() << LL_ENDL;
}
catch (...)
{
LL_WARNS("FSPrimfeedAuth") << "Unknown exception during instant message disconnect." << LL_ENDL;
}
}
}
// Factory method to create a shared pointer to FSPrimfeedAuth.
std::shared_ptr<FSPrimfeedAuth> FSPrimfeedAuth::create(authorized_callback_t callback)
{
// Ensure only one authentication attempt is in progress.
if (sPrimfeedAuth)
{
// Already in progress; return the existing instance.
return sPrimfeedAuth;
}
auto auth = std::shared_ptr<FSPrimfeedAuth>(new FSPrimfeedAuth(callback));
if(!auth)
{
return nullptr;
}
auth->mAuthenticating = true;
// If no token stored, begin the login request; otherwise check user status.
if (gSavedPerAccountSettings.getString("FSPrimfeedOAuthToken").empty())
{
auth->beginLoginRequest();
}
else
{
auth->checkUserStatus();
}
return auth;
}
void FSPrimfeedAuth::beginLoginRequest()
{
// Get our API key and user UUID.
std::string viewer_api_key = gSavedSettings.getString("FSPrimfeedViewerApiKey");
std::string user_uuid = gAgent.getID().asString();
std::string url = "https://api.primfeed.com/pf/viewer/create-login-request";
std::string post_data = ""; // No body parameters required.
// Create the headers object.
LLCore::HttpHeaders::ptr_t pHeader(new LLCore::HttpHeaders());
LLCore::HttpOptions::ptr_t options(new LLCore::HttpOptions());
pHeader->append("pf-viewer-api-key", viewer_api_key);
pHeader->append("pf-user-uuid", user_uuid);
// Set up HTTP options
options->setWantHeaders(true);
options->setRetries(0);
options->setTimeout(PRIMFEED_CONNECT_TIMEOUT);
// Capture shared_ptr to self
auto self = shared_from_this();
const auto end(pHeader->end());
for (auto it(pHeader->begin()); end != it; ++it)
{
LL_DEBUGS("Primfeed") << "Header: " << it->first << " = " << it->second << LL_ENDL;
}
// Pass both success and failure callbacks
FSCoreHttpUtil::callbackHttpPostRaw(
url,
post_data,
[self](LLSD const &aData) {
LL_DEBUGS("FSPrimfeedAuth") << "Login request response(OK): " << aData << LL_ENDL;
FSPrimfeedAuthResponse(aData,
[self](bool success, const LLSD &response) {
self->gotRequestId(success, response);
}
);
},
[self](LLSD const &aData) {
LL_DEBUGS("FSPrimfeedAuth") << "Login request response(FAIL): " << aData << LL_ENDL;
FSPrimfeedAuthResponse(aData,
[self](bool success, const LLSD &response) {
self->gotRequestId(success, response);
}
);
},
pHeader,
options
);
}
void FSPrimfeedAuth::gotRequestId(bool success, const LLSD &response)
{
if (!success)
{
LLNotificationsUtil::add("PrimfeedLoginRequestFailed");
mCallback(false, LLSD());
return;
}
mRequestId = response["requestId"].asString();
if (mRequestId.empty())
{
LLNotificationsUtil::add("PrimfeedLoginRequestFailed");
mCallback(false, LLSD());
return;
}
// Open the browser for user approval.
std::string viewer_api_key = gSavedSettings.getString("FSPrimfeedViewerApiKey");
std::string auth_url = "https://www.primfeed.com/oauth/viewer?r=" + mRequestId + "&v=" + viewer_api_key;
gViewerWindow->getWindow()->spawnWebBrowser(auth_url, true);
}
/// This function is called by the chat interceptor when the message
/// "#PRIMFEED_OAUTH: <oauth_token>" is intercepted.
void FSPrimfeedAuth::onOauthTokenReceived(const std::string_view& oauth_token)
{
if (oauth_token.empty())
{
mCallback(false, LLSD());
return;
}
mOauthToken = oauth_token;
validateRequest();
}
void FSPrimfeedAuth::onChatMessage(const LLSD& message)
{
constexpr std::string_view oauth_msg_prefix = "#PRIMFEED_OAUTH: ";
const std::string msg = message["message"].asString();
if (msg.find(std::string(oauth_msg_prefix)) == 0)
{
std::string_view oauth_token(msg.data() + oauth_msg_prefix.size(), msg.size() - oauth_msg_prefix.size());
LL_DEBUGS("Primfeed") << "Received OAuth token: " << msg << "extracted:<" << oauth_token << ">" << LL_ENDL;
onOauthTokenReceived(oauth_token);
}
}
void FSPrimfeedAuth::validateRequest()
{
// No POST body needed.
std::string post_data = "";
std::string url = "https://api.primfeed.com/pf/viewer/validate-request";
// Retrieve the viewer API key.
std::string viewer_api_key = gSavedSettings.getString("FSPrimfeedViewerApiKey");
// Create and populate the headers.
LLCore::HttpHeaders::ptr_t pHeader(new LLCore::HttpHeaders());
pHeader->append("Authorization", "Bearer " + mOauthToken);
pHeader->append("pf-viewer-api-key", viewer_api_key);
pHeader->append("pf-viewer-request-id", mRequestId);
// Set HTTP options
LLCore::HttpOptions::ptr_t options(new LLCore::HttpOptions());
options->setWantHeaders(true);
options->setRetries(0);
options->setTimeout(PRIMFEED_CONNECT_TIMEOUT);
// print out pHeader for debuging using iterating over pHeader and using LL_DEBUGS
const auto end(pHeader->end());
for (auto it(pHeader->begin()); end != it; ++it)
{
LL_DEBUGS("Primfeed") << "Header: " << it->first << " = " << it->second << LL_ENDL;
}
auto self = shared_from_this();
try
{
FSCoreHttpUtil::callbackHttpPostRaw(
url,
post_data,
[self](LLSD const &aData) {
LL_DEBUGS("FSPrimfeedAuth") << "Validation-request response(OK): " << aData << LL_ENDL;
FSPrimfeedAuthResponse(aData,
[self](bool success, const LLSD &response) {
self->gotValidateResponse(success, response);
}
);
},
[self](LLSD const &aData) {
LL_INFOS("FSPrimfeedAuth") << "Validation-request response(FAIL): " << aData << LL_ENDL;
FSPrimfeedAuthResponse(aData,
[self](bool success, const LLSD &response) {
self->gotValidateResponse(success, response);
}
);
},
pHeader,
options
);
}
catch(const std::exception& e)
{
LL_WARNS("Primfeed") << "Primfeed validation failed " << e.what() << LL_ENDL;
}
}
void FSPrimfeedAuth::gotValidateResponse(bool success, const LLSD &response)
{
if (!success)
{
LLNotificationsUtil::add("PrimfeedValidateFailed");
mCallback(false, response);
return;
}
checkUserStatus();
}
void FSPrimfeedAuth::checkUserStatus()
{
std::string viewer_api_key = gSavedSettings.getString("FSPrimfeedViewerApiKey");
// Build the base URL without query parameters.
std::string url = "https://api.primfeed.com/pf/viewer/user";
LL_DEBUGS("Primfeed") << "URL: " << url << LL_ENDL;
// Create and populate the headers.
LLCore::HttpHeaders::ptr_t pHeader(new LLCore::HttpHeaders());
pHeader->append("Authorization", "Bearer " + mOauthToken);
pHeader->append("pf-viewer-api-key", viewer_api_key);
// Set HTTP options.
LLCore::HttpOptions::ptr_t options(new LLCore::HttpOptions());
options->setWantHeaders(true);
options->setRetries(0);
options->setTimeout(PRIMFEED_CONNECT_TIMEOUT);
// Make the HTTP GET request, passing in the headers and options.
FSCoreHttpUtil::callbackHttpGetRaw(
url,
[this](LLSD const &aData) {
LL_DEBUGS("FSPrimfeedAuth") << "Check-user-status response: " << aData << LL_ENDL;
FSPrimfeedAuthResponse(aData, [this](bool success, const LLSD &response) {
this->gotUserStatus(success, response);
});
},
[this](LLSD const &aData) {
LL_INFOS("FSPrimfeedAuth") << "Check-user-status response (failure): " << aData << LL_ENDL;
// Optionally, call the same processing for failure or handle separately.
FSPrimfeedAuthResponse(aData, [this](bool success, const LLSD &response){
this->gotUserStatus(success, response);
});
},
pHeader,
options
);
}
void FSPrimfeedAuth::gotUserStatus(bool success, const LLSD &response)
{
LL_INFOS("Primfeed") << "User status: " << response << "(" << success << ")" << LL_ENDL;
if (success && response.has("plan"))
{
gSavedPerAccountSettings.setString("FSPrimfeedOAuthToken", mOauthToken);
gSavedPerAccountSettings.setString("FSPrimfeedRequestId", mRequestId);
gSavedPerAccountSettings.setString("FSPrimfeedPlan", response["plan"].asString());
gSavedPerAccountSettings.setString("FSPrimfeedProfileLink", response["link"].asString());
gSavedPerAccountSettings.setString("FSPrimfeedUsername", response["username"].asString());
FSPrimfeedConnect::instance().setConnectionState(FSPrimfeedConnect::PRIMFEED_CONNECTED);
mCallback(true, response);
}
else
{
LLNotificationsUtil::add("PrimfeedUserStatusFailed");
FSPrimfeedConnect::instance().setConnectionState(FSPrimfeedConnect::PRIMFEED_DISCONNECTED);
mCallback(false, response);
}
}

View File

@ -0,0 +1,93 @@
/**
* @file fsprimfeedauth.h
* @brief Primfeed Authorisation workflow class
* @author beq@firestorm
*
* $LicenseInfo:firstyear=2025&license=fsviewerlgpl$
* Phoenix Firestorm Viewer Source Code
* Copyright (C) 2025, Beq Janus
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* The Phoenix Firestorm Project, Inc., 1831 Oakwood Drive, Fairmont, Minnesota 56031-3225 USA
* http://www.firestormviewer.org
* $/LicenseInfo$
*/
#ifndef FSPRIMFEEDAUTH_H
#define FSPRIMFEEDAUTH_H
#include "llsd.h"
#include "llviewercontrol.h"
#include <string>
#include <functional>
/*
* Primfeed authentication workflow class.
*
* This class handles the Primfeed OAuth login flow and provides methods to
* check the user status and receive a callback when the authentication
* process is complete.
* based on the workflow documented at https://docs.primfeed.com/api/third-party-viewers
*/
class FSPrimfeedAuth : public std::enable_shared_from_this<FSPrimfeedAuth>
{
public:
// Callback type: first parameter indicates success and the second holds any LLSD response.
using authorized_callback_t = std::function<void(bool, const LLSD&)>;
static std::shared_ptr<FSPrimfeedAuth> create(authorized_callback_t callback);
static std::unique_ptr<LLEventPump> sPrimfeedAuthPump;
~FSPrimfeedAuth();
// Should be called by the chat interceptor when an oauth token is received.
void onOauthTokenReceived(const std::string_view& oauth_token);
void onInstantMessage(const LLSD& message);
void onChatMessage(const LLSD& message);
// Begin the login request flow.
void beginLoginRequest();
// Check the user status.
void checkUserStatus();
static bool isPendingAuth(){ return (sPrimfeedAuth != nullptr); }
static bool isAuthorized(){ return (!gSavedPerAccountSettings.getString("FSPrimfeedOAuthToken").empty()); }
static void initiateAuthRequest();
static void resetAuthStatus();
private:
static std::shared_ptr<FSPrimfeedAuth> sPrimfeedAuth;
explicit FSPrimfeedAuth(authorized_callback_t callback);
authorized_callback_t mCallback;
bool mAuthenticating;
std::string mOauthToken;
std::string mRequestId;
// Callback when a login request response is received.
void gotRequestId(bool success, const LLSD &response);
// Validate the login request.
void validateRequest();
// Callback when the validate response is received.
void gotValidateResponse(bool success, const LLSD &response);
// Callback when the user status response is received.
void gotUserStatus(bool success, const LLSD &response);
boost::signals2::connection mInstantMessageConnection;
boost::signals2::connection mChatMessageConnection;
// Static flag to prevent duplicate authentication attempts.
static std::atomic<bool> sAuthorisationInProgress;
static constexpr U32 PRIMFEED_CONNECT_TIMEOUT = 300; // 5 minute timeout should work
};
#endif // FSPRIMFEEDAUTH_H

View File

@ -0,0 +1,189 @@
/**
* @file fsprimfeedconnect.cpp
* @brief Primfeed connector class
* @author beq@firestorm
*
* $LicenseInfo:firstyear=2025&license=fsviewerlgpl$
* Phoenix Firestorm Viewer Source Code
* Copyright (C) 2025, Beq Janus
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* The Phoenix Firestorm Project, Inc., 1831 Oakwood Drive, Fairmont, Minnesota 56031-3225 USA
* http://www.firestormviewer.org
* $/LicenseInfo$
*/
#include "fsprimfeedconnect.h"
#include "fsprimfeedauth.h"
#include "llviewercontrol.h"
#include "llcoros.h"
#include "llsdjson.h"
// The connector workflow for Primfeed is realtively simple and mostly just builds on top of the established Auth workflow
// and the posting endpoint documented at https://docs.primfeed.com/api/third-party-viewers#creating-a-post
FSPrimfeedConnect::FSPrimfeedConnect() = default;
void FSPrimfeedConnect::uploadPhoto(const LLSD& params, LLImageFormatted* image, post_callback_t callback)
{
LL_DEBUGS("primfeed") << "uploadPhoto() called" << LL_ENDL;
if (!FSPrimfeedAuth::isAuthorized())
{
LL_WARNS("primfeed") << "Authorization failed, aborting.\n" << LL_ENDL;
callback(false, "");
return;
}
LL_DEBUGS("primfeed") << "Authorization successful" << LL_ENDL;
mPostCallback = callback;
LL_DEBUGS("primfeed") << "Launching upload coroutine" << LL_ENDL;
LLCoros::instance().launch(
"FSPrimfeedConnect::uploadPhotoCoro",
[this, params, image]() { uploadPhotoCoro(params, image); }
);
}
void FSPrimfeedConnect::uploadPhotoCoro(const LLSD& params, LLImageFormatted* image)
{
LL_DEBUGS("primfeed") << "Entered uploadPhotoCoro" << LL_ENDL;
setConnectionState(PRIMFEED_POSTING);
LL_DEBUGS("primfeed") << "Connection state set to PRIMFEED_POSTING" << LL_ENDL;
const std::string fmt = (image->getCodec() == EImageCodec::IMG_CODEC_JPEG) ? "jpg" : "png";
LL_DEBUGS("primfeed") << "Image format: " << fmt << LL_ENDL;
const std::string boundary = "----------------------------0123456789abcdef";
const std::string sep = "\n";
const std::string dash = "--" + boundary;
LL_DEBUGS("primfeed") << "Building multipart body" << LL_ENDL;
LLCore::BufferArray::ptr_t raw(new LLCore::BufferArray());
LLCore::BufferArrayStream body(raw.get());
auto addPart = [&](const std::string& name, const std::string& val)
{
LL_DEBUGS("primfeed") << "Adding part: " << name << "=" << val << LL_ENDL;
body << dash << sep
<< "Content-Disposition: form-data; name=\"" << name << "\"" << sep << sep
<< val << sep;
};
addPart("commercial", params["commercial"].asBoolean() ? "true" : "false");
addPart("rating", params["rating"].asString());
addPart("content", params["content"].asString());
addPart("publicGallery", params["post_to_public_gallery"].asBoolean()? "true" : "false");
if (params.has("location") && !params["location"].asString().empty())
{
addPart("location", params["location"].asString());
}
LL_DEBUGS("primfeed") << "Adding image file header" << LL_ENDL;
body << dash << sep
<< "Content-Disposition: form-data; name=\"image\"; filename=\"snapshot." << fmt << "\"" << sep
<< "Content-Type: image/" << fmt << sep << sep;
U8* data = image->getData();
S32 size = image->getDataSize();
LL_DEBUGS("primfeed") << "Appending image data, size=" << size << LL_ENDL;
// yep this seems inefficient, but all other occurrences in the codebase do it this way.
for (S32 i = 0; i < size; ++i)
{
body << data[i];
}
body << sep;
body << dash << "--" << sep;
LL_DEBUGS("primfeed") << "Multipart body ready" << LL_ENDL;
// Setup HTTP
LL_DEBUGS("primfeed") << "Preparing HTTP request" << LL_ENDL;
LLCore::HttpRequest::policy_t policy = LLCore::HttpRequest::DEFAULT_POLICY_ID;
LLCoreHttpUtil::HttpCoroutineAdapter adapter("PrimfeedUpload", policy);
LLCore::HttpRequest::ptr_t request(new LLCore::HttpRequest);
LLCore::HttpOptions::ptr_t options(new LLCore::HttpOptions);
options->setWantHeaders(true);
LL_DEBUGS("primfeed") << "Setting HTTP headers" << LL_ENDL;
LLCore::HttpHeaders::ptr_t headers(new LLCore::HttpHeaders);
std::string token = gSavedPerAccountSettings.getString("FSPrimfeedOAuthToken");
std::string apiKey = gSavedSettings.getString("FSPrimfeedViewerApiKey");
headers->append("Authorization", "Bearer " + token);
headers->append("pf-viewer-api-key", apiKey);
headers->append("Content-Type", "multipart/form-data; boundary=" + boundary);
LL_DEBUGS("primfeed") << "Dumping HTTP headers for POST:" << LL_ENDL;
for (auto it = headers->begin(); it != headers->end(); ++it)
{
LL_DEBUGS("primfeed") << it->first << ": " << it->second << LL_ENDL;
}
LL_DEBUGS("primfeed") << "Headers set" << LL_ENDL;
LL_DEBUGS("primfeed") << "Starting HTTP POST" << LL_ENDL;
LLSD result = adapter.postRawAndSuspend(request,
"https://api.primfeed.com/pf/viewer/post",
raw,
options,
headers);
LL_DEBUGS("primfeed") << "HTTP POST complete" << LL_ENDL;
const LLSD::Binary &rawData = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS_RAW].asBinary();
std::string response_raw;
response_raw.assign(rawData.begin(), rawData.end());
LLSD result_LLSD;
if(!response_raw.empty())
{
result_LLSD = LlsdFromJson(boost::json::parse(response_raw));
}
LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]);
bool success = (status.getType() == HTTP_OK);
LL_DEBUGS("primfeed") << "HTTP status =" << (success?"OK":"FAIL") << " "<< status.getMessage() << LL_ENDL;
std::string url;
if (success)
{
url = result_LLSD["url"].asString();
LL_DEBUGS("primfeed") << "Received URL=" << url << LL_ENDL;
}
LL_DEBUGS("primfeed") << "Invoking callback" << LL_ENDL;
mPostCallback(success, url);
setConnectionState(success ? PRIMFEED_POSTED : PRIMFEED_POST_FAILED);
LL_DEBUGS("primfeed") << "Final state set" << LL_ENDL;
}
// Handle connection state transitions
void FSPrimfeedConnect::setConnectionState(EConnectionState state)
{
LL_DEBUGS("primfeed") << "setConnectionState(" << state << ")" << LL_ENDL;
mConnectionState = state;
}
FSPrimfeedConnect::EConnectionState FSPrimfeedConnect::getConnectionState() const
{
return mConnectionState;
}
bool FSPrimfeedConnect::isTransactionOngoing() const
{
return (mConnectionState == PRIMFEED_CONNECTING ||
mConnectionState == PRIMFEED_POSTING ||
mConnectionState == PRIMFEED_DISCONNECTING);
}
void FSPrimfeedConnect::loadPrimfeedInfo()
{
LL_DEBUGS("primfeed") << "loadPrimfeedInfo() called" << LL_ENDL;
// Nothing to do here for Primfeed
setConnectionState(PRIMFEED_CONNECTED);
}

View File

@ -0,0 +1,82 @@
/**
* @file fsprimfeedconect.h
* @brief Primfeed connector class
* @author beq@firestorm
*
* $LicenseInfo:firstyear=2025&license=fsviewerlgpl$
* Phoenix Firestorm Viewer Source Code
* Copyright (C) 2025, Beq Janus
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License only.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*
* The Phoenix Firestorm Project, Inc., 1831 Oakwood Drive, Fairmont, Minnesota 56031-3225 USA
* http://www.firestormviewer.org
* $/LicenseInfo$
*/
#ifndef FS_PRIMFEEDCONNECT_H
#define FS_PRIMFEEDCONNECT_H
#include "llsingleton.h"
#include "llsd.h"
#include "llimage.h"
#include "fsprimfeedauth.h"
#include "llcorehttputil.h"
#include "bufferarray.h"
#include "llcoros.h"
#include "llviewercontrol.h" // for gSavedSettings/gSavedPerAccountSettings
#include <functional>
// Coro based connector designed to interface with floater designed along the same principles as LLFloaterFlickr.cpp
class FSPrimfeedConnect : public LLSingleton<FSPrimfeedConnect>
{
LLSINGLETON(FSPrimfeedConnect);
public:
// Connection states for Primfeed operations
enum EConnectionState
{
PRIMFEED_DISCONNECTED = 0,
PRIMFEED_CONNECTING,
PRIMFEED_CONNECTED,
PRIMFEED_POSTING,
PRIMFEED_POSTED,
PRIMFEED_POST_FAILED,
PRIMFEED_DISCONNECTING
};
// Callback invoked on post completion: success flag and URL (empty on failure)
using post_callback_t = std::function<void(bool success, const std::string& url)>;
// Posts a snapshot to Primfeed; requires FSPrimfeedAuth::isAuthorized()
void uploadPhoto(const LLSD& params, LLImageFormatted* image, post_callback_t callback);
// Retrieve and update account info from Primfeed (not used kept for compatibility)
void loadPrimfeedInfo();
void setConnectionState(EConnectionState state);
EConnectionState getConnectionState() const;
bool isTransactionOngoing() const;
private:
// Internal coroutine entry-point for uploads
void uploadPhotoCoro(const LLSD& params, LLImageFormatted* image);
// Cached callback until coroutine completes
post_callback_t mPostCallback;
// Current connection/post state
EConnectionState mConnectionState = PRIMFEED_DISCONNECTED;
};
#endif // FS_PRIMFEEDCONNECT_H

View File

@ -235,6 +235,7 @@
#include "llfloatersimplesnapshot.h"
#include "llfloatersnapshot.h"
#include "llfloaterflickr.h"
#include "fsfloaterprimfeed.h" // <FS:Beq/> Primfeed Floater
#include "llsidepanelinventory.h"
#include "llatmosphere.h"
@ -1751,6 +1752,7 @@ bool LLAppViewer::doFrame()
LLFloaterSnapshot::update(); // take snapshots
LLFloaterSimpleSnapshot::update();
LLFloaterFlickr::update(); // <FS:Beq/> FIRE-35002 - Flickr preview not updating whne opened directly from tool tray icon
FSFloaterPrimfeed::update(); // <FS:Beq/> Primfeed support
gGLActive = false;
}

View File

@ -30,6 +30,7 @@
#include "llfloaterreg.h"
#include "llfloaterflickr.h" // <FS:Ansariel> Share to Flickr
#include "fsfloaterprimfeed.h" // <FS:Beq> Share to Primfeed
#include "llimagefiltersmanager.h"
#include "llcheckboxctrl.h"
#include "llcombobox.h"
@ -1485,12 +1486,12 @@ bool LLFloaterSnapshot::isWaitingState()
// <FS:Beq> FIRE-35002 - Post to flickr broken, improved solution
// bool LLFloaterSnapshotBase::ImplBase::updatePreviewList(bool initialized)
bool LLFloaterSnapshotBase::ImplBase::updatePreviewList(bool initialized, bool have_flickr)
bool LLFloaterSnapshotBase::ImplBase::updatePreviewList(bool initialized, bool have_socials)
// </FS:Beq>
{
// <FS:Ansariel> Share to Flickr
//if (!initialized)
if (!initialized && !have_flickr)
if (!initialized && !have_socials)
// </FS:Ansariel>
return false;
@ -1509,11 +1510,13 @@ void LLFloaterSnapshotBase::ImplBase::updateLivePreview()
{
// don't update preview for hidden floater
// <FS:Beq> FIRE-35002 - Post to flickr broken
LLFloaterFlickr* floater_flickr = LLFloaterReg::findTypedInstance<LLFloaterFlickr>("flickr");
auto have_flickr = floater_flickr != nullptr;
bool have_socials = (
LLFloaterReg::findTypedInstance<LLFloaterFlickr>("flickr") != nullptr ||
LLFloaterReg::findTypedInstance<FSFloaterPrimfeed>("primfeed") != nullptr
);
if ( ((mFloater && mFloater->isInVisibleChain()) ||
have_flickr) &&
ImplBase::updatePreviewList(true, have_flickr))
have_socials) &&
ImplBase::updatePreviewList(true, have_socials))
// </FS:Beq>
{
LL_DEBUGS() << "changed" << LL_ENDL;

View File

@ -123,7 +123,7 @@ public:
virtual EStatus getStatus() const { return mStatus; }
virtual void setNeedRefresh(bool need);
static bool updatePreviewList(bool initialized, bool have_flickr = false); // <FS:Beq/> FIRE-35002 - Post to flickr broken, improved solution
static bool updatePreviewList(bool initialized, bool have_socials = false); // <FS:Beq/> FIRE-35002 - Post to flickr broken, improved solution
void setAdvanced(bool advanced) { mAdvanced = advanced; }
void setSkipReshaping(bool skip) { mSkipReshaping = skip; }

View File

@ -32,6 +32,7 @@
#include "llfloatersnapshot.h" // FIXME: create a snapshot model
#include "llfloaterreg.h"
#include "llfloaterflickr.h" // <FS:Ansariel> Share to Flickr
#include "fsfloaterprimfeed.h" // <FS:Beq> Share to Primfeed
/**
* Provides several ways to save a snapshot.
@ -53,6 +54,7 @@ private:
void onSaveToInventory();
void onSaveToComputer();
void onSendToFlickr(); // <FS:Ansariel> Share to Flickr
void onSendToPrimfeed(); // <FS:Beq/> Share to Primfeed
LLFloaterSnapshotBase* mSnapshotFloater;
};
@ -66,6 +68,7 @@ LLPanelSnapshotOptions::LLPanelSnapshotOptions()
mCommitCallbackRegistrar.add("Snapshot.SaveToInventory", boost::bind(&LLPanelSnapshotOptions::onSaveToInventory, this));
mCommitCallbackRegistrar.add("Snapshot.SaveToComputer", boost::bind(&LLPanelSnapshotOptions::onSaveToComputer, this));
mCommitCallbackRegistrar.add("Snapshot.SendToFlickr", boost::bind(&LLPanelSnapshotOptions::onSendToFlickr, this)); // <FS:Ansariel> Share to Flickr
mCommitCallbackRegistrar.add("Snapshot.SendToPrimfeed", boost::bind(&LLPanelSnapshotOptions::onSendToPrimfeed, this)); // <FS:Beq/> Share to Primfeed
}
// virtual
@ -122,3 +125,17 @@ void LLPanelSnapshotOptions::onSendToFlickr()
LLFloaterReg::showInstance("flickr");
}
// </FS:Ansariel>
// <FS:Beq> Share to Primfeed
void LLPanelSnapshotOptions::onSendToPrimfeed()
{
LLFloaterReg::hideInstance("snapshot");
auto* primfeed_floater = dynamic_cast<FSFloaterPrimfeed*>(LLFloaterReg::getInstance("primfeed"));
if (primfeed_floater)
{
primfeed_floater->showPhotoPanel();
}
LLFloaterReg::showInstance("primfeed");
}
// </FS:Beq>

View File

@ -36,6 +36,7 @@
#include "llfloaterperms.h"
#include "llfloaterreg.h"
#include "llfloaterflickr.h" // <FS:Ansariel> Share to Flickr
#include "fsfloaterprimfeed.h" // <FS:Beq> Share to Primfeed
#include "llimagefilter.h"
#include "llimagefiltersmanager.h"
#include "llimagebmp.h"

View File

@ -225,6 +225,7 @@
#include "lggbeamcolormapfloater.h"
#include "lggbeammapfloater.h"
#include "llfloaterdisplayname.h"
#include "fsfloaterprimfeed.h"
#include "llfloaterflickr.h"
#include "llfloaterscriptrecover.h"
#include "llfloatersearchreplace.h"
@ -630,6 +631,7 @@ void LLViewerFloaterReg::registerFloaters()
LLFloaterReg::add("export_collada", "floater_export_collada.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<ColladaExportFloater>);
LLFloaterReg::add("delete_queue", "floater_script_queue.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterDeleteQueue>);
LLFloaterReg::add("flickr", "floater_flickr.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterFlickr>);
LLFloaterReg::add("primfeed", "floater_primfeed.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<FSFloaterPrimfeed>);
LLFloaterReg::add("fs_asset_blacklist", "floater_fs_asset_blacklist.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<FSFloaterAssetBlacklist>);
LLFloaterReg::add("fs_avatar_render_settings", "floater_fs_avatar_render_settings.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<FSFloaterAvatarRenderSettings>);
LLFloaterReg::add("fs_blocklist", "floater_fs_blocklist.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<FSFloaterBlocklist>);

View File

@ -2757,6 +2757,28 @@ class LLAdvancedCompressFileTest : public view_listener_t
}
};
// <FS:Beq> Primfeed integration test functions (can be removed when the feature is stable)
///////////////////
// PRIMFEED AUTH //
///////////////////
#include "fsprimfeedauth.h"
class LLAdvancedPrimfeedAuth : public view_listener_t
{
bool handleEvent(const LLSD& userdata)
{
FSPrimfeedAuth::initiateAuthRequest();
return true;
}
};
class LLAdvancedPrimfeedAuthReset : public view_listener_t
{
bool handleEvent(const LLSD& userdata)
{
FSPrimfeedAuth::resetAuthStatus();
return true;
}
};
// </FS:Beq>
/////////////////////////
// SHOW DEBUG SETTINGS //
@ -12809,6 +12831,8 @@ void initialize_menus()
view_listener_t::addMenu(new LLAdvancedCheckShowObjectUpdates(), "Advanced.CheckShowObjectUpdates");
view_listener_t::addMenu(new LLAdvancedCompressImage(), "Advanced.CompressImage");
view_listener_t::addMenu(new LLAdvancedCompressFileTest(), "Advanced.CompressFileTest");
view_listener_t::addMenu(new LLAdvancedPrimfeedAuth(), "Advanced.PrimfeedAuth");
view_listener_t::addMenu(new LLAdvancedPrimfeedAuthReset(), "Advanced.PrimfeedAuthReset");
view_listener_t::addMenu(new LLAdvancedShowDebugSettings(), "Advanced.ShowDebugSettings");
view_listener_t::addMenu(new LLAdvancedEnableViewAdminOptions(), "Advanced.EnableViewAdminOptions");
view_listener_t::addMenu(new LLAdvancedToggleViewAdminOptions(), "Advanced.ToggleViewAdminOptions");

View File

@ -111,6 +111,7 @@
#include "llfloatertools.h"
#include "llfloatersnapshot.h" // <FS:Beq/> for snapshotFrame
#include "llfloaterflickr.h" // <FS:Beq/> for snapshotFrame
#include "fsfloaterprimfeed.h" // <FS:Beq/> for snapshotFrame
#include "llsnapshotlivepreview.h" // <FS:Beq/> for snapshotFrame
// #include "llpanelface.h" // <FS:Zi> switchable edit texture/materials panel - include not needed
#include "llpathfindingpathtool.h"
@ -8047,12 +8048,12 @@ bool LLPipeline::renderSnapshotFrame(LLRenderTarget* src, LLRenderTarget* dst)
}
const bool simple_snapshot_visible = LLFloaterReg::instanceVisible("simple_snapshot");
const bool flickr_snapshot_visible = LLFloaterReg::instanceVisible("flickr");
const bool primfeed_snapshot_visible = LLFloaterReg::instanceVisible("primfeed"); // <FS:Beq/> Primfeed integration
const bool snapshot_visible = LLFloaterReg::instanceVisible("snapshot");
const bool any_snapshot_visible = simple_snapshot_visible || flickr_snapshot_visible || snapshot_visible;
const bool any_snapshot_visible = simple_snapshot_visible || flickr_snapshot_visible || primfeed_snapshot_visible || snapshot_visible; // <FS:Beq/> Primfeed integration
if (!show_frame || !any_snapshot_visible || !gPipeline.hasRenderDebugFeatureMask(LLPipeline::RENDER_DEBUG_FEATURE_UI))
{
return false;
}
LLSnapshotLivePreview * previewView = nullptr;
if (snapshot_visible)
@ -8066,6 +8067,13 @@ bool LLPipeline::renderSnapshotFrame(LLRenderTarget* src, LLRenderTarget* dst)
auto * floater = dynamic_cast<LLFloaterFlickr*>(LLFloaterReg::findInstance("flickr"));
previewView = floater->getPreviewView();
}
// <FS:Beq> Primfeed integration
if (primfeed_snapshot_visible && !previewView)
{
auto * floater = dynamic_cast<FSFloaterPrimfeed*>(LLFloaterReg::findInstance("primfeed"));
previewView = floater->getPreviewView();
}
// </FS:Beq>
if(!previewView)
{
return false;

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -166,6 +166,7 @@ with the same filename but different name
<texture name="Command_Places_Icon" file_name="toolbar_icons/places.png" preload="true" />
<texture name="Command_Poser_Icon" file_name="toolbar_icons/poser.png" preload="true" /> <!-- FS:AR FIRE-30873 -->
<texture name="Command_Preferences_Icon" file_name="toolbar_icons/preferences.png" preload="true" />
<texture name="Command_Primfeed_Icon" file_name="icons/primfeed_white.png" preload="true" /> <!-- FS:Beq Primfeed support -->
<texture name="Command_Profile_Icon" file_name="toolbar_icons/profile.png" preload="true" />
<texture name="Command_Report_Abuse_Icon" file_name="toolbar_icons/report_abuse.png" preload="true" />
<texture name="Command_Search_Icon" file_name="toolbar_icons/search.png" preload="true" />

View File

@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<floater
positioning="cascading"
can_close="true"
can_resize="false"
help_topic="floater_primfeed"
layout="topleft"
name="floater_primfeed"
save_rect="true"
single_instance="true"
reuse_instance="true"
title="Share to Primfeed"
height="600"
width="272">
<panel
height="590"
width="272"
visible="true"
name="background"
follows="all"
top="0"
left="0">
<tab_container
name="tabs"
tab_group="1"
tab_min_width="70"
tab_height="21"
tab_position="top"
top="7"
height="570"
follows="all"
halign="center">
<panel
filename="panel_primfeed_photo.xml"
class="fsprimfeedphotopanel"
follows="all"
label="Photo"
name="panel_primfeed_photo"/>
<panel
filename="panel_primfeed_account.xml"
class="fsprimfeedaccountpanel"
follows="all"
label="Account"
name="panel_primfeed_account"/>
</tab_container>
<panel
name="connection_status_panel"
follows="left|bottom|right"
height="24">
<text
name="connection_error_text"
type="string"
follows="left|bottom|right"
bottom="-5"
left="10"
width="250"
height="20"
wrap="true"
halign="left"
valign="center"
text_color="DrYellow"
font="SansSerif">
Error
</text>
<loading_indicator
follows="left|bottom|right"
height="24"
width="24"
name="connection_loading_indicator"
top_delta="-2"
left="10"
visible="true"/>
<text
name="connection_loading_text"
type="string"
follows="left|bottom|right"
top_delta="2"
left_pad="5"
width="250"
height="20"
wrap="true"
halign="left"
valign="center"
text_color="EmphasisColor"
font="SansSerif">
Loading...
</text>
</panel>
</panel>
</floater>

View File

@ -5669,6 +5669,18 @@
<menu_item_call.on_click
function="Advanced.CompressFileTest" />
</menu_item_call>
<menu_item_call
label="PrimFeed Auth Test"
name="primfeed_auth_test">
<menu_item_call.on_click
function="Advanced.PrimfeedAuth" />
</menu_item_call>
<menu_item_call
label="PrimFeed Auth Reset"
name="primfeed_auth_clear">
<menu_item_call.on_click
function="Advanced.PrimfeedAuthReset" />
</menu_item_call>
<menu_item_call
label="Enable Visual Leak Detector"

View File

@ -7912,6 +7912,13 @@ Please select at least one type of content to search (General, Moderate, or Adul
[MESSAGE]
</notification>
<notification
icon="notify.tga"
name="PrimfeedConnect"
type="notifytip">
[MESSAGE]
</notification>
<notification
icon="notify.tga"
name="PaymentReceived"
@ -13962,6 +13969,12 @@ Flickr verification failed. Please try again, and be sure to double check the ve
name="ExodusFlickrUploadComplete"
type="notifytip">
Your snapshot can now be viewed [https://www.flickr.com/photos/me/[ID] here].
</notification>
<notification
icon="notifytip.tga"
name="FSPrimfeedUploadComplete"
type="notifytip">
Your primfeed post can now be viewed [[PF_POSTURL] here].
</notification>
<!-- </FS:TS> FIRE-5453 -->
@ -14676,4 +14689,60 @@ https://wiki.firestormviewer.org/antivirus_whitelisting
yestext="Okay"/>
</notification>
<notification
icon="alertmodal.tga"
name="PrimfeedLoginRequestFailed"
persist="false"
log_to_im="true"
type="notify">
Login request denied by Primfeed.
</notification>
<notification
icon="alertmodal.tga"
name="PrimfeedAuthorisationFailed"
persist="false"
tag="fail"
type="alertmodal">
Primfeed authorisation failed. The authorisation sequence was not completed.
</notification>
<notification
icon="alertmodal.tga"
name="PrimfeedAuthorisationAlreadyInProgress"
persist="false"
tag="fail"
type="alertmodal">
Primfeed authorisation is already in progress. Please complete the primfeed authorisation in your web browser before trying again.
</notification>
<notification
icon="alertmodal.tga"
name="PrimfeedAuthorisationSuccessful"
persist="false"
tag="success"
type="alertmodal">
Primfeed authorisation completed. You may now post images to Primfeed.
</notification>
<notification
icon="alertmodal.tga"
name="PrimfeedValidateFailed"
persist="false"
tag="fail"
type="alertmodal">
Primfeed user validation failed. Primfeed did not recognise this account, or the login failed.
</notification>
<notification
icon="alertmodal.tga"
name="PrimfeedAlreadyAuthorized"
persist="false"
tag="success"
type="alertmodal">
You have already linked this account to Primfeed. Use the reset button if you wish to start over.
</notification>
<notification
icon="alertmodal.tga"
name="PrimfeedUserStatusFailed"
persist="false"
tag="fail"
type="alertmodal">
Primfeed user login successful, but status checks have failed. Please check the Primfeed is working.
</notification>
</notifications>

View File

@ -0,0 +1,105 @@
<panel
height="540"
width="272"
layout="topleft"
name="panel_primfeed_account">
<string
name="primfeed_connected"
value="You are connected to Primfeed as:" />
<string
name="primfeed_disconnected"
value="Not connected to Primfeed" />
<string
name="primfeed_plan_unknown"
value="Unknown" />
<text
layout="topleft"
length="1"
follows="top|left"
font="SansSerif"
height="16"
left="10"
name="connected_as_label"
top="5"
type="string">
Not connected to Primfeed.
</text>
<text
layout="topleft"
top_pad="2"
length="1"
follows="top|left"
font="SansSerif"
height="16"
left="10"
name="primfeed_account_name"
parse_urls="true"
type="string"/>
<text
layout="topleft"
length="1"
follows="top|left"
font="SansSerif"
height="16"
left="10"
name="primfeed_account_plan_label"
top_pad="2"
type="string">
Account type:
</text>
<text
layout="topleft"
top_pad="2"
length="1"
follows="top|left"
font="SansSerif"
height="16"
left="10"
name="primfeed_account_plan"
parse_urls="true"
type="string"/>
<panel
layout="topleft"
name="panel_buttons"
height="345"
left="0">
<button
layout="topleft"
follows="left|top|right"
top_pad="9"
visible="true"
left="10"
right="-10"
height="23"
label="Connect..."
name="connect_btn"
width="210">
<commit_callback function="SocialSharing.Connect"/>
</button>
<button
layout="topleft"
follows="left|top|right"
top_delta="0"
left="10"
right="-10"
height="23"
label="Disconnect"
name="disconnect_btn"
width="210"
visible="false">
<commit_callback function="SocialSharing.Disconnect"/>
</button>
<text
layout="topleft"
length="1"
follows="top|left"
height="16"
left="10"
name="account_learn_more_label"
top_pad="5"
type="string">
[https://docs.primfeed.com Learn more about Primfeed]
</text>
</panel>
</panel>

View File

@ -0,0 +1,350 @@
<panel
height="540"
width="272"
follows="all"
layout="topleft"
name="panel_primfeed_photo">
<combo_box
control_name="FSPrimfeedPhotoResolution"
follows="left|top"
layout="topleft"
top="5"
left="10"
name="resolution_combobox"
tool_tip="Image resolution"
height="21"
width="124">
<combo_box.item
label="Current Window"
name="CurrentWindow"
value="[i0,i0]" />
<combo_box.item
label="320x240"
name="320x240"
value="[i320,i240]" />
<combo_box.item
label="640x480"
name="640x480"
value="[i640,i480]" />
<combo_box.item
label="800x600"
name="800x600"
value="[i800,i600]" />
<combo_box.item
label="1024x768"
name="1024x768"
value="[i1024,i768]" />
<combo_box.item
label="1280x1024"
name="1280x1024"
value="[i1280,i1024]" />
<combo_box.item
label="1600x1200"
name="1600x1200"
value="[i1600,i1200]" />
<combo_box.item
label="Custom"
name="Custom"
value="[i-1,i-1]" />
</combo_box>
<combo_box
follows="left|top"
layout="topleft"
name="filters_combobox"
tool_tip="Image filters"
top_delta="0"
left_pad="4"
height="21"
width="124">
<combo_box.item
label="No Filter"
name="NoFilter"
value="NoFilter" />
</combo_box>
<spinner
allow_text_entry="false"
decimal_digits="0"
follows="left|top"
height="20"
increment="32"
layout="topleft"
left="10"
max_val="6016"
min_val="32"
name="custom_snapshot_width"
top_pad="7"
width="54" />
<text
length="1"
follows="top|left|right"
layout="topleft"
height="16"
left_pad="3"
name="spinner_x_lbl"
top_delta="3"
width="8"
type="string">
x
</text>
<spinner
allow_text_entry="false"
decimal_digits="0"
follows="left|top"
height="20"
increment="32"
label=""
label_width="0"
layout="topleft"
left_pad="0"
max_val="6016"
min_val="32"
name="custom_snapshot_height"
top_delta="-3"
width="54" />
<check_box
follows="left|top"
layout="topleft"
initial_value="true"
label="Keep Aspect ratio"
name="keep_aspect_ratio"
left_pad="4"
height="16"
top_delta="4" />
<panel
height="150"
width="250"
visible="true"
name="thumbnail_placeholder"
top_pad="3"
follows="left|top|right"
layout="topleft"
right="-10"
left="10">
</panel>
<text
follows="left|top"
layout="topleft"
font="SansSerif"
text_color="EmphasisColor"
height="14"
top_pad="2"
left="10"
length="1"
halign="center"
name="working_lbl"
type="string"
visible="true"
width="251">
Refreshing...
</text>
<check_box
control_name="FSSnapshotShowCaptureFrame"
label="Show capture frame"
tool_tip="Show a frame on-screen that surrounds the areas of the snapshot. Parts of the scene that are outside of the snapshot will be de-saturated and slightly blurred."
layout="topleft"
left="10"
top_pad="7"
width="124"
name="show_frame" />
<check_box
enabled_control="FSSnapshotShowCaptureFrame"
control_name="FSSnapshotShowGuides"
label="Framing guide"
tool_tip="Show framing guide (rule of thirds) inside the snapshot frame."
layout="topleft"
left_pad="10"
width="60"
name="show_guides" />
<view_border
bevel_style="in"
follows="left|top"
layout="topleft"
height="1"
left="10"
name="refresh_border"
width="250"
top_pad="0" />
<button
follows="left|top"
layout="topleft"
height="23"
label="Refresh"
left="10"
top_pad="5"
name="new_snapshot_btn"
tool_tip="Click to refresh"
visible="true"
width="100">
<button.commit_callback function="SocialSharing.RefreshPhoto" />
</button>
<button
follows="right|top"
layout="topleft"
height="23"
label="Preview"
right="-10"
top_delta="0"
name="big_preview_btn"
tool_tip="Click to toggle preview"
is_toggle="true"
visible="true"
width="100">
<button.commit_callback function="SocialSharing.BigPreview" />
</button>
<text
length="1"
follows="top|left|right"
layout="topleft"
font="SansSerif"
height="16"
left="10"
right="-10"
name="description_label"
top_pad="5"
width="25"
type="string">
Description:
</text>
<text_editor
follows="left|top"
layout="topleft"
height="100"
width="249"
left="10"
length="1"
top_pad="0"
max_length="700"
name="photo_description"
spellcheck="true"
type="string"
word_wrap="true">
</text_editor>
<check_box
follows="left|top"
layout="topleft"
initial_value="true"
label="Include location"
name="add_location_cb"
left="9"
height="16"
top_pad="8" />
<check_box
control_name="FSPrimfeedAddToPublicGallery"
follows="top|left"
layout="topleft"
initial_value="false"
label="Add to public gallery?"
name="primfeed_add_to_public_gallery"
left="9"
height="16"
top_pad="8" />
<button
follows="top|left"
layout="topleft"
height="16"
image_pressed="Info_Press"
image_unselected="Info_Over"
name="info_btn_pub_gallery"
right="-3"
top_delta="-2"
width="16"
commit_callback.function="Primfeed.Info"
commit_callback.parameter="https://docs.primfeed.com/featured-content/public-gallery"
/>
<check_box
enabled_control="FSPrimfeedAddToPublicGallery"
follows="top|left"
layout="topleft"
initial_value="false"
label="Is this commercial content?"
name="primfeed_commercial_content"
left="9"
height="16"
top_pad="8" />
<button
follows="top|left"
layout="topleft"
height="16"
image_pressed="Info_Press"
image_unselected="Info_Over"
right="-3"
top_delta="-2"
name="info_btn_commercial_content"
width="16"
commit_callback.function="Primfeed.Info"
commit_callback.parameter="https://docs.primfeed.com/legal/terms-of-service#commercial-content"
/>
<combo_box
control_name="FSPrimfeedPhotoRating"
follows="left|top"
layout="topleft"
top_pad="8"
left="10"
name="rating_combobox"
tool_tip="Primfeed content rating"
height="21"
width="235">
<combo_box.item
label="General"
name="GeneralRating"
value="1" />
<combo_box.item
label="Moderate"
name="ModerateRating"
value="2" />
<combo_box.item
label="Adult"
name="AdultRating"
value="3" />
<combo_box.item
label="Adult+"
name="AdultPlusRating"
value="4" />
</combo_box>
<button
follows="top|left"
layout="topleft"
height="16"
image_pressed="Info_Press"
image_unselected="Info_Over"
right="-3"
name="info_btn_ratings"
top_delta="0"
width="16"
commit_callback.function="Primfeed.Info"
commit_callback.parameter="https://docs.primfeed.com/help-and-faq/maturity-ratings"
/>
<check_box
control_name="FSPrimfeedOpenURLOnPost"
follows="top|left"
layout="topleft"
initial_value="false"
label="Open in browser after posting?"
tool_tip="Automatically open the Primfeed post in your web browser after posting."
name="primfeed_open_url_on_post"
left="9"
height="16"
top_pad="8" />
<button
follows="left|top"
layout="topleft"
top_pad="8"
left="10"
height="23"
label="Share"
name="post_photo_btn"
width="100">
<button.commit_callback function="SocialSharing.SendPhoto" />
</button>
<button
follows="right|top"
layout="topleft"
height="23"
label="Cancel"
name="cancel_photo_btn"
right="-10"
top_delta="0"
width="100">
<button.commit_callback function="SocialSharing.Cancel" />
</button>
</panel>

View File

@ -52,6 +52,7 @@
height="22"
image_overlay="Snapshot_Inventory"
image_overlay_alignment="left"
image_overlay_width="16"
image_top_pad="-1"
imgoverlay_label_space="10"
label="Save to Inventory"
@ -111,6 +112,30 @@
function="Snapshot.SendToFlickr"/>
</button>
</layout_panel>
<layout_panel
auto_resize="false"
user_resize="false"
top_pad="0"
height="22"
name="lp_primfeed">
<button
follows="left|top"
font="SansSerif"
halign="left"
height="22"
image_overlay="Command_Primfeed_Icon"
image_overlay_alignment="left"
image_top_pad="0"
imgoverlay_label_space="10"
label="Share to Primfeed"
layout="topleft"
left="9"
name="send_to_primfeed_btn"
top_pad="0">
<button.commit_callback
function="Snapshot.SendToPrimfeed"/>
</button>
</layout_panel>
<layout_panel
auto_resize="false"
user_resize="false"

View File

@ -238,6 +238,10 @@ If you feel this is an error, please contact support@secondlife.com</string>
<string name="SocialFlickrErrorConnecting">Problem connecting to Flickr</string>
<string name="SocialFlickrErrorPosting">Problem posting to Flickr</string>
<string name="SocialFlickrErrorDisconnecting">Problem disconnecting from Flickr</string>
<string name="SocialPrimfeedConnecting">Connecting to Primfeed...</string>
<string name="SocialPrimfeedNotAuthorized">Not Authorized...</string>
<string name="SocialPrimfeedPosting">Posting...</string>
<string name="SocialPrimfeedErrorPosting">Problem posting to Primfeed</string>
<!-- SLShare: User Friendly Filter Names Translation -->
<string name="BlackAndWhite">Black &amp; White</string>
@ -2767,6 +2771,8 @@ name="Command_360_Capture_Label">360° Snapshot</string>
<string name="Command_Beacons_Label">Beacons</string>
<string name="Command_Poser_Label">Poser</string>
<string name="Command_Poser_Tooltip">Pose your avatar and animated objects</string>
<string name="Command_Primfeed_Label">Primfeed</string>
<string name="Command_Primfeed_Tooltip">Post directly to your Primfeed account.</string>
<string
name="Command_360_Capture_Tooltip">Capture a 360° equirectangular image</string>