NAK_CVR_Mods/ShareBubbles/Patches.cs
2025-01-15 16:42:53 -06:00

1731 lines
No EOL
69 KiB
C#

using ABI_RC.Core.InteractionSystem;
using ABI_RC.Core.Networking.API.Responses;
using ABI_RC.Core.Networking.API.UserWebsocket;
using ABI_RC.Core.Player;
using ABI_RC.Core.Savior;
using HarmonyLib;
using MTJobSystem;
using NAK.ShareBubbles.API;
using NAK.ShareBubbles.API.Exceptions;
using NAK.ShareBubbles.API.Responses;
using Newtonsoft.Json;
namespace NAK.ShareBubbles.Patches;
internal static class PlayerSetup_Patches
{
[HarmonyPostfix]
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.SetSafeScale))]
public static void Postfix_PlayerSetup_SetSafeScale()
{
// I wish there was a callback to listen for player scale changes
ShareBubbleManager.Instance.OnPlayerScaleChanged();
}
[HarmonyPostfix]
[HarmonyPatch(typeof(PlayerSetup), nameof(PlayerSetup.GetCurrentPropSelectionMode))]
private static void Postfix_PlayerSetup_GetCurrentPropSelectionMode(ref PlayerSetup.PropSelectionMode __result)
{
// Stickers mod uses invalid enum value 4, so we use 5
// https://github.com/NotAKidoS/NAK_CVR_Mods/blob/d0c8298074c4dcfc089ccb34ed8b8bd7e0b9cedf/Stickers/Patches.cs#L17
if (ShareBubbleManager.Instance.IsPlacingBubbleMode) __result = (PlayerSetup.PropSelectionMode)5;
}
}
internal static class ControllerRay_Patches
{
[HarmonyPostfix]
[HarmonyPatch(typeof(ControllerRay), nameof(ControllerRay.DeleteSpawnable))]
public static void Postfix_ControllerRay_DeleteSpawnable(ref ControllerRay __instance)
{
if (!__instance._interactDown)
return; // not interacted, no need to check
if (PlayerSetup.Instance.GetCurrentPropSelectionMode()
!= PlayerSetup.PropSelectionMode.Delete)
return; // not in delete mode, no need to check
ShareBubble shareBubble = __instance.hitTransform.GetComponentInParent<ShareBubble>();
if (shareBubble == null) return;
ShareBubbleManager.Instance.DestroyBubble(shareBubble);
}
[HarmonyPrefix]
[HarmonyPatch(typeof(ControllerRay), nameof(ControllerRay.HandlePropSpawn))]
private static void Prefix_ControllerRay_HandlePropSpawn(ref ControllerRay __instance)
{
if (!ShareBubbleManager.Instance.IsPlacingBubbleMode)
return;
if (__instance._gripDown) ShareBubbleManager.Instance.IsPlacingBubbleMode = false;
if (__instance._hitUIInternal || !__instance._interactDown)
return;
ShareBubbleManager.Instance.PlaceSelectedBubbleFromControllerRay(__instance.rayDirectionTransform);
}
}
internal static class ViewManager_Patches
{
private const string DETAILS_TOOLBAR_PATCHES = """
const ContentShareMod = {
debugMode: false,
currentContentData: null,
themeColors: null,
/* Theme Handling */
getThemeColors: function() {
if (this.themeColors) return this.themeColors;
// Default fallback colors
const defaultColors = {
background: '#373021',
border: '#59885d'
};
// Try to get colors from favorite category element
const favoriteCategoryElement = document.querySelector('.favorite-category-selection');
if (!favoriteCategoryElement) return defaultColors;
const computedStyle = window.getComputedStyle(favoriteCategoryElement);
this.themeColors = {
background: computedStyle.backgroundColor || defaultColors.background,
border: computedStyle.borderColor || defaultColors.border
};
return this.themeColors;
},
applyThemeToDialog: function(dialog) {
const colors = this.getThemeColors();
dialog.style.backgroundColor = colors.background;
dialog.style.borderColor = colors.border;
// Update any close or page buttons to match theme
const buttons = dialog.querySelectorAll('.close-btn, .page-btn');
buttons.forEach(button => {
button.style.borderColor = colors.border;
});
return colors;
},
/* Core Initialization */
init: function() {
const styles = [
this.getSharedStyles(),
this.ShareBubble.initStyles(),
this.ShareSelect.initStyles(),
this.DirectShare.initStyles(),
this.Unshare.initStyles()
].join('\n');
const styleElement = document.createElement('style');
styleElement.type = 'text/css';
styleElement.innerHTML = styles;
document.head.appendChild(styleElement);
this.shareBubbleDialog = this.ShareBubble.createDialog();
this.shareSelectDialog = this.ShareSelect.createDialog();
this.directShareDialog = this.DirectShare.createDialog();
this.unshareDialog = this.Unshare.createDialog();
this.initializeToolbars();
this.bindEvents();
},
getSharedStyles: function() {
return `
.content-sharing-base-dialog {
position: fixed;
background-color: #373021;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 800px;
min-width: 500px;
border: 3px solid #59885d;
padding: 20px;
z-index: 100000;
opacity: 0;
transition: opacity 0.2s linear;
}
.content-sharing-base-dialog.in {
opacity: 1;
}
.content-sharing-base-dialog.out {
opacity: 0;
}
.content-sharing-base-dialog.hidden {
display: none;
}
.content-sharing-base-dialog h2,
.content-sharing-base-dialog h3 {
margin-top: 0;
margin-bottom: 0.5em;
text-align: left;
}
.content-sharing-base-dialog .description {
margin-bottom: 1em;
text-align: left;
font-size: 0.9em;
color: #aaa;
}
.content-sharing-base-dialog .close-btn {
position: absolute;
top: 1%;
right: 1%;
border-radius: 0.25em;
border: 3px solid #59885d;
padding: 0.5em;
width: 8em;
text-align: center;
}
.page-btn {
border-radius: 0.25em;
border: 3px solid #59885d;
padding: 0.5em;
width: 8em;
text-align: center;
}
.page-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.inp-hidden {
display: none;
}
`;
},
/* Feature Modules */
ShareBubble: {
initStyles: function() {
return `
.share-bubble-dialog {
max-width: 800px;
transform: translate(-50%, -60%);
}
.share-bubble-dialog .content-btn {
position: relative;
margin-bottom: 0.5em;
}
.share-bubble-dialog .btn-group {
display: flex;
margin-bottom: 1em;
}
.share-bubble-dialog .option-select {
flex: 1 1 0;
min-width: 0;
background-color: inherit;
text-align: center;
padding: 0.5em;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid inherit;
}
.share-bubble-dialog .option-select + .option-select {
margin-left: -1px;
}
.share-bubble-dialog .option-select.active,
.share-bubble-dialog .option-select:hover {
background-color: rgba(27, 80, 55, 1);
}
.share-bubble-dialog .action-buttons {
margin-top: 1em;
display: flex;
justify-content: space-between;
}
.share-bubble-dialog .action-btn {
flex: 1;
text-align: center;
padding: 0.5em;
border-radius: 0.25em;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
margin-right: 0.5em;
}
.share-bubble-dialog .action-btn:last-child {
margin-right: 0;
}
`;
},
createDialog: function() {
const dialog = document.createElement('div');
dialog.id = 'content-share-bubble-dialog';
dialog.className = 'content-sharing-base-dialog share-bubble-dialog hidden';
dialog.innerHTML = `
<h2>Share Bubble</h2>
<div class="close-btn button" onclick="ContentShareMod.ShareBubble.hide();">Close</div>
<h3>Visibility</h3>
<p class="description">Choose who can see your Sharing Bubble.</p>
<div class="content-btn btn-group">
<div class="option-select visibility-btn active" data-visibility-value="Everyone"
onclick="ContentShareMod.ShareBubble.changeVisibility(this);">Everyone</div>
<div class="option-select visibility-btn" data-visibility-value="FriendsOnly"
onclick="ContentShareMod.ShareBubble.changeVisibility(this);">Friends Only</div>
</div>
<h3>Lifetime</h3>
<p class="description">How long the Sharing Bubble lasts. You can delete it at any time.</p>
<div class="content-btn btn-group">
<div class="option-select duration-btn active" data-duration-value="2Minutes"
onclick="ContentShareMod.ShareBubble.changeDuration(this);">2 Minutes</div>
<div class="option-select duration-btn" data-duration-value="Session"
onclick="ContentShareMod.ShareBubble.changeDuration(this);">For Session</div>
</div>
<div class="grant-access-section">
<h3>Access Control</h3>
<div class="access-descriptions">
<p class="description access-desc" data-access-type="Permanent" style="display: none;">
Users will keep access to this Private Content. You can manage permissions later on the Hub.
</p>
<p class="description access-desc" data-access-type="Session" style="display: none;">
Users can only access this Private Content while in the same instance.
</p>
<p class="description access-desc" data-access-type="None">
Users cannot access your Private Content through this share.
</p>
</div>
<div class="content-btn btn-group">
<div class="option-select access-btn" data-access-value="Permanent"
onclick="ContentShareMod.ShareBubble.changeAccess(this);">Keep</div>
<div class="option-select access-btn" data-access-value="Session"
onclick="ContentShareMod.ShareBubble.changeAccess(this);">Instance Only</div>
<div class="option-select access-btn active" data-access-value="None"
onclick="ContentShareMod.ShareBubble.changeAccess(this);">None</div>
</div>
</div>
<div class="no-access-control-message" style="display: none;">
<h3>Access Control</h3>
<p class="description">You cannot control access to this content because it is Public or you do not own it.</p>
</div>
<input id="share-visibility" class="inp-hidden" value="Everyone">
<input id="share-duration" class="inp-hidden" value="2Minutes">
<input id="share-access" class="inp-hidden" value="None">
<div class="action-buttons">
<div class="action-btn button" onclick="ContentShareMod.ShareBubble.submit('drop');">Drop</div>
<div class="action-btn button" onclick="ContentShareMod.ShareBubble.submit('select');">Select</div>
</div>
`;
document.body.appendChild(dialog);
return dialog;
},
show: function(contentDetails) {
const dialog = ContentShareMod.shareBubbleDialog;
const colors = ContentShareMod.applyThemeToDialog(dialog);
// Additional ShareBubble-specific theming
const optionSelects = dialog.querySelectorAll('.option-select');
optionSelects.forEach(element => {
element.style.borderColor = colors.border;
});
const grantAccessSection = dialog.querySelector('.grant-access-section');
const noAccessControlMessage = dialog.querySelector('.no-access-control-message');
const showGrantAccess = contentDetails.IsMine && !contentDetails.IsPublic;
if (grantAccessSection && noAccessControlMessage) {
grantAccessSection.style.display = showGrantAccess ? '' : 'none';
noAccessControlMessage.style.display = showGrantAccess ? 'none' : '';
}
dialog.classList.remove('hidden', 'out');
setTimeout(() => dialog.classList.add('in'), 50);
ContentShareMod.currentContentData = contentDetails;
},
hide: function() {
const dialog = ContentShareMod.shareBubbleDialog;
dialog.classList.remove('in');
dialog.classList.add('out');
setTimeout(() => {
dialog.classList.add('hidden');
dialog.classList.remove('out');
}, 200);
},
changeVisibility: function(element) {
document.getElementById('share-visibility').value = element.dataset.visibilityValue;
const buttons = ContentShareMod.shareBubbleDialog.querySelectorAll('.visibility-btn');
buttons.forEach(btn => btn.classList.remove('active'));
element.classList.add('active');
},
changeDuration: function(element) {
document.getElementById('share-duration').value = element.dataset.durationValue;
const buttons = ContentShareMod.shareBubbleDialog.querySelectorAll('.duration-btn');
buttons.forEach(btn => btn.classList.remove('active'));
element.classList.add('active');
},
changeAccess: function(element) {
document.getElementById('share-access').value = element.dataset.accessValue;
const buttons = ContentShareMod.shareBubbleDialog.querySelectorAll('.access-btn');
buttons.forEach(btn => btn.classList.remove('active'));
element.classList.add('active');
const descriptions = ContentShareMod.shareBubbleDialog.querySelectorAll('.access-desc');
descriptions.forEach(desc => {
desc.style.display = desc.dataset.accessType === element.dataset.accessValue ? '' : 'none';
});
},
submit: function(action) {
const contentDetails = ContentShareMod.currentContentData;
let bubbleImpl, bubbleContent, contentImage, contentName;
if (contentDetails.AvatarId) {
bubbleImpl = 'Avatar';
bubbleContent = contentDetails.AvatarId;
contentImage = contentDetails.AvatarImageCoui;
contentName = contentDetails.AvatarName;
} else if (contentDetails.SpawnableId) {
bubbleImpl = 'Spawnable';
bubbleContent = contentDetails.SpawnableId;
contentImage = contentDetails.SpawnableImageCoui;
contentName = contentDetails.SpawnableName;
} else if (contentDetails.WorldId) {
bubbleImpl = 'World';
bubbleContent = contentDetails.WorldId;
contentImage = contentDetails.WorldImageCoui;
contentName = contentDetails.WorldName;
} else if (contentDetails.UserId) {
bubbleImpl = 'User';
bubbleContent = contentDetails.UserId;
contentImage = contentDetails.UserImageCoui;
contentName = contentDetails.UserName;
} else {
console.error('No valid content ID found');
return;
}
const visibility = document.getElementById('share-visibility').value;
const duration = document.getElementById('share-duration').value;
const access = document.getElementById('share-access').value;
const shareRule = visibility === 'FriendsOnly' ? 'FriendsOnly' : 'Everyone';
const shareLifetime = duration === 'Session' ? 'Session' : 'TwoMinutes';
const shareAccess = access === 'Permanent' ? 'Permanent' :
access === 'Session' ? 'Session' : 'NoAccess';
if (ContentShareMod.debugMode) {
console.log('Sharing content:', {
action, bubbleImpl, bubbleContent,
shareRule, shareLifetime, shareAccess
});
}
engine.call('NAKCallShareContent', action, bubbleImpl, bubbleContent,
shareRule, shareLifetime, shareAccess, contentImage, contentName);
this.hide();
}
},
Unshare: {
currentPage: 1,
totalPages: 1,
sharesPerPage: 5,
sharesList: null,
initStyles: function() {
return `
.unshare-dialog {
width: 800px;
height: 1000px;
transform: translate(-50%, -60%);
display: flex;
flex-direction: column;
}
.unshare-dialog .shares-container {
flex: 1;
overflow-y: auto;
margin: 20px 0;
min-height: 0;
}
.unshare-dialog #shares-loading,
.unshare-dialog #shares-error,
.unshare-dialog #shares-empty {
text-align: center;
padding: 2em;
font-size: 1.1em;
}
.unshare-dialog #shares-error {
color: #ff6b6b;
}
.unshare-dialog .share-item {
display: flex;
align-items: center;
padding: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 10px;
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
min-height: 120px;
}
.unshare-dialog .share-item img {
width: 96px;
height: 96px;
border-radius: 4px;
margin-right: 15px;
cursor: pointer;
}
.unshare-dialog .share-item .user-name {
flex: 1;
font-size: 1.3em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 15px;
cursor: pointer;
}
.unshare-dialog .action-btn {
width: 180px;
height: 60px;
font-size: 1.2em;
color: white;
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 10px;
padding: 0 20px;
text-align: center;
}
.unshare-dialog .revoke-btn {
background-color: #ff6b6b;
}
.unshare-dialog .undo-btn {
background-color: #4a9eff;
}
.unshare-dialog .action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
background-color: #666;
}
.unshare-dialog .pagination {
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding-top: 20px;
}
.unshare-dialog .page-info {
font-size: 1.1em;
opacity: 0.8;
margin-bottom: 15px;
text-align: center;
}
.unshare-dialog .page-buttons {
display: flex;
justify-content: center;
gap: 20px;
}
.unshare-dialog .share-item.revoked {
opacity: 0.7;
}
`;
},
createDialog: function() {
const dialog = document.createElement('div');
dialog.id = 'content-unshare-dialog';
dialog.className = 'content-sharing-base-dialog unshare-dialog hidden';
dialog.innerHTML = `
<h2>Manage Shares</h2>
<div class="close-btn button" onclick="ContentShareMod.Unshare.hide();">Close</div>
<div class="shares-container">
<div id="shares-loading">Loading shares...</div>
<div id="shares-error" style="display: none;">Failed to load shares. Please try again.</div>
<div id="shares-empty" style="display: none;">No active shares found.</div>
<div id="shares-list" style="display: none;"></div>
</div>
<div class="pagination">
<div class="page-info">1/1</div>
<div class="page-buttons">
<div class="page-btn button" onclick="ContentShareMod.Unshare.previousPage();">Previous</div>
<div class="page-btn button" onclick="ContentShareMod.Unshare.nextPage();">Next</div>
</div>
</div>
`;
document.body.appendChild(dialog);
return dialog;
},
show: function(contentDetails) {
const dialog = ContentShareMod.unshareDialog;
ContentShareMod.applyThemeToDialog(dialog);
dialog.classList.remove('hidden', 'out');
setTimeout(() => dialog.classList.add('in'), 50);
ContentShareMod.currentContentData = contentDetails;
this.currentPage = 1;
this.totalPages = 1;
this.sharesList = null;
this.requestShares();
},
hide: function() {
const dialog = ContentShareMod.unshareDialog;
dialog.classList.remove('in');
dialog.classList.add('out');
setTimeout(() => {
dialog.classList.add('hidden');
dialog.classList.remove('out');
}, 200);
},
requestShares: function() {
const dialog = ContentShareMod.unshareDialog;
const sharesContainer = dialog.querySelector('.shares-container');
sharesContainer.querySelector('#shares-loading').style.display = '';
sharesContainer.querySelector('#shares-error').style.display = 'none';
sharesContainer.querySelector('#shares-empty').style.display = 'none';
sharesContainer.querySelector('#shares-list').style.display = 'none';
const contentDetails = ContentShareMod.currentContentData;
const contentType = contentDetails.AvatarId ? 'Avatar' : 'Spawnable';
const contentId = contentDetails.AvatarId || contentDetails.SpawnableId;
engine.call('NAKGetContentShares', contentType, contentId);
},
handleSharesResponse: function(success, shares) {
const dialog = ContentShareMod.unshareDialog;
const sharesContainer = dialog.querySelector('.shares-container');
const loadingElement = sharesContainer.querySelector('#shares-loading');
const errorElement = sharesContainer.querySelector('#shares-error');
const emptyElement = sharesContainer.querySelector('#shares-empty');
const sharesListElement = sharesContainer.querySelector('#shares-list');
loadingElement.style.display = 'none';
if (!success) {
errorElement.style.display = '';
return;
}
try {
const response = JSON.parse(shares);
this.sharesList = response.Data.value;
if (!this.sharesList || this.sharesList.length === 0) {
emptyElement.style.display = '';
const pagination = dialog.querySelector('.pagination');
const [prevButton, nextButton] = pagination.querySelectorAll('.page-btn');
prevButton.disabled = true;
nextButton.disabled = true;
pagination.querySelector('.page-info').textContent = '1/1';
return;
}
this.totalPages = Math.ceil(this.sharesList.length / this.sharesPerPage);
this.updatePageContent();
} catch (error) {
console.error('Error parsing shares:', error);
errorElement.style.display = '';
}
},
updatePageContent: function() {
const dialog = ContentShareMod.unshareDialog;
const sharesListElement = dialog.querySelector('#shares-list');
const startIndex = (this.currentPage - 1) * this.sharesPerPage;
const endIndex = startIndex + this.sharesPerPage;
const currentShares = this.sharesList.slice(startIndex, endIndex);
sharesListElement.innerHTML = currentShares.map(share => `
<div class="share-item" data-user-id="${share.id}">
<img src="${share.image || '/api/placeholder/96/96'}"
alt="${share.name}'s avatar"
onerror="this.src='/api/placeholder/96/96'"
onclick="ContentShareMod.Unshare.viewUserProfile('${share.id}')">
<span class="user-name"
onclick="ContentShareMod.Unshare.viewUserProfile('${share.id}')">${share.name}</span>
<button class="action-btn revoke-btn button"
onclick="ContentShareMod.Unshare.revokeShare('${share.id}', this)">
Revoke
</button>
</div>
`).join('');
sharesListElement.style.display = '';
const pagination = dialog.querySelector('.pagination');
pagination.querySelector('.page-info').textContent = `${this.currentPage}/${this.totalPages}`;
const [prevButton, nextButton] = pagination.querySelectorAll('.page-btn');
prevButton.disabled = this.currentPage === 1;
nextButton.disabled = this.currentPage === this.totalPages;
},
previousPage: function() {
if (this.currentPage > 1) {
this.currentPage--;
this.updatePageContent();
}
},
nextPage: function() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
this.updatePageContent();
}
},
viewUserProfile: function(userId) {
this.hide();
getUserDetails(userId);
},
revokeShare: function(userId, buttonElement) {
const contentDetails = ContentShareMod.currentContentData;
const contentType = contentDetails.AvatarId ? 'Avatar' : 'Spawnable';
const contentId = contentDetails.AvatarId || contentDetails.SpawnableId;
buttonElement.disabled = true;
buttonElement.textContent = 'Revoking...';
engine.call('NAKRevokeContentShare', contentType, contentId, userId);
},
handleRevokeResponse: function(success, userId, error) {
const dialog = ContentShareMod.unshareDialog;
const shareItem = dialog.querySelector(`[data-user-id="${userId}"]`);
if (!shareItem) return;
const actionButton = shareItem.querySelector('button');
if (success) {
shareItem.classList.add('revoked');
actionButton.className = 'action-btn undo-btn button';
actionButton.textContent = 'Undo';
actionButton.onclick = () => {
actionButton.disabled = true;
actionButton.textContent = 'Restoring...';
const contentDetails = ContentShareMod.currentContentData;
const contentType = contentDetails.AvatarId ? 'Avatar' : 'Spawnable';
const contentId = contentDetails.AvatarId || contentDetails.SpawnableId;
engine.call('NAKCallShareContentDirect', contentType, contentId, userId);
};
uiPushShow("Share revoked successfully", 3);
} else {
actionButton.textContent = 'Failed';
actionButton.classList.add('failed');
uiPushShow(error || "Failed to revoke share", 3);
// Reset button after a moment
setTimeout(() => {
actionButton.disabled = false;
actionButton.textContent = 'Revoke';
actionButton.classList.remove('failed');
}, 1000);
}
},
handleShareResponse: function(success, userId, error) {
const dialog = ContentShareMod.unshareDialog;
const shareItem = dialog.querySelector(`[data-user-id="${userId}"]`);
if (!shareItem) return;
const actionButton = shareItem.querySelector('button');
if (success) {
this.requestShares();
uiPushShow("Share restored successfully", 3);
} else {
actionButton.textContent = 'Failed';
actionButton.classList.add('failed');
uiPushShow(error || "Failed to restore share", 3);
// Reset button after a moment
setTimeout(() => {
actionButton.disabled = false;
actionButton.textContent = 'Undo';
actionButton.classList.remove('failed');
}, 1000);
}
}
},
DirectShare: {
currentPage: 1,
totalPages: 1,
usersPerPage: 5,
usersList: null,
isInstanceUsers: true,
initStyles: function() {
return `
.direct-share-dialog {
width: 800px;
height: 1000px;
transform: translate(-50%, -60%);
display: flex;
flex-direction: column;
}
.direct-share-dialog .search-container {
margin: 20px 0;
padding: 10px;
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
.direct-share-dialog .search-input {
width: 100%;
padding: 10px;
border: none;
background: transparent;
color: inherit;
font-size: 1.1em;
}
.direct-share-dialog .source-indicator {
padding: 10px;
text-align: center;
opacity: 0.8;
background: rgba(0, 0, 0, 0.1);
border-radius: 4px;
margin-bottom: 20px;
}
.direct-share-dialog .users-container {
flex: 1;
overflow-y: auto;
margin-bottom: 20px;
min-height: 0;
}
.direct-share-dialog .user-item {
display: flex;
align-items: center;
padding: 15px;
border: 1px solid rgba(255, 255, 255, 0.1);
margin-bottom: 10px;
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
min-height: 96px;
}
.direct-share-dialog .user-item img {
width: 96px;
height: 96px;
margin-right: 15px;
cursor: pointer;
border-radius: 4px;
}
.direct-share-dialog .user-item .user-name {
flex: 1;
font-size: 1.3em;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
margin-right: 15px;
cursor: pointer;
}
.direct-share-dialog .user-item .action-btn {
width: 140px;
height: 50px;
font-size: 1.2em;
color: white;
background-color: rgba(27, 80, 55, 1);
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
}
.direct-share-dialog .user-item .action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
background-color: #4a9eff;
}
.direct-share-dialog .pagination {
border-top: 1px solid rgba(255, 255, 255, 0.1);
padding-top: 20px;
}
.direct-share-dialog .page-info {
font-size: 1.1em;
opacity: 0.8;
margin-bottom: 15px;
text-align: center;
}
.direct-share-dialog .page-buttons {
display: flex;
justify-content: center;
gap: 20px;
}
.direct-share-dialog .user-item .action-btn {
width: 180px;
height: 60px;
font-size: 1.2em;
color: white;
border-radius: 4px;
display: inline-flex;
align-items: center;
justify-content: center;
margin-left: 10px;
padding: 0 20px;
text-align: center;
background-color: rgba(27, 80, 55, 1);
}
.direct-share-dialog .user-item .action-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.direct-share-dialog .user-item .action-btn.shared {
background-color: #4a9eff;
}
.direct-share-dialog .user-item .action-btn.failed {
background-color: #ff6b6b;
}
`;
},
createDialog: function() {
const dialog = document.createElement('div');
dialog.id = 'content-direct-share-dialog';
dialog.className = 'content-sharing-base-dialog direct-share-dialog hidden';
dialog.innerHTML = `
<h2>Direct Share</h2>
<div class="close-btn button" onclick="ContentShareMod.DirectShare.hide();">Close</div>
<div class="users-container">
<div id="users-loading">Loading users...</div>
<div id="users-error" style="display: none;">Failed to load users. Please try again.</div>
<div id="users-empty" style="display: none;">No users found.</div>
<div id="users-list" style="display: none;"></div>
</div>
<div class="pagination">
<div class="page-info">Page 1/1</div>
<div class="page-buttons">
<div class="page-btn button" onclick="ContentShareMod.DirectShare.previousPage();">Previous</div>
<div class="page-btn button" onclick="ContentShareMod.DirectShare.nextPage();">Next</div>
</div>
</div>
`;
document.body.appendChild(dialog);
return dialog;
},
show: function(contentDetails) {
const dialog = ContentShareMod.directShareDialog;
ContentShareMod.applyThemeToDialog(dialog);
dialog.classList.remove('hidden', 'out');
setTimeout(() => dialog.classList.add('in'), 50);
ContentShareMod.currentContentData = contentDetails;
this.currentPage = 1;
this.totalPages = 1;
this.usersList = null;
this.requestUsers(true);
},
hide: function() {
const dialog = ContentShareMod.directShareDialog;
dialog.classList.remove('in');
dialog.classList.add('out');
setTimeout(() => {
dialog.classList.add('hidden');
dialog.classList.remove('out');
}, 200);
},
handleUsersResponse: function(success, users, isInstanceUsers) {
const dialog = ContentShareMod.directShareDialog;
const usersContainer = dialog.querySelector('.users-container');
// const sourceIndicator = dialog.querySelector('.source-indicator');
const loadingElement = usersContainer.querySelector('#users-loading');
const errorElement = usersContainer.querySelector('#users-error');
const emptyElement = usersContainer.querySelector('#users-empty');
const usersListElement = usersContainer.querySelector('#users-list');
loadingElement.style.display = 'none';
// sourceIndicator.textContent = isInstanceUsers ?
// 'Showing users in current instance' :
// 'Showing search results';
// TODO: Add source indicator to html:
// <div class="source-indicator">
// Showing users in current instance
// </div>
if (!success) {
errorElement.style.display = '';
return;
}
try {
const response = JSON.parse(users);
this.usersList = response.entries;
this.isInstanceUsers = isInstanceUsers;
if (!this.usersList || this.usersList.length === 0) {
emptyElement.style.display = '';
this.updatePagination();
return;
}
this.totalPages = Math.ceil(this.usersList.length / this.usersPerPage);
this.updatePageContent();
} catch (error) {
console.error('Error parsing users:', error);
errorElement.style.display = '';
}
},
handleSearch: function(event) {
if (event.key === 'Enter') {
const searchValue = event.target.value.trim();
// Pass true for instance users when empty search, false for search results
this.requestUsers(searchValue === '', searchValue);
}
},
requestUsers: function(isInstanceUsers, searchQuery = '') {
const dialog = ContentShareMod.directShareDialog;
const usersContainer = dialog.querySelector('.users-container');
usersContainer.querySelector('#users-loading').style.display = '';
usersContainer.querySelector('#users-error').style.display = 'none';
usersContainer.querySelector('#users-empty').style.display = 'none';
usersContainer.querySelector('#users-list').style.display = 'none';
engine.call('NAKGetUsersForSharing', searchQuery);
},
updatePageContent: function() {
const dialog = ContentShareMod.directShareDialog;
const usersListElement = dialog.querySelector('#users-list');
const startIndex = (this.currentPage - 1) * this.usersPerPage;
const endIndex = startIndex + this.usersPerPage;
const currentUsers = this.usersList.slice(startIndex, endIndex);
usersListElement.innerHTML = currentUsers.map(user => `
<div class="user-item" data-user-id="${user.id}">
<img src="${user.image || '/api/placeholder/96/96'}"
alt="${user.name}'s avatar"
onerror="this.src='/api/placeholder/96/96'"
onclick="ContentShareMod.DirectShare.viewUserProfile('${user.id}')">
<span class="user-name"
onclick="ContentShareMod.DirectShare.viewUserProfile('${user.id}')">${user.name}</span>
<button class="action-btn button"
onclick="ContentShareMod.DirectShare.shareWithUser('${user.id}', this)">
Share
</button>
</div>
`).join('');
usersListElement.style.display = '';
this.updatePagination();
},
updatePagination: function() {
const dialog = ContentShareMod.directShareDialog;
const pagination = dialog.querySelector('.pagination');
const [prevButton, nextButton] = pagination.querySelectorAll('.page-btn');
pagination.querySelector('.page-info').textContent = `Page ${this.currentPage}/${this.totalPages}`;
prevButton.disabled = this.currentPage === 1;
nextButton.disabled = this.currentPage === this.totalPages;
},
previousPage: function() {
if (this.currentPage > 1) {
this.currentPage--;
this.updatePageContent();
}
},
nextPage: function() {
if (this.currentPage < this.totalPages) {
this.currentPage++;
this.updatePageContent();
}
},
viewUserProfile: function(userId) {
this.hide();
getUserDetails(userId);
},
shareWithUser: function(userId, buttonElement) {
const contentDetails = ContentShareMod.currentContentData;
const contentType = contentDetails.AvatarId ? 'Avatar' : 'Spawnable';
const contentId = contentDetails.AvatarId || contentDetails.SpawnableId;
const contentName = contentDetails.AvatarName || contentDetails.SpawnableName;
const contentImage = contentDetails.AvatarImageURL || contentDetails.SpawnableImageURL;
buttonElement.disabled = true;
buttonElement.textContent = 'Sharing...';
engine.call('NAKCallShareContentDirect', contentType, contentId, userId, contentName, contentImage);
},
handleShareResponse: function(success, userId, error) {
const dialog = ContentShareMod.directShareDialog;
const userItem = dialog.querySelector(`[data-user-id="${userId}"]`);
if (!userItem) return;
const actionButton = userItem.querySelector('button');
if (success) {
actionButton.textContent = 'Shared';
actionButton.disabled = true;
actionButton.classList.add('shared');
uiPushShow("Content shared successfully", 3, "shareresponse");
} else {
actionButton.disabled = false;
actionButton.textContent = 'Failed';
actionButton.classList.add('failed');
uiPushShow(error || "Failed to share content", 3, "shareresponse");
// Reset button after a moment
setTimeout(() => {
actionButton.disabled = false;
actionButton.textContent = 'Share';
actionButton.classList.remove('failed', 'shared');
}, 1000);
}
}
},
ShareSelect: {
initStyles: function() {
return `
.share-select-dialog {
width: 650px;
height: 480px;
transform: translate(-50%, -80%);
}
.share-select-dialog .share-options {
display: flex;
flex-direction: column;
gap: 15px;
margin-top: 20px;
}
.share-select-dialog .share-option {
padding: 20px;
text-align: left;
cursor: pointer;
background: rgba(0, 0, 0, 0.2);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 4px;
transition: background-color 0.2s ease;
}
.share-select-dialog .share-option:hover {
background-color: rgba(27, 80, 55, 1);
border-color: rgba(255, 255, 255, 0.2);
}
.share-select-dialog h3 {
margin: 0 0 8px 0;
font-size: 1.2em;
}
.share-select-dialog p {
margin: 0;
opacity: 0.8;
font-size: 0.95em;
line-height: 1.4;
}
`;
},
createDialog: function() {
const dialog = document.createElement('div');
dialog.id = 'content-share-select-dialog';
dialog.className = 'content-sharing-base-dialog share-select-dialog hidden';
dialog.innerHTML = `
<h2>Share Content</h2>
<div class="close-btn button" onclick="ContentShareMod.ShareSelect.hide();">Close</div>
<div class="share-options">
<div class="share-option" onclick="ContentShareMod.ShareSelect.openShareBubble();">
<h3>Share Bubble</h3>
<p>Drop or place a bubble in the world that others can interact with</p>
</div>
<div class="share-option" onclick="ContentShareMod.ShareSelect.openDirectShare();">
<h3>Direct Share</h3>
<p>Share directly with specific users</p>
</div>
</div>
`;
document.body.appendChild(dialog);
return dialog;
},
show: function(contentDetails) {
const dialog = ContentShareMod.shareSelectDialog;
ContentShareMod.applyThemeToDialog(dialog);
dialog.classList.remove('hidden', 'out');
setTimeout(() => dialog.classList.add('in'), 50);
ContentShareMod.currentContentData = contentDetails;
},
hide: function() {
const dialog = ContentShareMod.shareSelectDialog;
dialog.classList.remove('in');
dialog.classList.add('out');
setTimeout(() => {
dialog.classList.add('hidden');
dialog.classList.remove('out');
}, 200);
},
openShareBubble: function() {
this.hide();
ContentShareMod.ShareBubble.show(ContentShareMod.currentContentData);
},
openDirectShare: function() {
this.hide();
ContentShareMod.DirectShare.show(ContentShareMod.currentContentData);
}
},
// Toolbar initialization and event bindings
initializeToolbars: function() {
const findEmptyButtons = (toolbar) => {
return Array.from(toolbar.querySelectorAll('.toolbar-btn')).filter(
btn => btn.textContent.trim() === ""
);
};
const setupToolbar = (selector) => {
const toolbar = document.querySelector(selector);
if (!toolbar) return;
const emptyButtons = findEmptyButtons(toolbar);
if (emptyButtons.length >= 2) {
emptyButtons[0].classList.add('content-share-btn');
emptyButtons[0].textContent = 'Share';
emptyButtons[1].classList.add('content-unshare-btn');
emptyButtons[1].textContent = 'Unshare';
}
};
setupToolbar('#avatar-detail .avatar-toolbar');
setupToolbar('#prop-detail .avatar-toolbar');
},
bindEvents: function() {
// Avatar events
engine.on("LoadAvatarDetails", (avatarDetails) => {
const shareBtn = document.querySelector('#avatar-detail .content-share-btn');
const unshareBtn = document.querySelector('#avatar-detail .content-unshare-btn');
const canShareDirectly = avatarDetails.IsMine;
const canUnshare = avatarDetails.IsMine || avatarDetails.IsSharedWithMe;
if (shareBtn) {
shareBtn.classList.remove('disabled');
if (canShareDirectly) {
shareBtn.onclick = () => ContentShareMod.ShareSelect.show(avatarDetails);
} else {
shareBtn.onclick = () => ContentShareMod.ShareBubble.show(avatarDetails);
}
}
if (unshareBtn) {
if (canUnshare) {
unshareBtn.classList.remove('disabled');
unshareBtn.onclick = () => {
if (avatarDetails.IsMine) {
ContentShareMod.Unshare.show(avatarDetails);
} else {
uiConfirmShow("Unshare Avatar",
"Are you sure you want to unshare this avatar?",
"unshare_avatar_confirmation",
avatarDetails.AvatarId);
}
};
} else {
unshareBtn.classList.add('disabled');
unshareBtn.onclick = null;
}
}
ContentShareMod.currentContentData = avatarDetails;
});
// Prop events
engine.on("LoadPropDetails", (propDetails) => {
const shareBtn = document.querySelector('#prop-detail .content-share-btn');
const unshareBtn = document.querySelector('#prop-detail .content-unshare-btn');
const canShareDirectly = propDetails.IsMine;
const canUnshare = propDetails.IsMine || propDetails.IsSharedWithMe;
if (shareBtn) {
shareBtn.classList.remove('disabled');
if (canShareDirectly) {
shareBtn.onclick = () => ContentShareMod.ShareSelect.show(propDetails);
} else {
shareBtn.onclick = () => ContentShareMod.ShareBubble.show(propDetails);
}
}
if (unshareBtn) {
if (canUnshare) {
unshareBtn.classList.remove('disabled');
unshareBtn.onclick = () => {
if (propDetails.IsMine) {
ContentShareMod.Unshare.show(propDetails);
} else {
uiConfirmShow("Unshare Prop",
"Are you sure you want to unshare this prop?",
"unshare_prop_confirmation",
propDetails.SpawnableId);
}
};
} else {
unshareBtn.classList.add('disabled');
unshareBtn.onclick = null;
}
}
ContentShareMod.currentContentData = propDetails;
});
// Share response handlers
engine.on("OnHandleSharesResponse", (success, shares) => {
if (ContentShareMod.debugMode) {
console.log('Shares response:', success, shares);
}
ContentShareMod.Unshare.handleSharesResponse(success, shares);
});
engine.on("OnHandleRevokeResponse", (success, userId, error) => {
ContentShareMod.Unshare.handleRevokeResponse(success, userId, error);
});
engine.on("OnHandleShareResponse", function(success, userId, error) {
// Pass event to Unshare and DirectShare modules depending on which dialog is open
if (ContentShareMod.unshareDialog && !ContentShareMod.unshareDialog.classList.contains('hidden')) {
ContentShareMod.Unshare.handleShareResponse(success, userId, error);
} else if (ContentShareMod.directShareDialog && !ContentShareMod.directShareDialog.classList.contains('hidden')) {
ContentShareMod.DirectShare.handleShareResponse(success, userId, error);
}
});
// Share release handlers
engine.on("OnReleasedAvatarShare", (contentId) => {
if (!ContentShareMod.currentContentData ||
ContentShareMod.currentContentData.AvatarId !== contentId) return;
const unshareBtn = document.querySelector('#avatar-detail .content-unshare-btn');
ContentShareMod.currentContentData.IsSharedWithMe = false;
if (unshareBtn) {
unshareBtn.classList.add('disabled');
unshareBtn.onclick = null;
}
const contentIsAccessible = ContentShareMod.currentContentData.IsMine ||
ContentShareMod.currentContentData.IsPublic;
if (!contentIsAccessible) {
const detail = document.querySelector('#avatar-detail');
if (detail) {
['drop-btn', 'select-btn', 'fav-btn'].forEach(className => {
const button = detail.querySelector('.' + className);
if (button) {
button.classList.add('disabled');
button.removeAttribute('onclick');
}
});
}
}
});
engine.on("OnReleasedPropShare", (contentId) => {
if (!ContentShareMod.currentContentData ||
ContentShareMod.currentContentData.SpawnableId !== contentId) return;
const unshareBtn = document.querySelector('#prop-detail .content-unshare-btn');
ContentShareMod.currentContentData.IsSharedWithMe = false;
if (unshareBtn) {
unshareBtn.classList.add('disabled');
unshareBtn.onclick = null;
}
const contentIsAccessible = ContentShareMod.currentContentData.IsMine ||
ContentShareMod.currentContentData.IsPublic;
if (!contentIsAccessible) {
const detail = document.querySelector('#prop-detail');
if (detail) {
['drop-btn', 'select-btn', 'fav-btn'].forEach(className => {
const button = detail.querySelector('.' + className);
if (button) {
button.classList.add('disabled');
button.removeAttribute('onclick');
}
});
}
}
});
engine.on("OnHandleUsersResponse", (success, users, isInstanceUsers) => {
if (ContentShareMod.debugMode) {
console.log('Users response:', success, isInstanceUsers, users);
}
ContentShareMod.DirectShare.handleUsersResponse(success, users, isInstanceUsers);
});
}
};
ContentShareMod.init();
""";
private const string UiConfirmId_ReleaseAvatarShareWarning = "unshare_avatar_confirmation";
private const string UiConfirmId_ReleasePropShareWarning = "unshare_prop_confirmation";
[HarmonyPostfix]
[HarmonyPatch(typeof(ViewManager), nameof(ViewManager.Start))]
public static void Postfix_ViewManager_Start(ViewManager __instance)
{
// Inject the details toolbar patches when the game menu view is loaded
__instance.gameMenuView.Listener.FinishLoad += (_) => {
__instance.gameMenuView.View._view.ExecuteScript(DETAILS_TOOLBAR_PATCHES);
__instance.gameMenuView.View.BindCall("NAKCallShareContent", OnShareContent);
__instance.gameMenuView.View.BindCall("NAKGetContentShares", OnGetContentShares);
__instance.gameMenuView.View.BindCall("NAKRevokeContentShare", OnRevokeContentShare);
__instance.gameMenuView.View.BindCall("NAKCallShareContentDirect", OnShareContentDirect);
__instance.gameMenuView.View.BindCall("NAKGetUsersForSharing", OnGetUsersForSharing);
};
// Add the event listener for the unshare confirmation dialog
__instance.OnUiConfirm.AddListener(OnReleaseContentShareConfirmation);
return;
void OnShareContent(
string action,
string bubbleImpl,
string bubbleContent,
string shareRule,
string shareLifetime,
string shareAccess,
string contentImage,
string contentName)
{
// Action: drop, select
// BubbleImpl: Avatar, Prop, World, User
// BubbleContent: AvatarId, PropId, WorldId, UserId
// ShareRule: Public, FriendsOnly
// ShareLifetime: TwoMinutes, Session
// ShareAccess: PermanentAccess, SessionAccess, NoAccess
ShareRule rule = shareRule switch
{
"Everyone" => ShareRule.Everyone,
"FriendsOnly" => ShareRule.FriendsOnly,
_ => ShareRule.Everyone
};
ShareLifetime lifetime = shareLifetime switch
{
"Session" => ShareLifetime.Session,
"TwoMinutes" => ShareLifetime.TwoMinutes,
_ => ShareLifetime.TwoMinutes
};
ShareAccess access = shareAccess switch
{
"Permanent" => ShareAccess.Permanent,
"Session" => ShareAccess.Session,
"None" => ShareAccess.None,
_ => ShareAccess.None
};
uint implTypeHash = ShareBubbleManager.GetMaskedHash(bubbleImpl);
ShareBubbleData bubbleData = new()
{
BubbleId = ShareBubbleManager.GenerateBubbleId(bubbleContent, implTypeHash),
ImplTypeHash = implTypeHash,
ContentId = bubbleContent,
Rule = rule,
Lifetime = lifetime,
Access = access,
CreatedAt = DateTime.UtcNow
};
switch (action)
{
case "drop":
ShareBubbleManager.Instance.DropBubbleInFront(bubbleData);
break;
case "select":
ShareBubbleManager.Instance.SelectBubbleForPlace(contentImage, contentName, bubbleData);
break;
}
// Close menu
ViewManager.Instance.UiStateToggle(false);
}
void OnReleaseContentShareConfirmation(string id, string value, string contentId)
{
// Check if the confirmation event is for unsharing content
if (id != UiConfirmId_ReleaseAvatarShareWarning
&& id != UiConfirmId_ReleasePropShareWarning)
return;
//ShareBubblesMod.Logger.Msg($"Unshare confirmation received: {id}, {value}");
// Check if the user confirmed the unshare action
if (value != "true")
{
//ShareBubblesMod.Logger.Msg("Unshare action cancelled by user");
return;
}
//ShareBubblesMod.Logger.Msg("Releasing share...");
// Determine the content type based on the confirmation ID
ShareApiHelper.ShareContentType contentType = id == UiConfirmId_ReleaseAvatarShareWarning
? ShareApiHelper.ShareContentType.Avatar
: ShareApiHelper.ShareContentType.Spawnable;
Task.Run(async () => {
try
{
await ShareApiHelper.ReleaseShareAsync<BaseResponse>(contentType, contentId);
MTJobManager.RunOnMainThread("release_share_response", () =>
{
// Cannot display a success message as opening details page pushes itself to top
// after talking to api, so success message would need to be timed to show after
// if (contentType == ApiShareHelper.ShareContentType.Avatar)
// ViewManager.Instance.RequestAvatarDetailsPage(contentId);
// else
// ViewManager.Instance.GetPropDetails(contentId);
ViewManager.Instance.gameMenuView.View._view.TriggerEvent(
contentType == ShareApiHelper.ShareContentType.Avatar
? "OnReleasedAvatarShare" : "OnReleasedPropShare",
contentId);
ViewManager.Instance.TriggerPushNotification("Content unshared successfully", 3f);
});
}
catch (ShareApiException ex)
{
ShareBubblesMod.Logger.Error($"Share API error: {ex.Message}");
MTJobManager.RunOnMainThread("release_share_error", () => {
ViewManager.Instance.TriggerAlert("Release Share Error", ex.UserFriendlyMessage, -1, true);
});
}
catch (Exception ex)
{
ShareBubblesMod.Logger.Error($"Unexpected error releasing share: {ex.Message}");
MTJobManager.RunOnMainThread("release_share_error", () => {
ViewManager.Instance.TriggerAlert("Release Share Error", "An unexpected error occurred", -1, true);
});
}
});
}
async void OnGetContentShares(string contentType, string contentId)
{
try
{
var response = await ShareApiHelper.GetSharesAsync<BaseResponse<ActiveSharesResponse>>(
contentType == "Avatar" ? ShareApiHelper.ShareContentType.Avatar : ShareApiHelper.ShareContentType.Spawnable,
contentId
);
// TODO: somethign better than this cause this is ass and i need to replace the image urls with ImageCache coui ones
// FUICJK<
string json = JsonConvert.SerializeObject(response.Data);
// log the json to console
//ShareBubblesMod.Logger.Msg($"Shares response: {json}");
__instance.gameMenuView.View.TriggerEvent("OnHandleSharesResponse", true, json);
}
catch (Exception ex)
{
ShareBubblesMod.Logger.Error($"Failed to get content shares: {ex.Message}");
__instance.gameMenuView.View.TriggerEvent("OnHandleSharesResponse", false);
}
}
async void OnRevokeContentShare(string contentType, string contentId, string userId)
{
try
{
await ShareApiHelper.ReleaseShareAsync<BaseResponse>(
contentType == "Avatar" ? ShareApiHelper.ShareContentType.Avatar : ShareApiHelper.ShareContentType.Spawnable,
contentId,
userId
);
__instance.gameMenuView.View.TriggerEvent("OnHandleRevokeResponse", true, userId);
}
catch (ShareApiException ex)
{
ShareBubblesMod.Logger.Error($"Share API error revoking share: {ex.Message}");
__instance.gameMenuView.View.TriggerEvent("OnHandleRevokeResponse", false, userId, ex.UserFriendlyMessage);
}
catch (Exception ex)
{
ShareBubblesMod.Logger.Error($"Unexpected error revoking share: {ex.Message}");
__instance.gameMenuView.View.TriggerEvent("OnHandleRevokeResponse", false, userId, "An unexpected error occurred");
}
}
async void OnShareContentDirect(string contentType, string contentId, string userId, string contentName = "", string contentImage = "")
{
try
{
ShareApiHelper.ShareContentType shareContentType = contentType == "Avatar"
? ShareApiHelper.ShareContentType.Avatar
: ShareApiHelper.ShareContentType.Spawnable;
await ShareApiHelper.ShareContentAsync<BaseResponse>(
shareContentType,
contentId,
userId
);
// Alert the user that the share occurred
//ModNetwork.SendDirectShareNotification(userId, shareContentType, contentId);
__instance.gameMenuView.View._view.TriggerEvent("OnHandleShareResponse", true, userId);
}
catch (ShareApiException ex)
{
ShareBubblesMod.Logger.Error($"Share API error: {ex.Message}");
__instance.gameMenuView.View._view.TriggerEvent("OnHandleShareResponse", false, userId, ex.UserFriendlyMessage);
}
catch (Exception ex)
{
ShareBubblesMod.Logger.Error($"Unexpected error sharing content: {ex.Message}");
__instance.gameMenuView.View._view.TriggerEvent("OnHandleShareResponse", false, userId, "An unexpected error occurred");
}
}
void OnGetUsersForSharing(string searchTerm = "")
{
try
{
if (!string.IsNullOrEmpty(searchTerm))
{
// TODO: Search users implementation will go here
// For now just return an empty list
var response = new { entries = new List<object>() };
string json = JsonConvert.SerializeObject(response);
__instance.gameMenuView.View.TriggerEvent("OnHandleUsersResponse", true, json, false);
}
else
{
// Get instance users
CVRPlayerManager playerManager = CVRPlayerManager.Instance;
if (playerManager == null)
{
__instance.gameMenuView.View.TriggerEvent("OnHandleUsersResponse", false, false, true);
return;
}
var response = new
{
entries = playerManager.NetworkPlayers
.Where(p => p != null && !string.IsNullOrEmpty(p.Uuid)
&& !MetaPort.Instance.blockedUserIds.Contains(p.Uuid)) // You SHOULDNT HAVE TO DO THIS, but GS dumb
.Select(p => new
{
id = p.Uuid,
name = p.Username,
image = p.ApiProfileImageUrl
})
.ToList()
};
string json = JsonConvert.SerializeObject(response);
__instance.gameMenuView.View.TriggerEvent("OnHandleUsersResponse", true, json, true);
}
}
catch (Exception ex)
{
ShareBubblesMod.Logger.Error($"Failed to get users: {ex.Message}");
__instance.gameMenuView.View.TriggerEvent("OnHandleUsersResponse", false, false, string.IsNullOrEmpty(searchTerm));
}
}
}
}